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 | * 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 — plugin | 213 | /** 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`. */ |
| @@ -37,6 +37,7 @@ import { · 36 unmodified lines | |||
| 37 | import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys"; | 37 | import { integrationWriteKeys } from "@executor-js/react/api/reactivity-keys"; |
| 38 | import type { McpAuthMethodInput } from "../sdk/types"; | 38 | import type { McpAuthMethodInput } from "../sdk/types"; |
| 39 | import { probeMcpEndpoint, addMcpServer } from "./atoms"; | 39 | import { probeMcpEndpoint, addMcpServer } from "./atoms"; |
| 40 | import { placementFromHeaderPattern } from "@executor-js/react/lib/auth-placements"; | ||
| 40 | import { McpRemoteIntegrationFields } from "./McpRemoteIntegrationFields"; | 41 | import { McpRemoteIntegrationFields } from "./McpRemoteIntegrationFields"; |
| 41 | import { mcpAuthMethodInputFromEditorValue, mcpWireAuthInput } from "./auth-method-config"; | 42 | import { mcpAuthMethodInputFromEditorValue, mcpWireAuthInput } from "./auth-method-config"; |
| 42 | import { cloudflareNeedsCodemodeOptOut } from "../sdk/cloudflare-codemode"; | 43 | import { 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-auth | 221 | // 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"> |
| @@ -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, |
| @@ -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 }, |
| @@ -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 | <AddIntegrationPage | 26 | <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 | }, |