@12-apps/mcp 3.2.0 → 3.3.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.
@@ -0,0 +1,407 @@
1
+ import { J as JsonSchema, G as GeneratedTool, D as DispatchConfig, a as DispatchResult, b as ToolAnnotations, R as RequestAuth, T as ToolManifest } from './generate-Dx3cK8th.js';
2
+ export { A as AuthResolver, c as GenerateOptions, O as OpenApiDocument, d as OpenApiOperation, e as OpenApiParameter, f as OpenApiRequestBody, g as OpenApiResponse, P as ParameterLocation, h as ToolParameter, i as generateTools } from './generate-Dx3cK8th.js';
3
+ export { A as AI_CAPABILITIES, a as AI_PERMISSION_MODEL, b as AiCapability, c as AiConnectPromptSpec, d as AiHostBrand, e as AiHostConfigureStage, f as AiHostGuide, g as AiHostLink, h as AiProvider, i as aiConnectPrompt, j as aiHostGuides, p as providerForHostId } from './guide-DV5MQbCg.js';
4
+
5
+ /**
6
+ * Raised when a JSON Schema cannot be turned into a flat, self-contained tool
7
+ * input — an unresolvable `$ref`, an unsupported pointer, or a recursive schema.
8
+ * The MCP tool surface is deliberately finite and flat (it is handed to an LLM
9
+ * and committed to the drift manifest), so recursion is rejected rather than
10
+ * silently truncated.
11
+ */
12
+ declare class UnsupportedSchemaError extends Error {
13
+ constructor(message: string);
14
+ }
15
+ /**
16
+ * Inline every local `$ref` in a JSON Schema and drop the now-empty `$defs`/
17
+ * `definitions` containers, yielding a flat, self-contained schema. Diamond reuse
18
+ * (the same definition referenced by sibling branches) is fine; only a true cycle
19
+ * — a definition that references itself up the resolution stack — is rejected.
20
+ *
21
+ * A schema with no `$ref`/definitions is returned structurally unchanged, so
22
+ * inlining an already-flat spec is a no-op (the drift gate stays a stable diff).
23
+ */
24
+ declare function inlineSchemaRefs(schema: JsonSchema): JsonSchema;
25
+
26
+ /** Raised when tool arguments cannot be routed onto the HTTP request. */
27
+ declare class DispatchInputError extends Error {
28
+ constructor(message: string);
29
+ }
30
+ /**
31
+ * Execute one generated tool by proxying to its HTTP endpoint, forwarding the
32
+ * caller's bearer verbatim. This function performs NO authorization — the
33
+ * endpoint does, exactly as it would for a first-party request. That is the whole
34
+ * point of the passthrough: the agent can do precisely what the user can.
35
+ */
36
+ declare function dispatchTool(tool: GeneratedTool, args: Record<string, unknown>, config: DispatchConfig): Promise<DispatchResult>;
37
+
38
+ /**
39
+ * Return `schema` without the listed dotted paths — the advertised half.
40
+ *
41
+ * THROWS when a path names nothing, rather than returning quietly: a typo'd or
42
+ * stale redaction would otherwise protect nothing at all, and it would do so
43
+ * invisibly, which is the one outcome a redaction list must never have. Failing
44
+ * here turns it into a generator error naming the offending path.
45
+ *
46
+ * The input is cloned, not narrowed in place: a caller may hold the converted
47
+ * schema for other uses (a shared `$defs` component, a schema reused across two
48
+ * operations), and mutating it would redact those too.
49
+ *
50
+ * `operationId` only shapes the error message — pass it so the failure names
51
+ * which tool declared the bad path.
52
+ */
53
+ declare function redactResponseSchema(schema: JsonSchema, paths: readonly string[], operationId?: string): JsonSchema;
54
+ /**
55
+ * Return `body` without the listed dotted paths.
56
+ *
57
+ * The input is deep-cloned first: dispatch results are handed straight to the
58
+ * JSON-RPC encoder AND reused as `structuredContent`, so mutating in place
59
+ * could leak a half-redacted object into one of the two surfaces.
60
+ */
61
+ declare function redactResponseBody(body: unknown, paths: readonly string[] | undefined): unknown;
62
+
63
+ /**
64
+ * The registry is the transport-agnostic seam between the generated tools and the
65
+ * MCP SDK. The consuming app owns the HTTP/JSON-RPC transport (mounting it at
66
+ * `/api/mcp`) and, per request, resolves {@link RequestAuth} and calls
67
+ * {@link ToolRegistry.listTools} / {@link ToolRegistry.callTool}. Keeping the
68
+ * SDK out of this package means the core stays testable and portable.
69
+ */
70
+ /** An MCP tool descriptor as advertised to clients (subset of the MCP schema). */
71
+ interface McpToolDescriptor {
72
+ name: string;
73
+ description: string;
74
+ inputSchema: JsonSchema;
75
+ outputSchema?: JsonSchema;
76
+ annotations: ToolAnnotations;
77
+ }
78
+ /**
79
+ * `_meta` key carrying the upstream HTTP status of a dispatched call.
80
+ *
81
+ * `isError` is one bit, and it collapses answers that mean opposite things: a
82
+ * 404 for a record that does not exist, a 403 a guard correctly refused, a
83
+ * domain refusal ("this store does not use comandas"), and a 500 where the route
84
+ * threw all arrive identical. Callers that need to tell "correctly refused" from
85
+ * "actually broken" — `mcp:smoke` above all — cannot, because the status is
86
+ * known at dispatch and then dropped. Publishing it under a namespaced `_meta`
87
+ * key (permitted by the MCP result schema) keeps `isError` as the agent-facing
88
+ * signal while making the distinction recoverable.
89
+ */
90
+ declare const HTTP_STATUS_META_KEY = "dispatch/httpStatus";
91
+ /** An MCP tool-call result (subset of the MCP schema). */
92
+ interface McpToolResult {
93
+ content: Array<{
94
+ type: "text";
95
+ text: string;
96
+ }>;
97
+ isError: boolean;
98
+ /** Machine-readable output matching the advertised outputSchema. */
99
+ structuredContent?: Record<string, unknown>;
100
+ /** Out-of-band metadata; carries {@link HTTP_STATUS_META_KEY} when dispatched. */
101
+ _meta?: Record<string, unknown>;
102
+ }
103
+ interface ToolRegistry {
104
+ listTools(auth?: RequestAuth): McpToolDescriptor[];
105
+ callTool(name: string, args: Record<string, unknown>, auth: RequestAuth): Promise<McpToolResult>;
106
+ }
107
+ interface RegistryOptions {
108
+ tools: GeneratedTool[];
109
+ /** Origin the tools proxy to (usually the app's own public URL). */
110
+ baseUrl: string;
111
+ fetchImpl?: typeof fetch;
112
+ /**
113
+ * Optional visibility filter — e.g. hide mutating tools, or tools whose
114
+ * required scope the caller lacks. Authorization is still enforced upstream;
115
+ * this only shapes what the agent is shown.
116
+ */
117
+ isVisible?: (tool: GeneratedTool, auth?: RequestAuth) => boolean;
118
+ }
119
+ declare function createToolRegistry(options: RegistryOptions): ToolRegistry;
120
+
121
+ /**
122
+ * The MCP JSON-RPC 2.0 request half of the Streamable HTTP transport.
123
+ *
124
+ * Implemented directly rather than via the MCP SDK because the SDK's transport is
125
+ * Node-`http` oriented, and a host serving Web `Request`/`Response` (a Next route
126
+ * handler, a Hono route) has no `http.IncomingMessage` to hand it. It covers the
127
+ * methods a client needs to discover and call tools: `initialize`, `tools/list`,
128
+ * `tools/call`, plus `ping`.
129
+ *
130
+ * WHAT IS MECHANISM AND LIVES HERE: the envelope, the method table, the error
131
+ * codes, the well-formedness rule, and the notification convention. None of it
132
+ * varies per host — it is JSON-RPC 2.0 and the MCP specification.
133
+ *
134
+ * WHAT IS VOCABULARY AND STAYS WITH THE HOST: the server's NAME, its version, and
135
+ * the `instructions` string an agent reads on connect. Those describe one
136
+ * particular product's tool surface, so they arrive as {@link McpJsonRpcOptions}
137
+ * rather than being written here.
138
+ */
139
+ /** The MCP protocol revision this transport implements. */
140
+ declare const MCP_PROTOCOL_VERSION = "2025-06-18";
141
+ /**
142
+ * JSON-RPC error code for "authentication required", returned by `tools/call`
143
+ * when the request carried no valid bearer. Outside the reserved -32768..-32000
144
+ * band's *defined* codes on purpose: it is an implementation-defined server
145
+ * error, and a host maps it to HTTP 401.
146
+ */
147
+ declare const UNAUTHORIZED_CODE = -32001;
148
+ interface JsonRpcRequest {
149
+ jsonrpc: "2.0";
150
+ id?: string | number | null;
151
+ method: string;
152
+ params?: unknown;
153
+ }
154
+ interface JsonRpcResponse {
155
+ jsonrpc: "2.0";
156
+ id: string | number | null;
157
+ result?: unknown;
158
+ error?: {
159
+ code: number;
160
+ message: string;
161
+ };
162
+ }
163
+ /** What a client is told it connected to, in `initialize`'s `serverInfo`. */
164
+ interface McpServerInfo {
165
+ /** The server's name, as a connected host displays it. */
166
+ name: string;
167
+ /**
168
+ * The advertised surface version.
169
+ *
170
+ * This is the ONLY signal a client gets that the tool surface changed: the
171
+ * transport is request/response only, so `notifications/tools/list_changed`
172
+ * can never be sent, and a host that cached `tools/list` at the handshake has
173
+ * no other reason to ask again. See `server/surface-lock.ts` for the guard
174
+ * that makes forgetting to move it a build error instead of a comment.
175
+ */
176
+ version: string;
177
+ }
178
+ interface McpJsonRpcOptions {
179
+ /** The host's identity, returned verbatim in `initialize`. */
180
+ serverInfo: McpServerInfo;
181
+ /**
182
+ * Server-level guidance surfaced to the model on `initialize` (the MCP spec's
183
+ * optional `instructions` field). Omitted from the result when absent, rather
184
+ * than sent empty — a blank string is a claim that there is guidance.
185
+ */
186
+ instructions?: string;
187
+ /**
188
+ * Override the advertised protocol revision. Defaults to
189
+ * {@link MCP_PROTOCOL_VERSION}; a host should not normally set it.
190
+ */
191
+ protocolVersion?: string;
192
+ }
193
+ /**
194
+ * Handle one MCP JSON-RPC request.
195
+ *
196
+ * Returns `null` for notifications (no id, no reply expected). `auth` is the
197
+ * verified caller identity, or `null` when the request carried no valid bearer —
198
+ * `tools/call` then returns {@link UNAUTHORIZED_CODE}, which the host surfaces as
199
+ * HTTP 401. Discovery (`initialize`, `ping`, `tools/list`) stays open, so a client
200
+ * can read the surface before it has a token.
201
+ */
202
+ declare function handleMcpJsonRpc(request: JsonRpcRequest, registry: ToolRegistry, auth: RequestAuth | null, options: McpJsonRpcOptions): Promise<JsonRpcResponse | null>;
203
+
204
+ /**
205
+ * The manifest is the committed source-of-truth artifact the CI drift gate
206
+ * (`mcp:check` → `12-apps/ci` `mcp-contract.yml`) diffs against a fresh
207
+ * regeneration. If an endpoint's schema changes without the manifest being
208
+ * regenerated, the diff fails the build — that is how the served MCP surface is
209
+ * kept in lockstep with the endpoint surface.
210
+ */
211
+ interface BuildManifestOptions {
212
+ /** Bumped intentionally on any tool-shape change (mirrors the golden catalog). */
213
+ version: number;
214
+ /** Human label for the spec, e.g. "acme web @ openapi.json". */
215
+ source: string;
216
+ }
217
+ declare function buildManifest(tools: GeneratedTool[], options: BuildManifestOptions): ToolManifest;
218
+ /**
219
+ * Canonical JSON for a manifest — deep-key-sorted and trailing-newline'd, so the
220
+ * committed artifact and a regeneration diff cleanly (no key-order or whitespace
221
+ * churn). `mcp:check` regenerates, serializes with this, and `git diff --exit-code`s.
222
+ */
223
+ declare function serializeManifest(manifest: ToolManifest): string;
224
+
225
+ /**
226
+ * Making a server's advertised version impossible to leave behind.
227
+ *
228
+ * THE PROBLEM, which every MCP server on this transport has. `tools/list` is
229
+ * answered on request and there is no server→client stream, so a server cannot
230
+ * push `notifications/tools/list_changed` — and one that declares
231
+ * `capabilities.tools.listChanged` without being able to send it is worse than
232
+ * one that is honest, because the host then stops checking for itself. What is
233
+ * left is `serverInfo.version` from the `initialize` handshake. A host caches the
234
+ * tool list against it, so a version that never moves gives it no reason to ever
235
+ * ask again: a tool that shipped stays invisible to every ALREADY CONNECTED
236
+ * client for as long as that connection lives.
237
+ *
238
+ * That is not a hypothetical. In the origin host a new tool reached production,
239
+ * answered on its route, and did not appear in a live connector — behind a
240
+ * `serverInfo.version` frozen at its initial value while ~280 tools were added
241
+ * underneath it. Nothing was broken; the only thing asking anyone to bump it was
242
+ * a comment, and a rule enforced by a comment is not enforced.
243
+ *
244
+ * THE MECHANISM. An app commits a lock recording WHICH surface its current
245
+ * version stands for. Its generator recomputes the digest and refuses to write
246
+ * the artifacts when the digest moved while the version did not, naming the
247
+ * value to set. Because the same generator run under `--check` is what the
248
+ * contract gate already diffs, the failure lands in CI and in a pre-push hook
249
+ * without a new job, without git history, and without any event-sensitivity.
250
+ *
251
+ * WHY A DIGEST OF THE SURFACE, NOT A PATHS FILTER. A `paths:` list over the
252
+ * server's own directory is wrong in both directions: it fires on edits no
253
+ * client can see (a comment in an auth helper) and misses real ones that enter
254
+ * from outside it (a schema whose ceiling is imported from a storage module).
255
+ * Hashing what the tools ARE — the canonical manifest serialization — has
256
+ * neither failure mode: it is exactly the bytes `tools/list` would return.
257
+ */
258
+ /** The committed record: which surface an app's current version stands for. */
259
+ interface SurfaceLock {
260
+ /** The app's surface version at the time `digest` was recorded. */
261
+ version: number;
262
+ /** Digest of the served tool surface (see {@link surfaceDigest}). */
263
+ digest: string;
264
+ }
265
+ /** Everything {@link surfaceLockProblem} needs to judge one generation. */
266
+ interface SurfaceLockCheck {
267
+ /** The lock as committed, or `null` when there is none to contradict. */
268
+ previous: SurfaceLock | null;
269
+ /** The surface version the app currently declares. */
270
+ version: number;
271
+ /** Digest of the surface being generated now. */
272
+ digest: string;
273
+ /**
274
+ * Where the app's version constant lives, repo-relative — quoted in the
275
+ * failure so the fix is a path and a value rather than a hunt.
276
+ */
277
+ versionLocation: string;
278
+ /** The constant's name, if the app does not use the default. */
279
+ versionName?: string;
280
+ }
281
+ /**
282
+ * Digest of a served tool surface — every tool's name, description, annotations
283
+ * and input/output schemas, in the manifest's own canonical (deep-key-sorted)
284
+ * serialization. Stable across unrelated reordering, and identical for two
285
+ * surfaces that a client could not tell apart.
286
+ */
287
+ declare function surfaceDigest(tools: GeneratedTool[], source: string): string;
288
+ /** Canonical JSON for a committed lock (trailing newline, like the manifest). */
289
+ declare function serializeSurfaceLock(lock: SurfaceLock): string;
290
+ /**
291
+ * Decide whether an app's current version may stand for its current surface.
292
+ *
293
+ * Returns the problem as a sentence ready to print, or `null` when the pair is
294
+ * consistent. Four outcomes, and three of them pass:
295
+ *
296
+ * - surface unchanged → fine, whatever the version did (a release bump with no
297
+ * surface change is legitimate and must not be blocked);
298
+ * - surface changed AND version moved → fine, that is the whole contract;
299
+ * - no lock to contradict (first run, or the file was deleted) → fine, it is
300
+ * simply recorded;
301
+ * - surface changed and version did not → the failure this exists for.
302
+ */
303
+ declare function surfaceLockProblem(check: SurfaceLockCheck): string | null;
304
+
305
+ /**
306
+ * OAuth 2.0 Protected Resource Metadata (RFC 9728), as required by the MCP
307
+ * authorization spec: the MCP endpoint is an OAuth *resource server*. Agent hosts
308
+ * (Claude.ai / ChatGPT connectors) discover where to obtain a token by reading
309
+ * `/.well-known/oauth-protected-resource`, and on a 401 the resource server points
310
+ * them at that document via a `WWW-Authenticate` challenge.
311
+ *
312
+ * This module only builds the discovery documents/headers — validating the
313
+ * resulting access token is the app's job (the {@link import("../types").AuthResolver}),
314
+ * because it depends on the app's authorization server and key material.
315
+ */
316
+ interface ProtectedResourceMetadataInput {
317
+ /** Canonical resource identifier — the MCP endpoint URL (the token audience). */
318
+ resource: string;
319
+ /** Authorization server issuer URLs that can mint tokens for this resource. */
320
+ authorizationServers: string[];
321
+ /** Scopes the resource server understands (advertised to clients). */
322
+ scopesSupported?: string[];
323
+ /** Human-facing docs URL for the protected resource, if any. */
324
+ resourceDocumentation?: string;
325
+ }
326
+ /** The RFC 9728 metadata document served at `/.well-known/oauth-protected-resource`. */
327
+ interface ProtectedResourceMetadata {
328
+ resource: string;
329
+ authorization_servers: string[];
330
+ bearer_methods_supported: string[];
331
+ scopes_supported?: string[];
332
+ resource_documentation?: string;
333
+ }
334
+ declare function buildProtectedResourceMetadata(input: ProtectedResourceMetadataInput): ProtectedResourceMetadata;
335
+ /**
336
+ * Build the `WWW-Authenticate` value for an unauthorized MCP response, pointing
337
+ * the client at the protected-resource metadata so it can start the OAuth flow.
338
+ * Per RFC 9728 §5.1 the challenge carries a `resource_metadata` parameter.
339
+ */
340
+ declare function bearerChallenge(params: {
341
+ resourceMetadataUrl: string;
342
+ error?: "invalid_token" | "insufficient_scope";
343
+ errorDescription?: string;
344
+ }): string;
345
+ /** Standard path for the protected-resource metadata document. */
346
+ declare const PROTECTED_RESOURCE_METADATA_PATH = "/.well-known/oauth-protected-resource";
347
+
348
+ /**
349
+ * OAuth 2.0 Authorization Server Metadata (RFC 8414), the discovery half that
350
+ * complements the RFC 9728 protected-resource metadata in `resource-metadata.ts`.
351
+ * Agent hosts (Claude.ai / ChatGPT connectors) read
352
+ * `/.well-known/oauth-authorization-server` to learn where to start the OAuth
353
+ * 2.1 Authorization Code + PKCE flow.
354
+ *
355
+ * This builder is a pure function of `(issuer, scopes)` with no Next.js/request
356
+ * coupling, so it can move verbatim into the future `@12-apps/mcp` extraction.
357
+ * It derives every endpoint from the same issuer origin the resource metadata
358
+ * advertises, so the two discovery documents cannot drift.
359
+ */
360
+ interface AuthorizationServerMetadataInput {
361
+ /** Authorization server issuer URL (origin) — also the resource issuer. */
362
+ issuer: string;
363
+ /** Scopes the authorization server advertises (from the shared scope source). */
364
+ scopesSupported: string[];
365
+ /**
366
+ * Client authentication methods the token endpoint accepts. Defaults to
367
+ * public PKCE clients (`none`) plus HTTP Basic client-secret auth.
368
+ */
369
+ tokenEndpointAuthMethods?: string[];
370
+ /**
371
+ * Where the endpoints are actually mounted, if not at the defaults below. A
372
+ * host that moves an endpoint MUST move it here too: this document is the only
373
+ * thing a connector reads before its first request, so a path that lies here is
374
+ * a flow that fails at the first hop (12-23 — `createApiMcpOauth` passes its
375
+ * resolved paths, so the two cannot disagree).
376
+ */
377
+ paths?: Partial<AuthorizationServerPaths>;
378
+ }
379
+ /** The endpoint paths this document advertises, relative to the issuer origin. */
380
+ interface AuthorizationServerPaths {
381
+ authorize: string;
382
+ token: string;
383
+ register: string;
384
+ jwks: string;
385
+ }
386
+ /** The RFC 8414 document served at `/.well-known/oauth-authorization-server`. */
387
+ interface AuthorizationServerMetadata {
388
+ issuer: string;
389
+ authorization_endpoint: string;
390
+ token_endpoint: string;
391
+ registration_endpoint: string;
392
+ jwks_uri: string;
393
+ scopes_supported: string[];
394
+ response_types_supported: string[];
395
+ grant_types_supported: string[];
396
+ code_challenge_methods_supported: string[];
397
+ token_endpoint_auth_methods_supported: string[];
398
+ }
399
+ /**
400
+ * Build the RFC 8414 authorization-server metadata document from an issuer
401
+ * origin and the supported scopes. Endpoints are derived from `issuer`; the
402
+ * OAuth 2.1 + PKCE contract fixes `response_types_supported`,
403
+ * `grant_types_supported`, and `code_challenge_methods_supported`.
404
+ */
405
+ declare function buildAuthorizationServerMetadata(input: AuthorizationServerMetadataInput): AuthorizationServerMetadata;
406
+
407
+ export { type AuthorizationServerMetadata, type AuthorizationServerMetadataInput, type AuthorizationServerPaths, type BuildManifestOptions, DispatchConfig, DispatchInputError, DispatchResult, GeneratedTool, HTTP_STATUS_META_KEY, type JsonRpcRequest, type JsonRpcResponse, JsonSchema, MCP_PROTOCOL_VERSION, type McpJsonRpcOptions, type McpServerInfo, type McpToolDescriptor, type McpToolResult, PROTECTED_RESOURCE_METADATA_PATH, type ProtectedResourceMetadata, type ProtectedResourceMetadataInput, type RegistryOptions, RequestAuth, type SurfaceLock, type SurfaceLockCheck, ToolAnnotations, ToolManifest, type ToolRegistry, UNAUTHORIZED_CODE, UnsupportedSchemaError, bearerChallenge, buildAuthorizationServerMetadata, buildManifest, buildProtectedResourceMetadata, createToolRegistry, dispatchTool, handleMcpJsonRpc, inlineSchemaRefs, redactResponseBody, redactResponseSchema, serializeManifest, serializeSurfaceLock, surfaceDigest, surfaceLockProblem };