@12-apps/mcp 3.15.0 → 3.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/ADOPTING.md +41 -0
  2. package/README.md +1 -1
  3. package/dist/{chunk-VDD4YRNP.js → chunk-EANHJLDH.js} +13 -4
  4. package/dist/chunk-EANHJLDH.js.map +1 -0
  5. package/dist/chunk-F3LMK6OL.js +25 -0
  6. package/dist/chunk-F3LMK6OL.js.map +1 -0
  7. package/dist/{chunk-UIILEGAC.js → chunk-WCZC4TPX.js} +193 -48
  8. package/dist/chunk-WCZC4TPX.js.map +1 -0
  9. package/dist/{create-api-mcp-oauth-CsC0jlH7.d.ts → create-api-mcp-oauth-BEvYLRBV.d.ts} +96 -4
  10. package/dist/e2e/index.d.ts +109 -0
  11. package/dist/e2e/index.js +27 -0
  12. package/dist/e2e/index.js.map +1 -0
  13. package/dist/e2e/steps/journey.steps.d.ts +2 -0
  14. package/dist/e2e/steps/journey.steps.js +79 -0
  15. package/dist/e2e/steps/journey.steps.js.map +1 -0
  16. package/dist/{guide-KQNcXlMG.d.ts → guide-CrzdsdNf.d.ts} +1 -1
  17. package/dist/hono/index.d.ts +1 -1
  18. package/dist/hono/index.js +1 -1
  19. package/dist/index.d.ts +102 -4
  20. package/dist/index.js +71 -6
  21. package/dist/index.js.map +1 -1
  22. package/dist/{locales-eKE_OJw4.d.ts → locales-Cv0Pecvu.d.ts} +1 -1
  23. package/dist/manifest/index.d.ts +29 -7
  24. package/dist/manifest/index.js +2 -1
  25. package/dist/manifest/index.js.map +1 -1
  26. package/dist/manifest/server.d.ts +1 -1
  27. package/dist/manifest/server.js +2 -2
  28. package/dist/oauth/index.d.ts +19 -4
  29. package/dist/oauth/index.js +4 -2
  30. package/dist/react/index.d.ts +3 -3
  31. package/features/ai-connect.feature +46 -0
  32. package/package.json +25 -8
  33. package/prisma/mcp.prisma +10 -0
  34. package/prisma/migrations/20260910120000_add_refresh_grace_seal/migration.sql +37 -0
  35. package/src/e2e/globs.ts +70 -0
  36. package/src/e2e/index.ts +16 -0
  37. package/src/e2e/steps/journey.steps.ts +136 -0
  38. package/src/e2e/world.ts +84 -0
  39. package/src/index.ts +11 -0
  40. package/src/manifest/index.ts +24 -7
  41. package/src/oauth/access-token.ts +72 -10
  42. package/src/oauth/context.ts +20 -0
  43. package/src/oauth/index.ts +2 -0
  44. package/src/oauth/prisma-stores.ts +16 -5
  45. package/src/oauth/refresh-lineage.ts +77 -0
  46. package/src/oauth/refresh.ts +169 -82
  47. package/src/oauth/rotation-grace.ts +216 -0
  48. package/src/oauth/stores.ts +45 -1
  49. package/src/oauth/token-grants.ts +4 -1
  50. package/src/server/auth-failure.ts +145 -0
  51. package/src/server/jsonrpc.ts +44 -5
  52. package/dist/chunk-UIILEGAC.js.map +0 -1
  53. package/dist/chunk-VDD4YRNP.js.map +0 -1
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/openapi/refs.ts","../src/openapi/annotations.ts","../src/dispatch/proxy.ts","../src/server/redact.ts","../src/server/registry.ts","../src/server/jsonrpc.ts"],"sourcesContent":["import type { JsonSchema } from \"../types\";\n\n/**\n * Raised when a JSON Schema cannot be turned into a flat, self-contained tool\n * input — an unresolvable `$ref`, an unsupported pointer, or a recursive schema.\n * The MCP tool surface is deliberately finite and flat (it is handed to an LLM\n * and committed to the drift manifest), so recursion is rejected rather than\n * silently truncated.\n */\nexport class UnsupportedSchemaError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"UnsupportedSchemaError\";\n }\n}\n\n/** Local-definition containers a `$ref` may point into, in resolution order. */\nconst REF_PREFIXES = [\"#/$defs/\", \"#/definitions/\", \"#/components/schemas/\"] as const;\nconst DEF_CONTAINERS = [\"$defs\", \"definitions\"] as const;\n\n/** The definition name a supported local pointer targets, or `null` if unsupported. */\nfunction refName(ref: string): string | null {\n const prefix = REF_PREFIXES.find((candidate) => ref.startsWith(candidate));\n return prefix ? decodeURIComponent(ref.slice(prefix.length)) : null;\n}\n\n/** Collect the `$defs`/`definitions` maps hoisted onto a schema root into one lookup. */\nfunction collectDefs(root: JsonSchema): Record<string, JsonSchema> {\n const defs: Record<string, JsonSchema> = {};\n DEF_CONTAINERS.forEach((key) => {\n const container = root[key];\n if (container && typeof container === \"object\") {\n Object.assign(defs, container as Record<string, JsonSchema>);\n }\n });\n return defs;\n}\n\nfunction inlineRef(\n ref: string,\n defs: Record<string, JsonSchema>,\n active: Set<string>,\n): unknown {\n const name = refName(ref);\n if (name === null) throw new UnsupportedSchemaError(`Unsupported $ref pointer: ${ref}`);\n const target = defs[name];\n if (!target) throw new UnsupportedSchemaError(`Unresolved $ref: ${ref}`);\n if (active.has(name)) throw new UnsupportedSchemaError(`Recursive schema not supported: ${name}`);\n active.add(name);\n const resolved = walk(target, defs, active);\n active.delete(name);\n return resolved;\n}\n\n/** Deep-copy `node`, inlining every `$ref` and stripping definition containers. */\nfunction walk(node: unknown, defs: Record<string, JsonSchema>, active: Set<string>): unknown {\n if (Array.isArray(node)) return node.map((item) => walk(item, defs, active));\n if (!node || typeof node !== \"object\") return node;\n\n const obj = node as Record<string, unknown>;\n if (typeof obj.$ref === \"string\") return inlineRef(obj.$ref, defs, active);\n\n const out: Record<string, unknown> = {};\n Object.entries(obj).forEach(([key, value]) => {\n if (!DEF_CONTAINERS.includes(key as (typeof DEF_CONTAINERS)[number])) {\n out[key] = walk(value, defs, active);\n }\n });\n return out;\n}\n\n/**\n * Inline every local `$ref` in a JSON Schema and drop the now-empty `$defs`/\n * `definitions` containers, yielding a flat, self-contained schema. Diamond reuse\n * (the same definition referenced by sibling branches) is fine; only a true cycle\n * — a definition that references itself up the resolution stack — is rejected.\n *\n * A schema with no `$ref`/definitions is returned structurally unchanged, so\n * inlining an already-flat spec is a no-op (the drift gate stays a stable diff).\n */\nexport function inlineSchemaRefs(schema: JsonSchema): JsonSchema {\n const defs = collectDefs(schema);\n return walk(schema, defs, new Set<string>()) as JsonSchema;\n}\n","/**\n * Composing a tool's behavior classification out of two sources.\n *\n * The rule the host's gate enforces is unchanged: every served tool ends with\n * a COMPLETE `ToolAnnotations` — a title and all three hints — and a tool that\n * ends unclassified fails `mcp:lint`. ChatGPT App review treats a missing hint\n * as a blocker, and the Anthropic connector directory derives auto-permissions\n * from `readOnlyHint`/`destructiveHint`, so there is no defensible default for\n * \"we did not say\".\n *\n * What this adds is where the answer may COME FROM. A package that declares\n * `getSupplierVersions` knows it reads and does not destroy; the host cannot\n * know that without reading the package's source, so it restated the\n * classification by hand — one line per tool, per collection, wrong the moment\n * the package changed a verb. Now the package can assert what it knows and the\n * host's table becomes what it should always have been: OVERRIDES, plus the\n * tools the host itself owns.\n *\n * ## Precedence, and why it runs this way\n *\n * The HOST wins every field it states. A package's claim is a default, not a\n * fact about the host's deployment: the same endpoint can be read-only in one\n * app and reach an external service in another (a host that proxies its\n * catalog reads through a vendor), and the host is the only party that knows.\n * Inverting this would make a package version bump silently re-classify a tool\n * an operator had already audited — the exact thing an audited classification\n * exists to prevent.\n *\n * ## What it refuses\n *\n * A field neither side supplies. `resolveToolAnnotations` throws naming the\n * tool and the missing fields, which keeps the completeness property a\n * REFUSAL rather than a lint pass over a table that quietly grew a gap. The\n * host's own gate can keep its message; this one fires first and says the same\n * thing in the same terms.\n */\n\nimport type { ToolAnnotations } from \"../types\";\nimport type { McpAnnotationDefaults } from \"./endpoint\";\n\n/** The host's half — whatever it chose to state, per tool. */\nexport type ToolAnnotationOverrides = Partial<ToolAnnotations>;\n\n/**\n * Merge a package's declared defaults under a host's overrides.\n *\n * @param name the tool id, for the refusal message\n * @param defaults what the package asserted (`McpEndpoint.annotations`)\n * @param overrides what the host's own table says; wins every field it states\n */\n/** A title only counts when it has something in it. */\nfunction titleOf(candidate: string | undefined): string | undefined {\n return typeof candidate === \"string\" && candidate.trim() !== \"\" ? candidate : undefined;\n}\n\n/**\n * Merge a package's declared defaults under a host's overrides.\n *\n * The four fields are resolved into one record and checked generically rather\n * than branch by branch — which keeps the \"host wins, and `false` is an\n * answer\" rule stated exactly once per field instead of once per field per\n * check.\n *\n * `??` and not `||` throughout, and that is the trap the whole merge turns on:\n * `false` is a real classification — \"this tool does not destroy\" — and must\n * not fall through to the package's answer.\n *\n * @param name the tool id, for the refusal message\n * @param defaults what the package asserted (`McpEndpoint.annotations`)\n * @param overrides what the host's own table says; wins every field it states\n */\nexport function resolveToolAnnotations(\n name: string,\n defaults: McpAnnotationDefaults | undefined,\n overrides: ToolAnnotationOverrides | undefined,\n): ToolAnnotations {\n const host = overrides ?? {};\n const declared = defaults ?? {};\n const resolved: Partial<ToolAnnotations> = {\n title: titleOf(host.title ?? declared.title),\n readOnlyHint: host.readOnlyHint ?? declared.readOnly,\n openWorldHint: host.openWorldHint ?? declared.openWorld,\n destructiveHint: host.destructiveHint ?? declared.destructive,\n };\n\n const missing = REQUIRED_FIELDS.filter((field) => resolved[field] === undefined);\n if (missing.length > 0) refuse(name, missing);\n return resolved as ToolAnnotations;\n}\n\n/** Every field a served tool must end with — the completeness property itself. */\nconst REQUIRED_FIELDS = [\n \"title\",\n \"readOnlyHint\",\n \"openWorldHint\",\n \"destructiveHint\",\n] as const satisfies readonly (keyof ToolAnnotations)[];\n\nfunction refuse(name: string, missing: readonly string[]): never {\n throw new Error(\n `MCP tool \"${name}\" ends unclassified: neither the package nor the host supplied ` +\n `${missing.join(\", \")}. Every served tool needs a complete classification — a missing ` +\n `hint blocks ChatGPT App review, and the connector directory derives auto-permissions ` +\n `from readOnlyHint/destructiveHint.`,\n );\n}\n","import type { DispatchConfig, DispatchResult, GeneratedTool } from \"../types\";\n\n/** Raised when tool arguments cannot be routed onto the HTTP request. */\nexport class DispatchInputError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"DispatchInputError\";\n }\n}\n\n/** Expand a path template (`/products/{id}`) using the path args, URL-encoding each. */\nfunction expandPath(\n tool: GeneratedTool,\n args: Record<string, unknown>,\n): string {\n return tool.path.replace(/\\{([^}]+)\\}/g, (_match, key: string) => {\n const value = args[key];\n if (value === undefined || value === null) {\n throw new DispatchInputError(`Missing required path parameter: ${key}`);\n }\n return encodeURIComponent(String(value));\n });\n}\n\n/**\n * Route the non-path parameters (query + header) from the flat args. A missing\n * required parameter is a hard error; a missing optional one is simply omitted.\n */\nfunction routeParams(\n tool: GeneratedTool,\n args: Record<string, unknown>,\n): { query: URLSearchParams; headers: Record<string, string> } {\n const query = new URLSearchParams();\n const headers: Record<string, string> = {};\n\n tool.parameters\n .filter((param) => param.in !== \"path\")\n .forEach((param) => {\n const value = args[param.name];\n if (value === undefined || value === null) {\n if (param.required) {\n throw new DispatchInputError(`Missing required ${param.in} parameter: ${param.name}`);\n }\n return;\n }\n if (param.in === \"query\") query.set(param.name, String(value));\n else headers[param.name] = String(value);\n });\n\n return { query, headers };\n}\n\n/**\n * Reconstruct the request body from the flat args using the routing metadata.\n * Anything not claimed by a known body property is dropped — the input schema is\n * `additionalProperties: false`, so a validated call never carries extras and an\n * unvalidated one cannot smuggle fields upstream.\n */\nfunction routeBody(tool: GeneratedTool, args: Record<string, unknown>): unknown {\n if (tool.bodyIsWhole) return args.body;\n if (!tool.bodyProps.length) return undefined;\n const payload: Record<string, unknown> = {};\n tool.bodyProps.forEach((key) => {\n if (args[key] !== undefined) payload[key] = args[key];\n });\n return Object.keys(payload).length ? payload : undefined;\n}\n\n/**\n * Execute one generated tool by proxying to its HTTP endpoint, forwarding the\n * caller's bearer verbatim. This function performs NO authorization — the\n * endpoint does, exactly as it would for a first-party request. That is the whole\n * point of the passthrough: the agent can do precisely what the user can.\n */\nexport async function dispatchTool(\n tool: GeneratedTool,\n args: Record<string, unknown>,\n config: DispatchConfig,\n): Promise<DispatchResult> {\n const doFetch = config.fetchImpl ?? fetch;\n const pathname = expandPath(tool, args);\n const { query, headers } = routeParams(tool, args);\n const body = routeBody(tool, args);\n\n const url = new URL(pathname, config.baseUrl);\n for (const [key, value] of query) url.searchParams.set(key, value);\n\n // Carry the proxy origin as the standard reverse-proxy forwarded headers. The\n // wrapped endpoint's auth guard re-derives the request origin (to check the\n // access token's `aud`) WITHOUT a request object, so it can only see the origin\n // through these headers. `baseUrl` is exactly the origin the token was minted\n // and transport-verified against, so forwarding its scheme+host makes the\n // wrapped guard reconstruct the SAME origin — the `aud` survives the replay in\n // both dev (http) and prod (any proxied https origin). Omitting them let the\n // guard default the scheme to https and 401 a valid http-minted bearer.\n const base = new URL(config.baseUrl);\n\n const init: RequestInit = {\n method: tool.method,\n headers: {\n accept: \"application/json\",\n authorization: `Bearer ${config.bearer}`,\n \"x-forwarded-proto\": base.protocol.replace(/:$/, \"\"),\n \"x-forwarded-host\": base.host,\n ...headers,\n },\n };\n if (body !== undefined) {\n (init.headers as Record<string, string>)[\"content-type\"] = \"application/json\";\n init.body = JSON.stringify(body);\n }\n\n const response = await doFetch(url.toString(), init);\n const text = await response.text();\n let parsed: unknown = text;\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"application/json\") && text) {\n try {\n parsed = JSON.parse(text);\n } catch {\n parsed = text;\n }\n }\n\n return { status: response.status, ok: response.ok, body: parsed };\n}\n","import type { JsonSchema } from \"../types\";\n\n/**\n * Both halves of the redaction contract: the SCHEMA a tool advertises, and the\n * BODY it returns.\n *\n * They only work as a pair, and the failure mode when they disagree is not a\n * cosmetic one. The dispatcher forwards the wrapped endpoint's response body\n * verbatim, so a narrowed `outputSchema` alone would only change what the\n * manifest CLAIMS is returned — the value still reaches the agent. Strip the\n * body but leave the schema advertising the field, and every successful call\n * fails validation against the very schema the manifest published, worst of all\n * when the field was `required`.\n *\n * So `redactResponseSchema` removes the paths at generate time and\n * `redactResponseBody` removes the same paths at dispatch, from one list. A host\n * that takes one and hand-rolls the other is back to the disagreement this pair\n * exists to prevent.\n */\n\n/** Structural keywords that wrap a value without naming a field. */\nconst SCHEMA_WRAPPERS = [\"items\", \"anyOf\", \"oneOf\", \"allOf\"] as const;\n\nfunction isSchema(value: unknown): value is JsonSchema {\n return Boolean(value) && typeof value === \"object\";\n}\n\n/** Drop `field` from an object schema's properties AND its `required` list. */\nfunction deleteProperty(schema: JsonSchema, field: string): true {\n delete (schema.properties as Record<string, JsonSchema>)[field];\n if (Array.isArray(schema.required)) {\n const kept = (schema.required as string[]).filter((name) => name !== field);\n if (kept.length) schema.required = kept;\n else delete schema.required;\n }\n return true;\n}\n\n/**\n * Descend into wrappers without consuming a segment, so `data.taxId` addresses\n * an array of rows and a `.nullable()` union branch alike.\n */\nfunction omitInWrappers(schema: JsonSchema, segments: readonly string[]): boolean {\n return SCHEMA_WRAPPERS.reduce((removed, keyword) => {\n const child = schema[keyword];\n const branches = Array.isArray(child) ? child : [child];\n return branches.reduce<boolean>(\n (acc, branch) => (isSchema(branch) && omitSchemaPath(branch, segments)) || acc,\n removed,\n );\n }, false);\n}\n\nfunction omitSchemaPath(schema: JsonSchema, segments: readonly string[]): boolean {\n const inWrappers = omitInWrappers(schema, segments);\n\n const properties = schema.properties as Record<string, JsonSchema> | undefined;\n const [head, ...rest] = segments;\n if (!properties || !head || !(head in properties)) return inWrappers;\n\n if (rest.length === 0) return deleteProperty(schema, head);\n\n const child = properties[head];\n return (isSchema(child) && omitSchemaPath(child, rest)) || inWrappers;\n}\n\n/**\n * Return `schema` without the listed dotted paths — the advertised half.\n *\n * THROWS when a path names nothing, rather than returning quietly: a typo'd or\n * stale redaction would otherwise protect nothing at all, and it would do so\n * invisibly, which is the one outcome a redaction list must never have. Failing\n * here turns it into a generator error naming the offending path.\n *\n * The input is cloned, not narrowed in place: a caller may hold the converted\n * schema for other uses (a shared `$defs` component, a schema reused across two\n * operations), and mutating it would redact those too.\n *\n * `operationId` only shapes the error message — pass it so the failure names\n * which tool declared the bad path.\n */\nexport function redactResponseSchema(\n schema: JsonSchema,\n paths: readonly string[],\n operationId?: string,\n): JsonSchema {\n if (!paths.length) return schema;\n\n const clone = structuredClone(schema);\n paths.forEach((path) => {\n if (!omitSchemaPath(clone, path.split(\".\"))) {\n const subject = operationId ? `MCP endpoint \"${operationId}\"` : \"This response schema\";\n throw new Error(\n `${subject} declares a response redaction for \"${path}\", which is not a field of its response schema`,\n );\n }\n });\n return clone;\n}\n\n/** Walk one dotted path, mapping over arrays, and delete the leaf. */\nfunction stripPath(value: unknown, segments: readonly string[]): void {\n if (value === null || typeof value !== \"object\") return;\n\n if (Array.isArray(value)) {\n value.forEach((entry) => stripPath(entry, segments));\n return;\n }\n\n const [head, ...rest] = segments;\n if (!head) return;\n\n const record = value as Record<string, unknown>;\n if (rest.length === 0) {\n delete record[head];\n return;\n }\n if (head in record) stripPath(record[head], rest);\n}\n\n/**\n * Return `body` without the listed dotted paths.\n *\n * The input is deep-cloned first: dispatch results are handed straight to the\n * JSON-RPC encoder AND reused as `structuredContent`, so mutating in place\n * could leak a half-redacted object into one of the two surfaces.\n */\nexport function redactResponseBody(\n body: unknown,\n paths: readonly string[] | undefined,\n): unknown {\n if (!paths?.length || body === null || typeof body !== \"object\") return body;\n\n const clone = structuredClone(body);\n paths.forEach((path) => stripPath(clone, path.split(\".\")));\n return clone;\n}\n","import { dispatchTool } from \"../dispatch/proxy\";\nimport type {\n GeneratedTool,\n JsonSchema,\n RequestAuth,\n ToolAnnotations,\n} from \"../types\";\nimport { redactResponseBody } from \"./redact\";\n\n/**\n * The registry is the transport-agnostic seam between the generated tools and the\n * MCP SDK. The consuming app owns the HTTP/JSON-RPC transport (mounting it at\n * `/api/mcp`) and, per request, resolves {@link RequestAuth} and calls\n * {@link ToolRegistry.listTools} / {@link ToolRegistry.callTool}. Keeping the\n * SDK out of this package means the core stays testable and portable.\n */\n\n/** An MCP tool descriptor as advertised to clients (subset of the MCP schema). */\nexport interface McpToolDescriptor {\n name: string;\n description: string;\n inputSchema: JsonSchema;\n outputSchema?: JsonSchema;\n annotations: ToolAnnotations;\n}\n\n/**\n * `_meta` key carrying the upstream HTTP status of a dispatched call.\n *\n * `isError` is one bit, and it collapses answers that mean opposite things: a\n * 404 for a record that does not exist, a 403 a guard correctly refused, a\n * domain refusal (\"this store does not use comandas\"), and a 500 where the route\n * threw all arrive identical. Callers that need to tell \"correctly refused\" from\n * \"actually broken\" — `mcp:smoke` above all — cannot, because the status is\n * known at dispatch and then dropped. Publishing it under a namespaced `_meta`\n * key (permitted by the MCP result schema) keeps `isError` as the agent-facing\n * signal while making the distinction recoverable.\n */\nexport const HTTP_STATUS_META_KEY = \"dispatch/httpStatus\";\n\n/** An MCP tool-call result (subset of the MCP schema). */\nexport interface McpToolResult {\n content: Array<{ type: \"text\"; text: string }>;\n isError: boolean;\n /** Machine-readable output matching the advertised outputSchema. */\n structuredContent?: Record<string, unknown>;\n /** Out-of-band metadata; carries {@link HTTP_STATUS_META_KEY} when dispatched. */\n _meta?: Record<string, unknown>;\n}\n\nexport interface ToolRegistry {\n listTools(auth?: RequestAuth): McpToolDescriptor[];\n callTool(\n name: string,\n args: Record<string, unknown>,\n auth: RequestAuth,\n ): Promise<McpToolResult>;\n}\n\nexport interface RegistryOptions {\n tools: GeneratedTool[];\n /** Origin the tools proxy to (usually the app's own public URL). */\n baseUrl: string;\n fetchImpl?: typeof fetch;\n /**\n * Optional visibility filter — e.g. hide mutating tools, or tools whose\n * required scope the caller lacks. Authorization is still enforced upstream;\n * this only shapes what the agent is shown.\n */\n isVisible?: (tool: GeneratedTool, auth?: RequestAuth) => boolean;\n}\n\nfunction textResult(\n value: unknown,\n isError: boolean,\n httpStatus?: number,\n): McpToolResult {\n const text =\n typeof value === \"string\" ? value : JSON.stringify(value, null, 2);\n const result: McpToolResult = { content: [{ type: \"text\", text }], isError };\n if (httpStatus !== undefined) {\n result._meta = { [HTTP_STATUS_META_KEY]: httpStatus };\n }\n return result;\n}\n\nfunction successfulResult(tool: GeneratedTool, value: unknown): McpToolResult {\n // Redact BEFORE rendering the text block: the agent reads `content` even when\n // it ignores `structuredContent`, so stripping only the latter would still\n // hand over the field.\n const safe = redactResponseBody(value, tool.redactResponse);\n const result = textResult(safe, false);\n if (\n tool.outputSchema &&\n safe !== null &&\n typeof safe === \"object\" &&\n !Array.isArray(safe)\n ) {\n result.structuredContent = safe as Record<string, unknown>;\n }\n return result;\n}\n\nexport function createToolRegistry(options: RegistryOptions): ToolRegistry {\n const byName = new Map(options.tools.map((tool) => [tool.name, tool]));\n\n return {\n listTools(auth) {\n return options.tools\n .filter((tool) =>\n options.isVisible ? options.isVisible(tool, auth) : true,\n )\n .map((tool) => ({\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n ...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}),\n annotations: tool.annotations,\n }));\n },\n\n async callTool(name, args, auth) {\n const tool = byName.get(name);\n if (!tool) return textResult(`Unknown tool: ${name}`, true);\n\n try {\n const result = await dispatchTool(tool, args, {\n baseUrl: options.baseUrl,\n bearer: auth.bearer,\n fetchImpl: options.fetchImpl,\n });\n // A non-2xx from the endpoint (e.g. 403 tenant-forbidden) is surfaced to\n // the agent as an error result, NOT thrown — the permission decision was\n // made upstream and its message is the useful signal. The status rides\n // along in `_meta` so a caller can tell a correct refusal from a break.\n return result.ok\n ? successfulResult(tool, result.body)\n : textResult(result.body, true, result.status);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return textResult(`Tool dispatch failed: ${message}`, true);\n }\n },\n };\n}\n","import type { RequestAuth } from \"../types\";\n\nimport type { ToolRegistry } from \"./registry\";\n\n/**\n * The MCP JSON-RPC 2.0 request half of the Streamable HTTP transport.\n *\n * Implemented directly rather than via the MCP SDK because the SDK's transport is\n * Node-`http` oriented, and a host serving Web `Request`/`Response` (a Next route\n * handler, a Hono route) has no `http.IncomingMessage` to hand it. It covers the\n * methods a client needs to discover and call tools: `initialize`, `tools/list`,\n * `tools/call`, plus `ping`.\n *\n * WHAT IS MECHANISM AND LIVES HERE: the envelope, the method table, the error\n * codes, the well-formedness rule, and the notification convention. None of it\n * varies per host — it is JSON-RPC 2.0 and the MCP specification.\n *\n * WHAT IS VOCABULARY AND STAYS WITH THE HOST: the server's NAME, its version, and\n * the `instructions` string an agent reads on connect. Those describe one\n * particular product's tool surface, so they arrive as {@link McpJsonRpcOptions}\n * rather than being written here.\n */\n\n/** The MCP protocol revision this transport implements. */\nexport const MCP_PROTOCOL_VERSION = \"2025-06-18\";\n\n/**\n * JSON-RPC error code for \"authentication required\", returned by `tools/call`\n * when the request carried no valid bearer. Outside the reserved -32768..-32000\n * band's *defined* codes on purpose: it is an implementation-defined server\n * error, and a host maps it to HTTP 401.\n */\nexport const UNAUTHORIZED_CODE = -32001;\n\n/** JSON-RPC \"Invalid Request\" — a payload that isn't a well-formed request object. */\nconst INVALID_REQUEST_CODE = -32600;\n\n/** JSON-RPC \"Method not found\". */\nconst METHOD_NOT_FOUND_CODE = -32601;\n\n/** JSON-RPC \"Invalid params\". */\nconst INVALID_PARAMS_CODE = -32602;\n\nexport interface JsonRpcRequest {\n jsonrpc: \"2.0\";\n id?: string | number | null;\n method: string;\n params?: unknown;\n}\n\nexport interface JsonRpcResponse {\n jsonrpc: \"2.0\";\n id: string | number | null;\n result?: unknown;\n error?: { code: number; message: string };\n}\n\n/** What a client is told it connected to, in `initialize`'s `serverInfo`. */\nexport interface McpServerInfo {\n /** The server's name, as a connected host displays it. */\n name: string;\n /**\n * The advertised surface version.\n *\n * This is the ONLY signal a client gets that the tool surface changed: the\n * transport is request/response only, so `notifications/tools/list_changed`\n * can never be sent, and a host that cached `tools/list` at the handshake has\n * no other reason to ask again. See `server/surface-lock.ts` for the guard\n * that makes forgetting to move it a build error instead of a comment.\n */\n version: string;\n}\n\nexport interface McpJsonRpcOptions {\n /** The host's identity, returned verbatim in `initialize`. */\n serverInfo: McpServerInfo;\n /**\n * Server-level guidance surfaced to the model on `initialize` (the MCP spec's\n * optional `instructions` field). Omitted from the result when absent, rather\n * than sent empty — a blank string is a claim that there is guidance.\n */\n instructions?: string;\n /**\n * Override the advertised protocol revision. Defaults to\n * {@link MCP_PROTOCOL_VERSION}; a host should not normally set it.\n */\n protocolVersion?: string;\n}\n\nfunction ok(id: JsonRpcRequest[\"id\"], result: unknown): JsonRpcResponse {\n return { jsonrpc: \"2.0\", id: id ?? null, result };\n}\n\nfunction fail(id: JsonRpcRequest[\"id\"], code: number, message: string): JsonRpcResponse {\n return { jsonrpc: \"2.0\", id: id ?? null, error: { code, message } };\n}\n\n/** A parsed body is a usable request only if it's an object carrying a string `method`. */\nfunction isWellFormed(request: JsonRpcRequest): boolean {\n return request != null && typeof request === \"object\" && typeof request.method === \"string\";\n}\n\nasync function handleToolsCall(\n request: JsonRpcRequest,\n registry: ToolRegistry,\n auth: RequestAuth | null,\n): Promise<JsonRpcResponse> {\n if (!auth) return fail(request.id, UNAUTHORIZED_CODE, \"Authentication required\");\n const params = (request.params ?? {}) as { name?: string; arguments?: Record<string, unknown> };\n if (!params.name) return fail(request.id, INVALID_PARAMS_CODE, \"Missing tool name\");\n const result = await registry.callTool(params.name, params.arguments ?? {}, auth);\n return ok(request.id, result);\n}\n\nfunction handleInitialize(\n request: JsonRpcRequest,\n options: McpJsonRpcOptions,\n): JsonRpcResponse {\n return ok(request.id, {\n protocolVersion: options.protocolVersion ?? MCP_PROTOCOL_VERSION,\n // Deliberately does NOT claim `listChanged`: this transport has no\n // server→client stream, so the notification could never be sent, and\n // advertising it would stop a host from ever re-reading `tools/list`.\n capabilities: { tools: {} },\n serverInfo: options.serverInfo,\n ...(options.instructions ? { instructions: options.instructions } : {}),\n });\n}\n\n/**\n * Handle one MCP JSON-RPC request.\n *\n * Returns `null` for notifications (no id, no reply expected). `auth` is the\n * verified caller identity, or `null` when the request carried no valid bearer —\n * `tools/call` then returns {@link UNAUTHORIZED_CODE}, which the host surfaces as\n * HTTP 401. Discovery (`initialize`, `ping`, `tools/list`) stays open, so a client\n * can read the surface before it has a token.\n */\nexport async function handleMcpJsonRpc(\n request: JsonRpcRequest,\n registry: ToolRegistry,\n auth: RequestAuth | null,\n options: McpJsonRpcOptions,\n): Promise<JsonRpcResponse | null> {\n // A host casts the parsed body to JsonRpcRequest without validating it, so a\n // malformed payload can arrive here: a `null` body/batch element, a non-object,\n // or an object with no `method`. Reject any of these as Invalid Request rather\n // than dereferencing `request`/`request.method` and throwing a 500 below.\n if (!isWellFormed(request)) {\n return fail(request?.id ?? null, INVALID_REQUEST_CODE, \"Invalid Request\");\n }\n switch (request.method) {\n case \"initialize\":\n return handleInitialize(request, options);\n case \"ping\":\n return ok(request.id, {});\n case \"tools/list\":\n return ok(request.id, { tools: registry.listTools(auth ?? undefined) });\n case \"tools/call\":\n return handleToolsCall(request, registry, auth);\n default:\n // JSON-RPC notifications (`notifications/*`) expect no reply — silently\n // ignore any we don't explicitly handle, rather than returning an error.\n if (request.method.startsWith(\"notifications/\")) return null;\n return fail(request.id, METHOD_NOT_FOUND_CODE, `Method not found: ${request.method}`);\n }\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/openapi/refs.ts","../src/openapi/annotations.ts","../src/dispatch/proxy.ts","../src/server/redact.ts","../src/server/registry.ts","../src/server/auth-failure.ts","../src/server/jsonrpc.ts"],"sourcesContent":["import type { JsonSchema } from \"../types\";\n\n/**\n * Raised when a JSON Schema cannot be turned into a flat, self-contained tool\n * input — an unresolvable `$ref`, an unsupported pointer, or a recursive schema.\n * The MCP tool surface is deliberately finite and flat (it is handed to an LLM\n * and committed to the drift manifest), so recursion is rejected rather than\n * silently truncated.\n */\nexport class UnsupportedSchemaError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"UnsupportedSchemaError\";\n }\n}\n\n/** Local-definition containers a `$ref` may point into, in resolution order. */\nconst REF_PREFIXES = [\"#/$defs/\", \"#/definitions/\", \"#/components/schemas/\"] as const;\nconst DEF_CONTAINERS = [\"$defs\", \"definitions\"] as const;\n\n/** The definition name a supported local pointer targets, or `null` if unsupported. */\nfunction refName(ref: string): string | null {\n const prefix = REF_PREFIXES.find((candidate) => ref.startsWith(candidate));\n return prefix ? decodeURIComponent(ref.slice(prefix.length)) : null;\n}\n\n/** Collect the `$defs`/`definitions` maps hoisted onto a schema root into one lookup. */\nfunction collectDefs(root: JsonSchema): Record<string, JsonSchema> {\n const defs: Record<string, JsonSchema> = {};\n DEF_CONTAINERS.forEach((key) => {\n const container = root[key];\n if (container && typeof container === \"object\") {\n Object.assign(defs, container as Record<string, JsonSchema>);\n }\n });\n return defs;\n}\n\nfunction inlineRef(\n ref: string,\n defs: Record<string, JsonSchema>,\n active: Set<string>,\n): unknown {\n const name = refName(ref);\n if (name === null) throw new UnsupportedSchemaError(`Unsupported $ref pointer: ${ref}`);\n const target = defs[name];\n if (!target) throw new UnsupportedSchemaError(`Unresolved $ref: ${ref}`);\n if (active.has(name)) throw new UnsupportedSchemaError(`Recursive schema not supported: ${name}`);\n active.add(name);\n const resolved = walk(target, defs, active);\n active.delete(name);\n return resolved;\n}\n\n/** Deep-copy `node`, inlining every `$ref` and stripping definition containers. */\nfunction walk(node: unknown, defs: Record<string, JsonSchema>, active: Set<string>): unknown {\n if (Array.isArray(node)) return node.map((item) => walk(item, defs, active));\n if (!node || typeof node !== \"object\") return node;\n\n const obj = node as Record<string, unknown>;\n if (typeof obj.$ref === \"string\") return inlineRef(obj.$ref, defs, active);\n\n const out: Record<string, unknown> = {};\n Object.entries(obj).forEach(([key, value]) => {\n if (!DEF_CONTAINERS.includes(key as (typeof DEF_CONTAINERS)[number])) {\n out[key] = walk(value, defs, active);\n }\n });\n return out;\n}\n\n/**\n * Inline every local `$ref` in a JSON Schema and drop the now-empty `$defs`/\n * `definitions` containers, yielding a flat, self-contained schema. Diamond reuse\n * (the same definition referenced by sibling branches) is fine; only a true cycle\n * — a definition that references itself up the resolution stack — is rejected.\n *\n * A schema with no `$ref`/definitions is returned structurally unchanged, so\n * inlining an already-flat spec is a no-op (the drift gate stays a stable diff).\n */\nexport function inlineSchemaRefs(schema: JsonSchema): JsonSchema {\n const defs = collectDefs(schema);\n return walk(schema, defs, new Set<string>()) as JsonSchema;\n}\n","/**\n * Composing a tool's behavior classification out of two sources.\n *\n * The rule the host's gate enforces is unchanged: every served tool ends with\n * a COMPLETE `ToolAnnotations` — a title and all three hints — and a tool that\n * ends unclassified fails `mcp:lint`. ChatGPT App review treats a missing hint\n * as a blocker, and the Anthropic connector directory derives auto-permissions\n * from `readOnlyHint`/`destructiveHint`, so there is no defensible default for\n * \"we did not say\".\n *\n * What this adds is where the answer may COME FROM. A package that declares\n * `getSupplierVersions` knows it reads and does not destroy; the host cannot\n * know that without reading the package's source, so it restated the\n * classification by hand — one line per tool, per collection, wrong the moment\n * the package changed a verb. Now the package can assert what it knows and the\n * host's table becomes what it should always have been: OVERRIDES, plus the\n * tools the host itself owns.\n *\n * ## Precedence, and why it runs this way\n *\n * The HOST wins every field it states. A package's claim is a default, not a\n * fact about the host's deployment: the same endpoint can be read-only in one\n * app and reach an external service in another (a host that proxies its\n * catalog reads through a vendor), and the host is the only party that knows.\n * Inverting this would make a package version bump silently re-classify a tool\n * an operator had already audited — the exact thing an audited classification\n * exists to prevent.\n *\n * ## What it refuses\n *\n * A field neither side supplies. `resolveToolAnnotations` throws naming the\n * tool and the missing fields, which keeps the completeness property a\n * REFUSAL rather than a lint pass over a table that quietly grew a gap. The\n * host's own gate can keep its message; this one fires first and says the same\n * thing in the same terms.\n */\n\nimport type { ToolAnnotations } from \"../types\";\nimport type { McpAnnotationDefaults } from \"./endpoint\";\n\n/** The host's half — whatever it chose to state, per tool. */\nexport type ToolAnnotationOverrides = Partial<ToolAnnotations>;\n\n/**\n * Merge a package's declared defaults under a host's overrides.\n *\n * @param name the tool id, for the refusal message\n * @param defaults what the package asserted (`McpEndpoint.annotations`)\n * @param overrides what the host's own table says; wins every field it states\n */\n/** A title only counts when it has something in it. */\nfunction titleOf(candidate: string | undefined): string | undefined {\n return typeof candidate === \"string\" && candidate.trim() !== \"\" ? candidate : undefined;\n}\n\n/**\n * Merge a package's declared defaults under a host's overrides.\n *\n * The four fields are resolved into one record and checked generically rather\n * than branch by branch — which keeps the \"host wins, and `false` is an\n * answer\" rule stated exactly once per field instead of once per field per\n * check.\n *\n * `??` and not `||` throughout, and that is the trap the whole merge turns on:\n * `false` is a real classification — \"this tool does not destroy\" — and must\n * not fall through to the package's answer.\n *\n * @param name the tool id, for the refusal message\n * @param defaults what the package asserted (`McpEndpoint.annotations`)\n * @param overrides what the host's own table says; wins every field it states\n */\nexport function resolveToolAnnotations(\n name: string,\n defaults: McpAnnotationDefaults | undefined,\n overrides: ToolAnnotationOverrides | undefined,\n): ToolAnnotations {\n const host = overrides ?? {};\n const declared = defaults ?? {};\n const resolved: Partial<ToolAnnotations> = {\n title: titleOf(host.title ?? declared.title),\n readOnlyHint: host.readOnlyHint ?? declared.readOnly,\n openWorldHint: host.openWorldHint ?? declared.openWorld,\n destructiveHint: host.destructiveHint ?? declared.destructive,\n };\n\n const missing = REQUIRED_FIELDS.filter((field) => resolved[field] === undefined);\n if (missing.length > 0) refuse(name, missing);\n return resolved as ToolAnnotations;\n}\n\n/** Every field a served tool must end with — the completeness property itself. */\nconst REQUIRED_FIELDS = [\n \"title\",\n \"readOnlyHint\",\n \"openWorldHint\",\n \"destructiveHint\",\n] as const satisfies readonly (keyof ToolAnnotations)[];\n\nfunction refuse(name: string, missing: readonly string[]): never {\n throw new Error(\n `MCP tool \"${name}\" ends unclassified: neither the package nor the host supplied ` +\n `${missing.join(\", \")}. Every served tool needs a complete classification — a missing ` +\n `hint blocks ChatGPT App review, and the connector directory derives auto-permissions ` +\n `from readOnlyHint/destructiveHint.`,\n );\n}\n","import type { DispatchConfig, DispatchResult, GeneratedTool } from \"../types\";\n\n/** Raised when tool arguments cannot be routed onto the HTTP request. */\nexport class DispatchInputError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"DispatchInputError\";\n }\n}\n\n/** Expand a path template (`/products/{id}`) using the path args, URL-encoding each. */\nfunction expandPath(\n tool: GeneratedTool,\n args: Record<string, unknown>,\n): string {\n return tool.path.replace(/\\{([^}]+)\\}/g, (_match, key: string) => {\n const value = args[key];\n if (value === undefined || value === null) {\n throw new DispatchInputError(`Missing required path parameter: ${key}`);\n }\n return encodeURIComponent(String(value));\n });\n}\n\n/**\n * Route the non-path parameters (query + header) from the flat args. A missing\n * required parameter is a hard error; a missing optional one is simply omitted.\n */\nfunction routeParams(\n tool: GeneratedTool,\n args: Record<string, unknown>,\n): { query: URLSearchParams; headers: Record<string, string> } {\n const query = new URLSearchParams();\n const headers: Record<string, string> = {};\n\n tool.parameters\n .filter((param) => param.in !== \"path\")\n .forEach((param) => {\n const value = args[param.name];\n if (value === undefined || value === null) {\n if (param.required) {\n throw new DispatchInputError(`Missing required ${param.in} parameter: ${param.name}`);\n }\n return;\n }\n if (param.in === \"query\") query.set(param.name, String(value));\n else headers[param.name] = String(value);\n });\n\n return { query, headers };\n}\n\n/**\n * Reconstruct the request body from the flat args using the routing metadata.\n * Anything not claimed by a known body property is dropped — the input schema is\n * `additionalProperties: false`, so a validated call never carries extras and an\n * unvalidated one cannot smuggle fields upstream.\n */\nfunction routeBody(tool: GeneratedTool, args: Record<string, unknown>): unknown {\n if (tool.bodyIsWhole) return args.body;\n if (!tool.bodyProps.length) return undefined;\n const payload: Record<string, unknown> = {};\n tool.bodyProps.forEach((key) => {\n if (args[key] !== undefined) payload[key] = args[key];\n });\n return Object.keys(payload).length ? payload : undefined;\n}\n\n/**\n * Execute one generated tool by proxying to its HTTP endpoint, forwarding the\n * caller's bearer verbatim. This function performs NO authorization — the\n * endpoint does, exactly as it would for a first-party request. That is the whole\n * point of the passthrough: the agent can do precisely what the user can.\n */\nexport async function dispatchTool(\n tool: GeneratedTool,\n args: Record<string, unknown>,\n config: DispatchConfig,\n): Promise<DispatchResult> {\n const doFetch = config.fetchImpl ?? fetch;\n const pathname = expandPath(tool, args);\n const { query, headers } = routeParams(tool, args);\n const body = routeBody(tool, args);\n\n const url = new URL(pathname, config.baseUrl);\n for (const [key, value] of query) url.searchParams.set(key, value);\n\n // Carry the proxy origin as the standard reverse-proxy forwarded headers. The\n // wrapped endpoint's auth guard re-derives the request origin (to check the\n // access token's `aud`) WITHOUT a request object, so it can only see the origin\n // through these headers. `baseUrl` is exactly the origin the token was minted\n // and transport-verified against, so forwarding its scheme+host makes the\n // wrapped guard reconstruct the SAME origin — the `aud` survives the replay in\n // both dev (http) and prod (any proxied https origin). Omitting them let the\n // guard default the scheme to https and 401 a valid http-minted bearer.\n const base = new URL(config.baseUrl);\n\n const init: RequestInit = {\n method: tool.method,\n headers: {\n accept: \"application/json\",\n authorization: `Bearer ${config.bearer}`,\n \"x-forwarded-proto\": base.protocol.replace(/:$/, \"\"),\n \"x-forwarded-host\": base.host,\n ...headers,\n },\n };\n if (body !== undefined) {\n (init.headers as Record<string, string>)[\"content-type\"] = \"application/json\";\n init.body = JSON.stringify(body);\n }\n\n const response = await doFetch(url.toString(), init);\n const text = await response.text();\n let parsed: unknown = text;\n const contentType = response.headers.get(\"content-type\") ?? \"\";\n if (contentType.includes(\"application/json\") && text) {\n try {\n parsed = JSON.parse(text);\n } catch {\n parsed = text;\n }\n }\n\n return { status: response.status, ok: response.ok, body: parsed };\n}\n","import type { JsonSchema } from \"../types\";\n\n/**\n * Both halves of the redaction contract: the SCHEMA a tool advertises, and the\n * BODY it returns.\n *\n * They only work as a pair, and the failure mode when they disagree is not a\n * cosmetic one. The dispatcher forwards the wrapped endpoint's response body\n * verbatim, so a narrowed `outputSchema` alone would only change what the\n * manifest CLAIMS is returned — the value still reaches the agent. Strip the\n * body but leave the schema advertising the field, and every successful call\n * fails validation against the very schema the manifest published, worst of all\n * when the field was `required`.\n *\n * So `redactResponseSchema` removes the paths at generate time and\n * `redactResponseBody` removes the same paths at dispatch, from one list. A host\n * that takes one and hand-rolls the other is back to the disagreement this pair\n * exists to prevent.\n */\n\n/** Structural keywords that wrap a value without naming a field. */\nconst SCHEMA_WRAPPERS = [\"items\", \"anyOf\", \"oneOf\", \"allOf\"] as const;\n\nfunction isSchema(value: unknown): value is JsonSchema {\n return Boolean(value) && typeof value === \"object\";\n}\n\n/** Drop `field` from an object schema's properties AND its `required` list. */\nfunction deleteProperty(schema: JsonSchema, field: string): true {\n delete (schema.properties as Record<string, JsonSchema>)[field];\n if (Array.isArray(schema.required)) {\n const kept = (schema.required as string[]).filter((name) => name !== field);\n if (kept.length) schema.required = kept;\n else delete schema.required;\n }\n return true;\n}\n\n/**\n * Descend into wrappers without consuming a segment, so `data.taxId` addresses\n * an array of rows and a `.nullable()` union branch alike.\n */\nfunction omitInWrappers(schema: JsonSchema, segments: readonly string[]): boolean {\n return SCHEMA_WRAPPERS.reduce((removed, keyword) => {\n const child = schema[keyword];\n const branches = Array.isArray(child) ? child : [child];\n return branches.reduce<boolean>(\n (acc, branch) => (isSchema(branch) && omitSchemaPath(branch, segments)) || acc,\n removed,\n );\n }, false);\n}\n\nfunction omitSchemaPath(schema: JsonSchema, segments: readonly string[]): boolean {\n const inWrappers = omitInWrappers(schema, segments);\n\n const properties = schema.properties as Record<string, JsonSchema> | undefined;\n const [head, ...rest] = segments;\n if (!properties || !head || !(head in properties)) return inWrappers;\n\n if (rest.length === 0) return deleteProperty(schema, head);\n\n const child = properties[head];\n return (isSchema(child) && omitSchemaPath(child, rest)) || inWrappers;\n}\n\n/**\n * Return `schema` without the listed dotted paths — the advertised half.\n *\n * THROWS when a path names nothing, rather than returning quietly: a typo'd or\n * stale redaction would otherwise protect nothing at all, and it would do so\n * invisibly, which is the one outcome a redaction list must never have. Failing\n * here turns it into a generator error naming the offending path.\n *\n * The input is cloned, not narrowed in place: a caller may hold the converted\n * schema for other uses (a shared `$defs` component, a schema reused across two\n * operations), and mutating it would redact those too.\n *\n * `operationId` only shapes the error message — pass it so the failure names\n * which tool declared the bad path.\n */\nexport function redactResponseSchema(\n schema: JsonSchema,\n paths: readonly string[],\n operationId?: string,\n): JsonSchema {\n if (!paths.length) return schema;\n\n const clone = structuredClone(schema);\n paths.forEach((path) => {\n if (!omitSchemaPath(clone, path.split(\".\"))) {\n const subject = operationId ? `MCP endpoint \"${operationId}\"` : \"This response schema\";\n throw new Error(\n `${subject} declares a response redaction for \"${path}\", which is not a field of its response schema`,\n );\n }\n });\n return clone;\n}\n\n/** Walk one dotted path, mapping over arrays, and delete the leaf. */\nfunction stripPath(value: unknown, segments: readonly string[]): void {\n if (value === null || typeof value !== \"object\") return;\n\n if (Array.isArray(value)) {\n value.forEach((entry) => stripPath(entry, segments));\n return;\n }\n\n const [head, ...rest] = segments;\n if (!head) return;\n\n const record = value as Record<string, unknown>;\n if (rest.length === 0) {\n delete record[head];\n return;\n }\n if (head in record) stripPath(record[head], rest);\n}\n\n/**\n * Return `body` without the listed dotted paths.\n *\n * The input is deep-cloned first: dispatch results are handed straight to the\n * JSON-RPC encoder AND reused as `structuredContent`, so mutating in place\n * could leak a half-redacted object into one of the two surfaces.\n */\nexport function redactResponseBody(\n body: unknown,\n paths: readonly string[] | undefined,\n): unknown {\n if (!paths?.length || body === null || typeof body !== \"object\") return body;\n\n const clone = structuredClone(body);\n paths.forEach((path) => stripPath(clone, path.split(\".\")));\n return clone;\n}\n","import { dispatchTool } from \"../dispatch/proxy\";\nimport type {\n GeneratedTool,\n JsonSchema,\n RequestAuth,\n ToolAnnotations,\n} from \"../types\";\nimport { redactResponseBody } from \"./redact\";\n\n/**\n * The registry is the transport-agnostic seam between the generated tools and the\n * MCP SDK. The consuming app owns the HTTP/JSON-RPC transport (mounting it at\n * `/api/mcp`) and, per request, resolves {@link RequestAuth} and calls\n * {@link ToolRegistry.listTools} / {@link ToolRegistry.callTool}. Keeping the\n * SDK out of this package means the core stays testable and portable.\n */\n\n/** An MCP tool descriptor as advertised to clients (subset of the MCP schema). */\nexport interface McpToolDescriptor {\n name: string;\n description: string;\n inputSchema: JsonSchema;\n outputSchema?: JsonSchema;\n annotations: ToolAnnotations;\n}\n\n/**\n * `_meta` key carrying the upstream HTTP status of a dispatched call.\n *\n * `isError` is one bit, and it collapses answers that mean opposite things: a\n * 404 for a record that does not exist, a 403 a guard correctly refused, a\n * domain refusal (\"this store does not use comandas\"), and a 500 where the route\n * threw all arrive identical. Callers that need to tell \"correctly refused\" from\n * \"actually broken\" — `mcp:smoke` above all — cannot, because the status is\n * known at dispatch and then dropped. Publishing it under a namespaced `_meta`\n * key (permitted by the MCP result schema) keeps `isError` as the agent-facing\n * signal while making the distinction recoverable.\n */\nexport const HTTP_STATUS_META_KEY = \"dispatch/httpStatus\";\n\n/** An MCP tool-call result (subset of the MCP schema). */\nexport interface McpToolResult {\n content: Array<{ type: \"text\"; text: string }>;\n isError: boolean;\n /** Machine-readable output matching the advertised outputSchema. */\n structuredContent?: Record<string, unknown>;\n /** Out-of-band metadata; carries {@link HTTP_STATUS_META_KEY} when dispatched. */\n _meta?: Record<string, unknown>;\n}\n\nexport interface ToolRegistry {\n listTools(auth?: RequestAuth): McpToolDescriptor[];\n callTool(\n name: string,\n args: Record<string, unknown>,\n auth: RequestAuth,\n ): Promise<McpToolResult>;\n}\n\nexport interface RegistryOptions {\n tools: GeneratedTool[];\n /** Origin the tools proxy to (usually the app's own public URL). */\n baseUrl: string;\n fetchImpl?: typeof fetch;\n /**\n * Optional visibility filter — e.g. hide mutating tools, or tools whose\n * required scope the caller lacks. Authorization is still enforced upstream;\n * this only shapes what the agent is shown.\n */\n isVisible?: (tool: GeneratedTool, auth?: RequestAuth) => boolean;\n}\n\nfunction textResult(\n value: unknown,\n isError: boolean,\n httpStatus?: number,\n): McpToolResult {\n const text =\n typeof value === \"string\" ? value : JSON.stringify(value, null, 2);\n const result: McpToolResult = { content: [{ type: \"text\", text }], isError };\n if (httpStatus !== undefined) {\n result._meta = { [HTTP_STATUS_META_KEY]: httpStatus };\n }\n return result;\n}\n\nfunction successfulResult(tool: GeneratedTool, value: unknown): McpToolResult {\n // Redact BEFORE rendering the text block: the agent reads `content` even when\n // it ignores `structuredContent`, so stripping only the latter would still\n // hand over the field.\n const safe = redactResponseBody(value, tool.redactResponse);\n const result = textResult(safe, false);\n if (\n tool.outputSchema &&\n safe !== null &&\n typeof safe === \"object\" &&\n !Array.isArray(safe)\n ) {\n result.structuredContent = safe as Record<string, unknown>;\n }\n return result;\n}\n\nexport function createToolRegistry(options: RegistryOptions): ToolRegistry {\n const byName = new Map(options.tools.map((tool) => [tool.name, tool]));\n\n return {\n listTools(auth) {\n return options.tools\n .filter((tool) =>\n options.isVisible ? options.isVisible(tool, auth) : true,\n )\n .map((tool) => ({\n name: tool.name,\n description: tool.description,\n inputSchema: tool.inputSchema,\n ...(tool.outputSchema ? { outputSchema: tool.outputSchema } : {}),\n annotations: tool.annotations,\n }));\n },\n\n async callTool(name, args, auth) {\n const tool = byName.get(name);\n if (!tool) return textResult(`Unknown tool: ${name}`, true);\n\n try {\n const result = await dispatchTool(tool, args, {\n baseUrl: options.baseUrl,\n bearer: auth.bearer,\n fetchImpl: options.fetchImpl,\n });\n // A non-2xx from the endpoint (e.g. 403 tenant-forbidden) is surfaced to\n // the agent as an error result, NOT thrown — the permission decision was\n // made upstream and its message is the useful signal. The status rides\n // along in `_meta` so a caller can tell a correct refusal from a break.\n return result.ok\n ? successfulResult(tool, result.body)\n : textResult(result.body, true, result.status);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return textResult(`Tool dispatch failed: ${message}`, true);\n }\n },\n };\n}\n","/**\n * What an unauthorized MCP call is TOLD, as opposed to what it is refused with.\n *\n * ## The failure this module exists to remove\n *\n * A resource server has three RFC 6750 challenge codes and a boolean's worth of\n * expressiveness, so every way a bearer can fail arrives at the agent as one\n * opaque refusal. `Authentication required` is what a lapsed connection, a token\n * minted for a different origin, a surface an operator never switched on, and a\n * missing scope all look like — identical, and none of them actionable.\n *\n * The cost is not cosmetic. An agent that cannot tell those apart cannot tell the\n * user anything useful either: every tool call fails, the connector still reports\n * itself connected, and the one thing that would fix the common case — reconnect\n * it — is the one thing nobody is told to do. Reported from a live deployment as\n * \"every tool call failing, even the ones that read nothing\".\n *\n * ## The shape of the answer\n *\n * Each reason resolves to three things, and they are deliberately separate:\n *\n * - `challenge` — the RFC 6750 code for the `WWW-Authenticate` header. Only\n * ever one of the two the spec defines for this situation, because a host's\n * OAuth machinery keys off it;\n * - `action` — what would actually fix it, for a client that automates;\n * - `message` — one sentence an agent can relay to a person. English, like\n * everything else a developer or a model reads here; a host that wants its\n * own wording supplies it.\n *\n * Nothing here narrows `unverified`. Signature, issuer and audience stay\n * collapsed into one answer on purpose — see `../oauth/access-token.ts` for why\n * expiry is the single documented exception.\n */\n\n/** Why a call was refused, across both the transport and the token verifier. */\nexport type McpAuthFailureReason =\n /** No `Authorization` header at all — the client has not connected yet. */\n | \"no_token\"\n /** The token was fine until its `exp` passed. The common one, and recoverable. */\n | \"expired\"\n /** Signature, issuer or audience did not hold. Deliberately not narrowed. */\n | \"unverified\"\n /** Verified, but carrying no usable identity. */\n | \"incomplete\"\n /** The operator has not provisioned signing material, so nothing can verify. */\n | \"not_provisioned\"\n /** The whole MCP surface is switched off for this deployment. */\n | \"surface_disabled\"\n /** A valid token that lacks the scope this particular call needs. */\n | \"insufficient_scope\";\n\n/** What a client should do about it. */\nexport type McpAuthRecovery =\n /** Exchange the refresh token for a new access token, then retry. */\n | \"refresh\"\n /** Re-run the authorization flow — a human has to approve it again. */\n | \"reconnect\"\n /** Nothing the client can do; the deployment has to change. */\n | \"contact_operator\";\n\n/** The resolved answer for one refusal. */\nexport interface McpAuthFailure {\n reason: McpAuthFailureReason;\n /** The RFC 6750 code for the `WWW-Authenticate` challenge. */\n challenge: \"invalid_token\" | \"insufficient_scope\";\n action: McpAuthRecovery;\n /** One sentence, written to be relayed to a person by an agent. */\n message: string;\n}\n\nconst FAILURES: Record<McpAuthFailureReason, Omit<McpAuthFailure, \"reason\">> = {\n no_token: {\n challenge: \"invalid_token\",\n action: \"reconnect\",\n message:\n \"This request carried no access token. Ask the user to connect this MCP server in their assistant's connector settings, then retry.\",\n },\n expired: {\n challenge: \"invalid_token\",\n action: \"refresh\",\n message:\n \"The access token has expired. A client that holds a refresh token should renew it and retry; if renewal also fails, ask the user to reconnect this MCP server in their assistant's connector settings.\",\n },\n unverified: {\n challenge: \"invalid_token\",\n action: \"reconnect\",\n message:\n \"The access token could not be verified for this server. Ask the user to reconnect this MCP server in their assistant's connector settings — a token issued for a different deployment will never verify here.\",\n },\n incomplete: {\n challenge: \"invalid_token\",\n action: \"reconnect\",\n message:\n \"The access token verified but carries no usable identity. Ask the user to reconnect this MCP server in their assistant's connector settings.\",\n },\n not_provisioned: {\n challenge: \"invalid_token\",\n action: \"contact_operator\",\n message:\n \"This server has no signing key provisioned, so no access token can be verified. Reconnecting will not help; the deployment's operator has to configure it.\",\n },\n surface_disabled: {\n challenge: \"invalid_token\",\n action: \"contact_operator\",\n message:\n \"The MCP surface is switched off on this deployment. Reconnecting will not help; the deployment's operator has to enable it.\",\n },\n insufficient_scope: {\n challenge: \"insufficient_scope\",\n action: \"reconnect\",\n message:\n \"The access token does not grant the scope this tool needs. Ask the user to reconnect this MCP server and approve the wider scope.\",\n },\n};\n\n/** Resolve a reason to its challenge code, recovery and human-relayable message. */\nexport function describeAuthFailure(reason: McpAuthFailureReason): McpAuthFailure {\n return { reason, ...FAILURES[reason] };\n}\n\n/**\n * The machine-readable half, carried in the JSON-RPC error's `data` member.\n *\n * A model reads `message`; a host that automates its connection lifecycle reads\n * this. Both travel together so neither has to be inferred from the other.\n */\nexport interface McpAuthFailureData {\n reason: McpAuthFailureReason;\n action: McpAuthRecovery;\n /**\n * Whether presenting a NEW token could succeed. `false` means the deployment\n * itself is the problem, so a client that retries forever is wasting its time\n * and the user's — and should say so rather than loop.\n */\n recoverable: boolean;\n}\n\n/** Build the `data` payload for a refusal. */\nexport function authFailureData(failure: McpAuthFailure): McpAuthFailureData {\n return {\n reason: failure.reason,\n action: failure.action,\n recoverable: failure.action !== \"contact_operator\",\n };\n}\n","import type { RequestAuth } from \"../types\";\n\nimport {\n authFailureData,\n describeAuthFailure,\n type McpAuthFailureReason,\n} from \"./auth-failure\";\nimport type { ToolRegistry } from \"./registry\";\n\n/**\n * The MCP JSON-RPC 2.0 request half of the Streamable HTTP transport.\n *\n * Implemented directly rather than via the MCP SDK because the SDK's transport is\n * Node-`http` oriented, and a host serving Web `Request`/`Response` (a Next route\n * handler, a Hono route) has no `http.IncomingMessage` to hand it. It covers the\n * methods a client needs to discover and call tools: `initialize`, `tools/list`,\n * `tools/call`, plus `ping`.\n *\n * WHAT IS MECHANISM AND LIVES HERE: the envelope, the method table, the error\n * codes, the well-formedness rule, and the notification convention. None of it\n * varies per host — it is JSON-RPC 2.0 and the MCP specification.\n *\n * WHAT IS VOCABULARY AND STAYS WITH THE HOST: the server's NAME, its version, and\n * the `instructions` string an agent reads on connect. Those describe one\n * particular product's tool surface, so they arrive as {@link McpJsonRpcOptions}\n * rather than being written here.\n */\n\n/** The MCP protocol revision this transport implements. */\nexport const MCP_PROTOCOL_VERSION = \"2025-06-18\";\n\n/**\n * JSON-RPC error code for \"authentication required\", returned by `tools/call`\n * when the request carried no valid bearer. Outside the reserved -32768..-32000\n * band's *defined* codes on purpose: it is an implementation-defined server\n * error, and a host maps it to HTTP 401.\n */\nexport const UNAUTHORIZED_CODE = -32001;\n\n/** JSON-RPC \"Invalid Request\" — a payload that isn't a well-formed request object. */\nconst INVALID_REQUEST_CODE = -32600;\n\n/** JSON-RPC \"Method not found\". */\nconst METHOD_NOT_FOUND_CODE = -32601;\n\n/** JSON-RPC \"Invalid params\". */\nconst INVALID_PARAMS_CODE = -32602;\n\nexport interface JsonRpcRequest {\n jsonrpc: \"2.0\";\n id?: string | number | null;\n method: string;\n params?: unknown;\n}\n\nexport interface JsonRpcResponse {\n jsonrpc: \"2.0\";\n id: string | number | null;\n result?: unknown;\n /**\n * `data` is the JSON-RPC 2.0 optional member, and it is what carries the\n * machine-readable half of a refusal ({@link McpAuthFailureData}) while\n * `message` carries the half a model relays to a person.\n */\n error?: { code: number; message: string; data?: unknown };\n}\n\n/** What a client is told it connected to, in `initialize`'s `serverInfo`. */\nexport interface McpServerInfo {\n /** The server's name, as a connected host displays it. */\n name: string;\n /**\n * The advertised surface version.\n *\n * This is the ONLY signal a client gets that the tool surface changed: the\n * transport is request/response only, so `notifications/tools/list_changed`\n * can never be sent, and a host that cached `tools/list` at the handshake has\n * no other reason to ask again. See `server/surface-lock.ts` for the guard\n * that makes forgetting to move it a build error instead of a comment.\n */\n version: string;\n}\n\nexport interface McpJsonRpcOptions {\n /** The host's identity, returned verbatim in `initialize`. */\n serverInfo: McpServerInfo;\n /**\n * Server-level guidance surfaced to the model on `initialize` (the MCP spec's\n * optional `instructions` field). Omitted from the result when absent, rather\n * than sent empty — a blank string is a claim that there is guidance.\n */\n instructions?: string;\n /**\n * Override the advertised protocol revision. Defaults to\n * {@link MCP_PROTOCOL_VERSION}; a host should not normally set it.\n */\n protocolVersion?: string;\n}\n\nfunction ok(id: JsonRpcRequest[\"id\"], result: unknown): JsonRpcResponse {\n return { jsonrpc: \"2.0\", id: id ?? null, result };\n}\n\nfunction fail(\n id: JsonRpcRequest[\"id\"],\n code: number,\n message: string,\n data?: unknown,\n): JsonRpcResponse {\n return {\n jsonrpc: \"2.0\",\n id: id ?? null,\n error: { code, message, ...(data === undefined ? {} : { data }) },\n };\n}\n\n/** A parsed body is a usable request only if it's an object carrying a string `method`. */\nfunction isWellFormed(request: JsonRpcRequest): boolean {\n return request != null && typeof request === \"object\" && typeof request.method === \"string\";\n}\n\nasync function handleToolsCall(\n request: JsonRpcRequest,\n registry: ToolRegistry,\n auth: RequestAuth | null,\n failure?: McpAuthFailureReason,\n): Promise<JsonRpcResponse> {\n if (!auth) {\n // It used to be the bare string \"Authentication required\", which is true of\n // every refusal and useful for none of them: an agent reading it cannot tell\n // a lapsed connection from a misconfigured server, so it cannot tell the user\n // to reconnect. The host resolves WHY and passes it in; `no_token` is the\n // honest default when it says nothing.\n const described = describeAuthFailure(failure ?? \"no_token\");\n return fail(\n request.id,\n UNAUTHORIZED_CODE,\n described.message,\n authFailureData(described),\n );\n }\n const params = (request.params ?? {}) as { name?: string; arguments?: Record<string, unknown> };\n if (!params.name) return fail(request.id, INVALID_PARAMS_CODE, \"Missing tool name\");\n const result = await registry.callTool(params.name, params.arguments ?? {}, auth);\n return ok(request.id, result);\n}\n\nfunction handleInitialize(\n request: JsonRpcRequest,\n options: McpJsonRpcOptions,\n): JsonRpcResponse {\n return ok(request.id, {\n protocolVersion: options.protocolVersion ?? MCP_PROTOCOL_VERSION,\n // Deliberately does NOT claim `listChanged`: this transport has no\n // server→client stream, so the notification could never be sent, and\n // advertising it would stop a host from ever re-reading `tools/list`.\n capabilities: { tools: {} },\n serverInfo: options.serverInfo,\n ...(options.instructions ? { instructions: options.instructions } : {}),\n });\n}\n\n/**\n * Handle one MCP JSON-RPC request.\n *\n * Returns `null` for notifications (no id, no reply expected). `auth` is the\n * verified caller identity, or `null` when the request carried no valid bearer —\n * `tools/call` then returns {@link UNAUTHORIZED_CODE}, which the host surfaces as\n * HTTP 401. Discovery (`initialize`, `ping`, `tools/list`) stays open, so a client\n * can read the surface before it has a token.\n *\n * `failure` is WHY `auth` is null, which only the host's verifier knows. It is\n * optional so an existing caller keeps compiling, and passing it is what turns a\n * refusal an agent can only report into one it can act on — see\n * `./auth-failure.ts`.\n */\nexport async function handleMcpJsonRpc(\n request: JsonRpcRequest,\n registry: ToolRegistry,\n auth: RequestAuth | null,\n options: McpJsonRpcOptions,\n failure?: McpAuthFailureReason,\n): Promise<JsonRpcResponse | null> {\n // A host casts the parsed body to JsonRpcRequest without validating it, so a\n // malformed payload can arrive here: a `null` body/batch element, a non-object,\n // or an object with no `method`. Reject any of these as Invalid Request rather\n // than dereferencing `request`/`request.method` and throwing a 500 below.\n if (!isWellFormed(request)) {\n return fail(request?.id ?? null, INVALID_REQUEST_CODE, \"Invalid Request\");\n }\n switch (request.method) {\n case \"initialize\":\n return handleInitialize(request, options);\n case \"ping\":\n return ok(request.id, {});\n case \"tools/list\":\n return ok(request.id, { tools: registry.listTools(auth ?? undefined) });\n case \"tools/call\":\n return handleToolsCall(request, registry, auth, failure);\n default:\n // JSON-RPC notifications (`notifications/*`) expect no reply — silently\n // ignore any we don't explicitly handle, rather than returning an error.\n if (request.method.startsWith(\"notifications/\")) return null;\n return fail(request.id, METHOD_NOT_FOUND_CODE, `Method not found: ${request.method}`);\n }\n}\n"],"mappings":"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","names":[]}
@@ -1,4 +1,4 @@
1
- import { b as AiConnectPromptSpec, e as AiHostGuide, A as AiCapability, i as AiPermissionModel } from './guide-KQNcXlMG.js';
1
+ import { A as AiConnectPromptSpec, a as AiHostGuide, b as AiCapability, c as AiPermissionModel } from './guide-CrzdsdNf.js';
2
2
 
3
3
  /**
4
4
  * The AI hosts a store owner can connect, in recommended order. Same OAuth flow
@@ -53,13 +53,29 @@
53
53
  * three hosts pass differently, which is the opposite of what a surface
54
54
  * contribution is for. When the flow grows a real bound surface, the
55
55
  * inventory grows with it.
56
- * - **No `env`.** The signing-key variables (`DEFAULT_SIGNING_KEY_ENV`,
57
- * `DEFAULT_SIGNING_KEY_ID_ENV`) and `trustedOriginsFromEnv` are NAMES this
58
- * package exports for a host to read `process.env` with; the package reads
59
- * nothing itself, and the names are overridable per call. Declaring them
60
- * would oblige a host to answer for variables it may legitimately have
61
- * spelled differently.
62
- * - **No `e2e`.** This package packages no journeys.
56
+ * - **No `env`.** Three helpers here DO read `process.env`
57
+ * (`trustedOriginsFromEnv`, and `loadSigningKeyFromEnv` for the key and its
58
+ * id) this used to say "the package reads nothing itself", which was
59
+ * simply untrue and is the kind of sentence that makes a narrowing unfalsifiable.
60
+ * The accurate reason is narrower and still holds: nothing is read at import
61
+ * time or unconditionally, so a host that never calls these helpers has no
62
+ * environment dependency on this package at all; and where they ARE called,
63
+ * the variable's NAME is the caller's — passed as an argument by
64
+ * `trustedOriginsFromEnv`, and defaulted but overridable per call by
65
+ * `loadSigningKeyFromEnv`. `env` declares variables a host must answer for.
66
+ * Declaring these would oblige every host to answer for names it may
67
+ * legitimately have spelled differently, or never reads at all.
68
+ * - **`e2e` IS declared** (`../e2e`). It was narrowed away with "this package
69
+ * packages no journeys", which was true as a statement of fact and circular
70
+ * as an argument: it packaged none because nobody had written any, while
71
+ * `./react` shipped the entire AI-connect walkthrough — landing, assistant
72
+ * picker, endpoint copy, configure, connect, confirm — behind twenty test
73
+ * ids that NO suite touched. Not this package's (its two React tests cover
74
+ * the status board and the step primitives), and not the origin host's,
75
+ * whose `ai.e2e.ts` drives its own plan lock and upsell modal and only
76
+ * passes `ai-onboarding` on the way. A flow every adopter's owner has to
77
+ * walk was covered in neither repo, which is exactly the omission this
78
+ * capability exists to turn into a declaration.
63
79
  * - **No `jobs`.** Nothing here sweeps: authorization codes are stateless
64
80
  * signed blobs (the partial's header says so — there is no `oauth_codes`
65
81
  * table and nothing to expire), and refresh-token revocation happens on
@@ -88,6 +104,12 @@ declare const mcpManifest: {
88
104
  readonly namespace: "mcp";
89
105
  };
90
106
  readonly server: readonly ["http"];
107
+ readonly e2e: {
108
+ readonly entry: "@12-apps/mcp/e2e";
109
+ readonly world: {
110
+ readonly factory: "defineMcpConnectWorld";
111
+ };
112
+ };
91
113
  };
92
114
 
93
115
  export { mcpManifest };
@@ -14,7 +14,8 @@ var mcpManifest = {
14
14
  * code by specification.
15
15
  */
16
16
  observability: { namespace: "mcp" },
17
- server: ["http"]
17
+ server: ["http"],
18
+ e2e: { entry: "@12-apps/mcp/e2e", world: { factory: "defineMcpConnectWorld" } }
18
19
  };
19
20
  export {
20
21
  mcpManifest
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/manifest/index.ts"],"sourcesContent":["/**\n * `@12-apps/mcp/manifest` — the SHARED wiring manifest.\n *\n * Identity, the Prisma contribution (the three tables behind the\n * authorization server) and the runtime inventory: `http` on the server.\n *\n * ON THE `db` DECLARATION — the reason this manifest exists at all.\n *\n * This package ships `prisma/mcp.prisma` and a migration beside it, and until\n * now nothing said so in a form a host assembler could read. The origin\n * host's assembler discovers partials in two steps: a package that carries\n * `\"wiring\": { \"db\": ... }` in its package.json is taken at its word, and a\n * package that carries nothing falls back to a STRUCTURAL scan — every\n * `prisma/*.prisma` under the package root is treated as a partial. So three\n * tables reach somebody's database because a `readdir` found them, not\n * because this package said they should. `@12-apps/notifications`' manifest\n * closed exactly this gap for its four models and the anti-pattern audit\n * names it directly; declaring changes no assembler behaviour (the\n * declaration is read where the scan used to run) and closes the one case\n * where composition was happening by accident.\n *\n * The mirror is what makes the declaration reachable: host assemblers are\n * plain Node reading `node_modules` and cannot execute this TypeScript, so\n * the contribution is repeated under `package.json` `\"wiring\": { \"db\": … }`\n * and `assertDbMirror` pins the two together in this package's own test run.\n *\n * `composed`, not `isolated`, and the choice is forced. An isolated stack\n * needs models carrying no relation into host tables — true of the three\n * here as SHIPPED (`user_id` and `user_email` are deliberately by-value\n * scalars, see the partial's header) — but the host is invited to add the FK\n * in its own migration, and the origin host's is `ON DELETE CASCADE`. A\n * package cannot declare isolation for models whose adopters relate them\n * into their own account tables.\n *\n * ## THE NARROWINGS, each deliberate\n *\n * - **No `mcp` capability.** This package IS the MCP runtime — the\n * OpenAPI→tools generator, the registry, the JSON-RPC transport, the\n * coverage gate. It advertises no tools of its own, and a manifest that\n * declared any would be the runtime describing itself to itself.\n * - **No `permissions`.** Authorization here is the OAuth scope set\n * (`MCP_SUPPORTED_SCOPES`) plus whatever the host's own RBAC says about\n * the proxied endpoint — the point of bearer passthrough is that an agent\n * inherits the caller's permissions rather than holding its own. There is\n * no id for this package to contribute.\n * - **No `web` inventory**, though `./react` ships the whole AI-connect\n * onboarding flow. A `surface` contribution is a `createWeb*` FACTORY —\n * one config object in, an object of component types out, memoised once by\n * the binder. `./react` has no such factory: it exports components a host\n * mounts with its own props (`AiIntegrationOnboarding` takes the store,\n * the endpoint URL and the live connection at the call site). Inventing a\n * factory here to have something to declare would freeze a props table\n * three hosts pass differently, which is the opposite of what a surface\n * contribution is for. When the flow grows a real bound surface, the\n * inventory grows with it.\n * - **No `env`.** The signing-key variables (`DEFAULT_SIGNING_KEY_ENV`,\n * `DEFAULT_SIGNING_KEY_ID_ENV`) and `trustedOriginsFromEnv` are NAMES this\n * package exports for a host to read `process.env` with; the package reads\n * nothing itself, and the names are overridable per call. Declaring them\n * would oblige a host to answer for variables it may legitimately have\n * spelled differently.\n * - **No `e2e`.** This package packages no journeys.\n * - **No `jobs`.** Nothing here sweeps: authorization codes are stateless\n * signed blobs (the partial's header says so — there is no `oauth_codes`\n * table and nothing to expire), and refresh-token revocation happens on\n * the rotation path rather than on a clock.\n *\n * `@12-apps/wiring` is a TYPE-ONLY devDependency (the report-builder move):\n * the manifest is a plain `satisfies`-checked value, and the producer\n * factories' runtime assertions run in this package's own test suite.\n */\n\nimport type { PackageManifest } from \"@12-apps/wiring\";\n\nexport const mcpManifest = {\n name: \"@12-apps/mcp\",\n contract: 1,\n db: { partial: \"prisma/mcp.prisma\", migrations: \"prisma/migrations\" },\n /**\n * A refused token grant, an unresolvable signing key or a rejected\n * redirect URI files under `mcp` rather than under whichever host mounted\n * the authorization server. Mandatory for runtime manifests since wiring\n * 1.3.0, and this is the surface that most needs it: every failure here is\n * a caller who cannot connect, reported to them as an opaque OAuth error\n * code by specification.\n */\n observability: { namespace: \"mcp\" },\n server: [\"http\"],\n} as const satisfies PackageManifest;\n"],"mappings":";;;AA0EO,IAAM,cAAc;AAAA,EACzB,MAAM;AAAA,EACN,UAAU;AAAA,EACV,IAAI,EAAE,SAAS,qBAAqB,YAAY,oBAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASpE,eAAe,EAAE,WAAW,MAAM;AAAA,EAClC,QAAQ,CAAC,MAAM;AACjB;","names":[]}
1
+ {"version":3,"sources":["../../src/manifest/index.ts"],"sourcesContent":["/**\n * `@12-apps/mcp/manifest` — the SHARED wiring manifest.\n *\n * Identity, the Prisma contribution (the three tables behind the\n * authorization server) and the runtime inventory: `http` on the server.\n *\n * ON THE `db` DECLARATION — the reason this manifest exists at all.\n *\n * This package ships `prisma/mcp.prisma` and a migration beside it, and until\n * now nothing said so in a form a host assembler could read. The origin\n * host's assembler discovers partials in two steps: a package that carries\n * `\"wiring\": { \"db\": ... }` in its package.json is taken at its word, and a\n * package that carries nothing falls back to a STRUCTURAL scan — every\n * `prisma/*.prisma` under the package root is treated as a partial. So three\n * tables reach somebody's database because a `readdir` found them, not\n * because this package said they should. `@12-apps/notifications`' manifest\n * closed exactly this gap for its four models and the anti-pattern audit\n * names it directly; declaring changes no assembler behaviour (the\n * declaration is read where the scan used to run) and closes the one case\n * where composition was happening by accident.\n *\n * The mirror is what makes the declaration reachable: host assemblers are\n * plain Node reading `node_modules` and cannot execute this TypeScript, so\n * the contribution is repeated under `package.json` `\"wiring\": { \"db\": … }`\n * and `assertDbMirror` pins the two together in this package's own test run.\n *\n * `composed`, not `isolated`, and the choice is forced. An isolated stack\n * needs models carrying no relation into host tables — true of the three\n * here as SHIPPED (`user_id` and `user_email` are deliberately by-value\n * scalars, see the partial's header) — but the host is invited to add the FK\n * in its own migration, and the origin host's is `ON DELETE CASCADE`. A\n * package cannot declare isolation for models whose adopters relate them\n * into their own account tables.\n *\n * ## THE NARROWINGS, each deliberate\n *\n * - **No `mcp` capability.** This package IS the MCP runtime — the\n * OpenAPI→tools generator, the registry, the JSON-RPC transport, the\n * coverage gate. It advertises no tools of its own, and a manifest that\n * declared any would be the runtime describing itself to itself.\n * - **No `permissions`.** Authorization here is the OAuth scope set\n * (`MCP_SUPPORTED_SCOPES`) plus whatever the host's own RBAC says about\n * the proxied endpoint — the point of bearer passthrough is that an agent\n * inherits the caller's permissions rather than holding its own. There is\n * no id for this package to contribute.\n * - **No `web` inventory**, though `./react` ships the whole AI-connect\n * onboarding flow. A `surface` contribution is a `createWeb*` FACTORY —\n * one config object in, an object of component types out, memoised once by\n * the binder. `./react` has no such factory: it exports components a host\n * mounts with its own props (`AiIntegrationOnboarding` takes the store,\n * the endpoint URL and the live connection at the call site). Inventing a\n * factory here to have something to declare would freeze a props table\n * three hosts pass differently, which is the opposite of what a surface\n * contribution is for. When the flow grows a real bound surface, the\n * inventory grows with it.\n * - **No `env`.** Three helpers here DO read `process.env`\n * (`trustedOriginsFromEnv`, and `loadSigningKeyFromEnv` for the key and its\n * id) this used to say \"the package reads nothing itself\", which was\n * simply untrue and is the kind of sentence that makes a narrowing unfalsifiable.\n * The accurate reason is narrower and still holds: nothing is read at import\n * time or unconditionally, so a host that never calls these helpers has no\n * environment dependency on this package at all; and where they ARE called,\n * the variable's NAME is the caller's — passed as an argument by\n * `trustedOriginsFromEnv`, and defaulted but overridable per call by\n * `loadSigningKeyFromEnv`. `env` declares variables a host must answer for.\n * Declaring these would oblige every host to answer for names it may\n * legitimately have spelled differently, or never reads at all.\n * - **`e2e` IS declared** (`../e2e`). It was narrowed away with \"this package\n * packages no journeys\", which was true as a statement of fact and circular\n * as an argument: it packaged none because nobody had written any, while\n * `./react` shipped the entire AI-connect walkthrough — landing, assistant\n * picker, endpoint copy, configure, connect, confirm — behind twenty test\n * ids that NO suite touched. Not this package's (its two React tests cover\n * the status board and the step primitives), and not the origin host's,\n * whose `ai.e2e.ts` drives its own plan lock and upsell modal and only\n * passes `ai-onboarding` on the way. A flow every adopter's owner has to\n * walk was covered in neither repo, which is exactly the omission this\n * capability exists to turn into a declaration.\n * - **No `jobs`.** Nothing here sweeps: authorization codes are stateless\n * signed blobs (the partial's header says so — there is no `oauth_codes`\n * table and nothing to expire), and refresh-token revocation happens on\n * the rotation path rather than on a clock.\n *\n * `@12-apps/wiring` is a TYPE-ONLY devDependency (the report-builder move):\n * the manifest is a plain `satisfies`-checked value, and the producer\n * factories' runtime assertions run in this package's own test suite.\n */\n\nimport type { PackageManifest } from \"@12-apps/wiring\";\n\nexport const mcpManifest = {\n name: \"@12-apps/mcp\",\n contract: 1,\n db: { partial: \"prisma/mcp.prisma\", migrations: \"prisma/migrations\" },\n /**\n * A refused token grant, an unresolvable signing key or a rejected\n * redirect URI files under `mcp` rather than under whichever host mounted\n * the authorization server. Mandatory for runtime manifests since wiring\n * 1.3.0, and this is the surface that most needs it: every failure here is\n * a caller who cannot connect, reported to them as an opaque OAuth error\n * code by specification.\n */\n observability: { namespace: \"mcp\" },\n server: [\"http\"],\n e2e: { entry: \"@12-apps/mcp/e2e\", world: { factory: \"defineMcpConnectWorld\" } },\n} as const satisfies PackageManifest;\n"],"mappings":";;;AA0FO,IAAM,cAAc;AAAA,EACzB,MAAM;AAAA,EACN,UAAU;AAAA,EACV,IAAI,EAAE,SAAS,qBAAqB,YAAY,oBAAoB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASpE,eAAe,EAAE,WAAW,MAAM;AAAA,EAClC,QAAQ,CAAC,MAAM;AAAA,EACf,KAAK,EAAE,OAAO,oBAAoB,OAAO,EAAE,SAAS,wBAAwB,EAAE;AAChF;","names":[]}
@@ -1,5 +1,5 @@
1
1
  import { WireRequest, WireRouteAnswer } from '@12-apps/wiring';
2
- import { M as McpOauthConfig, A as ApiMcpOauth, a as McpOauthRoute } from '../create-api-mcp-oauth-CsC0jlH7.js';
2
+ import { M as McpOauthConfig, A as ApiMcpOauth, a as McpOauthRoute } from '../create-api-mcp-oauth-BEvYLRBV.js';
3
3
  import 'jose';
4
4
 
5
5
  /**
@@ -1,8 +1,8 @@
1
- import "../chunk-VDD4YRNP.js";
1
+ import "../chunk-EANHJLDH.js";
2
2
  import "../chunk-FBUSSQSK.js";
3
3
  import {
4
4
  createApiMcpOauth
5
- } from "../chunk-UIILEGAC.js";
5
+ } from "../chunk-WCZC4TPX.js";
6
6
  import "../chunk-WJJNKKNS.js";
7
7
  import {
8
8
  __name
@@ -1,6 +1,6 @@
1
- import { b as McpSigningKeyProvider, R as RefreshTokenStore, N as NewOAuthClient, S as StoredOAuthClient, c as NewRefreshToken, d as StoredRefreshToken, e as McpOauthStores, f as McpConnectionStore } from '../create-api-mcp-oauth-CsC0jlH7.js';
2
- export { g as ACCESS_TOKEN_TTL_SECONDS, h as AccessTokenError, i as AccessTokenErrorCode, A as ApiMcpOauth, C as CodeReplayStore, D as DEFAULT_MCP_RESOURCE_PATH, j as DEFAULT_OAUTH_PATHS, k as DEFAULT_PROVIDER_ROOTS, l as DEFAULT_SIGNING_KEY_ENV, m as DEFAULT_SIGNING_KEY_ID_ENV, n as MCP_SUPPORTED_SCOPES, o as McpConnectionRecording, M as McpOauthConfig, p as McpOauthContext, q as McpOauthHandlers, r as McpOauthPaths, a as McpOauthRoute, s as McpOauthSession, t as McpScope, u as McpSigningKey, O as OAuthClientStore, P as ProviderAttributionRule, v as PublicSigningJwk, w as RegisterClientInput, x as RegisteredClient, y as SIGNING_ALG, z as SignAccessTokenInput, B as StoredMcpConnection, T as TokenEndpointAuthMethod, V as VerifiedAccessToken, E as VerifyAccessTokenOptions, F as createApiMcpOauth, G as hashSecret, H as inProcessCodeReplayStore, I as issuer, J as loadSigningKeyFromEnv, K as matchesRedirectUri, L as originFromRequest, Q as providerFromRedirectUris, U as registerClient, W as resolveMcpOauthConfig, X as resolveTrustedOrigin, Y as resourceAudience, Z as signAccessToken, _ as signingKeyProvider, $ as trustedOriginsFromEnv, a0 as verifyAccessToken } from '../create-api-mcp-oauth-CsC0jlH7.js';
3
- import { g as AiProvider } from '../guide-KQNcXlMG.js';
1
+ import { b as McpSigningKeyProvider, R as RefreshTokenStore, N as NewOAuthClient, S as StoredOAuthClient, c as NewRefreshToken, d as StoredRefreshToken, e as McpOauthStores, f as McpConnectionStore } from '../create-api-mcp-oauth-BEvYLRBV.js';
2
+ export { g as ACCESS_TOKEN_TTL_SECONDS, h as AccessTokenError, i as AccessTokenErrorCode, j as AccessTokenFailureReason, A as ApiMcpOauth, C as CodeReplayStore, D as DEFAULT_MCP_RESOURCE_PATH, k as DEFAULT_OAUTH_PATHS, l as DEFAULT_PROVIDER_ROOTS, m as DEFAULT_SIGNING_KEY_ENV, n as DEFAULT_SIGNING_KEY_ID_ENV, o as MCP_SUPPORTED_SCOPES, p as McpConnectionRecording, M as McpOauthConfig, q as McpOauthContext, r as McpOauthHandlers, s as McpOauthPaths, a as McpOauthRoute, t as McpOauthSession, u as McpScope, v as McpSigningKey, O as OAuthClientStore, P as ProviderAttributionRule, w as PublicSigningJwk, x as RegisterClientInput, y as RegisteredClient, z as SIGNING_ALG, B as SignAccessTokenInput, E as StoredMcpConnection, T as TokenEndpointAuthMethod, V as VerifiedAccessToken, F as VerifyAccessTokenOptions, G as createApiMcpOauth, H as hashSecret, I as inProcessCodeReplayStore, J as issuer, K as loadSigningKeyFromEnv, L as matchesRedirectUri, Q as originFromRequest, U as providerFromRedirectUris, W as registerClient, X as resolveMcpOauthConfig, Y as resolveTrustedOrigin, Z as resourceAudience, _ as signAccessToken, $ as signingKeyProvider, a0 as trustedOriginsFromEnv, a1 as verifyAccessToken } from '../create-api-mcp-oauth-BEvYLRBV.js';
3
+ import { d as AiProvider } from '../guide-CrzdsdNf.js';
4
4
  import 'jose';
5
5
 
6
6
  /**
@@ -167,6 +167,16 @@ interface RefreshTokenContext {
167
167
  store: RefreshTokenStore;
168
168
  /** Lifetime of a newly stored token. Default 30 days. */
169
169
  ttlMs?: number;
170
+ /**
171
+ * How long a just-rotated token keeps answering with the successor it minted,
172
+ * instead of being treated as a replay. Default
173
+ * {@link DEFAULT_ROTATION_GRACE_MS}; `0` restores the strict rule.
174
+ *
175
+ * This is what makes a rotation RETRYABLE. See `./rotation-grace.ts` for why the
176
+ * window returns the same successor rather than minting a second one, and why
177
+ * that keeps replay detection intact.
178
+ */
179
+ graceMs?: number;
170
180
  }
171
181
  /**
172
182
  * Issue a fresh (root) refresh token bound to a user (email + OAuth `sub`) +
@@ -205,6 +215,9 @@ interface RefreshTokenIdentity {
205
215
  */
206
216
  declare function getRefreshTokenIdentity(context: RefreshTokenContext, plaintext: string): Promise<RefreshTokenIdentity | null>;
207
217
 
218
+ /** How long a just-rotated token keeps answering with its successor. */
219
+ declare const DEFAULT_ROTATION_GRACE_MS = 30000;
220
+
208
221
  /**
209
222
  * The ports of `./stores.ts`, filled by Prisma (12-23).
210
223
  *
@@ -271,6 +284,7 @@ interface McpOauthPrisma {
271
284
  };
272
285
  data: {
273
286
  revokedAt: Date;
287
+ graceSeal?: null;
274
288
  };
275
289
  }): Promise<{
276
290
  count: number;
@@ -362,6 +376,7 @@ interface McpOauthTx {
362
376
  };
363
377
  data: {
364
378
  revokedAt: Date;
379
+ graceSeal?: null;
365
380
  };
366
381
  }): Promise<{
367
382
  count: number;
@@ -443,4 +458,4 @@ declare function disconnectAiHost(stores: {
443
458
  refreshTokens: RefreshTokenStore;
444
459
  }, caller: AiConnectionCaller, host: AiProvider): Promise<AiDisconnectResult>;
445
460
 
446
- export { AUTHORIZATION_CODE_AUDIENCE, AUTHORIZATION_CODE_TTL_SECONDS, type AiConnectionCaller, type AiConnectionSnapshot, type AiDisconnectResult, AuthorizationCodeError, type AuthorizationCodeErrorCode, type CodeChallengeMethod, type IssuedRefreshToken, McpConnectionStore, type McpOauthPrisma, type McpOauthPrismaProvider, McpOauthStores, McpSigningKeyProvider, type MintCodeInput, NewOAuthClient, NewRefreshToken, REFRESH_TOKEN_TTL_MS, type RefreshTokenContext, RefreshTokenError, type RefreshTokenErrorCode, type RefreshTokenIdentity, RefreshTokenStore, SUPPORTED_CHALLENGE_METHOD, StoredOAuthClient, StoredRefreshToken, UnsupportedChallengeMethodError, type VerifiedAuthorizationCode, type VerifyCodeOptions, computeChallenge, createPrismaMcpStores, disconnectAiHost, getRefreshTokenIdentity, hashToken, issueRefreshToken, listAiConnections, mintCode, rotateRefreshToken, verifyChallenge, verifyCode };
461
+ export { AUTHORIZATION_CODE_AUDIENCE, AUTHORIZATION_CODE_TTL_SECONDS, type AiConnectionCaller, type AiConnectionSnapshot, type AiDisconnectResult, AuthorizationCodeError, type AuthorizationCodeErrorCode, type CodeChallengeMethod, DEFAULT_ROTATION_GRACE_MS, type IssuedRefreshToken, McpConnectionStore, type McpOauthPrisma, type McpOauthPrismaProvider, McpOauthStores, McpSigningKeyProvider, type MintCodeInput, NewOAuthClient, NewRefreshToken, REFRESH_TOKEN_TTL_MS, type RefreshTokenContext, RefreshTokenError, type RefreshTokenErrorCode, type RefreshTokenIdentity, RefreshTokenStore, SUPPORTED_CHALLENGE_METHOD, StoredOAuthClient, StoredRefreshToken, UnsupportedChallengeMethodError, type VerifiedAuthorizationCode, type VerifyCodeOptions, computeChallenge, createPrismaMcpStores, disconnectAiHost, getRefreshTokenIdentity, hashToken, issueRefreshToken, listAiConnections, mintCode, rotateRefreshToken, verifyChallenge, verifyCode };
@@ -2,7 +2,7 @@ import {
2
2
  createPrismaMcpStores,
3
3
  disconnectAiHost,
4
4
  listAiConnections
5
- } from "../chunk-VDD4YRNP.js";
5
+ } from "../chunk-EANHJLDH.js";
6
6
  import "../chunk-FBUSSQSK.js";
7
7
  import {
8
8
  ACCESS_TOKEN_TTL_SECONDS,
@@ -13,6 +13,7 @@ import {
13
13
  DEFAULT_MCP_RESOURCE_PATH,
14
14
  DEFAULT_OAUTH_PATHS,
15
15
  DEFAULT_PROVIDER_ROOTS,
16
+ DEFAULT_ROTATION_GRACE_MS,
16
17
  DEFAULT_SIGNING_KEY_ENV,
17
18
  DEFAULT_SIGNING_KEY_ID_ENV,
18
19
  MCP_SUPPORTED_SCOPES,
@@ -45,7 +46,7 @@ import {
45
46
  verifyAccessToken,
46
47
  verifyChallenge,
47
48
  verifyCode
48
- } from "../chunk-UIILEGAC.js";
49
+ } from "../chunk-WCZC4TPX.js";
49
50
  import "../chunk-WJJNKKNS.js";
50
51
  import "../chunk-7QVYU63E.js";
51
52
  export {
@@ -57,6 +58,7 @@ export {
57
58
  DEFAULT_MCP_RESOURCE_PATH,
58
59
  DEFAULT_OAUTH_PATHS,
59
60
  DEFAULT_PROVIDER_ROOTS,
61
+ DEFAULT_ROTATION_GRACE_MS,
60
62
  DEFAULT_SIGNING_KEY_ENV,
61
63
  DEFAULT_SIGNING_KEY_ID_ENV,
62
64
  MCP_SUPPORTED_SCOPES,
@@ -1,8 +1,8 @@
1
1
  import { ConfirmActionCopy } from '@12-apps/ui/copy';
2
2
  import { OnboardingStore, OnboardingStateSnapshot } from '@12-apps/onboarding';
3
- import { e as AiHostGuide, g as AiProvider, A as AiCapability, c as AiHostBrand } from '../guide-KQNcXlMG.js';
4
- export { b as AiConnectPromptSpec, d as AiHostConfigureStage, f as AiHostLink, h as aiConnectPrompt, p as providerForHostId } from '../guide-KQNcXlMG.js';
5
- export { A as AI_CAPABILITIES, a as AI_CONNECT_PROMPT, b as AI_HOST_GUIDES, c as AI_PERMISSION_MODEL, E as EN_US_AI_CAPABILITIES, d as EN_US_AI_CONNECT_PROMPT, e as EN_US_AI_HOST_GUIDES, f as EN_US_AI_PERMISSION_MODEL, P as PT_BR_AI_CAPABILITIES, g as PT_BR_AI_CONNECT_PROMPT, h as PT_BR_AI_HOST_GUIDES, i as PT_BR_AI_PERMISSION_MODEL } from '../locales-eKE_OJw4.js';
3
+ import { a as AiHostGuide, d as AiProvider, b as AiCapability, f as AiHostBrand } from '../guide-CrzdsdNf.js';
4
+ export { A as AiConnectPromptSpec, g as AiHostConfigureStage, h as AiHostLink, i as aiConnectPrompt, p as providerForHostId } from '../guide-CrzdsdNf.js';
5
+ export { A as AI_CAPABILITIES, a as AI_CONNECT_PROMPT, b as AI_HOST_GUIDES, c as AI_PERMISSION_MODEL, E as EN_US_AI_CAPABILITIES, d as EN_US_AI_CONNECT_PROMPT, e as EN_US_AI_HOST_GUIDES, f as EN_US_AI_PERMISSION_MODEL, P as PT_BR_AI_CAPABILITIES, g as PT_BR_AI_CONNECT_PROMPT, h as PT_BR_AI_HOST_GUIDES, i as PT_BR_AI_PERMISSION_MODEL } from '../locales-Cv0Pecvu.js';
6
6
 
7
7
  /**
8
8
  * Every word the AI-integration screens render, as REQUIRED host config
@@ -0,0 +1,46 @@
1
+ Feature: Connecting an AI assistant to the store
2
+
3
+ The walkthrough a store owner follows to let an assistant reach their data.
4
+ Every step below is this package's own component; what an assistant is
5
+ called, and which ones are offered, is the host's configuration.
6
+
7
+ Background:
8
+ Given I am signed in as somebody who may connect an assistant
9
+ When I open the AI integration screen
10
+
11
+ Scenario: The landing says what an assistant will be able to do
12
+ Then the landing explains the permission model before anything is connected
13
+ And it shows examples of what an assistant can be asked
14
+
15
+ Scenario: Choosing an assistant starts the manual walkthrough
16
+ When I start the walkthrough
17
+ And I choose an assistant that has no one-click install
18
+ Then I am asked to copy the store's endpoint
19
+
20
+ Scenario: Copying the endpoint is what advances the wizard
21
+ When I start the walkthrough
22
+ And I choose an assistant that has no one-click install
23
+ And I copy the endpoint
24
+ Then the walkthrough has moved on to configuring the connector
25
+
26
+ Scenario: Configuring will not advance until the connector page is opened
27
+ When I start the walkthrough
28
+ And I choose an assistant that has no one-click install
29
+ And I copy the endpoint
30
+ Then continuing is refused until I open the connector page
31
+ And once opened, continuing reaches the connect step
32
+
33
+ Scenario: Going back returns to the previous step
34
+ When I start the walkthrough
35
+ And I choose an assistant that has no one-click install
36
+ And I copy the endpoint
37
+ And I go back a step
38
+ Then I am asked to copy the store's endpoint
39
+
40
+ Scenario: The confirmation waits for the assistant, and can be re-tested
41
+ When I start the walkthrough
42
+ And I choose an assistant that has no one-click install
43
+ And I copy the endpoint
44
+ And I work through configuring and connecting
45
+ Then the confirmation is still waiting for the assistant
46
+ And it offers to test the connection again
package/package.json CHANGED
@@ -1,8 +1,10 @@
1
1
  {
2
2
  "name": "@12-apps/mcp",
3
- "version": "3.15.0",
3
+ "version": "3.17.0",
4
4
  "type": "module",
5
- "sideEffects": false,
5
+ "sideEffects": [
6
+ "**/e2e/**"
7
+ ],
6
8
  "description": "App-agnostic MCP server core: generate one MCP tool per OpenAPI operation and proxy each call carrying the caller's bearer token (permission passthrough). Also ships the OAuth 2.1 authorization server (./oauth, ./hono: register/authorize/token, JWKS and both .well-known documents), the package-owned Prisma partial + migration for its three tables, the mcp:generate/mcp:check (./generate) and mcp:coverage (./coverage) gates, and the reusable AI-connect onboarding UI (./react).",
7
9
  "exports": {
8
10
  ".": {
@@ -37,7 +39,11 @@
37
39
  "types": "./dist/manifest/server.d.ts",
38
40
  "default": "./dist/manifest/server.js"
39
41
  },
40
- "./package.json": "./package.json"
42
+ "./package.json": "./package.json",
43
+ "./e2e": {
44
+ "types": "./dist/e2e/index.d.ts",
45
+ "default": "./dist/e2e/index.js"
46
+ }
41
47
  },
42
48
  "scripts": {
43
49
  "build": "rm -rf dist && tsup",
@@ -52,15 +58,17 @@
52
58
  },
53
59
  "dependencies": {
54
60
  "@12-apps/onboarding": "^2.7.0",
55
- "@12-apps/rbac": "^4.11.0",
56
- "@12-apps/ui": "^6.10.0",
61
+ "@12-apps/rbac": "^4.14.0",
62
+ "@12-apps/ui": "^6.24.8",
57
63
  "@mui/icons-material": "^6.5.0",
58
64
  "jose": "^6.1.3",
59
65
  "react": "^19.2.0"
60
66
  },
61
67
  "peerDependencies": {
62
68
  "@12-apps/wiring": ">=1.9.0",
69
+ "@playwright/test": ">=1.55.0",
63
70
  "hono": ">=4.0.0",
71
+ "playwright-bdd": ">=8.3.0",
64
72
  "react": ">=19.0.0",
65
73
  "zod": ">=4.0.0"
66
74
  },
@@ -68,19 +76,26 @@
68
76
  "@12-apps/wiring": {
69
77
  "optional": true
70
78
  },
79
+ "@playwright/test": {
80
+ "optional": true
81
+ },
71
82
  "hono": {
72
83
  "optional": true
73
84
  },
85
+ "playwright-bdd": {
86
+ "optional": true
87
+ },
74
88
  "zod": {
75
89
  "optional": true
76
90
  }
77
91
  },
78
92
  "devDependencies": {
79
93
  "@12-apps/eslint-config": "^1.22.0",
80
- "@12-apps/i18n": "^1.2.0",
94
+ "@12-apps/i18n": "^1.3.0",
81
95
  "@12-apps/typescript-config": "^1.21.0",
82
- "@12-apps/wiring": "^1.14.0",
96
+ "@12-apps/wiring": "^1.16.0",
83
97
  "@mui/material": "^6.5.0",
98
+ "@playwright/test": "^1.61.1",
84
99
  "@testing-library/react": "^16.1.0",
85
100
  "@types/node": "^22.15.3",
86
101
  "@types/react": "19.2.2",
@@ -88,6 +103,7 @@
88
103
  "eslint-plugin-test-flakiness": "^1.4.0",
89
104
  "hono": "^4.6.0",
90
105
  "jsdom": "^25.0.1",
106
+ "playwright-bdd": "^9.2.0",
91
107
  "react-dom": "^19.2.0",
92
108
  "tsup": "^8.0.0",
93
109
  "typescript": "^5.8.2",
@@ -129,6 +145,7 @@
129
145
  "!**/*.spec.*",
130
146
  "!**/*.stories.*",
131
147
  "!**/*.test-story.*",
132
- "!**/test-helpers.*"
148
+ "!**/test-helpers.*",
149
+ "features"
133
150
  ]
134
151
  }
package/prisma/mcp.prisma CHANGED
@@ -71,6 +71,16 @@ model OAuthRefreshToken {
71
71
  revokedAt DateTime? @map("revoked_at")
72
72
  createdAt DateTime @default(now()) @map("created_at")
73
73
 
74
+ // This token's own plaintext, SEALED under a key derived from the plaintext of
75
+ // the token it was rotated from, with its grace deadline inside the sealed
76
+ // blob. It is what lets a rotation be RETRIED: re-presenting the consumed
77
+ // parent within the window returns this same successor instead of revoking the
78
+ // lineage, so a lost response or two concurrent refreshes no longer end the
79
+ // connection. Only a caller holding the parent can open it, so the "never store
80
+ // a token in the clear" rule is intact — a dump of this table yields ciphertext
81
+ // and no key. Null on a root token, and whenever the window is turned off.
82
+ graceSeal String? @map("grace_seal")
83
+
74
84
  // `tokenHash` is already indexed by its `@unique` constraint — the
75
85
  // lookup-on-presentation path — so no separate `@@index([tokenHash])` is added
76
86
  // (it would be redundant). The composite index serves per-user/per-client