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
41 }41 }
42 },42 },
43 "kind": "graphql"43 "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 }
44 }199 }
45 },200 },
46 "policies": {201 "policies": {
plugins/mcp/react/components.tsx+196−20
@@ -1,4 +1,4 @@
1import { startTransition, useMemo, useState, type ReactNode } from "react";1import { startTransition, useEffect, useMemo, useState, type ReactNode } from "react";
2import type { Source } from "@executor/react";2import type { Source } from "@executor/react";
3import {3import {
4 Result,4 Result,
@@ -14,6 +14,7 @@ import { · 9 unmodified lines
14 Badge,14 Badge,
15 Button,15 Button,
16 IconPencil,16 IconPencil,
17 IconSpinner,
17 Input,18 Input,
18 Label,19 Label,
19 Select,20 Select,
@@ -32,6 +33,7 @@ import { · 12 unmodified lines
32import {33import {
33 type McpConnectInput,34 type McpConnectInput,
34 type McpConnectionAuth,35 type McpConnectionAuth,
36 type McpDiscoverResult,
35 type McpOAuthPopupResult,37 type McpOAuthPopupResult,
36 type McpStartOAuthInput,38 type McpStartOAuthInput,
37} from "@executor/plugin-mcp-shared";39} from "@executor/plugin-mcp-shared";
@@ -47,6 +49,10 @@ import { · 9 unmodified lines
47 parseJsonStringArray,49 parseJsonStringArray,
48 parseJsonStringMap,50 parseJsonStringMap,
49} from "./json";51} from "./json";
52import {
53 buildMcpRemoteConfigKey,
54 mcpDiscoveryRequiresOAuth,
55} from "./oauth-detection";
50 56
51const OAUTH_STORAGE_PREFIX = "executor:mcp-oauth:";57const OAUTH_STORAGE_PREFIX = "executor:mcp-oauth:";
52const OAUTH_TIMEOUT_MS = 2 * 60_000;58const OAUTH_TIMEOUT_MS = 2 * 60_000;
@@ -261,6 +267,10 @@ function McpSourceForm(props: { · 208 unmodified lines
261}) {267}) {
262 const installation = useLocalInstallation();268 const installation = useLocalInstallation();
263 const client = getMcpHttpClient();269 const client = getMcpHttpClient();
270 const discoverSource = useAtomSet(
271 client.mutation("mcp", "discoverSource"),
272 { mode: "promise" },
273 );
264 const startOAuth = useAtomSet(274 const startOAuth = useAtomSet(
265 client.mutation("mcp", "startOAuth"),275 client.mutation("mcp", "startOAuth"),
266 { mode: "promise" },276 { mode: "promise" },
@@ -284,11 +294,58 @@ function McpSourceForm(props: { · 17 unmodified lines
284 const [oauthStatus, setOauthStatus] = useState<"idle" | "pending" | "connected">(294 const [oauthStatus, setOauthStatus] = useState<"idle" | "pending" | "connected">(
285 props.initialValue.auth.kind === "oauth2" ? "connected" : "idle",295 props.initialValue.auth.kind === "oauth2" ? "connected" : "idle",
286 );296 );
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 );
287 const [error, setError] = useState<string | null>(null);318 const [error, setError] = useState<string | null>(null);
288 319
289 const isStdio = transportFields.transport === "stdio";320 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 }
290 329
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" }>> => {
292 if (installation.status !== "ready") {349 if (installation.status !== "ready") {
293 throw new Error("Workspace is still loading.");350 throw new Error("Workspace is still loading.");
294 }351 }
@@ -333,9 +390,93 @@ function McpSourceForm(props: { · 38 unmodified lines
333 390
334 setOauthAuth(result.auth);391 setOauthAuth(result.auth);
335 setOauthStatus("connected");392 setOauthStatus("connected");
393 setOauthConnectionKey(remoteConfigKey);
394 return result.auth;
336 };395 };
337 396
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 => ({
339 name: name.trim(),480 name: name.trim(),
340 endpoint: isStdio ? null : (endpoint.trim() || null),481 endpoint: isStdio ? null : (endpoint.trim() || null),
341 transport:482 transport:
@@ -357,13 +498,37 @@ function McpSourceForm(props: { · 15 unmodified lines
357 : null,498 : null,
358 cwd: isStdio ? transportFields.cwd.trim() || null : null,499 cwd: isStdio ? transportFields.cwd.trim() || null : null,
359 auth:500 auth:
360 authKind === "oauth2"501 authOverride
361 ? oauthAuth ?? (() => {502 ?? (authKind === "oauth2"
503 ? connectedOauthAuth ?? (() => {
362 throw new Error("Complete MCP OAuth before saving.");504 throw new Error("Complete MCP OAuth before saving.");
363 })()505 })()
364 : { kind: "none" },506 : { kind: "none" }),
365 });507 });
366 508
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
367 return (532 return (
368 <div className="space-y-8">533 <div className="space-y-8">
369 <div className="space-y-6 rounded-lg border border-border bg-card p-6 text-sm ring-1 ring-foreground/[0.04]">534 <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
388 setAuthKind("none");553 setAuthKind("none");
389 setOauthAuth(null);554 setOauthAuth(null);
390 setOauthStatus("idle");555 setOauthStatus("idle");
556 setOauthConnectionKey(null);
391 }557 }
392 }}558 }}
393 >559 >
@@ -507,6 +673,7 @@ function McpSourceForm(props: { · 113 unmodified lines
507 if (nextKind !== "oauth2") {673 if (nextKind !== "oauth2") {
508 setOauthAuth(null);674 setOauthAuth(null);
509 setOauthStatus("idle");675 setOauthStatus("idle");
676 setOauthConnectionKey(null);
510 }677 }
511 }}678 }}
512 >679 >
@@ -522,7 +689,9 @@ function McpSourceForm(props: { · 9 unmodified lines
522 <div>689 <div>
523 <div className="text-sm font-medium text-foreground">OAuth</div>690 <div className="text-sm font-medium text-foreground">OAuth</div>
524 <div className="mt-1 text-xs text-muted-foreground">691 <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."}
526 </div>695 </div>
527 </div>696 </div>
528 <Button697 <Button
@@ -560,21 +729,28 @@ function McpSourceForm(props: { · 31 unmodified lines
560 <Button729 <Button
561 onClick={() => {730 onClick={() => {
562 setError(null);731 setError(null);
563 void submitMutation732 void (async () => {
564 .mutateAsync(buildInput())733 try {
565 .catch((cause: unknown) =>734 if (isDerivingSource) {
566 setError(cause instanceof Error ? cause.message : String(cause))735 return;
567 );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 })();
568 }}748 }}
569 disabled={submitMutation.status === "pending"}749 disabled={submitMutation.status === "pending" || isDerivingSource}
750 className={submitButtonClassName}
570 >751 >
571 {submitMutation.status === "pending"752 {isDerivingSource && <IconSpinner className="size-3.5" />}
572 ? props.mode === "create"753 {submitButtonLabel}
573 ? "Creating..."
574 : "Saving..."
575 : props.mode === "create"
576 ? "Create Source"
577 : "Save Changes"}
578 </Button>754 </Button>
579 </div>755 </div>
580 </div>756 </div>
plugins/mcp/react/oauth-detection.test.ts+85−0
@@ -0,0 +1,85 @@
1import {
2 describe,
3 expect,
4 it,
5} from "vitest";
6
7import {
8 buildMcpRemoteConfigKey,
9 mcpDiscoveryRequiresOAuth,
10} from "./oauth-detection";
11
12describe("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 @@
1import type {
2 McpDiscoverResult,
3} from "@executor/plugin-mcp-shared";
4
5import type {
6 McpTransportValue,
7} from "./transport";
8
9export 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
31export 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
13 "include": [13 "include": [
14 "*.ts",14 "*.ts",
15 "*.tsx"15 "*.tsx"
16 ],
17 "exclude": [
18 "*.test.ts",
19 "*.test.tsx"
16 ]20 ]
17}21}
plugins/mcp/sdk/catalog.ts+39−36
@@ -111,33 +111,33 @@ const createMcpCapability = (input: { · 110 unmodified lines
111 const callShapeId =111 const callShapeId =
112 input.operation.inputSchema === undefined112 input.operation.inputSchema === undefined
113 ? input.importer.importSchema(113 ? input.importer.importSchema(
114 {114 {
115 type: "object",115 type: "object",
116 properties: {},116 properties: {},
117 additionalProperties: false,117 additionalProperties: false,
118 },118 },
119 `#/mcp/${input.operation.providerData.toolId}/call`,119 `#/mcp/${input.operation.providerData.toolId}/call`,
120 )120 )
121 : isObjectLikeJsonSchema(input.operation.inputSchema)121 : isObjectLikeJsonSchema(input.operation.inputSchema)
122 ? input.importer.importSchema(122 ? input.importer.importSchema(
123 input.operation.inputSchema,123 input.operation.inputSchema,
124 `#/mcp/${input.operation.providerData.toolId}/call`,124 `#/mcp/${input.operation.providerData.toolId}/call`,
125 input.operation.inputSchema,125 input.operation.inputSchema,
126 )126 )
127 : input.importer.importSchema(127 : input.importer.importSchema(
128 schemaWithMergedDefs(128 schemaWithMergedDefs(
129 {129 {
130 type: "object",130 type: "object",
131 properties: {131 properties: {
132 input: input.operation.inputSchema,132 input: input.operation.inputSchema,
133 },
134 required: ["input"],
135 additionalProperties: false,
136 },133 },
137 input.operation.inputSchema,134 required: ["input"],
138 ),135 additionalProperties: false,
139 `#/mcp/${input.operation.providerData.toolId}/call`,136 },
140 );137 input.operation.inputSchema,
138 ),
139 `#/mcp/${input.operation.providerData.toolId}/call`,
140 );
141 const resultStatusShapeId = input.importer.importSchema(141 const resultStatusShapeId = input.importer.importSchema(
142 { type: "null" },142 { type: "null" },
143 `#/mcp/${input.operation.providerData.toolId}/status`,143 `#/mcp/${input.operation.providerData.toolId}/status`,
@@ -154,22 +154,22 @@ const createMcpCapability = (input: { · 10 unmodified lines
154 input.operation.providerData.description ?? input.operation.description,154 input.operation.providerData.description ?? input.operation.description,
155 })155 })
156 ? {156 ? {
157 docs: docsFrom({157 docs: docsFrom({
158 description:158 description:
159 input.operation.providerData.description ??159 input.operation.providerData.description ??
160 input.operation.description,160 input.operation.description,
161 })!,161 })!,
162 }162 }
163 : {}),163 : {}),
164 ...(outputShapeId164 ...(outputShapeId
165 ? {165 ? {
166 contents: [166 contents: [
167 {167 {
168 mediaType: "application/json",168 mediaType: "application/json",
169 shapeId: outputShapeId,169 shapeId: outputShapeId,
170 },170 },
171 ],171 ],
172 }172 }
173 : {}),173 : {}),
174 synthetic: false,174 synthetic: false,
175 provenance: provenanceFor(175 provenance: provenanceFor(
@@ -238,6 +238,9 @@ const createMcpCapability = (input: { · 62 unmodified lines
238 auth: { kind: "none" },238 auth: { kind: "none" },
239 interaction: {239 interaction: {
240 ...interaction,240 ...interaction,
241 approval: {
242 mayRequire: false,
243 },
241 resume: {244 resume: {
242 supported: mcpResumeSupport(input.operation.providerData.execution),245 supported: mcpResumeSupport(input.operation.providerData.execution),
243 },246 },
plugins/mcp/sdk/index.ts+139−129
@@ -390,7 +390,7 @@ const mcpCatalogOperationFromManifestEntry = (input: { · 389 unmodified lines
390 ?? input.entry.title390 ?? input.entry.title
391 ?? input.entry.toolName,391 ?? input.entry.toolName,
392 description: input.entry.description ?? null,392 description: input.entry.description ?? null,
393 effect: input.entry.annotations?.readOnlyHint === true ? "read" : "write",393 effect: 'read',
394 inputSchema: input.entry.inputSchema,394 inputSchema: input.entry.inputSchema,
395 outputSchema: input.entry.outputSchema,395 outputSchema: input.entry.outputSchema,
396 providerData: {396 providerData: {
@@ -513,10 +513,10 @@ const createPersistedMcpAuthProvider = (input: { · 116 unmodified lines
513 );513 );
514 const refreshToken = currentAuth.refreshTokenRef514 const refreshToken = currentAuth.refreshTokenRef
515 ? await Effect.runPromise(515 ? await Effect.runPromise(
516 resolveSecretMaterial({516 resolveSecretMaterial({
517 ref: currentAuth.refreshTokenRef,517 ref: currentAuth.refreshTokenRef,
518 }),518 }),
519 )519 )
520 : undefined;520 : undefined;
521 521
522 return {522 return {
@@ -604,13 +604,13 @@ const createPersistedMcpAuthProvider = (input: { · 81 unmodified lines
604 currentAuth.authorizationServerUrl === null604 currentAuth.authorizationServerUrl === null
605 ? undefined605 ? undefined
606 : {606 : {
607 resourceMetadataUrl: currentAuth.resourceMetadataUrl ?? undefined,607 resourceMetadataUrl: currentAuth.resourceMetadataUrl ?? undefined,
608 authorizationServerUrl: currentAuth.authorizationServerUrl,608 authorizationServerUrl: currentAuth.authorizationServerUrl,
609 resourceMetadata:609 resourceMetadata:
610 currentAuth.resourceMetadata as OAuthDiscoveryState["resourceMetadata"],610 currentAuth.resourceMetadata as OAuthDiscoveryState["resourceMetadata"],
611 authorizationServerMetadata:611 authorizationServerMetadata:
612 currentAuth.authorizationServerMetadata as OAuthDiscoveryState["authorizationServerMetadata"],612 currentAuth.authorizationServerMetadata as OAuthDiscoveryState["authorizationServerMetadata"],
613 },613 },
614 } satisfies OAuthClientProvider;614 } satisfies OAuthClientProvider;
615 });615 });
616 616
@@ -876,25 +876,25 @@ export const mcpSdkPlugin = ( · 259 unmodified lines
876 const executionContext: ToolExecutionContext | undefined =876 const executionContext: ToolExecutionContext | undefined =
877 input.onElicitation877 input.onElicitation
878 ? {878 ? {
879 path: input.descriptor.path as ToolPath,879 path: input.descriptor.path as ToolPath,
880 sourceKey: input.source.id,
881 metadata: {
880 sourceKey: input.source.id,882 sourceKey: input.source.id,
881 metadata: {883 interaction: input.descriptor.interaction,
882 sourceKey: input.source.id,884 contract: {
883 interaction: input.descriptor.interaction,885 ...(input.descriptor.contract?.inputSchema !== undefined
884 contract: {886 ? { inputSchema: input.descriptor.contract.inputSchema }
885 ...(input.descriptor.contract?.inputSchema !== undefined887 : {}),
886 ? { inputSchema: input.descriptor.contract.inputSchema }888 ...(input.descriptor.contract?.outputSchema !== undefined
887 : {}),889 ? { outputSchema: input.descriptor.contract.outputSchema }
888 ...(input.descriptor.contract?.outputSchema !== undefined890 : {}),
889 ? { outputSchema: input.descriptor.contract.outputSchema }
890 : {}),
891 },
892 pluginKind: input.descriptor.pluginKind,
893 pluginData: input.descriptor.pluginData,
894 },891 },
895 invocation: input.context,892 pluginKind: input.descriptor.pluginKind,
896 onElicitation: input.onElicitation,893 pluginData: input.descriptor.pluginData,
897 }894 },
895 invocation: input.context,
896 onElicitation: input.onElicitation,
897 }
898 : undefined;898 : undefined;
899 const result = yield* Effect.tryPromise({899 const result = yield* Effect.tryPromise({
900 try: async () =>900 try: async () =>
@@ -922,7 +922,12 @@ export const mcpSdkPlugin = ( · 21 unmodified lines
922 outputSchema: McpDiscoverResultSchema,922 outputSchema: McpDiscoverResultSchema,
923 execute: ({ args }) =>923 execute: ({ args }) =>
924 Effect.gen(function* () {924 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 );
926 const discovered = yield* detectMcpSource({931 const discovered = yield* detectMcpSource({
927 normalizedUrl,932 normalizedUrl,
928 headers: probeHeadersFromAuth(args.probeAuth ?? null),933 headers: probeHeadersFromAuth(args.probeAuth ?? null),
@@ -1043,10 +1048,10 @@ export const mcpSdkPlugin = ( · 114 unmodified lines
1043 });1048 });
1044 const refreshTokenRef = exchanged.tokens.refresh_token1049 const refreshTokenRef = exchanged.tokens.refresh_token
1045 ? yield* storeSecretMaterial({1050 ? yield* storeSecretMaterial({
1046 purpose: "oauth_refresh_token",1051 purpose: "oauth_refresh_token",
1047 value: exchanged.tokens.refresh_token,1052 value: exchanged.tokens.refresh_token,
1048 name: "MCP Refresh Token",1053 name: "MCP Refresh Token",
1049 })1054 })
1050 : null;1055 : null;
1051 1056
1052 if (options.oauthSessions.remove) {1057 if (options.oauthSessions.remove) {
@@ -1088,7 +1093,12 @@ export const mcpSdkPlugin = ( · 35 unmodified lines
1088 discoverSource: (input) =>1093 discoverSource: (input) =>
1089 provideRuntime(1094 provideRuntime(
1090 Effect.gen(function* () {1095 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 );
1092 const discovered = yield* detectMcpSource({1102 const discovered = yield* detectMcpSource({
1093 normalizedUrl,1103 normalizedUrl,
1094 headers: probeHeadersFromAuth(input.probeAuth ?? null),1104 headers: probeHeadersFromAuth(input.probeAuth ?? null),
@@ -1109,110 +1119,110 @@ export const mcpSdkPlugin = ( · 14 unmodified lines
1109 startOAuth: (input) =>1119 startOAuth: (input) =>
1110 provideRuntime(1120 provideRuntime(
1111 Effect.gen(function* () {1121 Effect.gen(function* () {
1112 const endpoint = resolveMcpEndpoint({1122 const endpoint = resolveMcpEndpoint({
1113 endpoint: input.endpoint.trim(),1123 endpoint: input.endpoint.trim(),
1114 queryParams: input.queryParams,1124 queryParams: input.queryParams,
1115 });1125 });
1116 const sessionId = `mcp_oauth_${crypto.randomUUID()}`;1126 const sessionId = `mcp_oauth_${crypto.randomUUID()}`;
1117 const started = yield* startMcpOAuthAuthorization({1127 const started = yield* startMcpOAuthAuthorization({
1118 endpoint,1128 endpoint,
1119 redirectUrl: input.redirectUrl,1129 redirectUrl: input.redirectUrl,
1120 state: sessionId,1130 state: sessionId,
1121 });1131 });
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 });
1136 1132
1137 return {1133 yield* options.oauthSessions.put({
1138 sessionId,1134 sessionId,
1139 authorizationUrl: started.authorizationUrl,1135 value: decodeSession({
1140 };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 };
1141 }),1151 }),
1142 ),1152 ),
1143 completeOAuth: (input) =>1153 completeOAuth: (input) =>
1144 provideRuntime(1154 provideRuntime(
1145 Effect.gen(function* () {1155 Effect.gen(function* () {
1146 if (input.error) {1156 if (input.error) {
1147 return yield* runtimeEffectError(1157 return yield* runtimeEffectError(
1148 "plugins/mcp/sdk",1158 "plugins/mcp/sdk",
1149 input.errorDescription || input.error || "MCP OAuth failed",1159 input.errorDescription || input.error || "MCP OAuth failed",
1150 );1160 );
1151 }1161 }
1152 if (!input.code) {1162 if (!input.code) {
1153 return yield* runtimeEffectError(1163 return yield* runtimeEffectError(
1154 "plugins/mcp/sdk",1164 "plugins/mcp/sdk",
1155 "Missing MCP OAuth code.",1165 "Missing MCP OAuth code.",
1156 );1166 );
1157 }1167 }
1158 1168
1159 const session = yield* options.oauthSessions.get(input.state);1169 const session = yield* options.oauthSessions.get(input.state);
1160 if (session === null) {1170 if (session === null) {
1161 return yield* runtimeEffectError(1171 return yield* runtimeEffectError(
1162 "plugins/mcp/sdk",1172 "plugins/mcp/sdk",
1163 `MCP OAuth session not found: ${input.state}`,1173 `MCP OAuth session not found: ${input.state}`,
1164 );1174 );
1165 }1175 }
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;
1184 1176
1185 if (options.oauthSessions.remove) {1177 const exchanged = yield* exchangeMcpOAuthAuthorizationCode({
1186 yield* options.oauthSessions.remove(input.state);1178 session,
1187 }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;
1188 1194
1189 return {1195 if (options.oauthSessions.remove) {
1190 type: "executor:oauth-result",1196 yield* options.oauthSessions.remove(input.state);
1191 ok: true,1197 }
1192 sessionId: input.state,1198
1193 auth: {1199 return {
1194 kind: "oauth2",1200 type: "executor:oauth-result",
1195 redirectUri: session.redirectUrl,1201 ok: true,
1196 accessTokenRef,1202 sessionId: input.state,
1197 refreshTokenRef,1203 auth: {
1198 tokenType: exchanged.tokens.token_type ?? "Bearer",1204 kind: "oauth2",
1199 expiresAt: expiresAtFromTokens(exchanged.tokens),1205 redirectUri: session.redirectUrl,
1200 scope: exchanged.tokens.scope ?? null,1206 accessTokenRef,
1201 resourceMetadataUrl:1207 refreshTokenRef,
1202 exchanged.resourceMetadataUrl ?? session.resourceMetadataUrl,1208 tokenType: exchanged.tokens.token_type ?? "Bearer",
1203 authorizationServerUrl:1209 expiresAt: expiresAtFromTokens(exchanged.tokens),
1204 exchanged.authorizationServerUrl ?? session.authorizationServerUrl,1210 scope: exchanged.tokens.scope ?? null,
1205 resourceMetadata:1211 resourceMetadataUrl:
1206 exchanged.resourceMetadata ?? session.resourceMetadata,1212 exchanged.resourceMetadataUrl ?? session.resourceMetadataUrl,
1207 authorizationServerMetadata:1213 authorizationServerUrl:
1208 exchanged.authorizationServerMetadata1214 exchanged.authorizationServerUrl ?? session.authorizationServerUrl,
1209 ?? session.authorizationServerMetadata,1215 resourceMetadata:
1210 clientInformation:1216 exchanged.resourceMetadata ?? session.resourceMetadata,
1211 exchanged.clientInformation ?? session.clientInformation,1217 authorizationServerMetadata:
1212 },1218 exchanged.authorizationServerMetadata
1213 };1219 ?? session.authorizationServerMetadata,
1220 clientInformation:
1221 exchanged.clientInformation ?? session.clientInformation,
1222 },
1223 };
1214 }),1224 }),
1215 ),1225 ),
1216 };1226 };
1217 },1227 },
1218});1228});
plugins/mcp/shared/index.ts+1−0
@@ -74,6 +74,7 @@ export const McpStartOAuthInputSchema = Schema.Struct({ · 73 unmodified lines
74 74
75export const McpDiscoverInputSchema = Schema.Struct({75export const McpDiscoverInputSchema = Schema.Struct({
76 endpoint: Schema.String,76 endpoint: Schema.String,
77 queryParams: Schema.optional(Schema.NullOr(StringMapSchema)),
77 probeAuth: Schema.optional(Schema.NullOr(SourceProbeAuthSchema)),78 probeAuth: Schema.optional(Schema.NullOr(SourceProbeAuthSchema)),
78});79});
79 80