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
204204 * seed their auth-method editor from it. */
205205 readonly initialAuthHeader?: string;
206206 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;
207212 }>;
208213 /** Legacy full-page edit surface. No host renders this anymore — plugin
209214 * 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
3737 import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys";
3838 import type { McpAuthMethodInput } from "../sdk/types";
3939 import { probeMcpEndpoint, addMcpServer } from "./atoms";
40+import { placementFromHeaderPattern } from "@executor-js/react/lib/auth-placements";
4041 import { McpRemoteIntegrationFields } from "./McpRemoteIntegrationFields";
4142 import { mcpAuthMethodInputFromEditorValue, mcpWireAuthInput } from "./auth-method-config";
4243 import { cloudflareNeedsCodemodeOptOut } from "../sdk/cloudflare-codemode";
@@ -168,6 +169,9 @@ export default function AddMcpIntegration(props: { · 125 unmodified lines
168169 onCancel: () => void;
169170 initialUrl?: string;
170171 initialPreset?: string;
172+ initialAuthHeader?: string;
173+ initialAuthNote?: string;
174+ initialAuthKind?: string;
171175 /** Whether the stdio transport is enabled on the server. */
172176 allowStdio?: boolean;
173177 }) {
@@ -217,28 +221,45 @@ export default function AddMcpIntegration(props: { · 43 unmodified lines
217221 // without OAuth metadata → a bearer-header row; an open server → a no-auth
218222 // row. The user can edit any row or add alternate methods alongside.
219223 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+ }
221236 if (probe.requiresOAuth) {
222− return [
223− {
224− value: { kind: "oauth", authorizationUrl: "", tokenUrl: "", scopes: [] },
225− label: "Detected",
226− },
227− ];
237+ const oauth: AuthMethodSeed = {
238+ value: { kind: "oauth", authorizationUrl: "", tokenUrl: "", scopes: [] },
239+ label: "Detected",
240+ };
241+ // GitHub's MCP server takes a PAT bearer header in clients without
242+ // OAuth; when the registry declared that placement, offer it alongside.
243+ return registryPlacement
244+ ? [oauth, { value: { kind: "apikey", placements: [registryPlacement] } }]
245+ : [oauth];
228246 }
229247 if (probe.requiresAuthentication) {
248+ // The registry's exact placement beats the generic Bearer guess.
230249 return [
231250 {
232251 value: {
233252 kind: "apikey",
234− placements: [{ carrier: "header", name: "Authorization", prefix: "Bearer " }],
253+ placements: [
254+ registryPlacement ?? { carrier: "header", name: "Authorization", prefix: "Bearer " },
255+ ],
235256 },
236257 label: "Detected",
237258 },
238259 ];
239260 }
240261 return [{ value: { kind: "none" }, label: "Detected" }];
241− }, [probe]);
262+ }, [probe, props.initialAuthHeader, props.initialAuthKind]);
242263 const authMethodList = useAuthMethodList(authMethodSeeds);
243264
244265 const remoteIdentity = useIntegrationIdentity({
@@ -460,6 +481,10 @@ export default function AddMcpIntegration(props: { · 215 unmodified lines
460481 />
461482 )}
462483
484+ {probe && props.initialAuthNote ? (
485+ <p className="text-xs leading-relaxed text-muted-foreground">{props.initialAuthNote}</p>
486+ ) : null}
487+
463488 {/* Error (add server). Probe errors show inline on the field. */}
464489 {otherError && (
465490 <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
1717 namespace?: string;
1818 authHeader?: string;
1919 authNote?: string;
20+ authKind?: string;
2021 }) {
2122 useExecutorDocumentTitle("Add integration");
22− const { pluginKey, url, preset, namespace, authHeader, authNote } = props;
23+ const { pluginKey, url, preset, namespace, authHeader, authNote, authKind } = props;
2324 const navigate = useNavigate();
2425 const integrationPlugins = useIntegrationPlugins();
2526 const refreshIntegrations = useAtomRefresh(integrationsOptimisticAtom);
@@ -61,6 +62,7 @@ export function AddIntegrationPage(props: { · 35 unmodified lines
6162 initialNamespace={namespace}
6263 initialAuthHeader={authHeader}
6364 initialAuthNote={authNote}
65+ initialAuthKind={authKind}
6466 onComplete={(slug?: string) => {
6567 trackEvent("integration_added", {
6668 plugin_key: pluginKey,
packages/react/src/pages/integration-browse.tsx+2−0
@@ -434,6 +434,7 @@ export function IntegrationBrowsePage() { · 433 unmodified lines
434434 ...(input.slug ? { namespace: input.slug } : {}),
435435 ...(input.auth?.header ? { authHeader: input.auth.header } : {}),
436436 ...(input.auth?.note ? { authNote: input.auth.note } : {}),
437+ ...(input.auth?.kind ? { authKind: input.auth.kind } : {}),
437438 },
438439 });
439440 },
@@ -489,6 +490,7 @@ export function IntegrationBrowsePage() { · 49 unmodified lines
489490 // header pattern.
490491 if (auth?.header) search.authHeader = auth.header;
491492 if (auth?.note) search.authNote = auth.note;
493+ if (auth?.kind) search.authKind = auth.kind;
492494 void navigate({
493495 to: "/{-$orgSlug}/integrations/add/$pluginKey",
494496 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
1313 // (GraphQL endpoints have no spec document).
1414 authHeader: Schema.optional(Schema.String),
1515 authNote: Schema.optional(Schema.String),
16+ authKind: Schema.optional(Schema.String),
1617 }),
1718 );
1819
@@ -20,7 +21,7 @@ export const Route = createFileRoute("/{-$orgSlug}/integrations/add/$pluginKey") · 1 unmodified line
2021 validateSearch: SearchParams,
2122 component: () => {
2223 const { pluginKey } = Route.useParams();
23− const { url, preset, namespace, authHeader, authNote } = Route.useSearch();
24+ const { url, preset, namespace, authHeader, authNote, authKind } = Route.useSearch();
2425 return (
2526 <AddIntegrationPage
2627 pluginKey={pluginKey}
@@ -29,6 +30,7 @@ export const Route = createFileRoute("/{-$orgSlug}/integrations/add/$pluginKey") · 2 unmodified lines
2930 namespace={namespace}
3031 authHeader={authHeader}
3132 authNote={authNote}
33+ authKind={authKind}
3234 />
3335 );
3436 },

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