Unified · Split
| @@ -41,6 +41,16 @@ import { · 40 unmodified lines | ||
| 41 | 41 | sourceTemplates, |
| 42 | 42 | type SourceTemplate, |
| 43 | 43 | } 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"; | |
| 44 | 54 | import { getDomain } from "tldts"; |
| 45 | 55 | |
| 46 | 56 | // --------------------------------------------------------------------------- |
| @@ -68,7 +78,7 @@ type ProbeAuthState = { · 21 unmodified lines | ||
| 68 | 78 | headersText: string; |
| 69 | 79 | }; |
| 70 | 80 | |
| 71 | −type ConnectFormState = { | |
| 81 | +type ConnectFormBase = { | |
| 72 | 82 | kind: "mcp" | "openapi" | "graphql" | "google_discovery"; |
| 73 | 83 | endpoint: string; |
| 74 | 84 | specUrl: string; |
| @@ -77,7 +87,6 @@ type ConnectFormState = { · 2 unmodified lines | ||
| 77 | 87 | discoveryUrl: string; |
| 78 | 88 | name: string; |
| 79 | 89 | namespace: string; |
| 80 | − transport: "" | "auto" | "streamable-http" | "sse" | "stdio"; | |
| 81 | 90 | authKind: "none" | "bearer" | "oauth2"; |
| 82 | 91 | authHeaderName: string; |
| 83 | 92 | authPrefix: string; |
| @@ -87,14 +96,10 @@ type ConnectFormState = { · 3 unmodified lines | ||
| 87 | 96 | workspaceOauthClientId: string; |
| 88 | 97 | oauthClientId: string; |
| 89 | 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 | |
| 101 | +type ConnectFormState = ConnectFormBase & McpTransportFields; | |
| 102 | + | |
| 98 | 103 | type OAuthRequiredInfo = { |
| 99 | 104 | source: Source; |
| 100 | 105 | sessionId: string; |
| @@ -118,9 +123,12 @@ const kindOptions: ReadonlyArray<ConnectFormState["kind"]> = [ · 17 unmodified lines | ||
| 118 | 123 | "google_discovery", |
| 119 | 124 | ]; |
| 120 | 125 | |
| 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 | +]; | |
| 124 | 132 | |
| 125 | 133 | const authOptions: ReadonlyArray<ConnectFormState["authKind"]> = [ |
| 126 | 134 | "none", |
| @@ -316,7 +324,6 @@ const defaultConnectForm = ( · 189 unmodified lines | ||
| 316 | 324 | name: discovery?.name ?? "", |
| 317 | 325 | namespace: |
| 318 | 326 | discovery?.namespace || namespaceFromUrl(discovery?.endpoint ?? ""), |
| 319 | − transport: "", | |
| 320 | 327 | authKind: "none", |
| 321 | 328 | authHeaderName: "Authorization", |
| 322 | 329 | authPrefix: "Bearer ", |
| @@ -326,12 +333,7 @@ const defaultConnectForm = ( · 3 unmodified lines | ||
| 326 | 333 | workspaceOauthClientId: "", |
| 327 | 334 | oauthClientId: "", |
| 328 | 335 | oauthClientSecret: "", |
| 329 | − queryParamsText: "", | |
| 330 | − headersText: "", | |
| 331 | − command: "", | |
| 332 | − argsText: "", | |
| 333 | − envText: "", | |
| 334 | − cwd: "", | |
| 336 | + ...defaultMcpRemoteTransportFields(), | |
| 335 | 337 | }; |
| 336 | 338 | } |
| 337 | 339 | |
| @@ -374,7 +376,6 @@ const defaultConnectForm = ( · 36 unmodified lines | ||
| 374 | 376 | discoveryUrl: googleDiscoveryDefaults?.discoveryUrl ?? "", |
| 375 | 377 | name: discovery.name ?? "", |
| 376 | 378 | namespace: discovery.namespace || namespaceFromUrl(discovery.endpoint), |
| 377 | − transport: kind === "mcp" ? (discovery.transport ?? "auto") : "", | |
| 378 | 379 | authKind, |
| 379 | 380 | authHeaderName, |
| 380 | 381 | authPrefix, |
| @@ -384,12 +385,9 @@ const defaultConnectForm = ( · 3 unmodified lines | ||
| 384 | 385 | workspaceOauthClientId: "", |
| 385 | 386 | oauthClientId: "", |
| 386 | 387 | oauthClientSecret: "", |
| 387 | − queryParamsText: "", | |
| 388 | − headersText: "", | |
| 389 | − command: "", | |
| 390 | − argsText: "", | |
| 391 | − envText: "", | |
| 392 | − cwd: "", | |
| 388 | + ...defaultMcpRemoteTransportFields( | |
| 389 | + kind === "mcp" ? asMcpRemoteTransportValue(discovery.transport) : "", | |
| 390 | + ), | |
| 393 | 391 | }; |
| 394 | 392 | }; |
| 395 | 393 | |
| @@ -409,18 +407,20 @@ const connectFormFromTemplate = ( · 13 unmodified lines | ||
| 409 | 407 | ("service" in template |
| 410 | 408 | ? googleDiscoveryNamespace(template.service) |
| 411 | 409 | : namespaceFromUrl(template.endpoint ?? "")), |
| 412 | − transport: | |
| 413 | − template.kind === "mcp" | |
| 414 | − ? template.connectionType === "command" | |
| 415 | − ? (template.transport ?? "stdio") | |
| 416 | − : (template.transport ?? "auto") | |
| 417 | − : "", | |
| 418 | 410 | authKind: template.kind === "google_discovery" ? "oauth2" : "none", |
| 419 | 411 | 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 | + )), | |
| 424 | 424 | }); |
| 425 | 425 | |
| 426 | 426 | const authKindForSourceKind = ( |
| @@ -463,34 +463,44 @@ const buildProbeAuth = ( · 36 unmodified lines | ||
| 463 | 463 | |
| 464 | 464 | const buildConnectPayload = (form: ConnectFormState): ConnectSourcePayload => { |
| 465 | 465 | 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."); | |
| 478 | 492 | return { |
| 479 | 493 | kind: "mcp", |
| 480 | 494 | endpoint, |
| 481 | 495 | name: trimToNull(form.name), |
| 482 | 496 | 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, | |
| 494 | 504 | }; |
| 495 | 505 | } |
| 496 | 506 | |
| @@ -831,13 +841,42 @@ export function AddSourcePage() { · 334 unmodified lines | ||
| 831 | 841 | text: string; |
| 832 | 842 | } | null>(null); |
| 833 | 843 | |
| 834 | − const setFormField = <K extends keyof ConnectFormState>( | |
| 844 | + const setFormField = <K extends keyof ConnectFormBase>( | |
| 835 | 845 | key: K, |
| 836 | − value: ConnectFormState[K], | |
| 846 | + value: ConnectFormBase[K], | |
| 837 | 847 | ) => { |
| 838 | 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 | 880 | const setSourceKind = (kind: ConnectFormState["kind"]) => { |
| 842 | 881 | setConnectForm((current) => ({ |
| 843 | 882 | ...current, |
| @@ -943,24 +982,32 @@ export function AddSourcePage() { · 99 unmodified lines | ||
| 943 | 982 | |
| 944 | 983 | try { |
| 945 | 984 | 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 | + }; | |
| 952 | 991 | 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 | + }; | |
| 961 | 1005 | } |
| 962 | 1006 | if ("specUrl" in template) { |
| 963 | − form.specUrl = template.specUrl; | |
| 1007 | + form = { | |
| 1008 | + ...form, | |
| 1009 | + specUrl: template.specUrl, | |
| 1010 | + }; | |
| 964 | 1011 | } |
| 965 | 1012 | setConnectForm(form); |
| 966 | 1013 | setPhase("editing"); |
| @@ -1804,12 +1851,7 @@ export function AddSourcePage() { · 837 unmodified lines | ||
| 1804 | 1851 | <Field label="Transport mode"> |
| 1805 | 1852 | <SelectInput |
| 1806 | 1853 | 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)} | |
| 1813 | 1855 | options={transportOptions.map((v) => ({ |
| 1814 | 1856 | value: v, |
| 1815 | 1857 | label: v, |
| @@ -1821,7 +1863,7 @@ export function AddSourcePage() { · 5 unmodified lines | ||
| 1821 | 1863 | <Field label="Command"> |
| 1822 | 1864 | <TextInput |
| 1823 | 1865 | value={connectForm.command} |
| 1824 | − onChange={(v) => setFormField("command", v)} | |
| 1866 | + onChange={(v) => setStdioTransportField("command", v)} | |
| 1825 | 1867 | placeholder="npx" |
| 1826 | 1868 | mono |
| 1827 | 1869 | /> |
| @@ -1829,7 +1871,7 @@ export function AddSourcePage() { · 1 unmodified line | ||
| 1829 | 1871 | <Field label="Working directory (optional)"> |
| 1830 | 1872 | <TextInput |
| 1831 | 1873 | value={connectForm.cwd} |
| 1832 | − onChange={(v) => setFormField("cwd", v)} | |
| 1874 | + onChange={(v) => setStdioTransportField("cwd", v)} | |
| 1833 | 1875 | placeholder="/path/to/project" |
| 1834 | 1876 | mono |
| 1835 | 1877 | /> |
| @@ -1837,7 +1879,9 @@ export function AddSourcePage() { · 1 unmodified line | ||
| 1837 | 1879 | <Field label="Args (JSON)" className="sm:col-span-2"> |
| 1838 | 1880 | <CodeEditor |
| 1839 | 1881 | value={connectForm.argsText} |
| 1840 | − onChange={(v) => setFormField("argsText", v)} | |
| 1882 | + onChange={(v) => | |
| 1883 | + setStdioTransportField("argsText", v) | |
| 1884 | + } | |
| 1841 | 1885 | placeholder={ |
| 1842 | 1886 | '[\n "-y",\n "chrome-devtools-mcp@latest"\n]' |
| 1843 | 1887 | } |
| @@ -1849,7 +1893,7 @@ export function AddSourcePage() { · 5 unmodified lines | ||
| 1849 | 1893 | > |
| 1850 | 1894 | <CodeEditor |
| 1851 | 1895 | value={connectForm.envText} |
| 1852 | − onChange={(v) => setFormField("envText", v)} | |
| 1896 | + onChange={(v) => setStdioTransportField("envText", v)} | |
| 1853 | 1897 | placeholder={ |
| 1854 | 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 | 1905 | <Field label="Query params (JSON)"> |
| 1862 | 1906 | <CodeEditor |
| 1863 | 1907 | value={connectForm.queryParamsText} |
| 1864 | − onChange={(v) => setFormField("queryParamsText", v)} | |
| 1908 | + onChange={(v) => | |
| 1909 | + setRemoteTransportField("queryParamsText", v) | |
| 1910 | + } | |
| 1865 | 1911 | placeholder={'{\n "workspace": "demo"\n}'} |
| 1866 | 1912 | /> |
| 1867 | 1913 | </Field> |
| 1868 | 1914 | <Field label="Headers (JSON)"> |
| 1869 | 1915 | <CodeEditor |
| 1870 | 1916 | value={connectForm.headersText} |
| 1871 | − onChange={(v) => setFormField("headersText", v)} | |
| 1917 | + onChange={(v) => | |
| 1918 | + setRemoteTransportField("headersText", v) | |
| 1919 | + } | |
| 1872 | 1920 | placeholder={'{\n "x-api-key": "..."\n}'} |
| 1873 | 1921 | /> |
| 1874 | 1922 | </Field> |
| @@ -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 | +}; | |
| @@ -34,6 +34,16 @@ import { · 33 unmodified lines | ||
| 34 | 34 | IconTrash, |
| 35 | 35 | } from "../components/icons"; |
| 36 | 36 | 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"; | |
| 37 | 47 | import { sourceTemplates, type SourceTemplate } from "./source-templates"; |
| 38 | 48 | import { getDomain } from "tldts"; |
| 39 | 49 | |
| @@ -63,21 +73,12 @@ const isSourceNotFoundLoadable = (loadable: Loadable<unknown>): boolean => · 23 unmodified lines | ||
| 63 | 73 | loadable.status === "error" && |
| 64 | 74 | loadable.error.message.toLowerCase().includes("source not found"); |
| 65 | 75 | |
| 66 | −type TransportValue = "" | "auto" | "streamable-http" | "sse" | "stdio"; | |
| 67 | − | |
| 68 | −type SourceFormState = { | |
| 76 | +type SourceFormBase = { | |
| 69 | 77 | name: string; |
| 70 | 78 | kind: Source["kind"]; |
| 71 | 79 | endpoint: string; |
| 72 | 80 | namespace: string; |
| 73 | 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 | 82 | specUrl: string; |
| 82 | 83 | service: string; |
| 83 | 84 | version: string; |
| @@ -97,6 +98,8 @@ type SourceFormState = { · 13 unmodified lines | ||
| 97 | 98 | > | null; |
| 98 | 99 | }; |
| 99 | 100 | |
| 101 | +type SourceFormState = SourceFormBase & McpTransportFields; | |
| 102 | + | |
| 100 | 103 | const kindOptions: ReadonlyArray<Source["kind"]> = [ |
| 101 | 104 | "mcp", |
| 102 | 105 | "openapi", |
| @@ -105,7 +108,7 @@ const kindOptions: ReadonlyArray<Source["kind"]> = [ · 2 unmodified lines | ||
| 105 | 108 | "internal", |
| 106 | 109 | ]; |
| 107 | 110 | |
| 108 | −const transportOptions: ReadonlyArray<Exclude<TransportValue, "">> = [ | |
| 111 | +const transportOptions: ReadonlyArray<Exclude<McpTransportValue, "">> = [ | |
| 109 | 112 | "auto", |
| 110 | 113 | "streamable-http", |
| 111 | 114 | "sse", |
| @@ -343,7 +346,7 @@ const readBindingStringArray = ( · 231 unmodified lines | ||
| 343 | 346 | const readBindingString = (source: Source, key: string): string => |
| 344 | 347 | typeof source.binding[key] === "string" ? String(source.binding[key]) : ""; |
| 345 | 348 | |
| 346 | −const readBindingTransport = (source: Source): TransportValue => { | |
| 349 | +const readBindingTransport = (source: Source): McpTransportValue => { | |
| 347 | 350 | const candidate = source.binding.transport; |
| 348 | 351 | return typeof candidate === "string" && |
| 349 | 352 | (candidate === "auto" || |
| @@ -413,19 +416,6 @@ const defaultFormState = (template?: SourceTemplate): SourceFormState => ({ · 63 unmodified lines | ||
| 413 | 416 | : namespaceFromUrl(template.endpoint ?? ""))) |
| 414 | 417 | : "", |
| 415 | 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 | 419 | specUrl: template && "specUrl" in template ? template.specUrl : "", |
| 430 | 420 | service: template && "service" in template ? template.service : "", |
| 431 | 421 | version: template && "version" in template ? template.version : "", |
| @@ -440,6 +430,18 @@ const defaultFormState = (template?: SourceTemplate): SourceFormState => ({ · 8 unmodified lines | ||
| 440 | 430 | oauthRefreshProviderId: "", |
| 441 | 431 | oauthRefreshHandle: "", |
| 442 | 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 | |
| 445 | 447 | const formStateFromSource = (source: Source): SourceFormState => ({ |
| @@ -448,16 +450,6 @@ const formStateFromSource = (source: Source): SourceFormState => ({ · 2 unmodified lines | ||
| 448 | 450 | endpoint: source.endpoint, |
| 449 | 451 | namespace: source.namespace ?? "", |
| 450 | 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 | 453 | specUrl: readBindingString(source, "specUrl"), |
| 462 | 454 | service: readBindingString(source, "service"), |
| 463 | 455 | version: readBindingString(source, "version"), |
| @@ -493,6 +485,26 @@ const formStateFromSource = (source: Source): SourceFormState => ({ · 29 unmodified lines | ||
| 493 | 485 | source.auth.kind === "mcp_oauth" |
| 494 | 486 | ? source.auth |
| 495 | 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 | |
| 498 | 510 | const parseJsonStringMap = ( |
| @@ -616,13 +628,14 @@ const buildRequestedSourceStatus = ( · 117 unmodified lines | ||
| 616 | 628 | const buildSourcePayload = (state: SourceFormState): CreateSourcePayload => { |
| 617 | 629 | const name = state.name.trim(); |
| 618 | 630 | 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(); | |
| 626 | 639 | |
| 627 | 640 | if (!name) { |
| 628 | 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 | 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 | 680 | return { |
| 653 | 681 | ...shared, |
| 654 | 682 | binding: { |
| 655 | 683 | 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, | |
| 674 | 690 | }, |
| 675 | 691 | }; |
| 676 | 692 | } |
| @@ -831,13 +847,42 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 154 unmodified lines | ||
| 831 | 847 | oauthSecretRefTarget !== null && |
| 832 | 848 | expandedOauthSecretRefTarget === oauthSecretRefTarget; |
| 833 | 849 | |
| 834 | − const setField = <K extends keyof SourceFormState>( | |
| 850 | + const setField = <K extends keyof SourceFormBase>( | |
| 835 | 851 | key: K, |
| 836 | − value: SourceFormState[K], | |
| 852 | + value: SourceFormBase[K], | |
| 837 | 853 | ) => { |
| 838 | 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 | 886 | const applyTemplate = (template: SourceTemplate) => { |
| 842 | 887 | setSelectedTemplateId(template.id); |
| 843 | 888 | setFormState((current) => ({ |
| @@ -1145,9 +1190,7 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 301 unmodified lines | ||
| 1145 | 1190 | <Field label="Transport mode"> |
| 1146 | 1191 | <SelectInput |
| 1147 | 1192 | value={formState.transport || "auto"} |
| 1148 | − onChange={(value) => | |
| 1149 | − setField("transport", value as TransportValue) | |
| 1150 | − } | |
| 1193 | + onChange={(value) => setTransport(value as McpTransportValue)} | |
| 1151 | 1194 | options={transportOptions.map((value) => ({ |
| 1152 | 1195 | value, |
| 1153 | 1196 | label: value, |
| @@ -1159,7 +1202,9 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 5 unmodified lines | ||
| 1159 | 1202 | <Field label="Command"> |
| 1160 | 1203 | <TextInput |
| 1161 | 1204 | value={formState.command} |
| 1162 | − onChange={(value) => setField("command", value)} | |
| 1205 | + onChange={(value) => | |
| 1206 | + setStdioTransportField("command", value) | |
| 1207 | + } | |
| 1163 | 1208 | placeholder="npx" |
| 1164 | 1209 | mono |
| 1165 | 1210 | /> |
| @@ -1167,7 +1212,9 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 1 unmodified line | ||
| 1167 | 1212 | <Field label="Working directory (optional)"> |
| 1168 | 1213 | <TextInput |
| 1169 | 1214 | value={formState.cwd} |
| 1170 | − onChange={(value) => setField("cwd", value)} | |
| 1215 | + onChange={(value) => | |
| 1216 | + setStdioTransportField("cwd", value) | |
| 1217 | + } | |
| 1171 | 1218 | placeholder="/path/to/project" |
| 1172 | 1219 | mono |
| 1173 | 1220 | /> |
| @@ -1175,7 +1222,9 @@ function SourceEditor(props: { mode: "create" | "edit"; source?: Source }) { · 1 unmodified line | ||
| 1175 | 1222 | <Field label="Args (JSON)" className="sm:col-span-2"> |
| 1176 | 1223 | <CodeEditor |
| 1177 | 1224 | value={formState.argsText} |
| 1178 | − onChange={(value) => setField("argsText", value)} | |
| 1225 | + onChange={(value) => | |
| 1226 | + setStdioTransportField("argsText", value) | |
| 1227 | + } | |
| 1179 | 1228 | placeholder={ |
| 1180 | 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 | 1233 | <Field label="Environment (JSON)" className="sm:col-span-2"> |
| 1185 | 1234 | <CodeEditor |
| 1186 | 1235 | value={formState.envText} |
| 1187 | − onChange={(value) => setField("envText", value)} | |
| 1236 | + onChange={(value) => | |
| 1237 | + setStdioTransportField("envText", value) | |
| 1238 | + } | |
| 1188 | 1239 | placeholder={ |
| 1189 | 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 | 1247 | <Field label="Query params (JSON)"> |
| 1197 | 1248 | <CodeEditor |
| 1198 | 1249 | value={formState.queryParamsText} |
| 1199 | − onChange={(value) => setField("queryParamsText", value)} | |
| 1250 | + onChange={(value) => | |
| 1251 | + setRemoteTransportField("queryParamsText", value) | |
| 1252 | + } | |
| 1200 | 1253 | placeholder={'{\n "workspace": "demo"\n}'} |
| 1201 | 1254 | /> |
| 1202 | 1255 | </Field> |
| 1203 | 1256 | <Field label="Headers (JSON)"> |
| 1204 | 1257 | <CodeEditor |
| 1205 | 1258 | value={formState.headersText} |
| 1206 | − onChange={(value) => setField("headersText", value)} | |
| 1259 | + onChange={(value) => | |
| 1260 | + setRemoteTransportField("headersText", value) | |
| 1261 | + } | |
| 1207 | 1262 | placeholder={'{\n "x-api-key": "..."\n}'} |
| 1208 | 1263 | /> |
| 1209 | 1264 | </Field> |