181 lines · 7.2 KB · TypeScript
RawDownload
// ---------------------------------------------------------------------------// MCP ↔ generic auth-method converters — a thin oauth adapter over the shared// codec (`@executor-js/react/lib/shared-auth-method-codec`). The apikey/none// paths (multi-placement, multi-variable) live in the shared codec; MCP only// contributes its oauth flavor: endpoint-less methods whose provider metadata// is discovered at connect time (`discoveryUrl` = the MCP endpoint), with an// optional declared scope override for servers whose metadata omits scopes.// --------------------------------------------------------------------------- import { AuthTemplateSlug } from "@executor-js/sdk/shared";import type { AuthTemplateEditorValue } from "@executor-js/react/components/auth-template-editor";import type { AuthMethodSeed } from "@executor-js/react/components/auth-method-list-editor";import type { AuthMethod, Placement } from "@executor-js/react/lib/auth-placements";import {  authMethodFromSharedTemplate,  editorValueFromSharedMethod,  sharedMethodInputFromEditorValue,  wirePlacementsFromEditor,} from "@executor-js/react/lib/shared-auth-method-codec"; import { wireAuthInputFromShared } from "@executor-js/react/lib/shared-auth-method-codec";import type {  McpAuthMethod,  McpAuthMethodInput,  McpCanonicalAuthMethodInput,  McpStdioEnvMethod,} from "../sdk/types"; /** Stdio env method → generic hub `AuthMethod`: one `env`-carrier placement per *  declared var, so the account form collects one secret per env var. Mirrors *  the server's `describeMcpAuthMethods`. */const stdioEnvAuthMethod = (method: McpStdioEnvMethod): AuthMethod => ({  id: method.slug,  label: "Environment variables",  kind: "apikey",  source: "spec",  template: AuthTemplateSlug.make(method.slug),  placements: method.vars.map((name) => ({ carrier: "env", name, prefix: "", variable: name })),}); /** Stdio env method → editor value (apikey over env placements). */const stdioEnvEditorValue = (method: McpStdioEnvMethod): AuthTemplateEditorValue => ({  kind: "apikey",  placements: method.vars.map((name) => ({ carrier: "env", name, prefix: "", variable: name })),}); /** Serialize a canonical method into the wire input union (apikey → the *  request-shaped dialect; none/oauth2 pass through). */export const mcpWireAuthInput = (  method: McpAuthMethod | McpCanonicalAuthMethodInput,): McpAuthMethodInput => wireAuthInputFromShared(method) as McpAuthMethodInput; const oauthAuthMethod = (  slug: string,  endpoint: string,  scopes: readonly string[] | undefined,): AuthMethod => ({  id: slug,  label: "OAuth",  kind: "oauth",  source: slug.startsWith("custom_") ? "custom" : "spec",  template: AuthTemplateSlug.make(slug),  placements: [],  oauth: {    discoveryUrl: endpoint,    ...(scopes !== undefined ? { scopes } : {}),    supportsDynamicRegistration: true,  },}); /** Convert a generic editor value into one MCP auth-method input (no slug — *  the backend assigns carrier-derived slugs). An apikey value keeps every *  named placement (headers and query params mix freely); one with no usable *  placement falls back to `none`. */export function mcpAuthMethodInputFromEditorValue(  value: AuthTemplateEditorValue,): McpCanonicalAuthMethodInput {  if (value.kind === "oauth") {    const [firstScope, ...remainingScopes] = value.scopes;    return {      kind: "oauth2",      ...(firstScope !== undefined ? { scopes: [firstScope, ...remainingScopes] } : {}),    };  }  return (sharedMethodInputFromEditorValue(value) ?? {    kind: "none",  }) as McpCanonicalAuthMethodInput;} /** Convert one stored MCP method into the generic editor value. */export function editorValueFromMcpAuthMethod(method: McpAuthMethod): AuthTemplateEditorValue {  if (method.kind === "oauth2") {    return {      kind: "oauth",      authorizationUrl: "",      tokenUrl: "",      scopes: method.scopes ?? [],    };  }  if (method.kind === "stdio_env") return stdioEnvEditorValue(method);  return editorValueFromSharedMethod(method);} /** Project the stored methods into the generic `AuthMethod[]` the hub renders. *  Mirrors the server's `describeMcpAuthMethods`; `custom_` slugs mark *  user-created methods (removable from the hub). `endpoint` feeds the oauth *  method's probe-at-connect `discoveryUrl`. */export function authMethodsFromConfig(  methods: readonly McpAuthMethod[],  endpoint: string,): AuthMethod[] {  return methods.map((method: McpAuthMethod): AuthMethod => {    if (method.kind === "oauth2") return oauthAuthMethod(method.slug, endpoint, method.scopes);    if (method.kind === "stdio_env") return stdioEnvAuthMethod(method);    return authMethodFromSharedTemplate(method);  });} /** Build the MCP method input for a custom method from generic placements — *  ONE method carrying every named placement (header + query mix in a single *  method; each placement renders from its own input variable, or shares one). *  Empty when no placement is usable. */export function mcpAuthMethodInputsFromPlacements(  placements: readonly Placement[],): McpCanonicalAuthMethodInput[] {  const wire = wirePlacementsFromEditor(placements);  if (wire.length === 0) return [];  return [{ kind: "apikey", placements: wire }];} /** The auth methods a registry-driven MCP add declares before any editing: *  the probe's detection plus the registry's declared facts. ONE policy for *  both the add page (as editor seeds) and the one-click quick add (mapped *  straight to wire inputs) — two copies drifted is how the picker and the *  add page end up declaring different methods for the same server. */export function mcpDetectedAuthSeeds(  probe: {    readonly requiresOAuth: boolean;    readonly requiresAuthentication: boolean;  } | null,  registry: {    readonly placement?: Placement | null;    readonly kind?: string | undefined;  },): readonly AuthMethodSeed[] {  const registryPlacement = registry.placement ?? null;  if (!probe) {    // No probe result (pending, or the server was unreachable from here).    // The registry's declared facts still stand: an authless server or a    // known header pattern seeds the list the probe would have produced.    if (registryPlacement) return [{ value: { kind: "apikey", placements: [registryPlacement] } }];    if (registry.kind === "none") return [{ value: { kind: "none" } }];    return [];  }  if (probe.requiresOAuth) {    const oauth: AuthMethodSeed = {      value: { kind: "oauth", authorizationUrl: "", tokenUrl: "", scopes: [] },      label: "Detected",    };    // GitHub's MCP server takes a PAT bearer header in clients without    // OAuth; when the registry declared that placement, offer it alongside.    return registryPlacement      ? [oauth, { value: { kind: "apikey", placements: [registryPlacement] } }]      : [oauth];  }  if (probe.requiresAuthentication) {    // The registry's exact placement beats the generic Bearer guess.    return [      {        value: {          kind: "apikey",          placements: [            registryPlacement ?? { carrier: "header", name: "Authorization", prefix: "Bearer " },          ],        },        label: "Detected",      },    ];  }  return [{ value: { kind: "none" }, label: "Detected" }];}