From 296d6c3283b725ea1c4d6005489b93454368b169 Mon Sep 17 00:00:00 2001 From: The-AarushiSingh <175547726+The-AarushiSingh@users.noreply.github.com> Date: Fri, 28 Aug 2026 00:59:54 +0530 Subject: [PATCH 1/5] feat(mcp): add edit support for stdio sources - Add StdioEditForm with Save/Cancel buttons for stdio MCP sources - Implement updateStdioServer backend function using Effect - Connect UI to backend with proper error handling - Enable canEdit for stdio sources - Use shared Input, Label, and Textarea components - 42/42 MCP tests passing Closes #812 --- packages/plugins/mcp/src/api/group.ts | 17 ++ packages/plugins/mcp/src/api/handlers.test.ts | 1 + packages/plugins/mcp/src/api/handlers.ts | 9 + .../mcp/src/react/EditMcpIntegration.tsx | 171 ++++++++++++++++-- packages/plugins/mcp/src/react/atoms.ts | 1 + packages/plugins/mcp/src/react/index.ts | 1 + packages/plugins/mcp/src/sdk/plugin.ts | 19 ++ 7 files changed, 203 insertions(+), 16 deletions(-) diff --git a/packages/plugins/mcp/src/api/group.ts b/packages/plugins/mcp/src/api/group.ts index 2de2fb1222..d3f10d9e79 100644 --- a/packages/plugins/mcp/src/api/group.ts +++ b/packages/plugins/mcp/src/api/group.ts @@ -12,6 +12,7 @@ import { McpAuthMethodInput, McpAuthShorthand, McpIntegrationConfig, + McpStdioIntegrationConfig, } from "../sdk/types"; // --------------------------------------------------------------------------- @@ -105,6 +106,14 @@ const ConfigureServerResponse = Schema.Struct({ config: McpIntegrationConfig, }); +const UpdateStdioServerPayload = Schema.Struct({ + config: McpStdioIntegrationConfig, +}); + +const UpdateStdioServerResponse = Schema.Struct({ + config: McpStdioIntegrationConfig, +}); + // The configureAuth payload/response — custom auth methods to merge-append // onto the integration's `authenticationTemplate` (or `replace` the set). // Mirrors the GraphQL/OpenAPI configure endpoints. @@ -185,4 +194,12 @@ export const McpGroup = HttpApiGroup.make("mcp") success: ConfigureAuthResponse, error: [InternalError, McpConnectionError, McpToolDiscoveryError], }), + ) + .add( + HttpApiEndpoint.post("updateStdioServer", "/mcp/servers/:slug/stdio", { + params: SlugParams, + payload: UpdateStdioServerPayload, + success: UpdateStdioServerResponse, + error: [InternalError, McpConnectionError, McpToolDiscoveryError], + }), ); diff --git a/packages/plugins/mcp/src/api/handlers.test.ts b/packages/plugins/mcp/src/api/handlers.test.ts index 6d9048878b..581b5a0327 100644 --- a/packages/plugins/mcp/src/api/handlers.test.ts +++ b/packages/plugins/mcp/src/api/handlers.test.ts @@ -29,6 +29,7 @@ const failingExtension: McpPluginExtension = { reconcileStdioConnections: () => unused, getServer: () => Effect.succeed(null), configureServer: () => unused, + updateStdioServer: () => unused, configureAuth: () => unused, }; diff --git a/packages/plugins/mcp/src/api/handlers.ts b/packages/plugins/mcp/src/api/handlers.ts index 2b05275ad3..89824815d7 100644 --- a/packages/plugins/mcp/src/api/handlers.ts +++ b/packages/plugins/mcp/src/api/handlers.ts @@ -146,6 +146,15 @@ export const McpHandlers = HttpApiBuilder.group(ExecutorApiWithMcp, "mcp", (hand }), ), ) + .handle("updateStdioServer", ({ params: path, payload }) => + capture( + Effect.gen(function* () { + const ext = yield* McpExtensionService; + yield* ext.updateStdioServer(path.slug, payload.config); + return { config: payload.config }; + }), + ), + ) .handle("configureAuth", ({ params: path, payload }) => capture( Effect.gen(function* () { diff --git a/packages/plugins/mcp/src/react/EditMcpIntegration.tsx b/packages/plugins/mcp/src/react/EditMcpIntegration.tsx index e1cb718a82..0bd2aa5ebb 100644 --- a/packages/plugins/mcp/src/react/EditMcpIntegration.tsx +++ b/packages/plugins/mcp/src/react/EditMcpIntegration.tsx @@ -13,14 +13,19 @@ import { type AuthMethodRow, type AuthMethodSeed, } from "@executor-js/react/components/auth-method-list-editor"; -import { Badge } from "@executor-js/react/components/badge"; import { FormErrorAlert } from "@executor-js/react/lib/integration-add"; +import { messageFromExit } from "@executor-js/react/api/error-reporting"; +import { Button } from "@executor-js/react/components/button"; +import { Input } from "@executor-js/react/components/input"; +import { Label } from "@executor-js/react/components/label"; +import { Textarea } from "@executor-js/react/components/textarea"; -import { configureMcpAuth, mcpServerAtom } from "./atoms"; +import { configureMcpAuth, mcpServerAtom, updateStdioServer } from "./atoms"; import type { McpAuthMethod, McpCanonicalAuthMethodInput, McpIntegrationConfig, + McpStdioIntegrationConfig, } from "../sdk/types"; import { editorValueFromMcpAuthMethod, @@ -177,26 +182,159 @@ function RemoteEdit(props: { // Stdio read-only view // --------------------------------------------------------------------------- -function StdioReadOnly(props: { +function StdioEditForm(props: { server: McpServer & { config: Extract }; + onPendingChange?: EditSheetSectionProps["onPendingChange"]; }) { - const { command, args } = props.server.config; + const { server } = props; + const doUpdate = useAtomSet(updateStdioServer, { mode: "promiseExit" }); + const [command, setCommand] = useState(server.config.command || ""); + const [args, setArgs] = useState((server.config.args || []).join(" ")); + const [cwd, setCwd] = useState(server.config.cwd || ""); + const [envVars, setEnvVars] = useState( + Object.entries(server.config.env || {}) + .map(([k, v]) => `${k}=${v}`) + .join("\n"), + ); + const [isSaving, setIsSaving] = useState(false); + const [error, setError] = useState(null); + + // Convert args string back to array + const argsArray = args.split(" ").filter(Boolean); + + // Parse env vars from textarea + const envObject = Object.fromEntries( + envVars + .split("\n") + .filter(Boolean) + .map((line) => line.split("=")) + .filter(([k, v]) => k && v), + ); + + const hasChanges = + command !== server.config.command || + argsArray.join(" ") !== (server.config.args || []).join(" ") || + cwd !== (server.config.cwd || "") || + JSON.stringify(envObject) !== JSON.stringify(server.config.env || {}); + + const applyStaged = useCallback(async (): Promise => { + setError(null); + setIsSaving(true); + const config: McpStdioIntegrationConfig = { + ...server.config, + command, + args: argsArray, + cwd: cwd || undefined, + env: Object.keys(envObject).length > 0 ? envObject : undefined, + }; + const exit = await doUpdate({ + params: { slug: server.slug }, + payload: { config }, + reactivityKeys: integrationWriteKeys, + }); + setIsSaving(false); + if (Exit.isFailure(exit)) { + setError(messageFromExit(exit, "Failed to update stdio server")); + return { ok: false }; + } + return { ok: true, summary: "Stdio server updated successfully." }; + }, [argsArray, command, cwd, doUpdate, envObject, server.config, server.slug]); + + const onPendingChangeRef = useRef(props.onPendingChange); + onPendingChangeRef.current = props.onPendingChange; + useEffect(() => { + onPendingChangeRef.current?.(hasChanges ? applyStaged : null); + return () => onPendingChangeRef.current?.(null); + }, [hasChanges, applyStaged]); + return ( -
+
-

Server command

+

Server configuration

- Stdio MCP integrations cannot be edited. Remove and recreate the integration with the - updated command. + Edit the stdio server command and environment variables.

-
-

- {command} {(args ?? []).join(" ")} -

- - stdio - + +
+
+ + setCommand(e.target.value)} + className="mt-1 w-full rounded-md border border-border/60 bg-muted/40 px-3 py-2 text-sm" + placeholder="e.g., node" + required + /> +
+ +
+ + setArgs(e.target.value)} + className="mt-1 w-full rounded-md border border-border/60 bg-muted/40 px-3 py-2 text-sm" + placeholder="server.js --port 3000" + /> +

Space-separated arguments

+
+ +
+ + setCwd(e.target.value)} + className="mt-1 w-full rounded-md border border-border/60 bg-muted/40 px-3 py-2 text-sm" + placeholder="/path/to/working/directory" + /> +
+ +
+ +