History

MCP add flow consumes registry auth facts

The probe stays authoritative when it lands: its OAuth and open-server verdicts are unchanged, but the registry's declared header pattern now replaces the generic Bearer guess on a bare 401, rides alongside detected OAuth (GitHub's MCP takes a PAT bearer in clients without OAuth support), and stands in entirely when there is no probe result — an authless server is a fact the registry already knew.

Rhys Sullivan committed 2 months ago

Commit 5d8aadd1 parent 97c4ef45 files · +47 −11Browse files

Unified · Split

5 files
  1. packages/core/sdk/src/client.ts +5 −0
  2. packages/plugins/mcp/src/react/AddMcpIntegration.tsx +34 −9
  3. packages/react/src/pages/integration-add.tsx +3 −1
  4. packages/react/src/pages/integration-browse.tsx +2 −0
  5. packages/react/src/routes/integrations.add.$pluginKey.tsx +3 −1
packages/core/sdk/src/client.ts+5−0
@@ -204,6 +204,11 @@ export interface IntegrationPlugin { · 203 unmodified lines
204 * seed their auth-method editor from it. */204 * seed their auth-method editor from it. */
205 readonly initialAuthHeader?: string;205 readonly initialAuthHeader?: string;
206 readonly initialAuthNote?: string;206 readonly initialAuthNote?: string;
207 /** Registry-declared credential kind ("none", "oauth", "api_key", …).
208 * Lets a flow whose live probe fails, or whose surface can't be probed,
209 * still declare the right method — an authless MCP server is a fact the
210 * registry already knows. */
211 readonly initialAuthKind?: string;
207 }>;212 }>;
208 /** Legacy full-page edit surface. No host renders this anymore — plugin213 /** Legacy full-page edit surface. No host renders this anymore — plugin
209 * configuration lives in the integration Edit sheet via `editSheet`. */214 * configuration lives in the integration Edit sheet via `editSheet`. */
packages/plugins/mcp/src/react/AddMcpIntegration.tsx+34−9
@@ -37,6 +37,7 @@ import { · 36 unmodified lines
37import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys";37import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys";
38import type { McpAuthMethodInput } from "../sdk/types";38import type { McpAuthMethodInput } from "../sdk/types";
39import { probeMcpEndpoint, addMcpServer } from "./atoms";39import { probeMcpEndpoint, addMcpServer } from "./atoms";
40import { placementFromHeaderPattern } from "@executor-js/react/lib/auth-placements";
40import { McpRemoteIntegrationFields } from "./McpRemoteIntegrationFields";41import { McpRemoteIntegrationFields } from "./McpRemoteIntegrationFields";
41import { mcpAuthMethodInputFromEditorValue, mcpWireAuthInput } from "./auth-method-config";42import { mcpAuthMethodInputFromEditorValue, mcpWireAuthInput } from "./auth-method-config";
42import { cloudflareNeedsCodemodeOptOut } from "../sdk/cloudflare-codemode";43import { cloudflareNeedsCodemodeOptOut } from "../sdk/cloudflare-codemode";
@@ -168,6 +169,9 @@ export default function AddMcpIntegration(props: { · 125 unmodified lines
168 onCancel: () => void;169 onCancel: () => void;
169 initialUrl?: string;170 initialUrl?: string;
170 initialPreset?: string;171 initialPreset?: string;
172 initialAuthHeader?: string;
173 initialAuthNote?: string;
174 initialAuthKind?: string;
171 /** Whether the stdio transport is enabled on the server. */175 /** Whether the stdio transport is enabled on the server. */
172 allowStdio?: boolean;176 allowStdio?: boolean;
173}) {177}) {
@@ -217,28 +221,45 @@ export default function AddMcpIntegration(props: { · 43 unmodified lines
217 // without OAuth metadata → a bearer-header row; an open server → a no-auth221 // without OAuth metadata → a bearer-header row; an open server → a no-auth
218 // row. The user can edit any row or add alternate methods alongside.222 // row. The user can edit any row or add alternate methods alongside.
219 const authMethodSeeds: readonly AuthMethodSeed[] = useMemo(() => {223 const authMethodSeeds: readonly AuthMethodSeed[] = useMemo(() => {
220 if (!probe) return [];224 const registryPlacement = props.initialAuthHeader
225 ? placementFromHeaderPattern(props.initialAuthHeader)
226 : null;
227 if (!probe) {
228 // No probe result (pending, or the server was unreachable from here).
229 // The registry's declared facts still stand: an authless server or a
230 // known header pattern seeds the list the probe would have produced.
231 if (registryPlacement)
232 return [{ value: { kind: "apikey", placements: [registryPlacement] } }];
233 if (props.initialAuthKind === "none") return [{ value: { kind: "none" } }];
234 return [];
235 }
221 if (probe.requiresOAuth) {236 if (probe.requiresOAuth) {
222 return [237 const oauth: AuthMethodSeed = {
223 {238 value: { kind: "oauth", authorizationUrl: "", tokenUrl: "", scopes: [] },
224 value: { kind: "oauth", authorizationUrl: "", tokenUrl: "", scopes: [] },239 label: "Detected",
225 label: "Detected",240 };
226 },241 // GitHub's MCP server takes a PAT bearer header in clients without
227 ];242 // OAuth; when the registry declared that placement, offer it alongside.
243 return registryPlacement
244 ? [oauth, { value: { kind: "apikey", placements: [registryPlacement] } }]
245 : [oauth];
228 }246 }
229 if (probe.requiresAuthentication) {247 if (probe.requiresAuthentication) {
248 // The registry's exact placement beats the generic Bearer guess.
230 return [249 return [
231 {250 {
232 value: {251 value: {
233 kind: "apikey",252 kind: "apikey",
234 placements: [{ carrier: "header", name: "Authorization", prefix: "Bearer " }],253 placements: [
254 registryPlacement ?? { carrier: "header", name: "Authorization", prefix: "Bearer " },
255 ],
235 },256 },
236 label: "Detected",257 label: "Detected",
237 },258 },
238 ];259 ];
239 }260 }
240 return [{ value: { kind: "none" }, label: "Detected" }];261 return [{ value: { kind: "none" }, label: "Detected" }];
241 }, [probe]);262 }, [probe, props.initialAuthHeader, props.initialAuthKind]);
242 const authMethodList = useAuthMethodList(authMethodSeeds);263 const authMethodList = useAuthMethodList(authMethodSeeds);
243 264
244 const remoteIdentity = useIntegrationIdentity({265 const remoteIdentity = useIntegrationIdentity({
@@ -460,6 +481,10 @@ export default function AddMcpIntegration(props: { · 215 unmodified lines
460 />481 />
461 )}482 )}
462 483
484 {probe && props.initialAuthNote ? (
485 <p className="text-xs leading-relaxed text-muted-foreground">{props.initialAuthNote}</p>
486 ) : null}
487
463 {/* Error (add server). Probe errors show inline on the field. */}488 {/* Error (add server). Probe errors show inline on the field. */}
464 {otherError && (489 {otherError && (
465 <div className="space-y-2">490 <div className="space-y-2">
packages/react/src/pages/integration-add.tsx+3−1
@@ -17,9 +17,10 @@ export function AddIntegrationPage(props: { · 16 unmodified lines
17 namespace?: string;17 namespace?: string;
18 authHeader?: string;18 authHeader?: string;
19 authNote?: string;19 authNote?: string;
20 authKind?: string;
20}) {21}) {
21 useExecutorDocumentTitle("Add integration");22 useExecutorDocumentTitle("Add integration");
22 const { pluginKey, url, preset, namespace, authHeader, authNote } = props;23 const { pluginKey, url, preset, namespace, authHeader, authNote, authKind } = props;
23 const navigate = useNavigate();24 const navigate = useNavigate();
24 const integrationPlugins = useIntegrationPlugins();25 const integrationPlugins = useIntegrationPlugins();
25 const refreshIntegrations = useAtomRefresh(integrationsOptimisticAtom);26 const refreshIntegrations = useAtomRefresh(integrationsOptimisticAtom);
@@ -61,6 +62,7 @@ export function AddIntegrationPage(props: { · 35 unmodified lines
61 initialNamespace={namespace}62 initialNamespace={namespace}
62 initialAuthHeader={authHeader}63 initialAuthHeader={authHeader}
63 initialAuthNote={authNote}64 initialAuthNote={authNote}
65 initialAuthKind={authKind}
64 onComplete={(slug?: string) => {66 onComplete={(slug?: string) => {
65 trackEvent("integration_added", {67 trackEvent("integration_added", {
66 plugin_key: pluginKey,68 plugin_key: pluginKey,
packages/react/src/pages/integration-browse.tsx+2−0
@@ -434,6 +434,7 @@ export function IntegrationBrowsePage() { · 433 unmodified lines
434 ...(input.slug ? { namespace: input.slug } : {}),434 ...(input.slug ? { namespace: input.slug } : {}),
435 ...(input.auth?.header ? { authHeader: input.auth.header } : {}),435 ...(input.auth?.header ? { authHeader: input.auth.header } : {}),
436 ...(input.auth?.note ? { authNote: input.auth.note } : {}),436 ...(input.auth?.note ? { authNote: input.auth.note } : {}),
437 ...(input.auth?.kind ? { authKind: input.auth.kind } : {}),
437 },438 },
438 });439 });
439 },440 },
@@ -489,6 +490,7 @@ export function IntegrationBrowsePage() { · 49 unmodified lines
489 // header pattern.490 // header pattern.
490 if (auth?.header) search.authHeader = auth.header;491 if (auth?.header) search.authHeader = auth.header;
491 if (auth?.note) search.authNote = auth.note;492 if (auth?.note) search.authNote = auth.note;
493 if (auth?.kind) search.authKind = auth.kind;
492 void navigate({494 void navigate({
493 to: "/{-$orgSlug}/integrations/add/$pluginKey",495 to: "/{-$orgSlug}/integrations/add/$pluginKey",
494 params: { pluginKey: entry.pluginKey },496 params: { pluginKey: entry.pluginKey },
packages/react/src/routes/integrations.add.$pluginKey.tsx+3−1
@@ -13,6 +13,7 @@ const SearchParams = Schema.toStandardSchemaV1( · 12 unmodified lines
13 // (GraphQL endpoints have no spec document).13 // (GraphQL endpoints have no spec document).
14 authHeader: Schema.optional(Schema.String),14 authHeader: Schema.optional(Schema.String),
15 authNote: Schema.optional(Schema.String),15 authNote: Schema.optional(Schema.String),
16 authKind: Schema.optional(Schema.String),
16 }),17 }),
17);18);
18 19
@@ -20,7 +21,7 @@ export const Route = createFileRoute("/{-$orgSlug}/integrations/add/$pluginKey") · 1 unmodified line
20 validateSearch: SearchParams,21 validateSearch: SearchParams,
21 component: () => {22 component: () => {
22 const { pluginKey } = Route.useParams();23 const { pluginKey } = Route.useParams();
23 const { url, preset, namespace, authHeader, authNote } = Route.useSearch();24 const { url, preset, namespace, authHeader, authNote, authKind } = Route.useSearch();
24 return (25 return (
25 <AddIntegrationPage26 <AddIntegrationPage
26 pluginKey={pluginKey}27 pluginKey={pluginKey}
@@ -29,6 +30,7 @@ export const Route = createFileRoute("/{-$orgSlug}/integrations/add/$pluginKey") · 2 unmodified lines
29 namespace={namespace}30 namespace={namespace}
30 authHeader={authHeader}31 authHeader={authHeader}
31 authNote={authNote}32 authNote={authNote}
33 authKind={authKind}
32 />34 />
33 );35 );
34 },36 },

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t