History

refactor: model MCP transport forms as unions

Rhys Sullivan committed 8 months ago

Commit b8a09bd1 parent 567c4893 files · +323 −158Browse files

Unified · Split

3 files
  1. apps/web/src/views/add-source.tsx +135 −87
  2. apps/web/src/views/mcp-transport-state.ts +62 −0
  3. apps/web/src/views/source-editor.tsx +126 −71
apps/web/src/views/add-source.tsx+135−87
@@ -41,6 +41,16 @@ import { · 40 unmodified lines
4141 sourceTemplates,
4242 type SourceTemplate,
4343 } from "./source-templates";
44+import {
45+ asMcpRemoteTransportValue,
46+ defaultMcpRemoteTransportFields,
47+ defaultMcpStdioTransportFields,
48+ setMcpTransportFieldsTransport,
49+ type McpRemoteTransportFields,
50+ type McpStdioTransportFields,
51+ type McpTransportFields,
52+ type McpTransportValue,
53+} from "./mcp-transport-state";
4454 import { getDomain } from "tldts";
4555
4656 // ---------------------------------------------------------------------------
@@ -68,7 +78,7 @@ type ProbeAuthState = { · 21 unmodified lines
6878 headersText: string;
6979 };
7080
71−type ConnectFormState = {
81+type ConnectFormBase = {
7282 kind: "mcp" | "openapi" | "graphql" | "google_discovery";
7383 endpoint: string;
7484 specUrl: string;
@@ -77,7 +87,6 @@ type ConnectFormState = { · 2 unmodified lines
7787 discoveryUrl: string;
7888 name: string;
7989 namespace: string;
80− transport: "" | "auto" | "streamable-http" | "sse" | "stdio";
8190 authKind: "none" | "bearer" | "oauth2";
8291 authHeaderName: string;
8392 authPrefix: string;
@@ -87,14 +96,10 @@ type ConnectFormState = { · 3 unmodified lines
8796 workspaceOauthClientId: string;
8897 oauthClientId: string;
8998 oauthClientSecret: string;
90− queryParamsText: string;
91− headersText: string;
92− command: string;
93− argsText: string;
94− envText: string;
95− cwd: string;
9699 };
97100
101+type ConnectFormState = ConnectFormBase & McpTransportFields;
102+
98103 type OAuthRequiredInfo = {
99104 source: Source;
100105 sessionId: string;
@@ -118,9 +123,12 @@ const kindOptions: ReadonlyArray<ConnectFormState["kind"]> = [ · 17 unmodified lines
118123 "google_discovery",
119124 ];
120125
121−const transportOptions: ReadonlyArray<
122− "auto" | "streamable-http" | "sse" | "stdio"
123−> = ["auto", "streamable-http", "sse", "stdio"];
126+const transportOptions: ReadonlyArray<Exclude<McpTransportValue, "">> = [
127+ "auto",
128+ "streamable-http",
129+ "sse",
130+ "stdio",
131+];
124132
125133 const authOptions: ReadonlyArray<ConnectFormState["authKind"]> = [
126134 "none",
@@ -316,7 +324,6 @@ const defaultConnectForm = ( · 189 unmodified lines
316324 name: discovery?.name ?? "",
317325 namespace:
318326 discovery?.namespace || namespaceFromUrl(discovery?.endpoint ?? ""),
319− transport: "",
320327 authKind: "none",
321328 authHeaderName: "Authorization",
322329 authPrefix: "Bearer ",
@@ -326,12 +333,7 @@ const defaultConnectForm = ( · 3 unmodified lines
326333 workspaceOauthClientId: "",
327334 oauthClientId: "",
328335 oauthClientSecret: "",
329− queryParamsText: "",
330− headersText: "",
331− command: "",
332− argsText: "",
333− envText: "",
334− cwd: "",
336+ ...defaultMcpRemoteTransportFields(),
335337 };
336338 }
337339
@@ -374,7 +376,6 @@ const defaultConnectForm = ( · 36 unmodified lines
374376 discoveryUrl: googleDiscoveryDefaults?.discoveryUrl ?? "",
375377 name: discovery.name ?? "",
376378 namespace: discovery.namespace || namespaceFromUrl(discovery.endpoint),
377− transport: kind === "mcp" ? (discovery.transport ?? "auto") : "",
378379 authKind,
379380 authHeaderName,
380381 authPrefix,
@@ -384,12 +385,9 @@ const defaultConnectForm = ( · 3 unmodified lines
384385 workspaceOauthClientId: "",
385386 oauthClientId: "",
386387 oauthClientSecret: "",
387− queryParamsText: "",
388− headersText: "",
389− command: "",
390− argsText: "",
391− envText: "",
392− cwd: "",
388+ ...defaultMcpRemoteTransportFields(
389+ kind === "mcp" ? asMcpRemoteTransportValue(discovery.transport) : "",
390+ ),
393391 };
394392 };
395393
@@ -409,18 +407,20 @@ const connectFormFromTemplate = ( · 13 unmodified lines
409407 ("service" in template
410408 ? googleDiscoveryNamespace(template.service)
411409 : namespaceFromUrl(template.endpoint ?? "")),
412− transport:
413− template.kind === "mcp"
414− ? template.connectionType === "command"
415− ? (template.transport ?? "stdio")
416− : (template.transport ?? "auto")
417− : "",
418410 authKind: template.kind === "google_discovery" ? "oauth2" : "none",
419411 workspaceOauthClientId: "",
420− command: template.kind === "mcp" ? (template.command ?? "") : "",
421− argsText: template.kind === "mcp" ? stringifyStringArray(template.args) : "",
422− envText: template.kind === "mcp" ? stringifyStringMap(template.env) : "",
423− cwd: template.kind === "mcp" ? (template.cwd ?? "") : "",
412+ ...(template.kind === "mcp" && template.connectionType === "command"
413+ ? defaultMcpStdioTransportFields({
414+ command: template.command ?? "",
415+ argsText: stringifyStringArray(template.args),
416+ envText: stringifyStringMap(template.env),
417+ cwd: template.cwd ?? "",
418+ })
419+ : defaultMcpRemoteTransportFields(
420+ template.kind === "mcp"
421+ ? asMcpRemoteTransportValue(template.transport)
422+ : "",
423+ )),
424424 });
425425
426426 const authKindForSourceKind = (
@@ -463,34 +463,44 @@ const buildProbeAuth = ( · 36 unmodified lines
463463
464464 const buildConnectPayload = (form: ConnectFormState): ConnectSourcePayload => {
465465 if (form.kind === "mcp") {
466− const transport = form.transport === "" ? "auto" : form.transport;
467− const isStdio = transport === "stdio";
468− const endpoint = isStdio
469− ? buildSyntheticMcpStdioEndpoint({
470− name: form.name,
471− endpoint: form.endpoint,
472− command: form.command,
473− })
474− : form.endpoint.trim();
475− if (!isStdio && !endpoint) throw new Error("Endpoint is required.");
476− if (isStdio && !form.command.trim())
477− throw new Error("Command is required for stdio MCP sources.");
466+ if (form.transport === "stdio") {
467+ const endpoint = buildSyntheticMcpStdioEndpoint({
468+ name: form.name,
469+ endpoint: form.endpoint,
470+ command: form.command,
471+ });
472+ if (!form.command.trim()) {
473+ throw new Error("Command is required for stdio MCP sources.");
474+ }
475+ return {
476+ kind: "mcp",
477+ endpoint,
478+ name: trimToNull(form.name),
479+ namespace: trimToNull(form.namespace),
480+ transport: "stdio",
481+ queryParams: null,
482+ headers: null,
483+ command: form.command.trim(),
484+ args: parseJsonStringArray("Args", form.argsText),
485+ env: parseJsonStringMap("Environment", form.envText),
486+ cwd: trimToNull(form.cwd),
487+ };
488+ }
489+
490+ const endpoint = form.endpoint.trim();
491+ if (!endpoint) throw new Error("Endpoint is required.");
478492 return {
479493 kind: "mcp",
480494 endpoint,
481495 name: trimToNull(form.name),
482496 namespace: trimToNull(form.namespace),
483− transport,
484− queryParams: isStdio
485− ? null
486− : parseJsonStringMap("Query params", form.queryParamsText),
487− headers: isStdio
488− ? null
489− : parseJsonStringMap("Request headers", form.headersText),
490− command: isStdio ? form.command.trim() : null,
491− args: isStdio ? parseJsonStringArray("Args", form.argsText) : null,
492− env: isStdio ? parseJsonStringMap("Environment", form.envText) : null,
493− cwd: isStdio ? trimToNull(form.cwd) : null,
497+ transport: form.transport === "" ? "auto" : form.transport,
498+ queryParams: parseJsonStringMap("Query params", form.queryParamsText),
499+ headers: parseJsonStringMap("Request headers", form.headersText),
500+ command: null,
501+ args: null,
502+ env: null,
503+ cwd: null,
494504 };
495505 }
496506
@@ -831,13 +841,42 @@ export function AddSourcePage() { · 334 unmodified lines
831841 text: string;
832842 } | null>(null);
833843
834− const setFormField = <K extends keyof ConnectFormState>(
844+ const setFormField = <K extends keyof ConnectFormBase>(
835845 key: K,
836− value: ConnectFormState[K],
846+ value: ConnectFormBase[K],
837847 ) => {
838848 setConnectForm((current) => ({ ...current, [key]: value }));
839849 };
840850
851+ const setTransport = (transport: McpTransportValue) => {
852+ setConnectForm((current) => ({
853+ ...current,
854+ ...setMcpTransportFieldsTransport(current, transport),
855+ }));
856+ };
857+
858+ const setRemoteTransportField = <
859+ K extends Exclude<keyof McpRemoteTransportFields, "transport">,
860+ >(
861+ key: K,
862+ value: McpRemoteTransportFields[K],
863+ ) => {
864+ setConnectForm((current) =>
865+ current.transport === "stdio" ? current : { ...current, [key]: value },
866+ );
867+ };
868+
869+ const setStdioTransportField = <
870+ K extends Exclude<keyof McpStdioTransportFields, "transport">,
871+ >(
872+ key: K,
873+ value: McpStdioTransportFields[K],
874+ ) => {
875+ setConnectForm((current) =>
876+ current.transport === "stdio" ? { ...current, [key]: value } : current,
877+ );
878+ };
879+
841880 const setSourceKind = (kind: ConnectFormState["kind"]) => {
842881 setConnectForm((current) => ({
843882 ...current,
@@ -943,24 +982,32 @@ export function AddSourcePage() { · 99 unmodified lines
943982
944983 try {
945984 const result = await discoverSource.mutateAsync({ url: discoveryUrl });
946− const form = defaultConnectForm(result);
947− // Prefer the template's own values over whatever discover returned
948− form.name = template.name;
949− form.endpoint = template.endpoint ?? "";
950− form.namespace =
951− template.namespace ?? namespaceFromUrl(template.endpoint ?? "");
985+ let form: ConnectFormState = {
986+ ...defaultConnectForm(result),
987+ name: template.name,
988+ endpoint: template.endpoint ?? "",
989+ namespace: template.namespace ?? namespaceFromUrl(template.endpoint ?? ""),
990+ };
952991 if (template.kind === "mcp") {
953− form.transport =
954− template.connectionType === "command"
955− ? (template.transport ?? "stdio")
956− : (template.transport ?? form.transport);
957− form.command = template.command ?? "";
958− form.argsText = stringifyStringArray(template.args);
959− form.envText = stringifyStringMap(template.env);
960− form.cwd = template.cwd ?? "";
992+ form = {
993+ ...form,
994+ ...(template.connectionType === "command"
995+ ? defaultMcpStdioTransportFields({
996+ command: template.command ?? "",
997+ argsText: stringifyStringArray(template.args),
998+ envText: stringifyStringMap(template.env),
999+ cwd: template.cwd ?? "",
1000+ })
1001+ : defaultMcpRemoteTransportFields(
1002+ asMcpRemoteTransportValue(template.transport),
1003+ )),
1004+ };
9611005 }
9621006 if ("specUrl" in template) {
963− form.specUrl = template.specUrl;
1007+ form = {
1008+ ...form,
1009+ specUrl: template.specUrl,
1010+ };
9641011 }
9651012 setConnectForm(form);
9661013 setPhase("editing");
@@ -1804,12 +1851,7 @@ export function AddSourcePage() { · 837 unmodified lines
18041851 <Field label="Transport mode">
18051852 <SelectInput
18061853 value={connectForm.transport || "auto"}
1807− onChange={(v) =>
1808− setFormField(
1809− "transport",
1810− v as ConnectFormState["transport"],
1811− )
1812− }
1854+ onChange={(v) => setTransport(v as McpTransportValue)}
18131855 options={transportOptions.map((v) => ({
18141856 value: v,
18151857 label: v,
@@ -1821,7 +1863,7 @@ export function AddSourcePage() { · 5 unmodified lines
18211863 <Field label="Command">
18221864 <TextInput
18231865 value={connectForm.command}
1824− onChange={(v) => setFormField("command", v)}
1866+ onChange={(v) => setStdioTransportField("command", v)}
18251867 placeholder="npx"
18261868 mono
18271869 />
@@ -1829,7 +1871,7 @@ export function AddSourcePage() { · 1 unmodified line
18291871 <Field label="Working directory (optional)">
18301872 <TextInput
18311873 value={connectForm.cwd}
1832− onChange={(v) => setFormField("cwd", v)}
1874+ onChange={(v) => setStdioTransportField("cwd", v)}
18331875 placeholder="/path/to/project"
18341876 mono
18351877 />
@@ -1837,7 +1879,9 @@ export function AddSourcePage() { · 1 unmodified line
18371879 <Field label="Args (JSON)" className="sm:col-span-2">
18381880 <CodeEditor
18391881 value={connectForm.argsText}
1840− onChange={(v) => setFormField("argsText", v)}
1882+ onChange={(v) =>
1883+ setStdioTransportField("argsText", v)
1884+ }
18411885 placeholder={
18421886 '[\n "-y",\n "chrome-devtools-mcp@latest"\n]'
18431887 }
@@ -1849,7 +1893,7 @@ export function AddSourcePage() { · 5 unmodified lines
18491893 >
18501894 <CodeEditor
18511895 value={connectForm.envText}
1852− onChange={(v) => setFormField("envText", v)}
1896+ onChange={(v) => setStdioTransportField("envText", v)}
18531897 placeholder={
18541898 '{\n "CHROME_PATH": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"\n}'
18551899 }
@@ -1861,14 +1905,18 @@ export function AddSourcePage() { · 5 unmodified lines
18611905 <Field label="Query params (JSON)">
18621906 <CodeEditor
18631907 value={connectForm.queryParamsText}
1864− onChange={(v) => setFormField("queryParamsText", v)}
1908+ onChange={(v) =>
1909+ setRemoteTransportField("queryParamsText", v)
1910+ }
18651911 placeholder={'{\n "workspace": "demo"\n}'}
18661912 />
18671913 </Field>
18681914 <Field label="Headers (JSON)">
18691915 <CodeEditor
18701916 value={connectForm.headersText}
1871− onChange={(v) => setFormField("headersText", v)}
1917+ onChange={(v) =>
1918+ setRemoteTransportField("headersText", v)
1919+ }
18721920 placeholder={'{\n "x-api-key": "..."\n}'}
18731921 />
18741922 </Field>
apps/web/src/views/mcp-transport-state.ts+62−0
@@ -0,0 +1,62 @@
1+export type McpRemoteTransportValue = "" | "auto" | "streamable-http" | "sse";
2+
3+export type McpTransportValue = McpRemoteTransportValue | "stdio";
4+
5+export type McpRemoteTransportFields = {
6+ transport: McpRemoteTransportValue;
7+ queryParamsText: string;
8+ headersText: string;
9+};
10+
11+export type McpStdioTransportFields = {
12+ transport: "stdio";
13+ command: string;
14+ argsText: string;
15+ envText: string;
16+ cwd: string;
17+};
18+
19+export type McpTransportFields =
20+ | McpRemoteTransportFields
21+ | McpStdioTransportFields;
22+
23+export const defaultMcpRemoteTransportFields = (
24+ transport: McpRemoteTransportValue = "",
25+): McpRemoteTransportFields => ({
26+ transport,
27+ queryParamsText: "",
28+ headersText: "",
29+});
30+
31+export const defaultMcpStdioTransportFields = (
32+ input?: Partial<Omit<McpStdioTransportFields, "transport">>,
33+): McpStdioTransportFields => ({
34+ transport: "stdio",
35+ command: input?.command ?? "",
36+ argsText: input?.argsText ?? "",
37+ envText: input?.envText ?? "",
38+ cwd: input?.cwd ?? "",
39+});
40+
41+export const asMcpRemoteTransportValue = (
42+ transport: McpTransportValue | null | undefined,
43+): McpRemoteTransportValue =>
44+ transport === "stdio" ? "auto" : (transport ?? "");
45+
46+export const setMcpTransportFieldsTransport = (
47+ current: McpTransportFields,
48+ transport: McpTransportValue,
49+): McpTransportFields => {
50+ if (transport === "stdio") {
51+ return current.transport === "stdio"
52+ ? current
53+ : defaultMcpStdioTransportFields();
54+ }
55+
56+ return current.transport === "stdio"
57+ ? defaultMcpRemoteTransportFields(transport)
58+ : {
59+ ...current,
60+ transport,
61+ };
62+};
apps/web/src/views/source-editor.tsx+126−71
@@ -34,6 +34,16 @@ import { · 33 unmodified lines
3434 IconTrash,
3535 } from "../components/icons";
3636 import { cn } from "../lib/utils";
37+import {
38+ asMcpRemoteTransportValue,
39+ defaultMcpRemoteTransportFields,
40+ defaultMcpStdioTransportFields,
41+ setMcpTransportFieldsTransport,
42+ type McpRemoteTransportFields,
43+ type McpStdioTransportFields,
44+ type McpTransportFields,
45+ type McpTransportValue,
46+} from "./mcp-transport-state";
3747 import { sourceTemplates, type SourceTemplate } from "./source-templates";
3848 import { getDomain } from "tldts";
3949
@@ -63,21 +73,12 @@ const isSourceNotFoundLoadable = (loadable: Loadable<unknown>): boolean => · 23 unmodified lines
6373 loadable.status === "error" &&
6474 loadable.error.message.toLowerCase().includes("source not found");
6575
66−type TransportValue = "" | "auto" | "streamable-http" | "sse" | "stdio";
67−
68−type SourceFormState = {
76+type SourceFormBase = {
6977 name: string;
7078 kind: Source["kind"];
7179 endpoint: string;
7280 namespace: string;
7381 enabled: boolean;
74− transport: TransportValue;
75− queryParamsText: string;
76− headersText: string;
77− command: string;
78− argsText: string;
79− envText: string;
80− cwd: string;
8182 specUrl: string;
8283 service: string;
8384 version: string;
@@ -97,6 +98,8 @@ type SourceFormState = { · 13 unmodified lines
9798 > | null;
9899 };
99100
101+type SourceFormState = SourceFormBase & McpTransportFields;
102+
100103 const kindOptions: ReadonlyArray<Source["kind"]> = [
101104 "mcp",
102105 "openapi",
@@ -105,7 +108,7 @@ const kindOptions: ReadonlyArray<Source["kind"]> = [ · 2 unmodified lines
105108 "internal",
106109 ];
107110
108−const transportOptions: ReadonlyArray<Exclude<TransportValue, "">> = [
111+const transportOptions: ReadonlyArray<Exclude<McpTransportValue, "">> = [
109112 "auto",
110113 "streamable-http",
111114 "sse",
@@ -343,7 +346,7 @@ const readBindingStringArray = ( · 231 unmodified lines
343346 const readBindingString = (source: Source, key: string): string =>
344347 typeof source.binding[key] === "string" ? String(source.binding[key]) : "";
345348
346−const readBindingTransport = (source: Source): TransportValue => {
349+const readBindingTransport = (source: Source): McpTransportValue => {
347350 const candidate = source.binding.transport;
348351 return typeof candidate === "string" &&
349352 (candidate === "auto" ||
@@ -413,19 +416,6 @@ const defaultFormState = (template?: SourceTemplate): SourceFormState => ({ · 63 unmodified lines
413416 : namespaceFromUrl(template.endpoint ?? "")))
414417 : "",
415418 enabled: true,
416− transport:
417− template?.kind === "mcp"
418− ? template.connectionType === "command"
419− ? (template.transport ?? "stdio")
420− : (template.transport ?? "auto")
421− : "",
422− queryParamsText: "",
423− headersText: "",
424− command: template?.kind === "mcp" ? (template.command ?? "") : "",
425− argsText: template?.kind === "mcp" ? stringArrayToEditor(template.args) : "",
426− envText:
427− template?.kind === "mcp" ? stringMapToEditor(template.env ?? null) : "",
428− cwd: template?.kind === "mcp" ? (template.cwd ?? "") : "",
429419 specUrl: template && "specUrl" in template ? template.specUrl : "",
430420 service: template && "service" in template ? template.service : "",
431421 version: template && "version" in template ? template.version : "",
@@ -440,6 +430,18 @@ const defaultFormState = (template?: SourceTemplate): SourceFormState => ({ · 8 unmodified lines
440430 oauthRefreshProviderId: "",
441431 oauthRefreshHandle: "",
442432 managedAuth: null,
433+ ...(template?.kind === "mcp" && template.connectionType === "command"
434+ ? defaultMcpStdioTransportFields({
435+ command: template.command ?? "",
436+ argsText: stringArrayToEditor(template.args),
437+ envText: stringMapToEditor(template.env ?? null),
438+ cwd: template.cwd ?? "",
439+ })
440+ : defaultMcpRemoteTransportFields(
441+ template?.kind === "mcp"
442+ ? asMcpRemoteTransportValue(template.transport)
443+ : "",
444+ )),
443445 });
444446
445447 const formStateFromSource = (source: Source): SourceFormState => ({
@@ -448,16 +450,6 @@ const formStateFromSource = (source: Source): SourceFormState => ({ · 2 unmodified lines
448450 endpoint: source.endpoint,
449451 namespace: source.namespace ?? "",
450452 enabled: source.enabled,
451− transport:
452− source.kind === "mcp" ? readBindingTransport(source) || "auto" : "",
453− queryParamsText: stringMapToEditor(
454− readBindingStringMap(source, "queryParams"),
455− ),
456− headersText: stringMapToEditor(readBindingStringMap(source, "headers")),
457− command: readBindingString(source, "command"),
458− argsText: stringArrayToEditor(readBindingStringArray(source, "args")),
459− envText: stringMapToEditor(readBindingStringMap(source, "env")),
460− cwd: readBindingString(source, "cwd"),
461453 specUrl: readBindingString(source, "specUrl"),
462454 service: readBindingString(source, "service"),
463455 version: readBindingString(source, "version"),
@@ -493,6 +485,26 @@ const formStateFromSource = (source: Source): SourceFormState => ({ · 29 unmodified lines
493485 source.auth.kind === "mcp_oauth"
494486 ? source.auth
495487 : null,
488+ ...(source.kind === "mcp" && readBindingTransport(source) === "stdio"
489+ ? defaultMcpStdioTransportFields({
490+ command: readBindingString(source, "command"),
491+ argsText: stringArrayToEditor(readBindingStringArray(source, "args")),
492+ envText: stringMapToEditor(readBindingStringMap(source, "env")),
493+ cwd: readBindingString(source, "cwd"),
494+ })
495+ : defaultMcpRemoteTransportFields(
496+ source.kind === "mcp"
497+ ? asMcpRemoteTransportValue(readBindingTransport(source) || "auto")
498+ : "",
499+ )),
500+ ...(source.kind === "mcp" && readBindingTransport(source) !== "stdio"
501+ ? {
502+ queryParamsText: stringMapToEditor(
503+ readBindingStringMap(source, "queryParams"),
504+ ),
505+ headersText: stringMapToEditor(readBindingStringMap(source, "headers")),
506+ }
507+ : {}),
496508 });
497509
498510 const parseJsonStringMap = (
@@ -616,13 +628,14 @@ const buildRequestedSourceStatus = ( · 117 unmodified lines
616628 const buildSourcePayload = (state: SourceFormState): CreateSourcePayload => {
617629 const name = state.name.trim();
618630 const isMcpStdio = state.kind === "mcp" && state.transport === "stdio";
619− const endpoint = isMcpStdio
620− ? buildSyntheticMcpStdioEndpoint({
621− name: state.name,
622− endpoint: state.endpoint,
623− command: state.command,
624− })
625− : state.endpoint.trim();
631+ const endpoint =
632+ state.kind === "mcp" && state.transport === "stdio"
633+ ? buildSyntheticMcpStdioEndpoint({
634+ name: state.name,
635+ endpoint: state.endpoint,
636+ command: state.command,
637+ })
638+ : state.endpoint.trim();
626639
627640 if (!name) {
628641 throw new Error("Source name is required.");
@@ -649,28 +662,31 @@ const buildSourcePayload = (state: SourceFormState): CreateSourcePayload => { · 20 unmodified lines
649662 >;
650663
651664 if (state.kind === "mcp") {
665+ if (state.transport === "stdio") {
666+ return {
667+ ...shared,
668+ binding: {
669+ transport: "stdio",
670+ queryParams: null,
671+ headers: null,
672+ command: state.command.trim(),
673+ args: parseJsonStringArray("Args", state.argsText),
674+ env: parseJsonStringMap("Environment", state.envText),
675+ cwd: trimToNull(state.cwd),
676+ },
677+ };
678+ }
679+
652680 return {
653681 ...shared,
654682 binding: {
655683 transport: state.transport === "" ? "auto" : state.transport,
656− queryParams:
657− state.transport === "stdio"
658− ? null
659− : parseJsonStringMap("Query params", state.queryParamsText),
660− headers:
661− state.transport === "stdio"
662− ? null
663− : parseJsonStringMap("Request headers", state.headersText),
664− command: state.transport === "stdio" ? state.command.trim() : null,
665− args:
666− state.transport === "stdio"
667− ? parseJsonStringArray("Args", state.argsText)
668− : null,
669− env:
670− state.transport === "stdio"
671− ? parseJsonStringMap("Environment", state.envText)
672− : null,
673− cwd: state.transport === "stdio" ? trimToNull(state.cwd) : null,
684+ queryParams: parseJsonStringMap("Query params", state.queryParamsText),
685+ headers: parseJsonStringMap("Request headers", state.headersText),
686+ command: null,
687+ args: null,
688+ env: null,
689+ cwd: null,
674690 },
675691 };
676692 }
@@ -831,13 +847,42 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 154 unmodified lines
831847 oauthSecretRefTarget !== null &&
832848 expandedOauthSecretRefTarget === oauthSecretRefTarget;
833849
834− const setField = <K extends keyof SourceFormState>(
850+ const setField = <K extends keyof SourceFormBase>(
835851 key: K,
836− value: SourceFormState[K],
852+ value: SourceFormBase[K],
837853 ) => {
838854 setFormState((current) => ({ ...current, [key]: value }));
839855 };
840856
857+ const setTransport = (transport: McpTransportValue) => {
858+ setFormState((current) => ({
859+ ...current,
860+ ...setMcpTransportFieldsTransport(current, transport),
861+ }));
862+ };
863+
864+ const setRemoteTransportField = <
865+ K extends Exclude<keyof McpRemoteTransportFields, "transport">,
866+ >(
867+ key: K,
868+ value: McpRemoteTransportFields[K],
869+ ) => {
870+ setFormState((current) =>
871+ current.transport === "stdio" ? current : { ...current, [key]: value },
872+ );
873+ };
874+
875+ const setStdioTransportField = <
876+ K extends Exclude<keyof McpStdioTransportFields, "transport">,
877+ >(
878+ key: K,
879+ value: McpStdioTransportFields[K],
880+ ) => {
881+ setFormState((current) =>
882+ current.transport === "stdio" ? { ...current, [key]: value } : current,
883+ );
884+ };
885+
841886 const applyTemplate = (template: SourceTemplate) => {
842887 setSelectedTemplateId(template.id);
843888 setFormState((current) => ({
@@ -1145,9 +1190,7 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 301 unmodified lines
11451190 <Field label="Transport mode">
11461191 <SelectInput
11471192 value={formState.transport || "auto"}
1148− onChange={(value) =>
1149− setField("transport", value as TransportValue)
1150− }
1193+ onChange={(value) => setTransport(value as McpTransportValue)}
11511194 options={transportOptions.map((value) => ({
11521195 value,
11531196 label: value,
@@ -1159,7 +1202,9 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 5 unmodified lines
11591202 <Field label="Command">
11601203 <TextInput
11611204 value={formState.command}
1162− onChange={(value) => setField("command", value)}
1205+ onChange={(value) =>
1206+ setStdioTransportField("command", value)
1207+ }
11631208 placeholder="npx"
11641209 mono
11651210 />
@@ -1167,7 +1212,9 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 1 unmodified line
11671212 <Field label="Working directory (optional)">
11681213 <TextInput
11691214 value={formState.cwd}
1170− onChange={(value) => setField("cwd", value)}
1215+ onChange={(value) =>
1216+ setStdioTransportField("cwd", value)
1217+ }
11711218 placeholder="/path/to/project"
11721219 mono
11731220 />
@@ -1175,7 +1222,9 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 1 unmodified line
11751222 <Field label="Args (JSON)" className="sm:col-span-2">
11761223 <CodeEditor
11771224 value={formState.argsText}
1178− onChange={(value) => setField("argsText", value)}
1225+ onChange={(value) =>
1226+ setStdioTransportField("argsText", value)
1227+ }
11791228 placeholder={
11801229 '[\n "-y",\n "chrome-devtools-mcp@latest"\n]'
11811230 }
@@ -1184,7 +1233,9 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 2 unmodified lines
11841233 <Field label="Environment (JSON)" className="sm:col-span-2">
11851234 <CodeEditor
11861235 value={formState.envText}
1187− onChange={(value) => setField("envText", value)}
1236+ onChange={(value) =>
1237+ setStdioTransportField("envText", value)
1238+ }
11881239 placeholder={
11891240 '{\n "CHROME_PATH": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"\n}'
11901241 }
@@ -1196,14 +1247,18 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 5 unmodified lines
11961247 <Field label="Query params (JSON)">
11971248 <CodeEditor
11981249 value={formState.queryParamsText}
1199− onChange={(value) => setField("queryParamsText", value)}
1250+ onChange={(value) =>
1251+ setRemoteTransportField("queryParamsText", value)
1252+ }
12001253 placeholder={'{\n "workspace": "demo"\n}'}
12011254 />
12021255 </Field>
12031256 <Field label="Headers (JSON)">
12041257 <CodeEditor
12051258 value={formState.headersText}
1206− onChange={(value) => setField("headersText", value)}
1259+ onChange={(value) =>
1260+ setRemoteTransportField("headersText", value)
1261+ }
12071262 placeholder={'{\n "x-api-key": "..."\n}'}
12081263 />
12091264 </Field>