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
41 sourceTemplates,41 sourceTemplates,
42 type SourceTemplate,42 type SourceTemplate,
43} from "./source-templates";43} from "./source-templates";
44import {
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";
44import { getDomain } from "tldts";54import { getDomain } from "tldts";
45 55
46// ---------------------------------------------------------------------------56// ---------------------------------------------------------------------------
@@ -68,7 +78,7 @@ type ProbeAuthState = { · 21 unmodified lines
68 headersText: string;78 headersText: string;
69};79};
70 80
71type ConnectFormState = {81type ConnectFormBase = {
72 kind: "mcp" | "openapi" | "graphql" | "google_discovery";82 kind: "mcp" | "openapi" | "graphql" | "google_discovery";
73 endpoint: string;83 endpoint: string;
74 specUrl: string;84 specUrl: string;
@@ -77,7 +87,6 @@ type ConnectFormState = { · 2 unmodified lines
77 discoveryUrl: string;87 discoveryUrl: string;
78 name: string;88 name: string;
79 namespace: string;89 namespace: string;
80 transport: "" | "auto" | "streamable-http" | "sse" | "stdio";
81 authKind: "none" | "bearer" | "oauth2";90 authKind: "none" | "bearer" | "oauth2";
82 authHeaderName: string;91 authHeaderName: string;
83 authPrefix: string;92 authPrefix: string;
@@ -87,14 +96,10 @@ type ConnectFormState = { · 3 unmodified lines
87 workspaceOauthClientId: string;96 workspaceOauthClientId: string;
88 oauthClientId: string;97 oauthClientId: string;
89 oauthClientSecret: string;98 oauthClientSecret: string;
90 queryParamsText: string;
91 headersText: string;
92 command: string;
93 argsText: string;
94 envText: string;
95 cwd: string;
96};99};
97 100
101type ConnectFormState = ConnectFormBase & McpTransportFields;
102
98type OAuthRequiredInfo = {103type OAuthRequiredInfo = {
99 source: Source;104 source: Source;
100 sessionId: string;105 sessionId: string;
@@ -118,9 +123,12 @@ const kindOptions: ReadonlyArray<ConnectFormState["kind"]> = [ · 17 unmodified lines
118 "google_discovery",123 "google_discovery",
119];124];
120 125
121const transportOptions: ReadonlyArray<126const transportOptions: ReadonlyArray<Exclude<McpTransportValue, "">> = [
122 "auto" | "streamable-http" | "sse" | "stdio"127 "auto",
123> = ["auto", "streamable-http", "sse", "stdio"];128 "streamable-http",
129 "sse",
130 "stdio",
131];
124 132
125const authOptions: ReadonlyArray<ConnectFormState["authKind"]> = [133const authOptions: ReadonlyArray<ConnectFormState["authKind"]> = [
126 "none",134 "none",
@@ -316,7 +324,6 @@ const defaultConnectForm = ( · 189 unmodified lines
316 name: discovery?.name ?? "",324 name: discovery?.name ?? "",
317 namespace:325 namespace:
318 discovery?.namespace || namespaceFromUrl(discovery?.endpoint ?? ""),326 discovery?.namespace || namespaceFromUrl(discovery?.endpoint ?? ""),
319 transport: "",
320 authKind: "none",327 authKind: "none",
321 authHeaderName: "Authorization",328 authHeaderName: "Authorization",
322 authPrefix: "Bearer ",329 authPrefix: "Bearer ",
@@ -326,12 +333,7 @@ const defaultConnectForm = ( · 3 unmodified lines
326 workspaceOauthClientId: "",333 workspaceOauthClientId: "",
327 oauthClientId: "",334 oauthClientId: "",
328 oauthClientSecret: "",335 oauthClientSecret: "",
329 queryParamsText: "",336 ...defaultMcpRemoteTransportFields(),
330 headersText: "",
331 command: "",
332 argsText: "",
333 envText: "",
334 cwd: "",
335 };337 };
336 }338 }
337 339
@@ -374,7 +376,6 @@ const defaultConnectForm = ( · 36 unmodified lines
374 discoveryUrl: googleDiscoveryDefaults?.discoveryUrl ?? "",376 discoveryUrl: googleDiscoveryDefaults?.discoveryUrl ?? "",
375 name: discovery.name ?? "",377 name: discovery.name ?? "",
376 namespace: discovery.namespace || namespaceFromUrl(discovery.endpoint),378 namespace: discovery.namespace || namespaceFromUrl(discovery.endpoint),
377 transport: kind === "mcp" ? (discovery.transport ?? "auto") : "",
378 authKind,379 authKind,
379 authHeaderName,380 authHeaderName,
380 authPrefix,381 authPrefix,
@@ -384,12 +385,9 @@ const defaultConnectForm = ( · 3 unmodified lines
384 workspaceOauthClientId: "",385 workspaceOauthClientId: "",
385 oauthClientId: "",386 oauthClientId: "",
386 oauthClientSecret: "",387 oauthClientSecret: "",
387 queryParamsText: "",388 ...defaultMcpRemoteTransportFields(
388 headersText: "",389 kind === "mcp" ? asMcpRemoteTransportValue(discovery.transport) : "",
389 command: "",390 ),
390 argsText: "",
391 envText: "",
392 cwd: "",
393 };391 };
394};392};
395 393
@@ -409,18 +407,20 @@ const connectFormFromTemplate = ( · 13 unmodified lines
409 ("service" in template407 ("service" in template
410 ? googleDiscoveryNamespace(template.service)408 ? googleDiscoveryNamespace(template.service)
411 : namespaceFromUrl(template.endpoint ?? "")),409 : namespaceFromUrl(template.endpoint ?? "")),
412 transport:
413 template.kind === "mcp"
414 ? template.connectionType === "command"
415 ? (template.transport ?? "stdio")
416 : (template.transport ?? "auto")
417 : "",
418 authKind: template.kind === "google_discovery" ? "oauth2" : "none",410 authKind: template.kind === "google_discovery" ? "oauth2" : "none",
419 workspaceOauthClientId: "",411 workspaceOauthClientId: "",
420 command: template.kind === "mcp" ? (template.command ?? "") : "",412 ...(template.kind === "mcp" && template.connectionType === "command"
421 argsText: template.kind === "mcp" ? stringifyStringArray(template.args) : "",413 ? defaultMcpStdioTransportFields({
422 envText: template.kind === "mcp" ? stringifyStringMap(template.env) : "",414 command: template.command ?? "",
423 cwd: template.kind === "mcp" ? (template.cwd ?? "") : "",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 )),
424});424});
425 425
426const authKindForSourceKind = (426const authKindForSourceKind = (
@@ -463,34 +463,44 @@ const buildProbeAuth = ( · 36 unmodified lines
463 463
464const buildConnectPayload = (form: ConnectFormState): ConnectSourcePayload => {464const buildConnectPayload = (form: ConnectFormState): ConnectSourcePayload => {
465 if (form.kind === "mcp") {465 if (form.kind === "mcp") {
466 const transport = form.transport === "" ? "auto" : form.transport;466 if (form.transport === "stdio") {
467 const isStdio = transport === "stdio";467 const endpoint = buildSyntheticMcpStdioEndpoint({
468 const endpoint = isStdio468 name: form.name,
469 ? buildSyntheticMcpStdioEndpoint({469 endpoint: form.endpoint,
470 name: form.name,470 command: form.command,
471 endpoint: form.endpoint,471 });
472 command: form.command,472 if (!form.command.trim()) {
473 })473 throw new Error("Command is required for stdio MCP sources.");
474 : form.endpoint.trim();474 }
475 if (!isStdio && !endpoint) throw new Error("Endpoint is required.");475 return {
476 if (isStdio && !form.command.trim())476 kind: "mcp",
477 throw new Error("Command is required for stdio MCP sources.");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.");
478 return {492 return {
479 kind: "mcp",493 kind: "mcp",
480 endpoint,494 endpoint,
481 name: trimToNull(form.name),495 name: trimToNull(form.name),
482 namespace: trimToNull(form.namespace),496 namespace: trimToNull(form.namespace),
483 transport,497 transport: form.transport === "" ? "auto" : form.transport,
484 queryParams: isStdio498 queryParams: parseJsonStringMap("Query params", form.queryParamsText),
485 ? null499 headers: parseJsonStringMap("Request headers", form.headersText),
486 : parseJsonStringMap("Query params", form.queryParamsText),500 command: null,
487 headers: isStdio501 args: null,
488 ? null502 env: null,
489 : parseJsonStringMap("Request headers", form.headersText),503 cwd: null,
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,
494 };504 };
495 }505 }
496 506
@@ -831,13 +841,42 @@ export function AddSourcePage() { · 334 unmodified lines
831 text: string;841 text: string;
832 } | null>(null);842 } | null>(null);
833 843
834 const setFormField = <K extends keyof ConnectFormState>(844 const setFormField = <K extends keyof ConnectFormBase>(
835 key: K,845 key: K,
836 value: ConnectFormState[K],846 value: ConnectFormBase[K],
837 ) => {847 ) => {
838 setConnectForm((current) => ({ ...current, [key]: value }));848 setConnectForm((current) => ({ ...current, [key]: value }));
839 };849 };
840 850
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
841 const setSourceKind = (kind: ConnectFormState["kind"]) => {880 const setSourceKind = (kind: ConnectFormState["kind"]) => {
842 setConnectForm((current) => ({881 setConnectForm((current) => ({
843 ...current,882 ...current,
@@ -943,24 +982,32 @@ export function AddSourcePage() { · 99 unmodified lines
943 982
944 try {983 try {
945 const result = await discoverSource.mutateAsync({ url: discoveryUrl });984 const result = await discoverSource.mutateAsync({ url: discoveryUrl });
946 const form = defaultConnectForm(result);985 let form: ConnectFormState = {
947 // Prefer the template's own values over whatever discover returned986 ...defaultConnectForm(result),
948 form.name = template.name;987 name: template.name,
949 form.endpoint = template.endpoint ?? "";988 endpoint: template.endpoint ?? "",
950 form.namespace =989 namespace: template.namespace ?? namespaceFromUrl(template.endpoint ?? ""),
951 template.namespace ?? namespaceFromUrl(template.endpoint ?? "");990 };
952 if (template.kind === "mcp") {991 if (template.kind === "mcp") {
953 form.transport =992 form = {
954 template.connectionType === "command"993 ...form,
955 ? (template.transport ?? "stdio")994 ...(template.connectionType === "command"
956 : (template.transport ?? form.transport);995 ? defaultMcpStdioTransportFields({
957 form.command = template.command ?? "";996 command: template.command ?? "",
958 form.argsText = stringifyStringArray(template.args);997 argsText: stringifyStringArray(template.args),
959 form.envText = stringifyStringMap(template.env);998 envText: stringifyStringMap(template.env),
960 form.cwd = template.cwd ?? "";999 cwd: template.cwd ?? "",
1000 })
1001 : defaultMcpRemoteTransportFields(
1002 asMcpRemoteTransportValue(template.transport),
1003 )),
1004 };
961 }1005 }
962 if ("specUrl" in template) {1006 if ("specUrl" in template) {
963 form.specUrl = template.specUrl;1007 form = {
1008 ...form,
1009 specUrl: template.specUrl,
1010 };
964 }1011 }
965 setConnectForm(form);1012 setConnectForm(form);
966 setPhase("editing");1013 setPhase("editing");
@@ -1804,12 +1851,7 @@ export function AddSourcePage() { · 837 unmodified lines
1804 <Field label="Transport mode">1851 <Field label="Transport mode">
1805 <SelectInput1852 <SelectInput
1806 value={connectForm.transport || "auto"}1853 value={connectForm.transport || "auto"}
1807 onChange={(v) =>1854 onChange={(v) => setTransport(v as McpTransportValue)}
1808 setFormField(
1809 "transport",
1810 v as ConnectFormState["transport"],
1811 )
1812 }
1813 options={transportOptions.map((v) => ({1855 options={transportOptions.map((v) => ({
1814 value: v,1856 value: v,
1815 label: v,1857 label: v,
@@ -1821,7 +1863,7 @@ export function AddSourcePage() { · 5 unmodified lines
1821 <Field label="Command">1863 <Field label="Command">
1822 <TextInput1864 <TextInput
1823 value={connectForm.command}1865 value={connectForm.command}
1824 onChange={(v) => setFormField("command", v)}1866 onChange={(v) => setStdioTransportField("command", v)}
1825 placeholder="npx"1867 placeholder="npx"
1826 mono1868 mono
1827 />1869 />
@@ -1829,7 +1871,7 @@ export function AddSourcePage() { · 1 unmodified line
1829 <Field label="Working directory (optional)">1871 <Field label="Working directory (optional)">
1830 <TextInput1872 <TextInput
1831 value={connectForm.cwd}1873 value={connectForm.cwd}
1832 onChange={(v) => setFormField("cwd", v)}1874 onChange={(v) => setStdioTransportField("cwd", v)}
1833 placeholder="/path/to/project"1875 placeholder="/path/to/project"
1834 mono1876 mono
1835 />1877 />
@@ -1837,7 +1879,9 @@ export function AddSourcePage() { · 1 unmodified line
1837 <Field label="Args (JSON)" className="sm:col-span-2">1879 <Field label="Args (JSON)" className="sm:col-span-2">
1838 <CodeEditor1880 <CodeEditor
1839 value={connectForm.argsText}1881 value={connectForm.argsText}
1840 onChange={(v) => setFormField("argsText", v)}1882 onChange={(v) =>
1883 setStdioTransportField("argsText", v)
1884 }
1841 placeholder={1885 placeholder={
1842 '[\n "-y",\n "chrome-devtools-mcp@latest"\n]'1886 '[\n "-y",\n "chrome-devtools-mcp@latest"\n]'
1843 }1887 }
@@ -1849,7 +1893,7 @@ export function AddSourcePage() { · 5 unmodified lines
1849 >1893 >
1850 <CodeEditor1894 <CodeEditor
1851 value={connectForm.envText}1895 value={connectForm.envText}
1852 onChange={(v) => setFormField("envText", v)}1896 onChange={(v) => setStdioTransportField("envText", v)}
1853 placeholder={1897 placeholder={
1854 '{\n "CHROME_PATH": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"\n}'1898 '{\n "CHROME_PATH": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"\n}'
1855 }1899 }
@@ -1861,14 +1905,18 @@ export function AddSourcePage() { · 5 unmodified lines
1861 <Field label="Query params (JSON)">1905 <Field label="Query params (JSON)">
1862 <CodeEditor1906 <CodeEditor
1863 value={connectForm.queryParamsText}1907 value={connectForm.queryParamsText}
1864 onChange={(v) => setFormField("queryParamsText", v)}1908 onChange={(v) =>
1909 setRemoteTransportField("queryParamsText", v)
1910 }
1865 placeholder={'{\n "workspace": "demo"\n}'}1911 placeholder={'{\n "workspace": "demo"\n}'}
1866 />1912 />
1867 </Field>1913 </Field>
1868 <Field label="Headers (JSON)">1914 <Field label="Headers (JSON)">
1869 <CodeEditor1915 <CodeEditor
1870 value={connectForm.headersText}1916 value={connectForm.headersText}
1871 onChange={(v) => setFormField("headersText", v)}1917 onChange={(v) =>
1918 setRemoteTransportField("headersText", v)
1919 }
1872 placeholder={'{\n "x-api-key": "..."\n}'}1920 placeholder={'{\n "x-api-key": "..."\n}'}
1873 />1921 />
1874 </Field>1922 </Field>
apps/web/src/views/mcp-transport-state.ts+62−0
@@ -0,0 +1,62 @@
1export type McpRemoteTransportValue = "" | "auto" | "streamable-http" | "sse";
2
3export type McpTransportValue = McpRemoteTransportValue | "stdio";
4
5export type McpRemoteTransportFields = {
6 transport: McpRemoteTransportValue;
7 queryParamsText: string;
8 headersText: string;
9};
10
11export type McpStdioTransportFields = {
12 transport: "stdio";
13 command: string;
14 argsText: string;
15 envText: string;
16 cwd: string;
17};
18
19export type McpTransportFields =
20 | McpRemoteTransportFields
21 | McpStdioTransportFields;
22
23export const defaultMcpRemoteTransportFields = (
24 transport: McpRemoteTransportValue = "",
25): McpRemoteTransportFields => ({
26 transport,
27 queryParamsText: "",
28 headersText: "",
29});
30
31export 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
41export const asMcpRemoteTransportValue = (
42 transport: McpTransportValue | null | undefined,
43): McpRemoteTransportValue =>
44 transport === "stdio" ? "auto" : (transport ?? "");
45
46export 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
34 IconTrash,34 IconTrash,
35} from "../components/icons";35} from "../components/icons";
36import { cn } from "../lib/utils";36import { cn } from "../lib/utils";
37import {
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";
37import { sourceTemplates, type SourceTemplate } from "./source-templates";47import { sourceTemplates, type SourceTemplate } from "./source-templates";
38import { getDomain } from "tldts";48import { getDomain } from "tldts";
39 49
@@ -63,21 +73,12 @@ const isSourceNotFoundLoadable = (loadable: Loadable<unknown>): boolean => · 23 unmodified lines
63 loadable.status === "error" &&73 loadable.status === "error" &&
64 loadable.error.message.toLowerCase().includes("source not found");74 loadable.error.message.toLowerCase().includes("source not found");
65 75
66type TransportValue = "" | "auto" | "streamable-http" | "sse" | "stdio";76type SourceFormBase = {
67
68type SourceFormState = {
69 name: string;77 name: string;
70 kind: Source["kind"];78 kind: Source["kind"];
71 endpoint: string;79 endpoint: string;
72 namespace: string;80 namespace: string;
73 enabled: boolean;81 enabled: boolean;
74 transport: TransportValue;
75 queryParamsText: string;
76 headersText: string;
77 command: string;
78 argsText: string;
79 envText: string;
80 cwd: string;
81 specUrl: string;82 specUrl: string;
82 service: string;83 service: string;
83 version: string;84 version: string;
@@ -97,6 +98,8 @@ type SourceFormState = { · 13 unmodified lines
97 > | null;98 > | null;
98};99};
99 100
101type SourceFormState = SourceFormBase & McpTransportFields;
102
100const kindOptions: ReadonlyArray<Source["kind"]> = [103const kindOptions: ReadonlyArray<Source["kind"]> = [
101 "mcp",104 "mcp",
102 "openapi",105 "openapi",
@@ -105,7 +108,7 @@ const kindOptions: ReadonlyArray<Source["kind"]> = [ · 2 unmodified lines
105 "internal",108 "internal",
106];109];
107 110
108const transportOptions: ReadonlyArray<Exclude<TransportValue, "">> = [111const transportOptions: ReadonlyArray<Exclude<McpTransportValue, "">> = [
109 "auto",112 "auto",
110 "streamable-http",113 "streamable-http",
111 "sse",114 "sse",
@@ -343,7 +346,7 @@ const readBindingStringArray = ( · 231 unmodified lines
343const readBindingString = (source: Source, key: string): string =>346const readBindingString = (source: Source, key: string): string =>
344 typeof source.binding[key] === "string" ? String(source.binding[key]) : "";347 typeof source.binding[key] === "string" ? String(source.binding[key]) : "";
345 348
346const readBindingTransport = (source: Source): TransportValue => {349const readBindingTransport = (source: Source): McpTransportValue => {
347 const candidate = source.binding.transport;350 const candidate = source.binding.transport;
348 return typeof candidate === "string" &&351 return typeof candidate === "string" &&
349 (candidate === "auto" ||352 (candidate === "auto" ||
@@ -413,19 +416,6 @@ const defaultFormState = (template?: SourceTemplate): SourceFormState => ({ · 63 unmodified lines
413 : namespaceFromUrl(template.endpoint ?? "")))416 : namespaceFromUrl(template.endpoint ?? "")))
414 : "",417 : "",
415 enabled: true,418 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 ?? "") : "",
429 specUrl: template && "specUrl" in template ? template.specUrl : "",419 specUrl: template && "specUrl" in template ? template.specUrl : "",
430 service: template && "service" in template ? template.service : "",420 service: template && "service" in template ? template.service : "",
431 version: template && "version" in template ? template.version : "",421 version: template && "version" in template ? template.version : "",
@@ -440,6 +430,18 @@ const defaultFormState = (template?: SourceTemplate): SourceFormState => ({ · 8 unmodified lines
440 oauthRefreshProviderId: "",430 oauthRefreshProviderId: "",
441 oauthRefreshHandle: "",431 oauthRefreshHandle: "",
442 managedAuth: null,432 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 )),
443});445});
444 446
445const formStateFromSource = (source: Source): SourceFormState => ({447const formStateFromSource = (source: Source): SourceFormState => ({
@@ -448,16 +450,6 @@ const formStateFromSource = (source: Source): SourceFormState => ({ · 2 unmodified lines
448 endpoint: source.endpoint,450 endpoint: source.endpoint,
449 namespace: source.namespace ?? "",451 namespace: source.namespace ?? "",
450 enabled: source.enabled,452 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"),
461 specUrl: readBindingString(source, "specUrl"),453 specUrl: readBindingString(source, "specUrl"),
462 service: readBindingString(source, "service"),454 service: readBindingString(source, "service"),
463 version: readBindingString(source, "version"),455 version: readBindingString(source, "version"),
@@ -493,6 +485,26 @@ const formStateFromSource = (source: Source): SourceFormState => ({ · 29 unmodified lines
493 source.auth.kind === "mcp_oauth"485 source.auth.kind === "mcp_oauth"
494 ? source.auth486 ? source.auth
495 : null,487 : 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 : {}),
496});508});
497 509
498const parseJsonStringMap = (510const parseJsonStringMap = (
@@ -616,13 +628,14 @@ const buildRequestedSourceStatus = ( · 117 unmodified lines
616const buildSourcePayload = (state: SourceFormState): CreateSourcePayload => {628const buildSourcePayload = (state: SourceFormState): CreateSourcePayload => {
617 const name = state.name.trim();629 const name = state.name.trim();
618 const isMcpStdio = state.kind === "mcp" && state.transport === "stdio";630 const isMcpStdio = state.kind === "mcp" && state.transport === "stdio";
619 const endpoint = isMcpStdio631 const endpoint =
620 ? buildSyntheticMcpStdioEndpoint({632 state.kind === "mcp" && state.transport === "stdio"
621 name: state.name,633 ? buildSyntheticMcpStdioEndpoint({
622 endpoint: state.endpoint,634 name: state.name,
623 command: state.command,635 endpoint: state.endpoint,
624 })636 command: state.command,
625 : state.endpoint.trim();637 })
638 : state.endpoint.trim();
626 639
627 if (!name) {640 if (!name) {
628 throw new Error("Source name is required.");641 throw new Error("Source name is required.");
@@ -649,28 +662,31 @@ const buildSourcePayload = (state: SourceFormState): CreateSourcePayload => { · 20 unmodified lines
649 >;662 >;
650 663
651 if (state.kind === "mcp") {664 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
652 return {680 return {
653 ...shared,681 ...shared,
654 binding: {682 binding: {
655 transport: state.transport === "" ? "auto" : state.transport,683 transport: state.transport === "" ? "auto" : state.transport,
656 queryParams:684 queryParams: parseJsonStringMap("Query params", state.queryParamsText),
657 state.transport === "stdio"685 headers: parseJsonStringMap("Request headers", state.headersText),
658 ? null686 command: null,
659 : parseJsonStringMap("Query params", state.queryParamsText),687 args: null,
660 headers:688 env: null,
661 state.transport === "stdio"689 cwd: null,
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,
674 },690 },
675 };691 };
676 }692 }
@@ -831,13 +847,42 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 154 unmodified lines
831 oauthSecretRefTarget !== null &&847 oauthSecretRefTarget !== null &&
832 expandedOauthSecretRefTarget === oauthSecretRefTarget;848 expandedOauthSecretRefTarget === oauthSecretRefTarget;
833 849
834 const setField = <K extends keyof SourceFormState>(850 const setField = <K extends keyof SourceFormBase>(
835 key: K,851 key: K,
836 value: SourceFormState[K],852 value: SourceFormBase[K],
837 ) => {853 ) => {
838 setFormState((current) => ({ ...current, [key]: value }));854 setFormState((current) => ({ ...current, [key]: value }));
839 };855 };
840 856
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
841 const applyTemplate = (template: SourceTemplate) => {886 const applyTemplate = (template: SourceTemplate) => {
842 setSelectedTemplateId(template.id);887 setSelectedTemplateId(template.id);
843 setFormState((current) => ({888 setFormState((current) => ({
@@ -1145,9 +1190,7 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 301 unmodified lines
1145 <Field label="Transport mode">1190 <Field label="Transport mode">
1146 <SelectInput1191 <SelectInput
1147 value={formState.transport || "auto"}1192 value={formState.transport || "auto"}
1148 onChange={(value) =>1193 onChange={(value) => setTransport(value as McpTransportValue)}
1149 setField("transport", value as TransportValue)
1150 }
1151 options={transportOptions.map((value) => ({1194 options={transportOptions.map((value) => ({
1152 value,1195 value,
1153 label: value,1196 label: value,
@@ -1159,7 +1202,9 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 5 unmodified lines
1159 <Field label="Command">1202 <Field label="Command">
1160 <TextInput1203 <TextInput
1161 value={formState.command}1204 value={formState.command}
1162 onChange={(value) => setField("command", value)}1205 onChange={(value) =>
1206 setStdioTransportField("command", value)
1207 }
1163 placeholder="npx"1208 placeholder="npx"
1164 mono1209 mono
1165 />1210 />
@@ -1167,7 +1212,9 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 1 unmodified line
1167 <Field label="Working directory (optional)">1212 <Field label="Working directory (optional)">
1168 <TextInput1213 <TextInput
1169 value={formState.cwd}1214 value={formState.cwd}
1170 onChange={(value) => setField("cwd", value)}1215 onChange={(value) =>
1216 setStdioTransportField("cwd", value)
1217 }
1171 placeholder="/path/to/project"1218 placeholder="/path/to/project"
1172 mono1219 mono
1173 />1220 />
@@ -1175,7 +1222,9 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 1 unmodified line
1175 <Field label="Args (JSON)" className="sm:col-span-2">1222 <Field label="Args (JSON)" className="sm:col-span-2">
1176 <CodeEditor1223 <CodeEditor
1177 value={formState.argsText}1224 value={formState.argsText}
1178 onChange={(value) => setField("argsText", value)}1225 onChange={(value) =>
1226 setStdioTransportField("argsText", value)
1227 }
1179 placeholder={1228 placeholder={
1180 '[\n "-y",\n "chrome-devtools-mcp@latest"\n]'1229 '[\n "-y",\n "chrome-devtools-mcp@latest"\n]'
1181 }1230 }
@@ -1184,7 +1233,9 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 2 unmodified lines
1184 <Field label="Environment (JSON)" className="sm:col-span-2">1233 <Field label="Environment (JSON)" className="sm:col-span-2">
1185 <CodeEditor1234 <CodeEditor
1186 value={formState.envText}1235 value={formState.envText}
1187 onChange={(value) => setField("envText", value)}1236 onChange={(value) =>
1237 setStdioTransportField("envText", value)
1238 }
1188 placeholder={1239 placeholder={
1189 '{\n "CHROME_PATH": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"\n}'1240 '{\n "CHROME_PATH": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"\n}'
1190 }1241 }
@@ -1196,14 +1247,18 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 5 unmodified lines
1196 <Field label="Query params (JSON)">1247 <Field label="Query params (JSON)">
1197 <CodeEditor1248 <CodeEditor
1198 value={formState.queryParamsText}1249 value={formState.queryParamsText}
1199 onChange={(value) => setField("queryParamsText", value)}1250 onChange={(value) =>
1251 setRemoteTransportField("queryParamsText", value)
1252 }
1200 placeholder={'{\n "workspace": "demo"\n}'}1253 placeholder={'{\n "workspace": "demo"\n}'}
1201 />1254 />
1202 </Field>1255 </Field>
1203 <Field label="Headers (JSON)">1256 <Field label="Headers (JSON)">
1204 <CodeEditor1257 <CodeEditor
1205 value={formState.headersText}1258 value={formState.headersText}
1206 onChange={(value) => setField("headersText", value)}1259 onChange={(value) =>
1260 setRemoteTransportField("headersText", value)
1261 }
1207 placeholder={'{\n "x-api-key": "..."\n}'}1262 placeholder={'{\n "x-api-key": "..."\n}'}
1208 />1263 />
1209 </Field>1264 </Field>