Unified · Split
| @@ -54,6 +54,7 @@ const ProbeEndpointPayload = Schema.Struct({ · 53 unmodified lines | ||
| 54 | 54 | |
| 55 | 55 | const ProbeEndpointResponse = Schema.Struct({ |
| 56 | 56 | connected: Schema.Boolean, |
| 57 | + requiresAuthentication: Schema.Boolean, | |
| 57 | 58 | requiresOAuth: Schema.Boolean, |
| 58 | 59 | supportsDynamicRegistration: Schema.Boolean, |
| 59 | 60 | name: Schema.String, |
| @@ -83,6 +83,7 @@ function findPreset(id: string | undefined): McpPreset | undefined { · 82 unmodified lines | ||
| 83 | 83 | |
| 84 | 84 | type ProbeResult = { |
| 85 | 85 | connected: boolean; |
| 86 | + requiresAuthentication: boolean; | |
| 86 | 87 | requiresOAuth: boolean; |
| 87 | 88 | supportsDynamicRegistration: boolean; |
| 88 | 89 | name: string; |
| @@ -220,7 +221,9 @@ export default function AddMcpSource(props: { · 131 unmodified lines | ||
| 220 | 221 | const allowedAuthKinds: readonly AuthTemplateEditorKind[] = |
| 221 | 222 | probe?.requiresOAuth && probe.supportsDynamicRegistration |
| 222 | 223 | ? ["oauth"] |
| 223 | − : ["none", "apikey", "oauth"]; | |
| 224 | + : probe?.requiresAuthentication | |
| 225 | + ? ["apikey", "oauth"] | |
| 226 | + : ["none", "apikey", "oauth"]; | |
| 224 | 227 | |
| 225 | 228 | const remoteIdentity = useIntegrationIdentity({ |
| 226 | 229 | fallbackName: |
| @@ -270,7 +273,12 @@ export default function AddMcpSource(props: { · 43 unmodified lines | ||
| 270 | 273 | setAuthValue( |
| 271 | 274 | exit.value.requiresOAuth |
| 272 | 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 | 283 | dispatch({ type: "probe-ok", probe: exit.value }); |
| 276 | 284 | }, [state.url, doProbe]); |
| @@ -25,6 +25,8 @@ export type McpRemoteSourcePreview = { · 24 unmodified lines | ||
| 25 | 25 | readonly name: string; |
| 26 | 26 | readonly serverName: string | null; |
| 27 | 27 | readonly connected: boolean; |
| 28 | + readonly requiresAuthentication: boolean; | |
| 29 | + readonly requiresOAuth: boolean; | |
| 28 | 30 | readonly toolCount: number | null; |
| 29 | 31 | }; |
| 30 | 32 | |
| @@ -44,7 +46,11 @@ export function McpRemoteSourceFields(props: { · 13 unmodified lines | ||
| 44 | 46 | ? props.preview.toolCount === null |
| 45 | 47 | ? null |
| 46 | 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 | 54 | : null; |
| 49 | 55 | |
| 50 | 56 | if (props.preview) { |
| @@ -71,13 +77,20 @@ export function McpRemoteSourceFields(props: { · 20 unmodified lines | ||
| 71 | 77 | > |
| 72 | 78 | Connected |
| 73 | 79 | </Badge> |
| 74 | − ) : ( | |
| 80 | + ) : props.preview.requiresOAuth ? ( | |
| 75 | 81 | <Badge |
| 76 | 82 | variant="outline" |
| 77 | 83 | className="border-amber-500/20 bg-amber-500/10 text-[10px] text-amber-600 dark:text-amber-400" |
| 78 | 84 | > |
| 79 | 85 | OAuth required |
| 80 | 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 | 95 | </CardStackEntryActions> |
| 83 | 96 | </CardStackEntry> |
| @@ -1,5 +1,6 @@ | ||
| 1 | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | 2 | import { Effect } from "effect"; |
| 3 | +import { HttpServerResponse } from "effect/unstable/http"; | |
| 3 | 4 | |
| 4 | 5 | import { |
| 5 | 6 | AuthTemplateSlug, |
| @@ -8,7 +9,11 @@ import { · 2 unmodified lines | ||
| 8 | 9 | ToolAddress, |
| 9 | 10 | createExecutor, |
| 10 | 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 | 18 | import { mcpPlugin, userFacingProbeMessage } from "./plugin"; |
| 14 | 19 | import { extractManifestFromListToolsResult, deriveMcpNamespace, joinToolPath } from "./manifest"; |
| @@ -245,6 +250,42 @@ describe("mcpPlugin", () => { · 230 unmodified lines | ||
| 245 | 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 | 124 | const McpProbeEndpointOutputSchema = Schema.Struct({ |
| 125 | 125 | connected: Schema.Boolean, |
| 126 | + requiresAuthentication: Schema.Boolean, | |
| 126 | 127 | requiresOAuth: Schema.Boolean, |
| 127 | 128 | supportsDynamicRegistration: Schema.Boolean, |
| 128 | 129 | name: Schema.String, |
| @@ -544,6 +545,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 415 unmodified lines | ||
| 544 | 545 | if (result.ok && result.manifest) { |
| 545 | 546 | return { |
| 546 | 547 | connected: true, |
| 548 | + requiresAuthentication: false, | |
| 547 | 549 | requiresOAuth: false, |
| 548 | 550 | supportsDynamicRegistration: false, |
| 549 | 551 | name: result.manifest.server?.name ?? name, |
| @@ -577,6 +579,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 27 unmodified lines | ||
| 577 | 579 | if (probeResult.ok) { |
| 578 | 580 | return { |
| 579 | 581 | connected: false, |
| 582 | + requiresAuthentication: true, | |
| 580 | 583 | requiresOAuth: true, |
| 581 | 584 | supportsDynamicRegistration: probeResult.oauth.registrationEndpoint != null, |
| 582 | 585 | name, |
| @@ -586,10 +589,23 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 3 unmodified lines | ||
| 586 | 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 | 605 | return yield* new McpConnectionError({ |
| 590 | 606 | transport: "remote", |
| 591 | 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 | 610 | }).pipe( |
| 595 | 611 | Effect.withSpan("mcp.plugin.probe_endpoint", { |