diff --git a/packages/plugins/mcp/src/api/group.ts b/packages/plugins/mcp/src/api/group.ts index 697cb0ad8..63da00005 100644 --- a/packages/plugins/mcp/src/api/group.ts +++ b/packages/plugins/mcp/src/api/group.ts @@ -72,6 +72,10 @@ const UpdateSourcePayload = Schema.Struct({ queryParams: Schema.optional(Schema.Record(Schema.String, McpCredentialInput)), credentialTargetScope: Schema.optional(ScopeId), auth: Schema.optional(AuthPayload), + command: Schema.optional(Schema.String), + args: Schema.optional(Schema.Array(Schema.String)), + env: Schema.optional(StringMap), + cwd: Schema.optional(Schema.String), }); const UpdateSourceResponse = Schema.Struct({ diff --git a/packages/plugins/mcp/src/api/handlers.ts b/packages/plugins/mcp/src/api/handlers.ts index 14ec9f3ae..21fe0d0ef 100644 --- a/packages/plugins/mcp/src/api/handlers.ts +++ b/packages/plugins/mcp/src/api/handlers.ts @@ -164,6 +164,10 @@ export const McpHandlers = HttpApiBuilder.group(ExecutorApiWithMcp, "mcp", (hand queryParams: payload.queryParams, credentialTargetScope: payload.credentialTargetScope, auth: payload.auth as McpUpdateSourceInput["auth"], + command: payload.command, + args: payload.args, + env: payload.env, + cwd: payload.cwd, }); return { updated: true }; }), diff --git a/packages/plugins/mcp/src/react/AddMcpSource.tsx b/packages/plugins/mcp/src/react/AddMcpSource.tsx index 6efbb94b0..02b3f6df8 100644 --- a/packages/plugins/mcp/src/react/AddMcpSource.tsx +++ b/packages/plugins/mcp/src/react/AddMcpSource.tsx @@ -2,16 +2,14 @@ import { useReducer, useCallback, useEffect, useRef, useState, type ReactNode } import { useAtomSet } from "@effect/atom-react"; import * as Exit from "effect/Exit"; import * as Match from "effect/Match"; -import * as Option from "effect/Option"; -import * as Schema from "effect/Schema"; +import { messageFromExit } from "@executor-js/react/api/error-reporting"; import { useScope } from "@executor-js/react/api/scope-context"; import { Button } from "@executor-js/react/components/button"; import { CardStack, CardStackContent, CardStackEntry, - CardStackEntryField, } from "@executor-js/react/components/card-stack"; import { FieldLabel } from "@executor-js/react/components/field"; import { FilterTabs } from "@executor-js/react/components/filter-tabs"; @@ -19,7 +17,6 @@ import { FloatActions } from "@executor-js/react/components/float-actions"; import { Input } from "@executor-js/react/components/input"; import { Label } from "@executor-js/react/components/label"; import { Spinner } from "@executor-js/react/components/spinner"; -import { Textarea } from "@executor-js/react/components/textarea"; import { emptyHttpCredentials, httpCredentialsValid, @@ -50,25 +47,11 @@ type RemoteAuthMode = "none" | "oauth2"; import { sourceWriteKeys } from "@executor-js/react/api/reactivity-keys"; import { probeMcpEndpoint, addMcpSourceOptimistic } from "./atoms"; import { McpRemoteSourceFields } from "./McpRemoteSourceFields"; +import { McpStdioSourceFields } from "./McpStdioSourceFields"; +import { parseStdioArgs, parseStdioEnv } from "./stdio-form-helpers"; import { mcpPresets, type McpPreset } from "../sdk/presets"; import { MCP_OAUTH_CONNECTION_SLOT, type McpCredentialInput } from "../sdk/types"; -const ErrorMessage = Schema.Struct({ message: Schema.String }); -const decodeErrorMessage = Schema.decodeUnknownOption(ErrorMessage); -const STDIO_ENV_ESCAPE_REPLACEMENTS: Readonly> = { - "\\": "\\", - n: "\n", - r: "\r", - t: "\t", - '"': '"', -}; - -const errorMessageFromExit = (exit: Exit.Exit, fallback: string): string => - Option.match(Option.flatMap(Exit.findErrorOption(exit), decodeErrorMessage), { - onNone: () => fallback, - onSome: ({ message }) => message, - }); - // --------------------------------------------------------------------------- // Preset lookup // --------------------------------------------------------------------------- @@ -361,7 +344,7 @@ export default function AddMcpSource(props: { if (Exit.isFailure(exit)) { dispatch({ type: "probe-fail", - error: errorMessageFromExit(exit, "Failed to connect"), + error: messageFromExit(exit, "Failed to connect"), }); return; } @@ -487,7 +470,7 @@ export default function AddMcpSource(props: { if (Exit.isFailure(exit)) { dispatch({ type: "add-fail", - error: errorMessageFromExit(exit, "Failed to add source"), + error: messageFromExit(exit, "Failed to add source"), }); return; } @@ -509,47 +492,6 @@ export default function AddMcpSource(props: { // ---- Stdio actions ---- - const parseStdioArgs = (raw: string): string[] => { - if (!raw.trim()) return []; - const args: string[] = []; - const regex = /[^\s"]+|"([^"]*)"/g; - let match; - while ((match = regex.exec(raw)) !== null) { - args.push(match[1] ?? match[0]); - } - return args; - }; - - const parseStdioEnvValue = (raw: string): string => { - const value = raw.trim(); - if (value.length < 2) return value; - - const quote = value[0]; - if ((quote !== '"' && quote !== "'") || value[value.length - 1] !== quote) { - return value; - } - - const inner = value.slice(1, -1); - if (quote === "'") return inner; - - return inner.replace( - /\\([\\nrt"])/g, - (_, escaped: string) => STDIO_ENV_ESCAPE_REPLACEMENTS[escaped] ?? escaped, - ); - }; - - const parseStdioEnv = (raw: string): Record | undefined => { - if (!raw.trim()) return undefined; - const env: Record = {}; - for (const line of raw.split("\n")) { - const eq = line.indexOf("="); - if (eq > 0) { - env[line.slice(0, eq).trim()] = parseStdioEnvValue(line.slice(eq + 1)); - } - } - return Object.keys(env).length > 0 ? env : undefined; - }; - const handleAddStdio = useCallback(async () => { const cmd = stdioCommand.trim(); if (!cmd) return; @@ -571,7 +513,7 @@ export default function AddMcpSource(props: { reactivityKeys: sourceWriteKeys, }); if (Exit.isFailure(exit)) { - setStdioError(errorMessageFromExit(exit, "Failed to add source")); + setStdioError(messageFromExit(exit, "Failed to add source")); setStdioAdding(false); return; } @@ -894,48 +836,14 @@ export default function AddMcpSource(props: { ) : ( <> - {/* Stdio form */} - - - - setStdioCommand((e.target as HTMLInputElement).value)} - placeholder="npx" - className="font-mono text-sm" - /> - - - - setStdioArgs((e.target as HTMLInputElement).value)} - placeholder="-y chrome-devtools-mcp@latest" - className="font-mono text-sm" - /> - - - -