Unified · Split
| @@ -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"; |
| 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 | import { getDomain } from "tldts"; | 54 | import { 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 | ||
| 71 | type ConnectFormState = { | 81 | type 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 | ||
| 101 | type ConnectFormState = ConnectFormBase & McpTransportFields; | ||
| 102 | |||
| 98 | type OAuthRequiredInfo = { | 103 | type 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 | ||
| 121 | const transportOptions: ReadonlyArray< | 126 | const 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 | ||
| 125 | const authOptions: ReadonlyArray<ConnectFormState["authKind"]> = [ | 133 | const 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 template | 407 | ("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 | ||
| 426 | const authKindForSourceKind = ( | 426 | const authKindForSourceKind = ( |
| @@ -463,34 +463,44 @@ const buildProbeAuth = ( · 36 unmodified lines | |||
| 463 | 463 | ||
| 464 | const buildConnectPayload = (form: ConnectFormState): ConnectSourcePayload => { | 464 | const 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 = isStdio | 468 | 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: isStdio | 498 | queryParams: parseJsonStringMap("Query params", form.queryParamsText), |
| 485 | ? null | 499 | headers: parseJsonStringMap("Request headers", form.headersText), |
| 486 | : parseJsonStringMap("Query params", form.queryParamsText), | 500 | command: null, |
| 487 | headers: isStdio | 501 | args: null, |
| 488 | ? null | 502 | 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 returned | 986 | ...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 | <SelectInput | 1852 | <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 | <TextInput | 1864 | <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 | mono | 1868 | 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 | <TextInput | 1872 | <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 | mono | 1876 | 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 | <CodeEditor | 1880 | <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 | <CodeEditor | 1894 | <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 | <CodeEditor | 1906 | <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 | <CodeEditor | 1915 | <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> |
| @@ -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 | IconTrash, | 34 | IconTrash, |
| 35 | } from "../components/icons"; | 35 | } from "../components/icons"; |
| 36 | import { cn } from "../lib/utils"; | 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 | import { sourceTemplates, type SourceTemplate } from "./source-templates"; | 47 | import { sourceTemplates, type SourceTemplate } from "./source-templates"; |
| 38 | import { getDomain } from "tldts"; | 48 | import { 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 | ||
| 66 | type TransportValue = "" | "auto" | "streamable-http" | "sse" | "stdio"; | 76 | type SourceFormBase = { |
| 67 | |||
| 68 | type 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 | ||
| 101 | type SourceFormState = SourceFormBase & McpTransportFields; | ||
| 102 | |||
| 100 | const kindOptions: ReadonlyArray<Source["kind"]> = [ | 103 | const 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 | ||
| 108 | const transportOptions: ReadonlyArray<Exclude<TransportValue, "">> = [ | 111 | const 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 | |||
| 343 | const readBindingString = (source: Source, key: string): string => | 346 | const 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 | ||
| 346 | const readBindingTransport = (source: Source): TransportValue => { | 349 | const 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 | ||
| 445 | const formStateFromSource = (source: Source): SourceFormState => ({ | 447 | const 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.auth | 486 | ? 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 | ||
| 498 | const parseJsonStringMap = ( | 510 | const parseJsonStringMap = ( |
| @@ -616,13 +628,14 @@ const buildRequestedSourceStatus = ( · 117 unmodified lines | |||
| 616 | const buildSourcePayload = (state: SourceFormState): CreateSourcePayload => { | 628 | const 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 = isMcpStdio | 631 | 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 | ? null | 686 | 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 | <SelectInput | 1191 | <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 | <TextInput | 1203 | <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 | mono | 1209 | 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 | <TextInput | 1213 | <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 | mono | 1219 | 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 | <CodeEditor | 1223 | <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 | <CodeEditor | 1234 | <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 | <CodeEditor | 1248 | <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 | <CodeEditor | 1257 | <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> |