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.
Unified · Split
| @@ -204,6 +204,11 @@ export interface IntegrationPlugin { · 203 unmodified lines | ||
| 204 | 204 | * seed their auth-method editor from it. */ |
| 205 | 205 | readonly initialAuthHeader?: string; |
| 206 | 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 | 213 | /** Legacy full-page edit surface. No host renders this anymore — plugin |
| 209 | 214 | * configuration lives in the integration Edit sheet via `editSheet`. */ |
| @@ -37,6 +37,7 @@ import { · 36 unmodified lines | ||
| 37 | 37 | import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys"; |
| 38 | 38 | import type { McpAuthMethodInput } from "../sdk/types"; |
| 39 | 39 | import { probeMcpEndpoint, addMcpServer } from "./atoms"; |
| 40 | +import { placementFromHeaderPattern } from "@executor-js/react/lib/auth-placements"; | |
| 40 | 41 | import { McpRemoteIntegrationFields } from "./McpRemoteIntegrationFields"; |
| 41 | 42 | import { mcpAuthMethodInputFromEditorValue, mcpWireAuthInput } from "./auth-method-config"; |
| 42 | 43 | import { cloudflareNeedsCodemodeOptOut } from "../sdk/cloudflare-codemode"; |
| @@ -168,6 +169,9 @@ export default function AddMcpIntegration(props: { · 125 unmodified lines | ||
| 168 | 169 | onCancel: () => void; |
| 169 | 170 | initialUrl?: string; |
| 170 | 171 | initialPreset?: string; |
| 172 | + initialAuthHeader?: string; | |
| 173 | + initialAuthNote?: string; | |
| 174 | + initialAuthKind?: string; | |
| 171 | 175 | /** Whether the stdio transport is enabled on the server. */ |
| 172 | 176 | allowStdio?: boolean; |
| 173 | 177 | }) { |
| @@ -217,28 +221,45 @@ export default function AddMcpIntegration(props: { · 43 unmodified lines | ||
| 217 | 221 | // without OAuth metadata → a bearer-header row; an open server → a no-auth |
| 218 | 222 | // row. The user can edit any row or add alternate methods alongside. |
| 219 | 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 | 236 | 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]; | |
| 228 | 246 | } |
| 229 | 247 | if (probe.requiresAuthentication) { |
| 248 | + // The registry's exact placement beats the generic Bearer guess. | |
| 230 | 249 | return [ |
| 231 | 250 | { |
| 232 | 251 | value: { |
| 233 | 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 | 257 | label: "Detected", |
| 237 | 258 | }, |
| 238 | 259 | ]; |
| 239 | 260 | } |
| 240 | 261 | return [{ value: { kind: "none" }, label: "Detected" }]; |
| 241 | − }, [probe]); | |
| 262 | + }, [probe, props.initialAuthHeader, props.initialAuthKind]); | |
| 242 | 263 | const authMethodList = useAuthMethodList(authMethodSeeds); |
| 243 | 264 | |
| 244 | 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 | 488 | {/* Error (add server). Probe errors show inline on the field. */} |
| 464 | 489 | {otherError && ( |
| 465 | 490 | <div className="space-y-2"> |
| @@ -17,9 +17,10 @@ export function AddIntegrationPage(props: { · 16 unmodified lines | ||
| 17 | 17 | namespace?: string; |
| 18 | 18 | authHeader?: string; |
| 19 | 19 | authNote?: string; |
| 20 | + authKind?: string; | |
| 20 | 21 | }) { |
| 21 | 22 | useExecutorDocumentTitle("Add integration"); |
| 22 | − const { pluginKey, url, preset, namespace, authHeader, authNote } = props; | |
| 23 | + const { pluginKey, url, preset, namespace, authHeader, authNote, authKind } = props; | |
| 23 | 24 | const navigate = useNavigate(); |
| 24 | 25 | const integrationPlugins = useIntegrationPlugins(); |
| 25 | 26 | const refreshIntegrations = useAtomRefresh(integrationsOptimisticAtom); |
| @@ -61,6 +62,7 @@ export function AddIntegrationPage(props: { · 35 unmodified lines | ||
| 61 | 62 | initialNamespace={namespace} |
| 62 | 63 | initialAuthHeader={authHeader} |
| 63 | 64 | initialAuthNote={authNote} |
| 65 | + initialAuthKind={authKind} | |
| 64 | 66 | onComplete={(slug?: string) => { |
| 65 | 67 | trackEvent("integration_added", { |
| 66 | 68 | plugin_key: pluginKey, |
| @@ -434,6 +434,7 @@ export function IntegrationBrowsePage() { · 433 unmodified lines | ||
| 434 | 434 | ...(input.slug ? { namespace: input.slug } : {}), |
| 435 | 435 | ...(input.auth?.header ? { authHeader: input.auth.header } : {}), |
| 436 | 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 | 490 | // header pattern. |
| 490 | 491 | if (auth?.header) search.authHeader = auth.header; |
| 491 | 492 | if (auth?.note) search.authNote = auth.note; |
| 493 | + if (auth?.kind) search.authKind = auth.kind; | |
| 492 | 494 | void navigate({ |
| 493 | 495 | to: "/{-$orgSlug}/integrations/add/$pluginKey", |
| 494 | 496 | params: { pluginKey: entry.pluginKey }, |
| @@ -13,6 +13,7 @@ const SearchParams = Schema.toStandardSchemaV1( · 12 unmodified lines | ||
| 13 | 13 | // (GraphQL endpoints have no spec document). |
| 14 | 14 | authHeader: Schema.optional(Schema.String), |
| 15 | 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 | 21 | validateSearch: SearchParams, |
| 21 | 22 | component: () => { |
| 22 | 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 | 25 | return ( |
| 25 | 26 | <AddIntegrationPage |
| 26 | 27 | pluginKey={pluginKey} |
| @@ -29,6 +30,7 @@ export const Route = createFileRoute("/{-$orgSlug}/integrations/add/$pluginKey") · 2 unmodified lines | ||
| 29 | 30 | namespace={namespace} |
| 30 | 31 | authHeader={authHeader} |
| 31 | 32 | authNote={authNote} |
| 33 | + authKind={authKind} | |
| 32 | 34 | /> |
| 33 | 35 | ); |
| 34 | 36 | }, |