Unified · Split
| @@ -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": { |
| @@ -1,4 +1,4 @@ | |||
| 1 | import { startTransition, useMemo, useState, type ReactNode } from "react"; | 1 | import { startTransition, useEffect, useMemo, useState, type ReactNode } from "react"; |
| 2 | import type { Source } from "@executor/react"; | 2 | import type { Source } from "@executor/react"; |
| 3 | import { | 3 | import { |
| 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 | |||
| 32 | import { | 33 | import { |
| 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"; |
| 52 | import { | ||
| 53 | buildMcpRemoteConfigKey, | ||
| 54 | mcpDiscoveryRequiresOAuth, | ||
| 55 | } from "./oauth-detection"; | ||
| 50 | 56 | ||
| 51 | const OAUTH_STORAGE_PREFIX = "executor:mcp-oauth:"; | 57 | const OAUTH_STORAGE_PREFIX = "executor:mcp-oauth:"; |
| 52 | const OAUTH_TIMEOUT_MS = 2 * 60_000; | 58 | const 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 | <Button | 697 | <Button |
| @@ -560,21 +729,28 @@ function McpSourceForm(props: { · 31 unmodified lines | |||
| 560 | <Button | 729 | <Button |
| 561 | onClick={() => { | 730 | onClick={() => { |
| 562 | setError(null); | 731 | setError(null); |
| 563 | void submitMutation | 732 | 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> |
| @@ -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 | }); | ||
| @@ -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"; | ||
| @@ -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 | } |
| @@ -111,33 +111,33 @@ const createMcpCapability = (input: { · 110 unmodified lines | |||
| 111 | const callShapeId = | 111 | const callShapeId = |
| 112 | input.operation.inputSchema === undefined | 112 | 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 | ...(outputShapeId | 164 | ...(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 | }, |
| @@ -390,7 +390,7 @@ const mcpCatalogOperationFromManifestEntry = (input: { · 389 unmodified lines | |||
| 390 | ?? input.entry.title | 390 | ?? 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.refreshTokenRef | 514 | 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 === null | 604 | currentAuth.authorizationServerUrl === null |
| 605 | ? undefined | 605 | ? 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.onElicitation | 877 | 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 !== undefined | 887 | : {}), |
| 886 | ? { inputSchema: input.descriptor.contract.inputSchema } | 888 | ...(input.descriptor.contract?.outputSchema !== undefined |
| 887 | : {}), | 889 | ? { outputSchema: input.descriptor.contract.outputSchema } |
| 888 | ...(input.descriptor.contract?.outputSchema !== undefined | 890 | : {}), |
| 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_token | 1049 | 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.authorizationServerMetadata | 1214 | 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 | }); |
| @@ -74,6 +74,7 @@ export const McpStartOAuthInputSchema = Schema.Struct({ · 73 unmodified lines | |||
| 74 | 74 | ||
| 75 | export const McpDiscoverInputSchema = Schema.Struct({ | 75 | export 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 | ||