Unified · Split
| @@ -54,6 +54,7 @@ const ProbeEndpointPayload = Schema.Struct({ · 53 unmodified lines | |||
| 54 | 54 | ||
| 55 | const ProbeEndpointResponse = Schema.Struct({ | 55 | const ProbeEndpointResponse = Schema.Struct({ |
| 56 | connected: Schema.Boolean, | 56 | connected: Schema.Boolean, |
| 57 | requiresAuthentication: Schema.Boolean, | ||
| 57 | requiresOAuth: Schema.Boolean, | 58 | requiresOAuth: Schema.Boolean, |
| 58 | supportsDynamicRegistration: Schema.Boolean, | 59 | supportsDynamicRegistration: Schema.Boolean, |
| 59 | name: Schema.String, | 60 | name: Schema.String, |
| @@ -83,6 +83,7 @@ function findPreset(id: string | undefined): McpPreset | undefined { · 82 unmodified lines | |||
| 83 | 83 | ||
| 84 | type ProbeResult = { | 84 | type ProbeResult = { |
| 85 | connected: boolean; | 85 | connected: boolean; |
| 86 | requiresAuthentication: boolean; | ||
| 86 | requiresOAuth: boolean; | 87 | requiresOAuth: boolean; |
| 87 | supportsDynamicRegistration: boolean; | 88 | supportsDynamicRegistration: boolean; |
| 88 | name: string; | 89 | name: string; |
| @@ -220,7 +221,9 @@ export default function AddMcpSource(props: { · 131 unmodified lines | |||
| 220 | const allowedAuthKinds: readonly AuthTemplateEditorKind[] = | 221 | const allowedAuthKinds: readonly AuthTemplateEditorKind[] = |
| 221 | probe?.requiresOAuth && probe.supportsDynamicRegistration | 222 | probe?.requiresOAuth && probe.supportsDynamicRegistration |
| 222 | ? ["oauth"] | 223 | ? ["oauth"] |
| 223 | : ["none", "apikey", "oauth"]; | 224 | : probe?.requiresAuthentication |
| 225 | ? ["apikey", "oauth"] | ||
| 226 | : ["none", "apikey", "oauth"]; | ||
| 224 | 227 | ||
| 225 | const remoteIdentity = useIntegrationIdentity({ | 228 | const remoteIdentity = useIntegrationIdentity({ |
| 226 | fallbackName: | 229 | fallbackName: |
| @@ -270,7 +273,12 @@ export default function AddMcpSource(props: { · 43 unmodified lines | |||
| 270 | setAuthValue( | 273 | setAuthValue( |
| 271 | exit.value.requiresOAuth | 274 | exit.value.requiresOAuth |
| 272 | ? { kind: "oauth", authorizationUrl: "", tokenUrl: "", scopes: [] } | 275 | ? { kind: "oauth", authorizationUrl: "", tokenUrl: "", scopes: [] } |
| 273 | : { kind: "none" }, | 276 | : exit.value.requiresAuthentication |
| 277 | ? { | ||
| 278 | kind: "apikey", | ||
| 279 | placements: [{ carrier: "header", name: "Authorization", prefix: "Bearer " }], | ||
| 280 | } | ||
| 281 | : { kind: "none" }, | ||
| 274 | ); | 282 | ); |
| 275 | dispatch({ type: "probe-ok", probe: exit.value }); | 283 | dispatch({ type: "probe-ok", probe: exit.value }); |
| 276 | }, [state.url, doProbe]); | 284 | }, [state.url, doProbe]); |
| @@ -25,6 +25,8 @@ export type McpRemoteSourcePreview = { · 24 unmodified lines | |||
| 25 | readonly name: string; | 25 | readonly name: string; |
| 26 | readonly serverName: string | null; | 26 | readonly serverName: string | null; |
| 27 | readonly connected: boolean; | 27 | readonly connected: boolean; |
| 28 | readonly requiresAuthentication: boolean; | ||
| 29 | readonly requiresOAuth: boolean; | ||
| 28 | readonly toolCount: number | null; | 30 | readonly toolCount: number | null; |
| 29 | }; | 31 | }; |
| 30 | 32 | ||
| @@ -44,7 +46,11 @@ export function McpRemoteSourceFields(props: { · 13 unmodified lines | |||
| 44 | ? props.preview.toolCount === null | 46 | ? props.preview.toolCount === null |
| 45 | ? null | 47 | ? null |
| 46 | : `${props.preview.toolCount} tool${props.preview.toolCount !== 1 ? "s" : ""} available` | 48 | : `${props.preview.toolCount} tool${props.preview.toolCount !== 1 ? "s" : ""} available` |
| 47 | : "OAuth required to discover tools" | 49 | : props.preview.requiresOAuth |
| 50 | ? "OAuth required to discover tools" | ||
| 51 | : props.preview.requiresAuthentication | ||
| 52 | ? "Authentication required to discover tools" | ||
| 53 | : "Ready to add" | ||
| 48 | : null; | 54 | : null; |
| 49 | 55 | ||
| 50 | if (props.preview) { | 56 | if (props.preview) { |
| @@ -71,13 +77,20 @@ export function McpRemoteSourceFields(props: { · 20 unmodified lines | |||
| 71 | > | 77 | > |
| 72 | Connected | 78 | Connected |
| 73 | </Badge> | 79 | </Badge> |
| 74 | ) : ( | 80 | ) : props.preview.requiresOAuth ? ( |
| 75 | <Badge | 81 | <Badge |
| 76 | variant="outline" | 82 | variant="outline" |
| 77 | className="border-amber-500/20 bg-amber-500/10 text-[10px] text-amber-600 dark:text-amber-400" | 83 | className="border-amber-500/20 bg-amber-500/10 text-[10px] text-amber-600 dark:text-amber-400" |
| 78 | > | 84 | > |
| 79 | OAuth required | 85 | OAuth required |
| 80 | </Badge> | 86 | </Badge> |
| 87 | ) : ( | ||
| 88 | <Badge | ||
| 89 | variant="outline" | ||
| 90 | className="border-amber-500/20 bg-amber-500/10 text-[10px] text-amber-600 dark:text-amber-400" | ||
| 91 | > | ||
| 92 | Auth required | ||
| 93 | </Badge> | ||
| 81 | )} | 94 | )} |
| 82 | </CardStackEntryActions> | 95 | </CardStackEntryActions> |
| 83 | </CardStackEntry> | 96 | </CardStackEntry> |
| @@ -1,5 +1,6 @@ | |||
| 1 | import { describe, expect, it } from "@effect/vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import { Effect } from "effect"; | 2 | import { Effect } from "effect"; |
| 3 | import { HttpServerResponse } from "effect/unstable/http"; | ||
| 3 | 4 | ||
| 4 | import { | 5 | import { |
| 5 | AuthTemplateSlug, | 6 | AuthTemplateSlug, |
| @@ -8,7 +9,11 @@ import { · 2 unmodified lines | |||
| 8 | ToolAddress, | 9 | ToolAddress, |
| 9 | createExecutor, | 10 | createExecutor, |
| 10 | } from "@executor-js/sdk"; | 11 | } from "@executor-js/sdk"; |
| 11 | import { makeTestConfig, memoryCredentialsPlugin } from "@executor-js/sdk/testing"; | 12 | import { |
| 13 | makeTestConfig, | ||
| 14 | memoryCredentialsPlugin, | ||
| 15 | serveTestHttpApp, | ||
| 16 | } from "@executor-js/sdk/testing"; | ||
| 12 | 17 | ||
| 13 | import { mcpPlugin, userFacingProbeMessage } from "./plugin"; | 18 | import { mcpPlugin, userFacingProbeMessage } from "./plugin"; |
| 14 | import { extractManifestFromListToolsResult, deriveMcpNamespace, joinToolPath } from "./manifest"; | 19 | import { extractManifestFromListToolsResult, deriveMcpNamespace, joinToolPath } from "./manifest"; |
| @@ -245,6 +250,42 @@ describe("mcpPlugin", () => { · 230 unmodified lines | |||
| 245 | yield* Effect.promise(() => config.testDb.close()); | 250 | yield* Effect.promise(() => config.testDb.close()); |
| 246 | }), | 251 | }), |
| 247 | ); | 252 | ); |
| 253 | |||
| 254 | it.effect("probeEndpoint returns manual auth when MCP requires auth without OAuth metadata", () => | ||
| 255 | Effect.scoped( | ||
| 256 | Effect.gen(function* () { | ||
| 257 | const server = yield* serveTestHttpApp((request) => | ||
| 258 | Effect.succeed( | ||
| 259 | (request.url ?? "").includes("/.well-known/") | ||
| 260 | ? HttpServerResponse.text("missing", { status: 404 }) | ||
| 261 | : HttpServerResponse.jsonUnsafe( | ||
| 262 | { | ||
| 263 | jsonrpc: "2.0", | ||
| 264 | id: null, | ||
| 265 | error: { code: -32000, message: "Unauthorized: Valid API key required." }, | ||
| 266 | }, | ||
| 267 | { status: 401, headers: { "www-authenticate": "Bearer" } }, | ||
| 268 | ), | ||
| 269 | ), | ||
| 270 | ); | ||
| 271 | const config = makeTestConfig({ plugins: [mcpPlugin()] as const }); | ||
| 272 | const executor = yield* createExecutor(config); | ||
| 273 | |||
| 274 | const result = yield* executor.mcp.probeEndpoint(server.url("/mcp")); | ||
| 275 | |||
| 276 | expect(result).toMatchObject({ | ||
| 277 | connected: false, | ||
| 278 | requiresAuthentication: true, | ||
| 279 | requiresOAuth: false, | ||
| 280 | supportsDynamicRegistration: false, | ||
| 281 | toolCount: null, | ||
| 282 | }); | ||
| 283 | |||
| 284 | yield* executor.close(); | ||
| 285 | yield* Effect.promise(() => config.testDb.close()); | ||
| 286 | }), | ||
| 287 | ), | ||
| 288 | ); | ||
| 248 | }); | 289 | }); |
| 249 | 290 | ||
| 250 | // --------------------------------------------------------------------------- | 291 | // --------------------------------------------------------------------------- |
| @@ -123,6 +123,7 @@ const McpProbeEndpointInputSchema = Schema.Struct({ · 122 unmodified lines | |||
| 123 | 123 | ||
| 124 | const McpProbeEndpointOutputSchema = Schema.Struct({ | 124 | const McpProbeEndpointOutputSchema = Schema.Struct({ |
| 125 | connected: Schema.Boolean, | 125 | connected: Schema.Boolean, |
| 126 | requiresAuthentication: Schema.Boolean, | ||
| 126 | requiresOAuth: Schema.Boolean, | 127 | requiresOAuth: Schema.Boolean, |
| 127 | supportsDynamicRegistration: Schema.Boolean, | 128 | supportsDynamicRegistration: Schema.Boolean, |
| 128 | name: Schema.String, | 129 | name: Schema.String, |
| @@ -544,6 +545,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 415 unmodified lines | |||
| 544 | if (result.ok && result.manifest) { | 545 | if (result.ok && result.manifest) { |
| 545 | return { | 546 | return { |
| 546 | connected: true, | 547 | connected: true, |
| 548 | requiresAuthentication: false, | ||
| 547 | requiresOAuth: false, | 549 | requiresOAuth: false, |
| 548 | supportsDynamicRegistration: false, | 550 | supportsDynamicRegistration: false, |
| 549 | name: result.manifest.server?.name ?? name, | 551 | name: result.manifest.server?.name ?? name, |
| @@ -577,6 +579,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 27 unmodified lines | |||
| 577 | if (probeResult.ok) { | 579 | if (probeResult.ok) { |
| 578 | return { | 580 | return { |
| 579 | connected: false, | 581 | connected: false, |
| 582 | requiresAuthentication: true, | ||
| 580 | requiresOAuth: true, | 583 | requiresOAuth: true, |
| 581 | supportsDynamicRegistration: probeResult.oauth.registrationEndpoint != null, | 584 | supportsDynamicRegistration: probeResult.oauth.registrationEndpoint != null, |
| 582 | name, | 585 | name, |
| @@ -586,10 +589,23 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 3 unmodified lines | |||
| 586 | } satisfies McpProbeResult; | 589 | } satisfies McpProbeResult; |
| 587 | } | 590 | } |
| 588 | 591 | ||
| 592 | if (shape.requiresAuth) { | ||
| 593 | return { | ||
| 594 | connected: false, | ||
| 595 | requiresAuthentication: true, | ||
| 596 | requiresOAuth: false, | ||
| 597 | supportsDynamicRegistration: false, | ||
| 598 | name, | ||
| 599 | slug, | ||
| 600 | toolCount: null, | ||
| 601 | serverName: null, | ||
| 602 | } satisfies McpProbeResult; | ||
| 603 | } | ||
| 604 | |||
| 589 | return yield* new McpConnectionError({ | 605 | return yield* new McpConnectionError({ |
| 590 | transport: "remote", | 606 | transport: "remote", |
| 591 | message: | 607 | message: |
| 592 | "This server requires authentication, but OAuth metadata wasn't found. Add credentials (Authorization header, query parameter, or API key) below and retry.", | 608 | "This endpoint looks like MCP, but Executor couldn't discover tools from it. Check the URL and try again.", |
| 593 | }); | 609 | }); |
| 594 | }).pipe( | 610 | }).pipe( |
| 595 | Effect.withSpan("mcp.plugin.probe_endpoint", { | 611 | Effect.withSpan("mcp.plugin.probe_endpoint", { |