@12-apps/mcp 3.2.1 → 3.4.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.
- package/ADOPTING.md +31 -0
- package/dist/index.d.ts +90 -1
- package/dist/index.js +52 -0
- package/dist/index.js.map +1 -1
- package/package.json +8 -3
- package/src/index.ts +10 -0
- package/src/openapi/endpoint.ts +71 -0
- package/src/server/redact.ts +96 -6
package/ADOPTING.md
CHANGED
|
@@ -188,6 +188,37 @@ surface moved — this transport has no server→client stream, so
|
|
|
188
188
|
`capabilities.tools` deliberately does not claim `listChanged`. Pair it with the
|
|
189
189
|
surface lock (`mcp:generate`) so forgetting the bump is a build error.
|
|
190
190
|
|
|
191
|
+
## Redaction: take both halves
|
|
192
|
+
|
|
193
|
+
A redacted field has to disappear from two places, and they are reached at
|
|
194
|
+
different times:
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
import { redactResponseSchema, redactResponseBody } from '@12-apps/mcp';
|
|
198
|
+
|
|
199
|
+
// generate time — what the tool ADVERTISES
|
|
200
|
+
schema = redactResponseSchema(responseSchema, paths, operationId);
|
|
201
|
+
// dispatch time — what it RETURNS (the registry already does this for you
|
|
202
|
+
// from `x-mcp-redact-response`)
|
|
203
|
+
body = redactResponseBody(body, paths);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Drive both from ONE list — in an OpenAPI-generated surface that list is the
|
|
207
|
+
operation's `x-mcp-redact-response`, which `generateTools` carries onto the tool
|
|
208
|
+
and the registry applies at dispatch. Take one half only and you get the failure
|
|
209
|
+
the pair exists to prevent:
|
|
210
|
+
|
|
211
|
+
- **body stripped, schema not narrowed** → every successful call returns
|
|
212
|
+
`structuredContent` that fails validation against the schema the manifest
|
|
213
|
+
itself published; worst when the field was `required`.
|
|
214
|
+
- **schema narrowed, body not stripped** → the manifest claims the field is gone
|
|
215
|
+
while the value still reaches the agent. The redaction protected nothing.
|
|
216
|
+
|
|
217
|
+
`redactResponseSchema` **throws** when a path names no field, rather than
|
|
218
|
+
returning quietly — a typo'd or stale redaction otherwise protects nothing,
|
|
219
|
+
invisibly, which is the one outcome a redaction list must never have. Let it fail
|
|
220
|
+
the generator.
|
|
221
|
+
|
|
191
222
|
## Minimal host (Hono)
|
|
192
223
|
|
|
193
224
|
```ts
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
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
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
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
|
+
import { z } from 'zod';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Raised when a JSON Schema cannot be turned into a flat, self-contained tool
|
|
@@ -23,6 +24,69 @@ declare class UnsupportedSchemaError extends Error {
|
|
|
23
24
|
*/
|
|
24
25
|
declare function inlineSchemaRefs(schema: JsonSchema): JsonSchema;
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* How a route is DECLARED, one step before it becomes an OpenAPI operation and
|
|
29
|
+
* two before it becomes a tool.
|
|
30
|
+
*
|
|
31
|
+
* This package already owns everything downstream of an OpenAPI document —
|
|
32
|
+
* `generateTools` turns operations into tools, `dispatchTool` proxies a call,
|
|
33
|
+
* `redactResponseSchema`/`redactResponseBody` narrow both halves. What it did
|
|
34
|
+
* not own was the shape a consumer writes its routes down in, so every consumer
|
|
35
|
+
* declared its own. That is fine for one app and wrong for several: a monorepo
|
|
36
|
+
* where the shift routes, the lifecycle routes and the audit routes are each
|
|
37
|
+
* packaged separately needs those packages to produce endpoint lists the HOST
|
|
38
|
+
* can concatenate, which they can only do if they all mean the same thing by
|
|
39
|
+
* "an endpoint".
|
|
40
|
+
*
|
|
41
|
+
* Deliberately zod-shaped rather than JSON-Schema-shaped. A route validates its
|
|
42
|
+
* input with zod at runtime; describing it a second time in JSON Schema is a
|
|
43
|
+
* copy that drifts, and the drift is invisible — the manifest keeps advertising
|
|
44
|
+
* the shape the route stopped accepting. Converting zod → JSON Schema at
|
|
45
|
+
* generate time makes the validator the single source of truth.
|
|
46
|
+
*
|
|
47
|
+
* zod is a PEER dependency: it is referenced here as a type only, so this
|
|
48
|
+
* package pulls no copy of its own and cannot end up type-checking against a
|
|
49
|
+
* different one than the consumer declares its schemas with.
|
|
50
|
+
*/
|
|
51
|
+
/** The methods an MCP-exposed route may use. */
|
|
52
|
+
type HttpMethod = "get" | "post" | "put" | "patch" | "delete";
|
|
53
|
+
interface McpEndpointBase {
|
|
54
|
+
/** Stable tool id — this becomes the MCP tool name, so renaming it is a
|
|
55
|
+
* breaking change for every agent that has learned the old one. */
|
|
56
|
+
operationId: string;
|
|
57
|
+
method: HttpMethod;
|
|
58
|
+
/** OpenAPI path template, e.g. `/api/products/{id}`. */
|
|
59
|
+
path: string;
|
|
60
|
+
/** What the tool is FOR, in the words an agent reads when choosing it. */
|
|
61
|
+
summary: string;
|
|
62
|
+
tags?: string[];
|
|
63
|
+
/** Object schema whose properties become query parameters. */
|
|
64
|
+
query?: z.ZodType;
|
|
65
|
+
/** Object schema whose properties become path parameters. */
|
|
66
|
+
params?: z.ZodType;
|
|
67
|
+
/** Request body schema (writes only). */
|
|
68
|
+
body?: z.ZodType;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* A declared endpoint either answers 200 with a schema'd JSON body (the
|
|
72
|
+
* default) or 204 No Content (fire-and-forget writes).
|
|
73
|
+
*
|
|
74
|
+
* The union is what makes the two mutually exclusive: a 204 entry cannot carry
|
|
75
|
+
* a response schema, so a manifest can never advertise a body its route will
|
|
76
|
+
* not send — a mismatch an agent experiences as a tool that returns nothing
|
|
77
|
+
* where its own schema promised an object.
|
|
78
|
+
*/
|
|
79
|
+
type McpEndpoint = McpEndpointBase & ({
|
|
80
|
+
/** Success status (defaults to 200 with a JSON body). */
|
|
81
|
+
status?: 200;
|
|
82
|
+
/** Success (200) response schema. */
|
|
83
|
+
response: z.ZodType;
|
|
84
|
+
} | {
|
|
85
|
+
/** 204 No Content — no response schema. */
|
|
86
|
+
status: 204;
|
|
87
|
+
response?: never;
|
|
88
|
+
});
|
|
89
|
+
|
|
26
90
|
/** Raised when tool arguments cannot be routed onto the HTTP request. */
|
|
27
91
|
declare class DispatchInputError extends Error {
|
|
28
92
|
constructor(message: string);
|
|
@@ -35,6 +99,31 @@ declare class DispatchInputError extends Error {
|
|
|
35
99
|
*/
|
|
36
100
|
declare function dispatchTool(tool: GeneratedTool, args: Record<string, unknown>, config: DispatchConfig): Promise<DispatchResult>;
|
|
37
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Return `schema` without the listed dotted paths — the advertised half.
|
|
104
|
+
*
|
|
105
|
+
* THROWS when a path names nothing, rather than returning quietly: a typo'd or
|
|
106
|
+
* stale redaction would otherwise protect nothing at all, and it would do so
|
|
107
|
+
* invisibly, which is the one outcome a redaction list must never have. Failing
|
|
108
|
+
* here turns it into a generator error naming the offending path.
|
|
109
|
+
*
|
|
110
|
+
* The input is cloned, not narrowed in place: a caller may hold the converted
|
|
111
|
+
* schema for other uses (a shared `$defs` component, a schema reused across two
|
|
112
|
+
* operations), and mutating it would redact those too.
|
|
113
|
+
*
|
|
114
|
+
* `operationId` only shapes the error message — pass it so the failure names
|
|
115
|
+
* which tool declared the bad path.
|
|
116
|
+
*/
|
|
117
|
+
declare function redactResponseSchema(schema: JsonSchema, paths: readonly string[], operationId?: string): JsonSchema;
|
|
118
|
+
/**
|
|
119
|
+
* Return `body` without the listed dotted paths.
|
|
120
|
+
*
|
|
121
|
+
* The input is deep-cloned first: dispatch results are handed straight to the
|
|
122
|
+
* JSON-RPC encoder AND reused as `structuredContent`, so mutating in place
|
|
123
|
+
* could leak a half-redacted object into one of the two surfaces.
|
|
124
|
+
*/
|
|
125
|
+
declare function redactResponseBody(body: unknown, paths: readonly string[] | undefined): unknown;
|
|
126
|
+
|
|
38
127
|
/**
|
|
39
128
|
* The registry is the transport-agnostic seam between the generated tools and the
|
|
40
129
|
* MCP SDK. The consuming app owns the HTTP/JSON-RPC transport (mounting it at
|
|
@@ -379,4 +468,4 @@ interface AuthorizationServerMetadata {
|
|
|
379
468
|
*/
|
|
380
469
|
declare function buildAuthorizationServerMetadata(input: AuthorizationServerMetadataInput): AuthorizationServerMetadata;
|
|
381
470
|
|
|
382
|
-
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, serializeManifest, serializeSurfaceLock, surfaceDigest, surfaceLockProblem };
|
|
471
|
+
export { type AuthorizationServerMetadata, type AuthorizationServerMetadataInput, type AuthorizationServerPaths, type BuildManifestOptions, DispatchConfig, DispatchInputError, DispatchResult, GeneratedTool, HTTP_STATUS_META_KEY, type HttpMethod, type JsonRpcRequest, type JsonRpcResponse, JsonSchema, MCP_PROTOCOL_VERSION, type McpEndpoint, 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 };
|
package/dist/index.js
CHANGED
|
@@ -168,6 +168,56 @@ async function dispatchTool(tool, args, config) {
|
|
|
168
168
|
__name(dispatchTool, "dispatchTool");
|
|
169
169
|
|
|
170
170
|
// src/server/redact.ts
|
|
171
|
+
var SCHEMA_WRAPPERS = ["items", "anyOf", "oneOf", "allOf"];
|
|
172
|
+
function isSchema(value) {
|
|
173
|
+
return Boolean(value) && typeof value === "object";
|
|
174
|
+
}
|
|
175
|
+
__name(isSchema, "isSchema");
|
|
176
|
+
function deleteProperty(schema, field) {
|
|
177
|
+
delete schema.properties[field];
|
|
178
|
+
if (Array.isArray(schema.required)) {
|
|
179
|
+
const kept = schema.required.filter((name) => name !== field);
|
|
180
|
+
if (kept.length) schema.required = kept;
|
|
181
|
+
else delete schema.required;
|
|
182
|
+
}
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
__name(deleteProperty, "deleteProperty");
|
|
186
|
+
function omitInWrappers(schema, segments) {
|
|
187
|
+
return SCHEMA_WRAPPERS.reduce((removed, keyword) => {
|
|
188
|
+
const child = schema[keyword];
|
|
189
|
+
const branches = Array.isArray(child) ? child : [child];
|
|
190
|
+
return branches.reduce(
|
|
191
|
+
(acc, branch) => isSchema(branch) && omitSchemaPath(branch, segments) || acc,
|
|
192
|
+
removed
|
|
193
|
+
);
|
|
194
|
+
}, false);
|
|
195
|
+
}
|
|
196
|
+
__name(omitInWrappers, "omitInWrappers");
|
|
197
|
+
function omitSchemaPath(schema, segments) {
|
|
198
|
+
const inWrappers = omitInWrappers(schema, segments);
|
|
199
|
+
const properties = schema.properties;
|
|
200
|
+
const [head, ...rest] = segments;
|
|
201
|
+
if (!properties || !head || !(head in properties)) return inWrappers;
|
|
202
|
+
if (rest.length === 0) return deleteProperty(schema, head);
|
|
203
|
+
const child = properties[head];
|
|
204
|
+
return isSchema(child) && omitSchemaPath(child, rest) || inWrappers;
|
|
205
|
+
}
|
|
206
|
+
__name(omitSchemaPath, "omitSchemaPath");
|
|
207
|
+
function redactResponseSchema(schema, paths, operationId) {
|
|
208
|
+
if (!paths.length) return schema;
|
|
209
|
+
const clone = structuredClone(schema);
|
|
210
|
+
paths.forEach((path) => {
|
|
211
|
+
if (!omitSchemaPath(clone, path.split("."))) {
|
|
212
|
+
const subject = operationId ? `MCP endpoint "${operationId}"` : "This response schema";
|
|
213
|
+
throw new Error(
|
|
214
|
+
`${subject} declares a response redaction for "${path}", which is not a field of its response schema`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
return clone;
|
|
219
|
+
}
|
|
220
|
+
__name(redactResponseSchema, "redactResponseSchema");
|
|
171
221
|
function stripPath(value, segments) {
|
|
172
222
|
if (value === null || typeof value !== "object") return;
|
|
173
223
|
if (Array.isArray(value)) {
|
|
@@ -323,6 +373,8 @@ export {
|
|
|
323
373
|
handleMcpJsonRpc,
|
|
324
374
|
inlineSchemaRefs,
|
|
325
375
|
providerForHostId,
|
|
376
|
+
redactResponseBody,
|
|
377
|
+
redactResponseSchema,
|
|
326
378
|
serializeManifest,
|
|
327
379
|
serializeSurfaceLock,
|
|
328
380
|
surfaceDigest,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/openapi/refs.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","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","/**\n * Strip audited-sensitive fields out of a tool result before it reaches the\n * agent.\n *\n * The dispatcher forwards the wrapped endpoint's response body verbatim, so a\n * narrowed `outputSchema` alone would only change what the manifest CLAIMS is\n * returned. This module is what actually removes the value, keeping the served\n * payload and the advertised schema in agreement.\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/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","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":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@12-apps/mcp",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"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).",
|
|
6
6
|
"exports": {
|
|
@@ -51,7 +51,8 @@
|
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
53
|
"react": ">=19.0.0",
|
|
54
|
-
"hono": ">=4.0.0"
|
|
54
|
+
"hono": ">=4.0.0",
|
|
55
|
+
"zod": ">=4.0.0"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
57
58
|
"@12-apps/eslint-config": "^1.20.0",
|
|
@@ -67,7 +68,8 @@
|
|
|
67
68
|
"react-dom": "^19.2.0",
|
|
68
69
|
"tsup": "^8.0.0",
|
|
69
70
|
"typescript": "^5.8.2",
|
|
70
|
-
"vitest": "^3.2.4"
|
|
71
|
+
"vitest": "^3.2.4",
|
|
72
|
+
"zod": "^4.3.5"
|
|
71
73
|
},
|
|
72
74
|
"engines": {
|
|
73
75
|
"node": ">=22.0.0"
|
|
@@ -103,6 +105,9 @@
|
|
|
103
105
|
"peerDependenciesMeta": {
|
|
104
106
|
"hono": {
|
|
105
107
|
"optional": true
|
|
108
|
+
},
|
|
109
|
+
"zod": {
|
|
110
|
+
"optional": true
|
|
106
111
|
}
|
|
107
112
|
}
|
|
108
113
|
}
|
package/src/index.ts
CHANGED
|
@@ -28,6 +28,11 @@ export {
|
|
|
28
28
|
} from "./guide";
|
|
29
29
|
export { generateTools } from "./openapi/generate";
|
|
30
30
|
export { inlineSchemaRefs, UnsupportedSchemaError } from "./openapi/refs";
|
|
31
|
+
// The shape a route is DECLARED in, upstream of the OpenAPI document. It lives
|
|
32
|
+
// here so that packages which own a domain can ship that domain's endpoints and
|
|
33
|
+
// a host can concatenate them — which requires all of them to mean the same
|
|
34
|
+
// thing by "an endpoint". See `openapi/endpoint.ts`.
|
|
35
|
+
export type { McpEndpoint, HttpMethod } from "./openapi/endpoint";
|
|
31
36
|
export type {
|
|
32
37
|
OpenApiDocument,
|
|
33
38
|
OpenApiOperation,
|
|
@@ -36,6 +41,11 @@ export type {
|
|
|
36
41
|
OpenApiResponse,
|
|
37
42
|
} from "./openapi/generate";
|
|
38
43
|
export { dispatchTool, DispatchInputError } from "./dispatch/proxy";
|
|
44
|
+
// Both halves of the redaction contract. `redactResponseSchema` narrows what a
|
|
45
|
+
// tool ADVERTISES at generate time; `redactResponseBody` removes the same paths
|
|
46
|
+
// from what it RETURNS at dispatch. Taking one without the other reintroduces the
|
|
47
|
+
// schema/payload disagreement they exist to prevent — see `server/redact.ts`.
|
|
48
|
+
export { redactResponseSchema, redactResponseBody } from "./server/redact";
|
|
39
49
|
export {
|
|
40
50
|
createToolRegistry,
|
|
41
51
|
HTTP_STATUS_META_KEY,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { z } from "zod";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How a route is DECLARED, one step before it becomes an OpenAPI operation and
|
|
5
|
+
* two before it becomes a tool.
|
|
6
|
+
*
|
|
7
|
+
* This package already owns everything downstream of an OpenAPI document —
|
|
8
|
+
* `generateTools` turns operations into tools, `dispatchTool` proxies a call,
|
|
9
|
+
* `redactResponseSchema`/`redactResponseBody` narrow both halves. What it did
|
|
10
|
+
* not own was the shape a consumer writes its routes down in, so every consumer
|
|
11
|
+
* declared its own. That is fine for one app and wrong for several: a monorepo
|
|
12
|
+
* where the shift routes, the lifecycle routes and the audit routes are each
|
|
13
|
+
* packaged separately needs those packages to produce endpoint lists the HOST
|
|
14
|
+
* can concatenate, which they can only do if they all mean the same thing by
|
|
15
|
+
* "an endpoint".
|
|
16
|
+
*
|
|
17
|
+
* Deliberately zod-shaped rather than JSON-Schema-shaped. A route validates its
|
|
18
|
+
* input with zod at runtime; describing it a second time in JSON Schema is a
|
|
19
|
+
* copy that drifts, and the drift is invisible — the manifest keeps advertising
|
|
20
|
+
* the shape the route stopped accepting. Converting zod → JSON Schema at
|
|
21
|
+
* generate time makes the validator the single source of truth.
|
|
22
|
+
*
|
|
23
|
+
* zod is a PEER dependency: it is referenced here as a type only, so this
|
|
24
|
+
* package pulls no copy of its own and cannot end up type-checking against a
|
|
25
|
+
* different one than the consumer declares its schemas with.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/** The methods an MCP-exposed route may use. */
|
|
29
|
+
export type HttpMethod = "get" | "post" | "put" | "patch" | "delete";
|
|
30
|
+
|
|
31
|
+
interface McpEndpointBase {
|
|
32
|
+
/** Stable tool id — this becomes the MCP tool name, so renaming it is a
|
|
33
|
+
* breaking change for every agent that has learned the old one. */
|
|
34
|
+
operationId: string;
|
|
35
|
+
method: HttpMethod;
|
|
36
|
+
/** OpenAPI path template, e.g. `/api/products/{id}`. */
|
|
37
|
+
path: string;
|
|
38
|
+
/** What the tool is FOR, in the words an agent reads when choosing it. */
|
|
39
|
+
summary: string;
|
|
40
|
+
tags?: string[];
|
|
41
|
+
/** Object schema whose properties become query parameters. */
|
|
42
|
+
query?: z.ZodType;
|
|
43
|
+
/** Object schema whose properties become path parameters. */
|
|
44
|
+
params?: z.ZodType;
|
|
45
|
+
/** Request body schema (writes only). */
|
|
46
|
+
body?: z.ZodType;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A declared endpoint either answers 200 with a schema'd JSON body (the
|
|
51
|
+
* default) or 204 No Content (fire-and-forget writes).
|
|
52
|
+
*
|
|
53
|
+
* The union is what makes the two mutually exclusive: a 204 entry cannot carry
|
|
54
|
+
* a response schema, so a manifest can never advertise a body its route will
|
|
55
|
+
* not send — a mismatch an agent experiences as a tool that returns nothing
|
|
56
|
+
* where its own schema promised an object.
|
|
57
|
+
*/
|
|
58
|
+
export type McpEndpoint = McpEndpointBase &
|
|
59
|
+
(
|
|
60
|
+
| {
|
|
61
|
+
/** Success status (defaults to 200 with a JSON body). */
|
|
62
|
+
status?: 200;
|
|
63
|
+
/** Success (200) response schema. */
|
|
64
|
+
response: z.ZodType;
|
|
65
|
+
}
|
|
66
|
+
| {
|
|
67
|
+
/** 204 No Content — no response schema. */
|
|
68
|
+
status: 204;
|
|
69
|
+
response?: never;
|
|
70
|
+
}
|
|
71
|
+
);
|
package/src/server/redact.ts
CHANGED
|
@@ -1,12 +1,102 @@
|
|
|
1
|
+
import type { JsonSchema } from "../types";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
+
* Both halves of the redaction contract: the SCHEMA a tool advertises, and the
|
|
5
|
+
* BODY it returns.
|
|
6
|
+
*
|
|
7
|
+
* They only work as a pair, and the failure mode when they disagree is not a
|
|
8
|
+
* cosmetic one. The dispatcher forwards the wrapped endpoint's response body
|
|
9
|
+
* verbatim, so a narrowed `outputSchema` alone would only change what the
|
|
10
|
+
* manifest CLAIMS is returned — the value still reaches the agent. Strip the
|
|
11
|
+
* body but leave the schema advertising the field, and every successful call
|
|
12
|
+
* fails validation against the very schema the manifest published, worst of all
|
|
13
|
+
* when the field was `required`.
|
|
4
14
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
15
|
+
* So `redactResponseSchema` removes the paths at generate time and
|
|
16
|
+
* `redactResponseBody` removes the same paths at dispatch, from one list. A host
|
|
17
|
+
* that takes one and hand-rolls the other is back to the disagreement this pair
|
|
18
|
+
* exists to prevent.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** Structural keywords that wrap a value without naming a field. */
|
|
22
|
+
const SCHEMA_WRAPPERS = ["items", "anyOf", "oneOf", "allOf"] as const;
|
|
23
|
+
|
|
24
|
+
function isSchema(value: unknown): value is JsonSchema {
|
|
25
|
+
return Boolean(value) && typeof value === "object";
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Drop `field` from an object schema's properties AND its `required` list. */
|
|
29
|
+
function deleteProperty(schema: JsonSchema, field: string): true {
|
|
30
|
+
delete (schema.properties as Record<string, JsonSchema>)[field];
|
|
31
|
+
if (Array.isArray(schema.required)) {
|
|
32
|
+
const kept = (schema.required as string[]).filter((name) => name !== field);
|
|
33
|
+
if (kept.length) schema.required = kept;
|
|
34
|
+
else delete schema.required;
|
|
35
|
+
}
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Descend into wrappers without consuming a segment, so `data.taxId` addresses
|
|
41
|
+
* an array of rows and a `.nullable()` union branch alike.
|
|
9
42
|
*/
|
|
43
|
+
function omitInWrappers(schema: JsonSchema, segments: readonly string[]): boolean {
|
|
44
|
+
return SCHEMA_WRAPPERS.reduce((removed, keyword) => {
|
|
45
|
+
const child = schema[keyword];
|
|
46
|
+
const branches = Array.isArray(child) ? child : [child];
|
|
47
|
+
return branches.reduce<boolean>(
|
|
48
|
+
(acc, branch) => (isSchema(branch) && omitSchemaPath(branch, segments)) || acc,
|
|
49
|
+
removed,
|
|
50
|
+
);
|
|
51
|
+
}, false);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function omitSchemaPath(schema: JsonSchema, segments: readonly string[]): boolean {
|
|
55
|
+
const inWrappers = omitInWrappers(schema, segments);
|
|
56
|
+
|
|
57
|
+
const properties = schema.properties as Record<string, JsonSchema> | undefined;
|
|
58
|
+
const [head, ...rest] = segments;
|
|
59
|
+
if (!properties || !head || !(head in properties)) return inWrappers;
|
|
60
|
+
|
|
61
|
+
if (rest.length === 0) return deleteProperty(schema, head);
|
|
62
|
+
|
|
63
|
+
const child = properties[head];
|
|
64
|
+
return (isSchema(child) && omitSchemaPath(child, rest)) || inWrappers;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Return `schema` without the listed dotted paths — the advertised half.
|
|
69
|
+
*
|
|
70
|
+
* THROWS when a path names nothing, rather than returning quietly: a typo'd or
|
|
71
|
+
* stale redaction would otherwise protect nothing at all, and it would do so
|
|
72
|
+
* invisibly, which is the one outcome a redaction list must never have. Failing
|
|
73
|
+
* here turns it into a generator error naming the offending path.
|
|
74
|
+
*
|
|
75
|
+
* The input is cloned, not narrowed in place: a caller may hold the converted
|
|
76
|
+
* schema for other uses (a shared `$defs` component, a schema reused across two
|
|
77
|
+
* operations), and mutating it would redact those too.
|
|
78
|
+
*
|
|
79
|
+
* `operationId` only shapes the error message — pass it so the failure names
|
|
80
|
+
* which tool declared the bad path.
|
|
81
|
+
*/
|
|
82
|
+
export function redactResponseSchema(
|
|
83
|
+
schema: JsonSchema,
|
|
84
|
+
paths: readonly string[],
|
|
85
|
+
operationId?: string,
|
|
86
|
+
): JsonSchema {
|
|
87
|
+
if (!paths.length) return schema;
|
|
88
|
+
|
|
89
|
+
const clone = structuredClone(schema);
|
|
90
|
+
paths.forEach((path) => {
|
|
91
|
+
if (!omitSchemaPath(clone, path.split("."))) {
|
|
92
|
+
const subject = operationId ? `MCP endpoint "${operationId}"` : "This response schema";
|
|
93
|
+
throw new Error(
|
|
94
|
+
`${subject} declares a response redaction for "${path}", which is not a field of its response schema`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
return clone;
|
|
99
|
+
}
|
|
10
100
|
|
|
11
101
|
/** Walk one dotted path, mapping over arrays, and delete the leaf. */
|
|
12
102
|
function stripPath(value: unknown, segments: readonly string[]): void {
|