History

Handle manual-auth MCP probes (#910)

Rhys Sullivan committed 5 months ago

Commit c252d4b1 parent 972ad8b5 files · +85 −6Browse files

Unified · Split

5 files
  1. packages/plugins/mcp/src/api/group.ts +1 −0
  2. packages/plugins/mcp/src/react/AddMcpSource.tsx +10 −2
  3. packages/plugins/mcp/src/react/McpRemoteSourceFields.tsx +15 −2
  4. packages/plugins/mcp/src/sdk/plugin.test.ts +42 −1
  5. packages/plugins/mcp/src/sdk/plugin.ts +17 −1
packages/plugins/mcp/src/api/group.ts+1−0
@@ -54,6 +54,7 @@ const ProbeEndpointPayload = Schema.Struct({ · 53 unmodified lines
5454
5555 const ProbeEndpointResponse = Schema.Struct({
5656 connected: Schema.Boolean,
57+ requiresAuthentication: Schema.Boolean,
5758 requiresOAuth: Schema.Boolean,
5859 supportsDynamicRegistration: Schema.Boolean,
5960 name: Schema.String,
packages/plugins/mcp/src/react/AddMcpSource.tsx+10−2
@@ -83,6 +83,7 @@ function findPreset(id: string | undefined): McpPreset | undefined { · 82 unmodified lines
8383
8484 type ProbeResult = {
8585 connected: boolean;
86+ requiresAuthentication: boolean;
8687 requiresOAuth: boolean;
8788 supportsDynamicRegistration: boolean;
8889 name: string;
@@ -220,7 +221,9 @@ export default function AddMcpSource(props: { · 131 unmodified lines
220221 const allowedAuthKinds: readonly AuthTemplateEditorKind[] =
221222 probe?.requiresOAuth && probe.supportsDynamicRegistration
222223 ? ["oauth"]
223− : ["none", "apikey", "oauth"];
224+ : probe?.requiresAuthentication
225+ ? ["apikey", "oauth"]
226+ : ["none", "apikey", "oauth"];
224227
225228 const remoteIdentity = useIntegrationIdentity({
226229 fallbackName:
@@ -270,7 +273,12 @@ export default function AddMcpSource(props: { · 43 unmodified lines
270273 setAuthValue(
271274 exit.value.requiresOAuth
272275 ? { 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" },
274282 );
275283 dispatch({ type: "probe-ok", probe: exit.value });
276284 }, [state.url, doProbe]);
packages/plugins/mcp/src/react/McpRemoteSourceFields.tsx+15−2
@@ -25,6 +25,8 @@ export type McpRemoteSourcePreview = { · 24 unmodified lines
2525 readonly name: string;
2626 readonly serverName: string | null;
2727 readonly connected: boolean;
28+ readonly requiresAuthentication: boolean;
29+ readonly requiresOAuth: boolean;
2830 readonly toolCount: number | null;
2931 };
3032
@@ -44,7 +46,11 @@ export function McpRemoteSourceFields(props: { · 13 unmodified lines
4446 ? props.preview.toolCount === null
4547 ? null
4648 : `${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"
4854 : null;
4955
5056 if (props.preview) {
@@ -71,13 +77,20 @@ export function McpRemoteSourceFields(props: { · 20 unmodified lines
7177 >
7278 Connected
7379 </Badge>
74− ) : (
80+ ) : props.preview.requiresOAuth ? (
7581 <Badge
7682 variant="outline"
7783 className="border-amber-500/20 bg-amber-500/10 text-[10px] text-amber-600 dark:text-amber-400"
7884 >
7985 OAuth required
8086 </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>
8194 )}
8295 </CardStackEntryActions>
8396 </CardStackEntry>
packages/plugins/mcp/src/sdk/plugin.test.ts+42−1
@@ -1,5 +1,6 @@
11 import { describe, expect, it } from "@effect/vitest";
22 import { Effect } from "effect";
3+import { HttpServerResponse } from "effect/unstable/http";
34
45 import {
56 AuthTemplateSlug,
@@ -8,7 +9,11 @@ import { · 2 unmodified lines
89 ToolAddress,
910 createExecutor,
1011 } 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";
1217
1318 import { mcpPlugin, userFacingProbeMessage } from "./plugin";
1419 import { extractManifestFromListToolsResult, deriveMcpNamespace, joinToolPath } from "./manifest";
@@ -245,6 +250,42 @@ describe("mcpPlugin", () => { · 230 unmodified lines
245250 yield* Effect.promise(() => config.testDb.close());
246251 }),
247252 );
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+ );
248289 });
249290
250291 // ---------------------------------------------------------------------------
packages/plugins/mcp/src/sdk/plugin.ts+17−1
@@ -123,6 +123,7 @@ const McpProbeEndpointInputSchema = Schema.Struct({ · 122 unmodified lines
123123
124124 const McpProbeEndpointOutputSchema = Schema.Struct({
125125 connected: Schema.Boolean,
126+ requiresAuthentication: Schema.Boolean,
126127 requiresOAuth: Schema.Boolean,
127128 supportsDynamicRegistration: Schema.Boolean,
128129 name: Schema.String,
@@ -544,6 +545,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 415 unmodified lines
544545 if (result.ok && result.manifest) {
545546 return {
546547 connected: true,
548+ requiresAuthentication: false,
547549 requiresOAuth: false,
548550 supportsDynamicRegistration: false,
549551 name: result.manifest.server?.name ?? name,
@@ -577,6 +579,7 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 27 unmodified lines
577579 if (probeResult.ok) {
578580 return {
579581 connected: false,
582+ requiresAuthentication: true,
580583 requiresOAuth: true,
581584 supportsDynamicRegistration: probeResult.oauth.registrationEndpoint != null,
582585 name,
@@ -586,10 +589,23 @@ export const mcpPlugin = definePlugin((options?: McpPluginOptions) => { · 3 unmodified lines
586589 } satisfies McpProbeResult;
587590 }
588591
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+
589605 return yield* new McpConnectionError({
590606 transport: "remote",
591607 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.",
593609 });
594610 }).pipe(
595611 Effect.withSpan("mcp.plugin.probe_endpoint", {