History

mcp fixes

Rhys Sullivan committed 7 months ago

Commit bf2b2121 parent af3f4f98 files · +654 −185Browse files

Unified · Split

8 files
  1. .executor/executor.jsonc +155 −0
  2. plugins/mcp/react/components.tsx +196 −20
  3. plugins/mcp/react/oauth-detection.test.ts +85 −0
  4. plugins/mcp/react/oauth-detection.ts +35 −0
  5. plugins/mcp/react/tsconfig.json +4 −0
  6. plugins/mcp/sdk/catalog.ts +39 −36
  7. plugins/mcp/sdk/index.ts +139 −129
  8. plugins/mcp/shared/index.ts +1 −0
.executor/executor.jsonc+155−0
@@ -41,6 +41,161 @@ · 40 unmodified lines
4141 }
4242 },
4343 "kind": "graphql"
44+ },
45+ "axiom-mcp": {
46+ "name": "Axiom MCP",
47+ "namespace": "axiom.mcp",
48+ "config": {
49+ "endpoint": "https://mcp.axiom.co/mcp",
50+ "transport": "auto",
51+ "queryParams": null,
52+ "headers": null,
53+ "command": null,
54+ "args": null,
55+ "env": null,
56+ "cwd": null,
57+ "auth": {
58+ "kind": "oauth2",
59+ "redirectUri": "http://localhost:8788/v1/plugins/mcp/oauth/callback",
60+ "accessTokenRef": {
61+ "secretId": "sec_534bd6a9-0bd3-4f66-97ac-ddf22ea2ae82"
62+ },
63+ "refreshTokenRef": {
64+ "secretId": "sec_3e665cbc-2ba9-4d5b-97a4-db811248574f"
65+ },
66+ "tokenType": "bearer",
67+ "expiresAt": 1806390691402,
68+ "scope": "",
69+ "resourceMetadataUrl": null,
70+ "authorizationServerUrl": "https://mcp.axiom.co/",
71+ "resourceMetadata": null,
72+ "authorizationServerMetadata": {
73+ "issuer": "https://mcp.axiom.co",
74+ "authorization_endpoint": "https://mcp.axiom.co/authorize",
75+ "token_endpoint": "https://mcp.axiom.co/token",
76+ "registration_endpoint": "https://mcp.axiom.co/register",
77+ "response_types_supported": [
78+ "code"
79+ ],
80+ "response_modes_supported": [
81+ "query"
82+ ],
83+ "grant_types_supported": [
84+ "authorization_code",
85+ "refresh_token"
86+ ],
87+ "token_endpoint_auth_methods_supported": [
88+ "client_secret_basic",
89+ "client_secret_post",
90+ "none"
91+ ],
92+ "revocation_endpoint": "https://mcp.axiom.co/token",
93+ "code_challenge_methods_supported": [
94+ "plain",
95+ "S256"
96+ ]
97+ },
98+ "clientInformation": {
99+ "redirect_uris": [
100+ "http://localhost:8788/v1/plugins/mcp/oauth/callback"
101+ ],
102+ "token_endpoint_auth_method": "none",
103+ "grant_types": [
104+ "authorization_code",
105+ "refresh_token"
106+ ],
107+ "response_types": [
108+ "code"
109+ ],
110+ "client_name": "Executor Local",
111+ "client_id": "PgIDrWWSMRBtHHsg",
112+ "client_id_issued_at": 1774854684
113+ }
114+ }
115+ },
116+ "kind": "mcp"
117+ },
118+ "mcp-mcp": {
119+ "name": "Mcp MCP",
120+ "namespace": "mcp.mcp",
121+ "kind": "mcp",
122+ "config": {
123+ "endpoint": "https://mcp.linear.app/mcp",
124+ "transport": "auto",
125+ "queryParams": null,
126+ "headers": null,
127+ "command": null,
128+ "args": null,
129+ "env": null,
130+ "cwd": null,
131+ "auth": {
132+ "kind": "oauth2",
133+ "redirectUri": "http://localhost:8788/v1/plugins/mcp/oauth/callback",
134+ "accessTokenRef": {
135+ "secretId": "sec_ae3503a2-f813-4fe4-8882-1ee7b0e86cab"
136+ },
137+ "refreshTokenRef": {
138+ "secretId": "sec_3e453bf7-9343-41e4-a00e-1b81448591f7"
139+ },
140+ "tokenType": "bearer",
141+ "expiresAt": 1775459747141,
142+ "scope": "",
143+ "resourceMetadataUrl": null,
144+ "authorizationServerUrl": "https://mcp.linear.app",
145+ "resourceMetadata": {
146+ "resource": "https://mcp.linear.app",
147+ "authorization_servers": [
148+ "https://mcp.linear.app"
149+ ],
150+ "bearer_methods_supported": [
151+ "header"
152+ ]
153+ },
154+ "authorizationServerMetadata": {
155+ "issuer": "https://mcp.linear.app",
156+ "authorization_endpoint": "https://mcp.linear.app/authorize",
157+ "token_endpoint": "https://mcp.linear.app/token",
158+ "registration_endpoint": "https://mcp.linear.app/register",
159+ "response_types_supported": [
160+ "code"
161+ ],
162+ "response_modes_supported": [
163+ "query"
164+ ],
165+ "grant_types_supported": [
166+ "authorization_code",
167+ "refresh_token"
168+ ],
169+ "token_endpoint_auth_methods_supported": [
170+ "client_secret_basic",
171+ "client_secret_post",
172+ "none"
173+ ],
174+ "revocation_endpoint": "https://mcp.linear.app/token",
175+ "code_challenge_methods_supported": [
176+ "plain",
177+ "S256"
178+ ],
179+ "client_id_metadata_document_supported": false
180+ },
181+ "clientInformation": {
182+ "redirect_uris": [
183+ "http://localhost:8788/v1/plugins/mcp/oauth/callback"
184+ ],
185+ "token_endpoint_auth_method": "none",
186+ "grant_types": [
187+ "authorization_code",
188+ "refresh_token"
189+ ],
190+ "response_types": [
191+ "code"
192+ ],
193+ "client_name": "Executor Local",
194+ "client_id": "xYJ-DOv7vWmXjAsi",
195+ "client_id_issued_at": 1774854942
196+ }
197+ }
198+ }
44199 }
45200 },
46201 "policies": {
plugins/mcp/react/components.tsx+196−20
@@ -1,4 +1,4 @@
1−import { startTransition, useMemo, useState, type ReactNode } from "react";
1+import { startTransition, useEffect, useMemo, useState, type ReactNode } from "react";
22 import type { Source } from "@executor/react";
33 import {
44 Result,
@@ -14,6 +14,7 @@ import { · 9 unmodified lines
1414 Badge,
1515 Button,
1616 IconPencil,
17+ IconSpinner,
1718 Input,
1819 Label,
1920 Select,
@@ -32,6 +33,7 @@ import { · 12 unmodified lines
3233 import {
3334 type McpConnectInput,
3435 type McpConnectionAuth,
36+ type McpDiscoverResult,
3537 type McpOAuthPopupResult,
3638 type McpStartOAuthInput,
3739 } from "@executor/plugin-mcp-shared";
@@ -47,6 +49,10 @@ import { · 9 unmodified lines
4749 parseJsonStringArray,
4850 parseJsonStringMap,
4951 } from "./json";
52+import {
53+ buildMcpRemoteConfigKey,
54+ mcpDiscoveryRequiresOAuth,
55+} from "./oauth-detection";
5056
5157 const OAUTH_STORAGE_PREFIX = "executor:mcp-oauth:";
5258 const OAUTH_TIMEOUT_MS = 2 * 60_000;
@@ -261,6 +267,10 @@ function McpSourceForm(props: { · 208 unmodified lines
261267 }) {
262268 const installation = useLocalInstallation();
263269 const client = getMcpHttpClient();
270+ const discoverSource = useAtomSet(
271+ client.mutation("mcp", "discoverSource"),
272+ { mode: "promise" },
273+ );
264274 const startOAuth = useAtomSet(
265275 client.mutation("mcp", "startOAuth"),
266276 { mode: "promise" },
@@ -284,11 +294,58 @@ function McpSourceForm(props: { · 17 unmodified lines
284294 const [oauthStatus, setOauthStatus] = useState<"idle" | "pending" | "connected">(
285295 props.initialValue.auth.kind === "oauth2" ? "connected" : "idle",
286296 );
297+ const [detectedOauthRequired, setDetectedOauthRequired] = useState(
298+ props.initialValue.auth.kind === "oauth2",
299+ );
300+ const [lastDiscoveryKey, setLastDiscoveryKey] = useState<string | null>(
301+ props.initialValue.auth.kind === "oauth2"
302+ ? buildMcpRemoteConfigKey({
303+ transport: transportFields.transport,
304+ endpoint: props.initialValue.endpoint ?? "",
305+ queryParams: props.initialValue.queryParams,
306+ })
307+ : null,
308+ );
309+ const [oauthConnectionKey, setOauthConnectionKey] = useState<string | null>(
310+ props.initialValue.auth.kind === "oauth2"
311+ ? buildMcpRemoteConfigKey({
312+ transport: transportFields.transport,
313+ endpoint: props.initialValue.endpoint ?? "",
314+ queryParams: props.initialValue.queryParams,
315+ })
316+ : null,
317+ );
287318 const [error, setError] = useState<string | null>(null);
288319
289320 const isStdio = transportFields.transport === "stdio";
321+ const remoteQueryParamsText =
322+ transportFields.transport === "stdio"
323+ ? ""
324+ : transportFields.queryParamsText;
325+ const parsedRemoteQueryParams = (() => {
326+ if (isStdio) {
327+ return null;
328+ }
290329
291− const runOauth = async () => {
330+ try {
331+ return parseJsonStringMap("Query params", remoteQueryParamsText);
332+ } catch {
333+ return null;
334+ }
335+ })();
336+ const remoteConfigKey = (() => {
337+ try {
338+ return buildMcpRemoteConfigKey({
339+ transport: transportFields.transport,
340+ endpoint,
341+ queryParams: parsedRemoteQueryParams,
342+ });
343+ } catch {
344+ return null;
345+ }
346+ })();
347+
348+ const runOauth = async (): Promise<Extract<McpConnectionAuth, { kind: "oauth2" }>> => {
292349 if (installation.status !== "ready") {
293350 throw new Error("Workspace is still loading.");
294351 }
@@ -333,9 +390,93 @@ function McpSourceForm(props: { · 38 unmodified lines
333390
334391 setOauthAuth(result.auth);
335392 setOauthStatus("connected");
393+ setOauthConnectionKey(remoteConfigKey);
394+ return result.auth;
336395 };
337396
338− const buildInput = (): McpConnectInput => ({
397+ const runDiscovery = async (input: {
398+ mode: "auto" | "manual";
399+ }): Promise<McpDiscoverResult> => {
400+ if (installation.status !== "ready") {
401+ throw new Error("Workspace is still loading.");
402+ }
403+ if (isStdio || remoteConfigKey === null) {
404+ setDetectedOauthRequired(false);
405+ setLastDiscoveryKey(null);
406+ return null;
407+ }
408+
409+ const payload = {
410+ endpoint: endpoint.trim(),
411+ queryParams:
412+ parseJsonStringMap("Query params", remoteQueryParamsText),
413+ };
414+
415+ try {
416+ const discovered = await discoverSource({
417+ path: {
418+ workspaceId: installation.data.scopeId,
419+ },
420+ payload,
421+ });
422+ const oauthRequired = mcpDiscoveryRequiresOAuth(discovered);
423+ setDetectedOauthRequired(oauthRequired);
424+ setLastDiscoveryKey(remoteConfigKey);
425+ if (oauthRequired) {
426+ setAuthKind((current) => current === "oauth2" ? current : "oauth2");
427+ }
428+ return discovered;
429+ } catch (cause) {
430+ setDetectedOauthRequired(false);
431+ setLastDiscoveryKey(remoteConfigKey);
432+ if (input.mode === "manual") {
433+ throw cause;
434+ }
435+ return null;
436+ }
437+ };
438+
439+ useEffect(() => {
440+ if (isStdio || remoteConfigKey === null) {
441+ setDetectedOauthRequired(false);
442+ setLastDiscoveryKey(null);
443+ return;
444+ }
445+
446+ if (remoteConfigKey === lastDiscoveryKey) {
447+ return;
448+ }
449+
450+ const timeout = window.setTimeout(() => {
451+ void runDiscovery({ mode: "auto" });
452+ }, 450);
453+
454+ return () => {
455+ window.clearTimeout(timeout);
456+ };
457+ }, [
458+ endpoint,
459+ installation.status,
460+ isStdio,
461+ lastDiscoveryKey,
462+ remoteConfigKey,
463+ remoteQueryParamsText,
464+ ]);
465+
466+ useEffect(() => {
467+ if (oauthConnectionKey === null || remoteConfigKey === oauthConnectionKey) {
468+ return;
469+ }
470+
471+ setOauthAuth(null);
472+ setOauthStatus("idle");
473+ setOauthConnectionKey(null);
474+ }, [oauthConnectionKey, remoteConfigKey]);
475+
476+ const buildInput = (
477+ connectedOauthAuth: Extract<McpConnectionAuth, { kind: "oauth2" }> | null = oauthAuth,
478+ authOverride?: McpConnectionAuth,
479+ ): McpConnectInput => ({
339480 name: name.trim(),
340481 endpoint: isStdio ? null : (endpoint.trim() || null),
341482 transport:
@@ -357,13 +498,37 @@ function McpSourceForm(props: { · 15 unmodified lines
357498 : null,
358499 cwd: isStdio ? transportFields.cwd.trim() || null : null,
359500 auth:
360− authKind === "oauth2"
361− ? oauthAuth ?? (() => {
501+ authOverride
502+ ?? (authKind === "oauth2"
503+ ? connectedOauthAuth ?? (() => {
362504 throw new Error("Complete MCP OAuth before saving.");
363505 })()
364− : { kind: "none" },
506+ : { kind: "none" }),
365507 });
366508
509+ const hasConnectedOauth =
510+ oauthConnectionKey !== null
511+ && oauthConnectionKey === remoteConfigKey
512+ && oauthAuth !== null;
513+ const isDerivingSource = !isStdio && remoteConfigKey !== null && remoteConfigKey !== lastDiscoveryKey;
514+ const shouldCreateWithoutSigningIn = detectedOauthRequired && !hasConnectedOauth;
515+ const submitButtonLabel =
516+ isDerivingSource
517+ ? "Checking source..."
518+ : props.mode === "create" && shouldCreateWithoutSigningIn
519+ ? "Create source without signing in"
520+ : submitMutation.status === "pending"
521+ ? props.mode === "create"
522+ ? "Creating..."
523+ : "Saving..."
524+ : props.mode === "create"
525+ ? "Create Source"
526+ : "Save Changes";
527+ const submitButtonClassName =
528+ props.mode === "create" && shouldCreateWithoutSigningIn
529+ ? "border-amber-500/30 bg-amber-500/12 text-amber-700 hover:bg-amber-500/20 dark:text-amber-300"
530+ : undefined;
531+
367532 return (
368533 <div className="space-y-8">
369534 <div className="space-y-6 rounded-lg border border-border bg-card p-6 text-sm ring-1 ring-foreground/[0.04]">
@@ -388,6 +553,7 @@ function McpSourceForm(props: { · 18 unmodified lines
388553 setAuthKind("none");
389554 setOauthAuth(null);
390555 setOauthStatus("idle");
556+ setOauthConnectionKey(null);
391557 }
392558 }}
393559 >
@@ -507,6 +673,7 @@ function McpSourceForm(props: { · 113 unmodified lines
507673 if (nextKind !== "oauth2") {
508674 setOauthAuth(null);
509675 setOauthStatus("idle");
676+ setOauthConnectionKey(null);
510677 }
511678 }}
512679 >
@@ -522,7 +689,9 @@ function McpSourceForm(props: { · 9 unmodified lines
522689 <div>
523690 <div className="text-sm font-medium text-foreground">OAuth</div>
524691 <div className="mt-1 text-xs text-muted-foreground">
525− Authenticate with the MCP server's built-in OAuth flow.
692+ {detectedOauthRequired
693+ ? "This MCP server requires OAuth before it can be connected."
694+ : "Authenticate with the MCP server's built-in OAuth flow."}
526695 </div>
527696 </div>
528697 <Button
@@ -560,21 +729,28 @@ function McpSourceForm(props: { · 31 unmodified lines
560729 <Button
561730 onClick={() => {
562731 setError(null);
563− void submitMutation
564− .mutateAsync(buildInput())
565− .catch((cause: unknown) =>
566− setError(cause instanceof Error ? cause.message : String(cause))
567− );
732+ void (async () => {
733+ try {
734+ if (isDerivingSource) {
735+ return;
736+ }
737+
738+ await submitMutation.mutateAsync(
739+ buildInput(
740+ hasConnectedOauth ? oauthAuth : null,
741+ shouldCreateWithoutSigningIn ? { kind: "none" } : undefined,
742+ ),
743+ );
744+ } catch (cause: unknown) {
745+ setError(cause instanceof Error ? cause.message : String(cause));
746+ }
747+ })();
568748 }}
569− disabled={submitMutation.status === "pending"}
749+ disabled={submitMutation.status === "pending" || isDerivingSource}
750+ className={submitButtonClassName}
570751 >
571− {submitMutation.status === "pending"
572− ? props.mode === "create"
573− ? "Creating..."
574− : "Saving..."
575− : props.mode === "create"
576− ? "Create Source"
577− : "Save Changes"}
752+ {isDerivingSource && <IconSpinner className="size-3.5" />}
753+ {submitButtonLabel}
578754 </Button>
579755 </div>
580756 </div>
plugins/mcp/react/oauth-detection.test.ts+85−0
@@ -0,0 +1,85 @@
1+import {
2+ describe,
3+ expect,
4+ it,
5+} from "vitest";
6+
7+import {
8+ buildMcpRemoteConfigKey,
9+ mcpDiscoveryRequiresOAuth,
10+} from "./oauth-detection";
11+
12+describe("oauth-detection", () => {
13+ it("builds a stable remote config key with query params", () => {
14+ expect(buildMcpRemoteConfigKey({
15+ transport: "auto",
16+ endpoint: "https://example.com/mcp",
17+ queryParams: {
18+ transport: "streamable-http",
19+ profile: "team",
20+ },
21+ })).toBe("https://example.com/mcp?transport=streamable-http&profile=team");
22+ });
23+
24+ it("ignores stdio transports", () => {
25+ expect(buildMcpRemoteConfigKey({
26+ transport: "stdio",
27+ endpoint: "https://example.com/mcp",
28+ queryParams: null,
29+ })).toBeNull();
30+ });
31+
32+ it("recognizes supported OAuth discovery results", () => {
33+ expect(mcpDiscoveryRequiresOAuth({
34+ detectedKind: "plugin",
35+ confidence: "high",
36+ endpoint: "https://example.com/mcp",
37+ specUrl: null,
38+ name: "Example",
39+ namespace: "example",
40+ transport: "auto",
41+ authInference: {
42+ suggestedKind: "oauth2",
43+ confidence: "high",
44+ supported: true,
45+ reason: "OAuth is required",
46+ headerName: "Authorization",
47+ prefix: "Bearer ",
48+ parameterName: null,
49+ parameterLocation: null,
50+ oauthAuthorizationUrl: "https://example.com/oauth/authorize",
51+ oauthTokenUrl: "https://example.com/oauth/token",
52+ oauthScopes: [],
53+ },
54+ toolCount: null,
55+ warnings: [],
56+ })).toBe(true);
57+ });
58+
59+ it("ignores non-OAuth or unsupported discovery results", () => {
60+ expect(mcpDiscoveryRequiresOAuth({
61+ detectedKind: "plugin",
62+ confidence: "high",
63+ endpoint: "https://example.com/mcp",
64+ specUrl: null,
65+ name: "Example",
66+ namespace: "example",
67+ transport: "auto",
68+ authInference: {
69+ suggestedKind: "basic",
70+ confidence: "medium",
71+ supported: false,
72+ reason: "Basic auth challenge",
73+ headerName: "Authorization",
74+ prefix: "Basic ",
75+ parameterName: null,
76+ parameterLocation: null,
77+ oauthAuthorizationUrl: null,
78+ oauthTokenUrl: null,
79+ oauthScopes: [],
80+ },
81+ toolCount: null,
82+ warnings: [],
83+ })).toBe(false);
84+ });
85+});
plugins/mcp/react/oauth-detection.ts+35−0
@@ -0,0 +1,35 @@
1+import type {
2+ McpDiscoverResult,
3+} from "@executor/plugin-mcp-shared";
4+
5+import type {
6+ McpTransportValue,
7+} from "./transport";
8+
9+export const buildMcpRemoteConfigKey = (input: {
10+ transport: McpTransportValue;
11+ endpoint: string;
12+ queryParams: Readonly<Record<string, string>> | null | undefined;
13+}): string | null => {
14+ if (input.transport === "stdio") {
15+ return null;
16+ }
17+
18+ const trimmedEndpoint = input.endpoint.trim();
19+ if (trimmedEndpoint.length === 0) {
20+ return null;
21+ }
22+
23+ const url = new URL(trimmedEndpoint);
24+ for (const [key, value] of Object.entries(input.queryParams ?? {})) {
25+ url.searchParams.set(key, value);
26+ }
27+
28+ return url.toString();
29+};
30+
31+export const mcpDiscoveryRequiresOAuth = (
32+ result: McpDiscoverResult | null | undefined,
33+): boolean =>
34+ result?.authInference.supported === true
35+ && result.authInference.suggestedKind === "oauth2";
plugins/mcp/react/tsconfig.json+4−0
@@ -13,5 +13,9 @@ · 12 unmodified lines
1313 "include": [
1414 "*.ts",
1515 "*.tsx"
16+ ],
17+ "exclude": [
18+ "*.test.ts",
19+ "*.test.tsx"
1620 ]
1721 }
plugins/mcp/sdk/catalog.ts+39−36
@@ -111,33 +111,33 @@ const createMcpCapability = (input: { · 110 unmodified lines
111111 const callShapeId =
112112 input.operation.inputSchema === undefined
113113 ? input.importer.importSchema(
114− {
115− type: "object",
116− properties: {},
117− additionalProperties: false,
118− },
119− `#/mcp/${input.operation.providerData.toolId}/call`,
120− )
114+ {
115+ type: "object",
116+ properties: {},
117+ additionalProperties: false,
118+ },
119+ `#/mcp/${input.operation.providerData.toolId}/call`,
120+ )
121121 : isObjectLikeJsonSchema(input.operation.inputSchema)
122122 ? input.importer.importSchema(
123− input.operation.inputSchema,
124− `#/mcp/${input.operation.providerData.toolId}/call`,
125− input.operation.inputSchema,
126− )
123+ input.operation.inputSchema,
124+ `#/mcp/${input.operation.providerData.toolId}/call`,
125+ input.operation.inputSchema,
126+ )
127127 : input.importer.importSchema(
128− schemaWithMergedDefs(
129− {
130− type: "object",
131− properties: {
132− input: input.operation.inputSchema,
133− },
134− required: ["input"],
135− additionalProperties: false,
128+ schemaWithMergedDefs(
129+ {
130+ type: "object",
131+ properties: {
132+ input: input.operation.inputSchema,
136133 },
137− input.operation.inputSchema,
138− ),
139− `#/mcp/${input.operation.providerData.toolId}/call`,
140− );
134+ required: ["input"],
135+ additionalProperties: false,
136+ },
137+ input.operation.inputSchema,
138+ ),
139+ `#/mcp/${input.operation.providerData.toolId}/call`,
140+ );
141141 const resultStatusShapeId = input.importer.importSchema(
142142 { type: "null" },
143143 `#/mcp/${input.operation.providerData.toolId}/status`,
@@ -154,22 +154,22 @@ const createMcpCapability = (input: { · 10 unmodified lines
154154 input.operation.providerData.description ?? input.operation.description,
155155 })
156156 ? {
157− docs: docsFrom({
158− description:
159− input.operation.providerData.description ??
160− input.operation.description,
161− })!,
162− }
157+ docs: docsFrom({
158+ description:
159+ input.operation.providerData.description ??
160+ input.operation.description,
161+ })!,
162+ }
163163 : {}),
164164 ...(outputShapeId
165165 ? {
166− contents: [
167− {
168− mediaType: "application/json",
169− shapeId: outputShapeId,
170− },
171− ],
172− }
166+ contents: [
167+ {
168+ mediaType: "application/json",
169+ shapeId: outputShapeId,
170+ },
171+ ],
172+ }
173173 : {}),
174174 synthetic: false,
175175 provenance: provenanceFor(
@@ -238,6 +238,9 @@ const createMcpCapability = (input: { · 62 unmodified lines
238238 auth: { kind: "none" },
239239 interaction: {
240240 ...interaction,
241+ approval: {
242+ mayRequire: false,
243+ },
241244 resume: {
242245 supported: mcpResumeSupport(input.operation.providerData.execution),
243246 },
plugins/mcp/sdk/index.ts+139−129
@@ -390,7 +390,7 @@ const mcpCatalogOperationFromManifestEntry = (input: { · 389 unmodified lines
390390 ?? input.entry.title
391391 ?? input.entry.toolName,
392392 description: input.entry.description ?? null,
393− effect: input.entry.annotations?.readOnlyHint === true ? "read" : "write",
393+ effect: 'read',
394394 inputSchema: input.entry.inputSchema,
395395 outputSchema: input.entry.outputSchema,
396396 providerData: {
@@ -513,10 +513,10 @@ const createPersistedMcpAuthProvider = (input: { · 116 unmodified lines
513513 );
514514 const refreshToken = currentAuth.refreshTokenRef
515515 ? await Effect.runPromise(
516− resolveSecretMaterial({
517− ref: currentAuth.refreshTokenRef,
518− }),
519− )
516+ resolveSecretMaterial({
517+ ref: currentAuth.refreshTokenRef,
518+ }),
519+ )
520520 : undefined;
521521
522522 return {
@@ -604,13 +604,13 @@ const createPersistedMcpAuthProvider = (input: { · 81 unmodified lines
604604 currentAuth.authorizationServerUrl === null
605605 ? undefined
606606 : {
607− resourceMetadataUrl: currentAuth.resourceMetadataUrl ?? undefined,
608− authorizationServerUrl: currentAuth.authorizationServerUrl,
609− resourceMetadata:
610− currentAuth.resourceMetadata as OAuthDiscoveryState["resourceMetadata"],
611− authorizationServerMetadata:
612− currentAuth.authorizationServerMetadata as OAuthDiscoveryState["authorizationServerMetadata"],
613− },
607+ resourceMetadataUrl: currentAuth.resourceMetadataUrl ?? undefined,
608+ authorizationServerUrl: currentAuth.authorizationServerUrl,
609+ resourceMetadata:
610+ currentAuth.resourceMetadata as OAuthDiscoveryState["resourceMetadata"],
611+ authorizationServerMetadata:
612+ currentAuth.authorizationServerMetadata as OAuthDiscoveryState["authorizationServerMetadata"],
613+ },
614614 } satisfies OAuthClientProvider;
615615 });
616616
@@ -876,25 +876,25 @@ export const mcpSdkPlugin = ( · 259 unmodified lines
876876 const executionContext: ToolExecutionContext | undefined =
877877 input.onElicitation
878878 ? {
879− path: input.descriptor.path as ToolPath,
879+ path: input.descriptor.path as ToolPath,
880+ sourceKey: input.source.id,
881+ metadata: {
880882 sourceKey: input.source.id,
881− metadata: {
882− sourceKey: input.source.id,
883− interaction: input.descriptor.interaction,
884− contract: {
885− ...(input.descriptor.contract?.inputSchema !== undefined
886− ? { inputSchema: input.descriptor.contract.inputSchema }
887− : {}),
888− ...(input.descriptor.contract?.outputSchema !== undefined
889− ? { outputSchema: input.descriptor.contract.outputSchema }
890− : {}),
891− },
892− pluginKind: input.descriptor.pluginKind,
893− pluginData: input.descriptor.pluginData,
883+ interaction: input.descriptor.interaction,
884+ contract: {
885+ ...(input.descriptor.contract?.inputSchema !== undefined
886+ ? { inputSchema: input.descriptor.contract.inputSchema }
887+ : {}),
888+ ...(input.descriptor.contract?.outputSchema !== undefined
889+ ? { outputSchema: input.descriptor.contract.outputSchema }
890+ : {}),
894891 },
895− invocation: input.context,
896− onElicitation: input.onElicitation,
897− }
892+ pluginKind: input.descriptor.pluginKind,
893+ pluginData: input.descriptor.pluginData,
894+ },
895+ invocation: input.context,
896+ onElicitation: input.onElicitation,
897+ }
898898 : undefined;
899899 const result = yield* Effect.tryPromise({
900900 try: async () =>
@@ -922,7 +922,12 @@ export const mcpSdkPlugin = ( · 21 unmodified lines
922922 outputSchema: McpDiscoverResultSchema,
923923 execute: ({ args }) =>
924924 Effect.gen(function* () {
925− const normalizedUrl = normalizeSourceDiscoveryUrl(args.endpoint);
925+ const normalizedUrl = normalizeSourceDiscoveryUrl(
926+ resolveMcpEndpoint({
927+ endpoint: args.endpoint.trim(),
928+ queryParams: args.queryParams,
929+ }),
930+ );
926931 const discovered = yield* detectMcpSource({
927932 normalizedUrl,
928933 headers: probeHeadersFromAuth(args.probeAuth ?? null),
@@ -1043,10 +1048,10 @@ export const mcpSdkPlugin = ( · 114 unmodified lines
10431048 });
10441049 const refreshTokenRef = exchanged.tokens.refresh_token
10451050 ? yield* storeSecretMaterial({
1046− purpose: "oauth_refresh_token",
1047− value: exchanged.tokens.refresh_token,
1048− name: "MCP Refresh Token",
1049− })
1051+ purpose: "oauth_refresh_token",
1052+ value: exchanged.tokens.refresh_token,
1053+ name: "MCP Refresh Token",
1054+ })
10501055 : null;
10511056
10521057 if (options.oauthSessions.remove) {
@@ -1088,7 +1093,12 @@ export const mcpSdkPlugin = ( · 35 unmodified lines
10881093 discoverSource: (input) =>
10891094 provideRuntime(
10901095 Effect.gen(function* () {
1091− const normalizedUrl = normalizeSourceDiscoveryUrl(input.endpoint);
1096+ const normalizedUrl = normalizeSourceDiscoveryUrl(
1097+ resolveMcpEndpoint({
1098+ endpoint: input.endpoint.trim(),
1099+ queryParams: input.queryParams,
1100+ }),
1101+ );
10921102 const discovered = yield* detectMcpSource({
10931103 normalizedUrl,
10941104 headers: probeHeadersFromAuth(input.probeAuth ?? null),
@@ -1109,110 +1119,110 @@ export const mcpSdkPlugin = ( · 14 unmodified lines
11091119 startOAuth: (input) =>
11101120 provideRuntime(
11111121 Effect.gen(function* () {
1112− const endpoint = resolveMcpEndpoint({
1113− endpoint: input.endpoint.trim(),
1114− queryParams: input.queryParams,
1115− });
1116− const sessionId = `mcp_oauth_${crypto.randomUUID()}`;
1117− const started = yield* startMcpOAuthAuthorization({
1118− endpoint,
1119− redirectUrl: input.redirectUrl,
1120− state: sessionId,
1121− });
1122−
1123− yield* options.oauthSessions.put({
1124− sessionId,
1125− value: decodeSession({
1126− endpoint,
1127− redirectUrl: input.redirectUrl,
1128− codeVerifier: started.codeVerifier,
1129− resourceMetadataUrl: started.resourceMetadataUrl,
1130− authorizationServerUrl: started.authorizationServerUrl,
1131− resourceMetadata: started.resourceMetadata,
1132− authorizationServerMetadata: started.authorizationServerMetadata,
1133− clientInformation: started.clientInformation,
1134− }),
1135− });
1122+ const endpoint = resolveMcpEndpoint({
1123+ endpoint: input.endpoint.trim(),
1124+ queryParams: input.queryParams,
1125+ });
1126+ const sessionId = `mcp_oauth_${crypto.randomUUID()}`;
1127+ const started = yield* startMcpOAuthAuthorization({
1128+ endpoint,
1129+ redirectUrl: input.redirectUrl,
1130+ state: sessionId,
1131+ });
11361132
1137− return {
1138− sessionId,
1139− authorizationUrl: started.authorizationUrl,
1140− };
1133+ yield* options.oauthSessions.put({
1134+ sessionId,
1135+ value: decodeSession({
1136+ endpoint,
1137+ redirectUrl: input.redirectUrl,
1138+ codeVerifier: started.codeVerifier,
1139+ resourceMetadataUrl: started.resourceMetadataUrl,
1140+ authorizationServerUrl: started.authorizationServerUrl,
1141+ resourceMetadata: started.resourceMetadata,
1142+ authorizationServerMetadata: started.authorizationServerMetadata,
1143+ clientInformation: started.clientInformation,
1144+ }),
1145+ });
1146+
1147+ return {
1148+ sessionId,
1149+ authorizationUrl: started.authorizationUrl,
1150+ };
11411151 }),
11421152 ),
11431153 completeOAuth: (input) =>
11441154 provideRuntime(
11451155 Effect.gen(function* () {
1146− if (input.error) {
1147− return yield* runtimeEffectError(
1148− "plugins/mcp/sdk",
1149− input.errorDescription || input.error || "MCP OAuth failed",
1150− );
1151− }
1152− if (!input.code) {
1153− return yield* runtimeEffectError(
1154− "plugins/mcp/sdk",
1155− "Missing MCP OAuth code.",
1156− );
1157− }
1156+ if (input.error) {
1157+ return yield* runtimeEffectError(
1158+ "plugins/mcp/sdk",
1159+ input.errorDescription || input.error || "MCP OAuth failed",
1160+ );
1161+ }
1162+ if (!input.code) {
1163+ return yield* runtimeEffectError(
1164+ "plugins/mcp/sdk",
1165+ "Missing MCP OAuth code.",
1166+ );
1167+ }
11581168
1159− const session = yield* options.oauthSessions.get(input.state);
1160− if (session === null) {
1161− return yield* runtimeEffectError(
1162− "plugins/mcp/sdk",
1163− `MCP OAuth session not found: ${input.state}`,
1164− );
1165− }
1166−
1167− const exchanged = yield* exchangeMcpOAuthAuthorizationCode({
1168− session,
1169− code: input.code,
1170− });
1171− const storeSecretMaterial = yield* SecretMaterialStorerService;
1172− const accessTokenRef = yield* storeSecretMaterial({
1173− purpose: "oauth_access_token",
1174− value: exchanged.tokens.access_token,
1175− name: "MCP Access Token",
1176− });
1177− const refreshTokenRef = exchanged.tokens.refresh_token
1178− ? yield* storeSecretMaterial({
1179− purpose: "oauth_refresh_token",
1180− value: exchanged.tokens.refresh_token,
1181− name: "MCP Refresh Token",
1182− })
1183− : null;
1169+ const session = yield* options.oauthSessions.get(input.state);
1170+ if (session === null) {
1171+ return yield* runtimeEffectError(
1172+ "plugins/mcp/sdk",
1173+ `MCP OAuth session not found: ${input.state}`,
1174+ );
1175+ }
11841176
1185− if (options.oauthSessions.remove) {
1186− yield* options.oauthSessions.remove(input.state);
1187− }
1177+ const exchanged = yield* exchangeMcpOAuthAuthorizationCode({
1178+ session,
1179+ code: input.code,
1180+ });
1181+ const storeSecretMaterial = yield* SecretMaterialStorerService;
1182+ const accessTokenRef = yield* storeSecretMaterial({
1183+ purpose: "oauth_access_token",
1184+ value: exchanged.tokens.access_token,
1185+ name: "MCP Access Token",
1186+ });
1187+ const refreshTokenRef = exchanged.tokens.refresh_token
1188+ ? yield* storeSecretMaterial({
1189+ purpose: "oauth_refresh_token",
1190+ value: exchanged.tokens.refresh_token,
1191+ name: "MCP Refresh Token",
1192+ })
1193+ : null;
11881194
1189− return {
1190− type: "executor:oauth-result",
1191− ok: true,
1192− sessionId: input.state,
1193− auth: {
1194− kind: "oauth2",
1195− redirectUri: session.redirectUrl,
1196− accessTokenRef,
1197− refreshTokenRef,
1198− tokenType: exchanged.tokens.token_type ?? "Bearer",
1199− expiresAt: expiresAtFromTokens(exchanged.tokens),
1200− scope: exchanged.tokens.scope ?? null,
1201− resourceMetadataUrl:
1202− exchanged.resourceMetadataUrl ?? session.resourceMetadataUrl,
1203− authorizationServerUrl:
1204− exchanged.authorizationServerUrl ?? session.authorizationServerUrl,
1205− resourceMetadata:
1206− exchanged.resourceMetadata ?? session.resourceMetadata,
1207− authorizationServerMetadata:
1208− exchanged.authorizationServerMetadata
1209− ?? session.authorizationServerMetadata,
1210− clientInformation:
1211− exchanged.clientInformation ?? session.clientInformation,
1212− },
1213− };
1195+ if (options.oauthSessions.remove) {
1196+ yield* options.oauthSessions.remove(input.state);
1197+ }
1198+
1199+ return {
1200+ type: "executor:oauth-result",
1201+ ok: true,
1202+ sessionId: input.state,
1203+ auth: {
1204+ kind: "oauth2",
1205+ redirectUri: session.redirectUrl,
1206+ accessTokenRef,
1207+ refreshTokenRef,
1208+ tokenType: exchanged.tokens.token_type ?? "Bearer",
1209+ expiresAt: expiresAtFromTokens(exchanged.tokens),
1210+ scope: exchanged.tokens.scope ?? null,
1211+ resourceMetadataUrl:
1212+ exchanged.resourceMetadataUrl ?? session.resourceMetadataUrl,
1213+ authorizationServerUrl:
1214+ exchanged.authorizationServerUrl ?? session.authorizationServerUrl,
1215+ resourceMetadata:
1216+ exchanged.resourceMetadata ?? session.resourceMetadata,
1217+ authorizationServerMetadata:
1218+ exchanged.authorizationServerMetadata
1219+ ?? session.authorizationServerMetadata,
1220+ clientInformation:
1221+ exchanged.clientInformation ?? session.clientInformation,
1222+ },
1223+ };
12141224 }),
1215− ),
1225+ ),
12161226 };
12171227 },
12181228 });
plugins/mcp/shared/index.ts+1−0
@@ -74,6 +74,7 @@ export const McpStartOAuthInputSchema = Schema.Struct({ · 73 unmodified lines
7474
7575 export const McpDiscoverInputSchema = Schema.Struct({
7676 endpoint: Schema.String,
77+ queryParams: Schema.optional(Schema.NullOr(StringMapSchema)),
7778 probeAuth: Schema.optional(Schema.NullOr(SourceProbeAuthSchema)),
7879 });
7980