@mastra/mcp 2.5.0 → 2.6.0-alpha.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/dist/cli/generate-K-rW1Wck.js +219 -0
- package/dist/cli/generate-K-rW1Wck.js.map +1 -0
- package/dist/cli/generate.d.ts +18 -0
- package/dist/cli/generate.d.ts.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +33 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/json-schema-dialect-B5B7l9Po.js +133 -0
- package/dist/cli/json-schema-dialect-B5B7l9Po.js.map +1 -0
- package/dist/cli/typegen-CLDjVF16.js +364 -0
- package/dist/cli/typegen-CLDjVF16.js.map +1 -0
- package/dist/cli/typegen.d.ts +14 -0
- package/dist/cli/typegen.d.ts.map +1 -0
- package/dist/client/client.d.ts +2 -1
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/configuration.d.ts +30 -10
- package/dist/client/configuration.d.ts.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/types.d.ts +38 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/docs/SKILL.md +2 -3
- package/dist/docs/assets/SOURCE_MAP.json +1 -1
- package/dist/docs/references/docs-connections-mcp.md +159 -50
- package/dist/docs/references/docs-connections-overview.md +1 -1
- package/dist/docs/references/docs-guides-mcp-authentication-authorization.md +2 -2
- package/dist/docs/references/reference-migrations-mcp-v2.md +1 -1
- package/dist/docs/references/reference-tools-mcp-client.md +202 -11
- package/dist/docs/references/reference-tools-mcp-server.md +5 -3
- package/dist/index.cjs +116 -121
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +116 -121
- package/dist/index.js.map +1 -1
- package/dist/server/server.d.ts +13 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/shared/json-schema-dialect.d.ts +34 -0
- package/dist/shared/json-schema-dialect.d.ts.map +1 -1
- package/package.json +18 -13
- package/dist/docs/references/docs-guides-mcp-events.md +0 -306
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { n as MAX_SCHEMA_VALUES, r as countJsonValues, t as MAX_CATALOG_VALUES } from "./json-schema-dialect-B5B7l9Po.js";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { Worker } from "node:worker_threads";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { lstat, mkdir, realpath, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
6
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
7
|
+
import { pathToFileURL } from "node:url";
|
|
8
|
+
import { noopLogger } from "@mastra/core/logger";
|
|
9
|
+
import { MCPClient } from "@mastra/mcp";
|
|
10
|
+
//#region src/cli/generate.ts
|
|
11
|
+
/** Failure whose message is safe to print: it is built from supplied paths and server names only. */
|
|
12
|
+
var GenerationError = class extends Error {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = "GenerationError";
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
/** A catalogue can widen thousands of schemas; report enough to act on without flooding a terminal. */
|
|
19
|
+
const MAX_REPORTED_WARNINGS = 25;
|
|
20
|
+
let CommonJSClient;
|
|
21
|
+
function isClient(value) {
|
|
22
|
+
if (value instanceof MCPClient) return true;
|
|
23
|
+
if (!value || typeof value !== "object") return false;
|
|
24
|
+
CommonJSClient ??= createRequire(import.meta.url)("@mastra/mcp").MCPClient;
|
|
25
|
+
return value instanceof CommonJSClient;
|
|
26
|
+
}
|
|
27
|
+
function missing(error) {
|
|
28
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Apply the value budget to a discovered catalogue before it crosses to the worker. The transfer
|
|
32
|
+
* copies whatever is sent, so an oversized schema has to be widened while it is still only being
|
|
33
|
+
* counted rather than after it has been copied in full. Widening to `true` generates `unknown`,
|
|
34
|
+
* which is the same contract the worker produces for a schema it widens itself.
|
|
35
|
+
*/
|
|
36
|
+
function boundTransfer(catalog) {
|
|
37
|
+
const bounded = {};
|
|
38
|
+
const warnings = [];
|
|
39
|
+
let values = 0;
|
|
40
|
+
for (const server of Object.keys(catalog)) {
|
|
41
|
+
const tools = {};
|
|
42
|
+
bounded[server] = tools;
|
|
43
|
+
for (const tool of Object.keys(catalog[server])) {
|
|
44
|
+
const definition = { ...catalog[server][tool] };
|
|
45
|
+
for (const key of ["inputSchema", "outputSchema"]) {
|
|
46
|
+
const schema = definition[key];
|
|
47
|
+
if (schema === void 0) continue;
|
|
48
|
+
const counted = countJsonValues(schema, MAX_SCHEMA_VALUES);
|
|
49
|
+
values += counted;
|
|
50
|
+
if (counted > 1e5 || values > 1e6) {
|
|
51
|
+
definition[key] = true;
|
|
52
|
+
const reason = counted > 1e5 ? `Schema exceeds the maximum value count of ${MAX_SCHEMA_VALUES}` : `Catalogue exceeds the maximum value count of ${MAX_CATALOG_VALUES}`;
|
|
53
|
+
warnings.push(`${reason} at server ${JSON.stringify(server)} tool ${JSON.stringify(tool)} ${key === "inputSchema" ? "input" : "output"}; widened to unknown`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
tools[tool] = definition;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
catalog: bounded,
|
|
61
|
+
warnings
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function canonical(path) {
|
|
65
|
+
try {
|
|
66
|
+
return await realpath(path);
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (!missing(error)) throw error;
|
|
69
|
+
if ((await lstat(path).catch(() => void 0))?.isSymbolicLink()) throw new GenerationError("Output path contains a dangling symlink");
|
|
70
|
+
const parent = dirname(path);
|
|
71
|
+
if (parent === path) throw error;
|
|
72
|
+
return join(await canonical(parent), basename(path));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Isolate the converter's environment-controlled logging from imported application code. */
|
|
76
|
+
function convert(definitions, signal) {
|
|
77
|
+
return new Promise((accept, reject) => {
|
|
78
|
+
const source = import.meta.url.endsWith(".ts");
|
|
79
|
+
const entry = new URL(source ? "./index.ts" : "./index.js", import.meta.url).href;
|
|
80
|
+
const worker = new Worker(`const { workerData } = require('node:worker_threads');
|
|
81
|
+
(async () => {
|
|
82
|
+
if (workerData.source) { const { register } = await import(workerData.loader); register(); }
|
|
83
|
+
await import(workerData.entry);
|
|
84
|
+
})().catch(() => process.exitCode = 1);`, {
|
|
85
|
+
eval: true,
|
|
86
|
+
execArgv: [],
|
|
87
|
+
env: {
|
|
88
|
+
...process.env,
|
|
89
|
+
VERBOSE: ""
|
|
90
|
+
},
|
|
91
|
+
workerData: {
|
|
92
|
+
typegen: true,
|
|
93
|
+
source,
|
|
94
|
+
loader: import.meta.resolve("tsx/esm/api"),
|
|
95
|
+
entry,
|
|
96
|
+
definitions
|
|
97
|
+
},
|
|
98
|
+
stdout: true,
|
|
99
|
+
stderr: true
|
|
100
|
+
});
|
|
101
|
+
worker.stdout.resume();
|
|
102
|
+
worker.stderr.resume();
|
|
103
|
+
const abort = () => {
|
|
104
|
+
worker.terminate();
|
|
105
|
+
};
|
|
106
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
107
|
+
if (signal.aborted) abort();
|
|
108
|
+
worker.once("message", (result) => {
|
|
109
|
+
if (result.ok) accept(result.value);
|
|
110
|
+
else reject(new GenerationError(result.message ?? "Schema conversion failed"));
|
|
111
|
+
});
|
|
112
|
+
worker.once("error", () => reject(new GenerationError("Schema conversion failed")));
|
|
113
|
+
worker.once("exit", (code) => {
|
|
114
|
+
signal.removeEventListener("abort", abort);
|
|
115
|
+
if (code !== 0 || signal.aborted) reject(new GenerationError("Schema conversion interrupted"));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
/** Internal command runner. Errors are deliberately independent of imported error messages. */
|
|
120
|
+
async function generate(files, cwd = process.cwd()) {
|
|
121
|
+
const clients = /* @__PURE__ */ new Set();
|
|
122
|
+
const staged = [];
|
|
123
|
+
const abort = new AbortController();
|
|
124
|
+
const cleanup = /* @__PURE__ */ new Map();
|
|
125
|
+
const disconnect = () => {
|
|
126
|
+
for (const client of clients) if (!cleanup.has(client)) cleanup.set(client, Promise.resolve().then(() => client.disconnect()));
|
|
127
|
+
return Promise.allSettled(cleanup.values());
|
|
128
|
+
};
|
|
129
|
+
const interrupt = () => {
|
|
130
|
+
abort.abort();
|
|
131
|
+
disconnect();
|
|
132
|
+
};
|
|
133
|
+
const { register } = await import("tsx/esm/api");
|
|
134
|
+
const unregister = register();
|
|
135
|
+
process.once("SIGINT", interrupt);
|
|
136
|
+
process.once("SIGTERM", interrupt);
|
|
137
|
+
let failure;
|
|
138
|
+
try {
|
|
139
|
+
const inputs = /* @__PURE__ */ new Set();
|
|
140
|
+
for (const file of files) {
|
|
141
|
+
const input = await realpath(resolve(cwd, file));
|
|
142
|
+
if (!(await stat(input)).isFile()) throw new GenerationError("Client input must be a file");
|
|
143
|
+
inputs.add(input);
|
|
144
|
+
}
|
|
145
|
+
for (const input of inputs) {
|
|
146
|
+
abort.signal.throwIfAborted();
|
|
147
|
+
const module = await import(pathToFileURL(input).href);
|
|
148
|
+
let eligible = false;
|
|
149
|
+
for (const value of Object.values(module)) if (isClient(value) && value.typegen) {
|
|
150
|
+
eligible = true;
|
|
151
|
+
clients.add(value);
|
|
152
|
+
value.__setLogger(noopLogger);
|
|
153
|
+
}
|
|
154
|
+
if (!eligible) throw new GenerationError("A supplied file has no exported typegen-enabled MCPClient");
|
|
155
|
+
}
|
|
156
|
+
const outputs = /* @__PURE__ */ new Map();
|
|
157
|
+
for (const client of clients) {
|
|
158
|
+
const output = await canonical(resolve(cwd, client.typegen.outFile));
|
|
159
|
+
if (inputs.has(output)) throw new GenerationError("Output overlaps a supplied client file");
|
|
160
|
+
if (outputs.has(output)) throw new GenerationError("Multiple clients target the same output file");
|
|
161
|
+
const existing = await stat(output).catch((error) => {
|
|
162
|
+
if (!missing(error)) throw error;
|
|
163
|
+
});
|
|
164
|
+
if (existing && !existing.isFile()) throw new GenerationError("Output must be a file");
|
|
165
|
+
outputs.set(output, client);
|
|
166
|
+
}
|
|
167
|
+
const prepared = [];
|
|
168
|
+
const warnings = [];
|
|
169
|
+
for (const [output, client] of outputs) {
|
|
170
|
+
abort.signal.throwIfAborted();
|
|
171
|
+
const result = await client.listToolDefinitionsWithErrors();
|
|
172
|
+
const failed = [.../* @__PURE__ */ new Set([...Object.keys(result.errors), ...Object.keys(result.errorDetails)])];
|
|
173
|
+
if (failed.length) throw new GenerationError(`MCP discovery failed for ${failed.map((name) => JSON.stringify(name)).join(", ")}; no generated files were replaced`);
|
|
174
|
+
const bounded = boundTransfer(result.definitions);
|
|
175
|
+
warnings.push(...bounded.warnings);
|
|
176
|
+
const converted = await convert(bounded.catalog, abort.signal);
|
|
177
|
+
warnings.push(...converted.warnings);
|
|
178
|
+
prepared.push({
|
|
179
|
+
output,
|
|
180
|
+
source: converted.source
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
if ((await disconnect()).some((result) => result.status === "rejected")) throw new GenerationError("MCP cleanup failed");
|
|
184
|
+
for (const { output, source } of prepared) {
|
|
185
|
+
abort.signal.throwIfAborted();
|
|
186
|
+
await mkdir(dirname(output), { recursive: true });
|
|
187
|
+
const temporary = join(dirname(output), `.mastra-mcp-${randomUUID()}.tmp`);
|
|
188
|
+
staged.push({
|
|
189
|
+
temporary,
|
|
190
|
+
output
|
|
191
|
+
});
|
|
192
|
+
await writeFile(temporary, source, { flag: "wx" });
|
|
193
|
+
}
|
|
194
|
+
for (const { temporary, output } of staged) {
|
|
195
|
+
abort.signal.throwIfAborted();
|
|
196
|
+
await rename(temporary, output);
|
|
197
|
+
}
|
|
198
|
+
console.log(`Generated ${prepared.length} MCP type file(s).`);
|
|
199
|
+
if (warnings.length) {
|
|
200
|
+
console.warn(`${warnings.length} schema portion(s) widened to unknown:`);
|
|
201
|
+
for (const warning of warnings.slice(0, MAX_REPORTED_WARNINGS)) console.warn(` ${warning}`);
|
|
202
|
+
if (warnings.length > MAX_REPORTED_WARNINGS) console.warn(` ...and ${warnings.length - MAX_REPORTED_WARNINGS} more.`);
|
|
203
|
+
}
|
|
204
|
+
} catch (error) {
|
|
205
|
+
failure = error instanceof GenerationError ? error : new GenerationError("MCP generation failed; check client modules, permissions, and server availability");
|
|
206
|
+
} finally {
|
|
207
|
+
const results = await disconnect();
|
|
208
|
+
const removed = await Promise.allSettled(staged.map(({ temporary }) => rm(temporary, { force: true })));
|
|
209
|
+
process.removeListener("SIGINT", interrupt);
|
|
210
|
+
process.removeListener("SIGTERM", interrupt);
|
|
211
|
+
await unregister();
|
|
212
|
+
if (results.some((result) => result.status === "rejected") || removed.some((result) => result.status === "rejected")) failure ??= new GenerationError("MCP cleanup failed");
|
|
213
|
+
}
|
|
214
|
+
if (failure) throw failure;
|
|
215
|
+
}
|
|
216
|
+
//#endregion
|
|
217
|
+
export { generate };
|
|
218
|
+
|
|
219
|
+
//# sourceMappingURL=generate-K-rW1Wck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate-K-rW1Wck.js","names":[],"sources":["../../src/cli/generate.ts"],"sourcesContent":["import { randomUUID } from 'node:crypto';\nimport { lstat, mkdir, realpath, rename, rm, stat, writeFile } from 'node:fs/promises';\nimport { createRequire } from 'node:module';\nimport { basename, dirname, join, resolve } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { Worker } from 'node:worker_threads';\nimport { noopLogger } from '@mastra/core/logger';\nimport { MCPClient } from '@mastra/mcp';\nimport type { SerializableMCPToolCatalog, SerializableMCPToolDefinition } from '../client/types';\nimport { countJsonValues, MAX_CATALOG_VALUES, MAX_SCHEMA_VALUES } from '../shared/json-schema-dialect';\n\n/** Failure whose message is safe to print: it is built from supplied paths and server names only. */\nexport class GenerationError extends Error {\n constructor(message: string) {\n super(message);\n // Checked by name rather than `instanceof` so the distinction survives bundling and separate\n // module registries, exactly as the CommonJS/ESM client identity does.\n this.name = 'GenerationError';\n }\n}\n\n/** A catalogue can widen thousands of schemas; report enough to act on without flooding a terminal. */\nconst MAX_REPORTED_WARNINGS = 25;\n\nlet CommonJSClient: typeof MCPClient | undefined;\nfunction isClient(value: unknown): value is MCPClient {\n if (value instanceof MCPClient) return true;\n if (!value || typeof value !== 'object') return false;\n CommonJSClient ??= createRequire(import.meta.url)('@mastra/mcp').MCPClient;\n return value instanceof CommonJSClient!;\n}\n\nfunction missing(error: unknown): boolean {\n return error instanceof Error && 'code' in error && error.code === 'ENOENT';\n}\n\n/**\n * Apply the value budget to a discovered catalogue before it crosses to the worker. The transfer\n * copies whatever is sent, so an oversized schema has to be widened while it is still only being\n * counted rather than after it has been copied in full. Widening to `true` generates `unknown`,\n * which is the same contract the worker produces for a schema it widens itself.\n */\nexport function boundTransfer(catalog: SerializableMCPToolCatalog): {\n catalog: SerializableMCPToolCatalog;\n warnings: string[];\n} {\n const bounded: SerializableMCPToolCatalog = {};\n const warnings: string[] = [];\n let values = 0;\n for (const server of Object.keys(catalog)) {\n const tools: Record<string, SerializableMCPToolDefinition> = {};\n bounded[server] = tools;\n for (const tool of Object.keys(catalog[server]!)) {\n const definition = { ...catalog[server]![tool]! };\n for (const key of ['inputSchema', 'outputSchema'] as const) {\n const schema = definition[key];\n if (schema === undefined) continue;\n const counted = countJsonValues(schema, MAX_SCHEMA_VALUES);\n values += counted;\n if (counted > MAX_SCHEMA_VALUES || values > MAX_CATALOG_VALUES) {\n definition[key] = true;\n const reason =\n counted > MAX_SCHEMA_VALUES\n ? `Schema exceeds the maximum value count of ${MAX_SCHEMA_VALUES}`\n : `Catalogue exceeds the maximum value count of ${MAX_CATALOG_VALUES}`;\n warnings.push(\n `${reason} at server ${JSON.stringify(server)} tool ${JSON.stringify(tool)} ${\n key === 'inputSchema' ? 'input' : 'output'\n }; widened to unknown`,\n );\n }\n }\n tools[tool] = definition;\n }\n }\n return { catalog: bounded, warnings };\n}\n\nasync function canonical(path: string): Promise<string> {\n try {\n return await realpath(path);\n } catch (error) {\n if (!missing(error)) throw error;\n // A dangling symlink must not become a new, unrelated output file.\n const entry = await lstat(path).catch(() => undefined);\n if (entry?.isSymbolicLink()) throw new GenerationError('Output path contains a dangling symlink');\n const parent = dirname(path);\n if (parent === path) throw error;\n return join(await canonical(parent), basename(path));\n }\n}\n\n/** Isolate the converter's environment-controlled logging from imported application code. */\nfunction convert(\n definitions: SerializableMCPToolCatalog,\n signal: AbortSignal,\n): Promise<{ source: string; warnings: string[] }> {\n return new Promise((accept, reject) => {\n const source = import.meta.url.endsWith('.ts');\n const entry = new URL(source ? './index.ts' : './index.js', import.meta.url).href;\n const bootstrap = `const { workerData } = require('node:worker_threads');\n (async () => {\n if (workerData.source) { const { register } = await import(workerData.loader); register(); }\n await import(workerData.entry);\n })().catch(() => process.exitCode = 1);`;\n const worker = new Worker(bootstrap, {\n eval: true,\n execArgv: [],\n env: { ...process.env, VERBOSE: '' },\n workerData: { typegen: true, source, loader: import.meta.resolve('tsx/esm/api'), entry, definitions },\n stdout: true,\n stderr: true,\n });\n worker.stdout.resume();\n worker.stderr.resume();\n const abort = () => {\n void worker.terminate();\n };\n signal.addEventListener('abort', abort, { once: true });\n if (signal.aborted) abort();\n worker.once('message', result => {\n if (result.ok) accept(result.value);\n // Only the generator's own `TypegenError` messages cross this boundary; converter failures\n // stay generic so their text cannot carry schema values into a diagnostic.\n else reject(new GenerationError(result.message ?? 'Schema conversion failed'));\n });\n worker.once('error', () => reject(new GenerationError('Schema conversion failed')));\n worker.once('exit', code => {\n signal.removeEventListener('abort', abort);\n if (code !== 0 || signal.aborted) reject(new GenerationError('Schema conversion interrupted'));\n });\n });\n}\n\n/** Internal command runner. Errors are deliberately independent of imported error messages. */\nexport async function generate(files: string[], cwd = process.cwd()): Promise<void> {\n const clients = new Set<MCPClient>();\n const staged: { temporary: string; output: string }[] = [];\n const abort = new AbortController();\n const cleanup = new Map<MCPClient, Promise<void>>();\n const disconnect = () => {\n for (const client of clients) {\n if (!cleanup.has(client))\n cleanup.set(\n client,\n Promise.resolve().then(() => client.disconnect()),\n );\n }\n return Promise.allSettled(cleanup.values());\n };\n const interrupt = () => {\n abort.abort();\n void disconnect();\n };\n // Loaded here rather than at module scope: a top-level `tsx/esm/api` specifier\n // sorts differently against the `@mastra/mcp` self-reference depending on how\n // that self-reference resolves, which makes the import order environment-dependent.\n const { register } = await import('tsx/esm/api');\n const unregister = register();\n process.once('SIGINT', interrupt);\n process.once('SIGTERM', interrupt);\n let failure: unknown;\n try {\n const inputs = new Set<string>();\n for (const file of files) {\n const input = await realpath(resolve(cwd, file));\n if (!(await stat(input)).isFile()) throw new GenerationError('Client input must be a file');\n inputs.add(input);\n }\n for (const input of inputs) {\n abort.signal.throwIfAborted();\n const module = await import(pathToFileURL(input).href);\n let eligible = false;\n for (const value of Object.values(module)) {\n if (isClient(value) && value.typegen) {\n eligible = true;\n clients.add(value);\n value.__setLogger(noopLogger);\n }\n }\n if (!eligible) throw new GenerationError('A supplied file has no exported typegen-enabled MCPClient');\n }\n const outputs = new Map<string, MCPClient>();\n for (const client of clients) {\n const output = await canonical(resolve(cwd, client.typegen!.outFile));\n if (inputs.has(output)) throw new GenerationError('Output overlaps a supplied client file');\n if (outputs.has(output)) throw new GenerationError('Multiple clients target the same output file');\n const existing = await stat(output).catch(error => {\n if (!missing(error)) throw error;\n return undefined;\n });\n if (existing && !existing.isFile()) throw new GenerationError('Output must be a file');\n outputs.set(output, client);\n }\n const prepared: { output: string; source: string }[] = [];\n const warnings: string[] = [];\n for (const [output, client] of outputs) {\n abort.signal.throwIfAborted();\n const result = await client.listToolDefinitionsWithErrors();\n const failed = [...new Set([...Object.keys(result.errors), ...Object.keys(result.errorDetails)])];\n if (failed.length) {\n throw new GenerationError(\n `MCP discovery failed for ${failed.map(name => JSON.stringify(name)).join(', ')}; no generated files were replaced`,\n );\n }\n const bounded = boundTransfer(result.definitions);\n warnings.push(...bounded.warnings);\n const converted = await convert(bounded.catalog, abort.signal);\n warnings.push(...converted.warnings);\n prepared.push({ output, source: converted.source });\n }\n // Disconnect before replacement so cleanup failures preserve previous output too.\n if ((await disconnect()).some(result => result.status === 'rejected'))\n throw new GenerationError('MCP cleanup failed');\n for (const { output, source } of prepared) {\n abort.signal.throwIfAborted();\n await mkdir(dirname(output), { recursive: true });\n const temporary = join(dirname(output), `.mastra-mcp-${randomUUID()}.tmp`);\n staged.push({ temporary, output });\n await writeFile(temporary, source, { flag: 'wx' });\n }\n for (const { temporary, output } of staged) {\n abort.signal.throwIfAborted();\n await rename(temporary, output);\n }\n console.log(`Generated ${prepared.length} MCP type file(s).`);\n if (warnings.length) {\n console.warn(`${warnings.length} schema portion(s) widened to unknown:`);\n for (const warning of warnings.slice(0, MAX_REPORTED_WARNINGS)) console.warn(` ${warning}`);\n if (warnings.length > MAX_REPORTED_WARNINGS)\n console.warn(` ...and ${warnings.length - MAX_REPORTED_WARNINGS} more.`);\n }\n } catch (error) {\n failure =\n error instanceof GenerationError\n ? error\n : new GenerationError('MCP generation failed; check client modules, permissions, and server availability');\n } finally {\n const results = await disconnect();\n const removed = await Promise.allSettled(staged.map(({ temporary }) => rm(temporary, { force: true })));\n process.removeListener('SIGINT', interrupt);\n process.removeListener('SIGTERM', interrupt);\n await unregister();\n if (results.some(result => result.status === 'rejected') || removed.some(result => result.status === 'rejected')) {\n failure ??= new GenerationError('MCP cleanup failed');\n }\n }\n if (failure) throw failure;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { SerializableMCPToolCatalog } from '../client/types.js';
|
|
2
|
+
/** Failure whose message is safe to print: it is built from supplied paths and server names only. */
|
|
3
|
+
export declare class GenerationError extends Error {
|
|
4
|
+
constructor(message: string);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Apply the value budget to a discovered catalogue before it crosses to the worker. The transfer
|
|
8
|
+
* copies whatever is sent, so an oversized schema has to be widened while it is still only being
|
|
9
|
+
* counted rather than after it has been copied in full. Widening to `true` generates `unknown`,
|
|
10
|
+
* which is the same contract the worker produces for a schema it widens itself.
|
|
11
|
+
*/
|
|
12
|
+
export declare function boundTransfer(catalog: SerializableMCPToolCatalog): {
|
|
13
|
+
catalog: SerializableMCPToolCatalog;
|
|
14
|
+
warnings: string[];
|
|
15
|
+
};
|
|
16
|
+
/** Internal command runner. Errors are deliberately independent of imported error messages. */
|
|
17
|
+
export declare function generate(files: string[], cwd?: string): Promise<void>;
|
|
18
|
+
//# sourceMappingURL=generate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/cli/generate.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,0BAA0B,EAAiC,MAAM,iBAAiB,CAAC;AAGjG,qGAAqG;AACrG,qBAAa,eAAgB,SAAQ,KAAK;IACxC,YAAY,OAAO,EAAE,MAAM,EAK1B;CACF;AAiBD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,0BAA0B,GAAG;IAClE,OAAO,EAAE,0BAA0B,CAAC;IACpC,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,CA+BA;AA0DD,+FAA+F;AAC/F,wBAAsB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,SAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiHlF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { isMainThread, parentPort, workerData } from "node:worker_threads";
|
|
3
|
+
//#region src/cli/index.ts
|
|
4
|
+
if (!isMainThread && workerData?.typegen === true) {
|
|
5
|
+
const { generateToolTypes, TypegenError } = await import("./typegen-CLDjVF16.js");
|
|
6
|
+
try {
|
|
7
|
+
parentPort.postMessage({
|
|
8
|
+
ok: true,
|
|
9
|
+
value: await generateToolTypes(workerData.definitions)
|
|
10
|
+
});
|
|
11
|
+
} catch (error) {
|
|
12
|
+
parentPort.postMessage({
|
|
13
|
+
ok: false,
|
|
14
|
+
message: error instanceof TypegenError ? error.message : void 0
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
} else {
|
|
18
|
+
const [command, ...files] = process.argv.slice(2);
|
|
19
|
+
if (command !== "generate" || !files.length || files.some((file) => file.startsWith("-"))) {
|
|
20
|
+
console.error("Usage: npx @mastra/mcp generate <client-files...>");
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
} else try {
|
|
23
|
+
const { generate } = await import("./generate-K-rW1Wck.js");
|
|
24
|
+
await generate(files);
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error(error instanceof Error && error.name === "GenerationError" ? error.message : "MCP generation failed. Check client exports, output paths, server access, and schema validity. Files may be partially replaced only if a filesystem replacement fails.");
|
|
27
|
+
process.exitCode = 1;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
export {};
|
|
32
|
+
|
|
33
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/cli/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { isMainThread, parentPort, workerData } from 'node:worker_threads';\n\nif (!isMainThread && workerData?.typegen === true) {\n const { generateToolTypes, TypegenError } = await import('./typegen');\n try {\n parentPort!.postMessage({ ok: true, value: await generateToolTypes(workerData.definitions) });\n } catch (error) {\n // Only the generator's own messages are safe to surface: converter failures can carry schema\n // values, so they stay generic.\n parentPort!.postMessage({ ok: false, message: error instanceof TypegenError ? error.message : undefined });\n }\n} else {\n const [command, ...files] = process.argv.slice(2);\n if (command !== 'generate' || !files.length || files.some(file => file.startsWith('-'))) {\n console.error('Usage: npx @mastra/mcp generate <client-files...>');\n process.exitCode = 1;\n } else {\n try {\n const { generate } = await import('./generate');\n await generate(files);\n } catch (error) {\n // A `GenerationError` message is built from our own text plus supplied paths and server\n // names. Anything else, including a failure inside an imported application module, stays\n // generic so it cannot leak configurations or credentials into the terminal.\n console.error(\n error instanceof Error && error.name === 'GenerationError'\n ? error.message\n : 'MCP generation failed. Check client exports, output paths, server access, and schema validity. Files may be partially replaced only if a filesystem replacement fails.',\n );\n process.exitCode = 1;\n }\n }\n}\n"],"mappings":";;;AAGA,IAAI,CAAC,gBAAgB,YAAY,YAAY,MAAM;CACjD,MAAM,EAAE,mBAAmB,iBAAiB,MAAM,OAAO;CACzD,IAAI;EACF,WAAY,YAAY;GAAE,IAAI;GAAM,OAAO,MAAM,kBAAkB,WAAW,WAAW;EAAE,CAAC;CAC9F,SAAS,OAAO;EAGd,WAAY,YAAY;GAAE,IAAI;GAAO,SAAS,iBAAiB,eAAe,MAAM,UAAU,KAAA;EAAU,CAAC;CAC3G;AACF,OAAO;CACL,MAAM,CAAC,SAAS,GAAG,SAAS,QAAQ,KAAK,MAAM,CAAC;CAChD,IAAI,YAAY,cAAc,CAAC,MAAM,UAAU,MAAM,MAAK,SAAQ,KAAK,WAAW,GAAG,CAAC,GAAG;EACvF,QAAQ,MAAM,mDAAmD;EACjE,QAAQ,WAAW;CACrB,OACE,IAAI;EACF,MAAM,EAAE,aAAa,MAAM,OAAO;EAClC,MAAM,SAAS,KAAK;CACtB,SAAS,OAAO;EAId,QAAQ,MACN,iBAAiB,SAAS,MAAM,SAAS,oBACrC,MAAM,UACN,wKACN;EACA,QAAQ,WAAW;CACrB;AAEJ"}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const MAX_JSON_SCHEMA_NODES = 1e4;
|
|
2
|
+
/**
|
|
3
|
+
* Raw JSON values a single schema may contribute before generation widens it, and the total a whole
|
|
4
|
+
* catalogue may contribute before generation widens the remainder. The node limit above counts only
|
|
5
|
+
* schema-bearing keywords, so these bound the annotation and extension values it deliberately
|
|
6
|
+
* ignores. They live here rather than with the generator so the CLI can apply them without loading
|
|
7
|
+
* the converter.
|
|
8
|
+
*/
|
|
9
|
+
const MAX_SCHEMA_VALUES = MAX_JSON_SCHEMA_NODES * 10;
|
|
10
|
+
const MAX_CATALOG_VALUES = MAX_SCHEMA_VALUES * 10;
|
|
11
|
+
/**
|
|
12
|
+
* Bounds the work a validator or type generator can be asked to do for an untrusted schema.
|
|
13
|
+
* Only schema-bearing keywords are walked, so deeply nested annotation data such as `default`
|
|
14
|
+
* or `examples` does not count. The counted nodes are returned so callers can also budget
|
|
15
|
+
* across a whole catalogue rather than one schema at a time.
|
|
16
|
+
*
|
|
17
|
+
* The keyword lists are intentionally wider than the conversion sets above: this walk has to
|
|
18
|
+
* reach every position that can carry validation rules, including `dependencies`,
|
|
19
|
+
* `prefixItems`, and `contentSchema`.
|
|
20
|
+
*/
|
|
21
|
+
function jsonSchemaComplexity(schema) {
|
|
22
|
+
const seen = /* @__PURE__ */ new Set();
|
|
23
|
+
let nodes = 0;
|
|
24
|
+
const stack = [{
|
|
25
|
+
value: schema,
|
|
26
|
+
depth: 0
|
|
27
|
+
}];
|
|
28
|
+
const schemaMapKeywords = [
|
|
29
|
+
"$defs",
|
|
30
|
+
"definitions",
|
|
31
|
+
"properties",
|
|
32
|
+
"patternProperties",
|
|
33
|
+
"dependentSchemas",
|
|
34
|
+
"dependencies"
|
|
35
|
+
];
|
|
36
|
+
const schemaArrayKeywords = [
|
|
37
|
+
"prefixItems",
|
|
38
|
+
"allOf",
|
|
39
|
+
"anyOf",
|
|
40
|
+
"oneOf",
|
|
41
|
+
"items"
|
|
42
|
+
];
|
|
43
|
+
const schemaKeywords = [
|
|
44
|
+
"additionalProperties",
|
|
45
|
+
"unevaluatedProperties",
|
|
46
|
+
"additionalItems",
|
|
47
|
+
"unevaluatedItems",
|
|
48
|
+
"items",
|
|
49
|
+
"contains",
|
|
50
|
+
"propertyNames",
|
|
51
|
+
"not",
|
|
52
|
+
"if",
|
|
53
|
+
"then",
|
|
54
|
+
"else",
|
|
55
|
+
"contentSchema"
|
|
56
|
+
];
|
|
57
|
+
while (stack.length > 0) {
|
|
58
|
+
const { value, depth } = stack.pop();
|
|
59
|
+
if (value === null || typeof value !== "object" || Array.isArray(value) || seen.has(value)) continue;
|
|
60
|
+
seen.add(value);
|
|
61
|
+
nodes += 1;
|
|
62
|
+
if (depth > 128) return {
|
|
63
|
+
error: `JSON Schema exceeds the maximum depth of 128`,
|
|
64
|
+
limit: "depth",
|
|
65
|
+
nodes
|
|
66
|
+
};
|
|
67
|
+
if (nodes > 1e4) return {
|
|
68
|
+
error: `JSON Schema exceeds the maximum node count of ${MAX_JSON_SCHEMA_NODES}`,
|
|
69
|
+
limit: "nodes",
|
|
70
|
+
nodes
|
|
71
|
+
};
|
|
72
|
+
const record = value;
|
|
73
|
+
for (const keyword of schemaMapKeywords) {
|
|
74
|
+
const schemas = record[keyword];
|
|
75
|
+
if (schemas && typeof schemas === "object" && !Array.isArray(schemas)) for (const child of Object.values(schemas)) stack.push({
|
|
76
|
+
value: child,
|
|
77
|
+
depth: depth + 1
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
for (const keyword of schemaArrayKeywords) {
|
|
81
|
+
const schemas = record[keyword];
|
|
82
|
+
if (Array.isArray(schemas)) for (const child of schemas) stack.push({
|
|
83
|
+
value: child,
|
|
84
|
+
depth: depth + 1
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
for (const keyword of schemaKeywords) if (record[keyword] !== void 0) stack.push({
|
|
88
|
+
value: record[keyword],
|
|
89
|
+
depth: depth + 1
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
return { nodes };
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Counts every value in a raw schema and stops once `limit` is exceeded. Unlike
|
|
96
|
+
* {@link jsonSchemaComplexity}, which counts only schema-bearing keywords, this covers
|
|
97
|
+
* annotation and extension data and boolean subschema entries, so callers that copy or transfer
|
|
98
|
+
* the whole value can bound that work rather than the validation work a schema describes.
|
|
99
|
+
*
|
|
100
|
+
* Children are pulled one at a time, so a container wider than the budget is never expanded.
|
|
101
|
+
* Values already seen are counted once, matching how this walk treats cycles.
|
|
102
|
+
*/
|
|
103
|
+
function countJsonValues(value, limit) {
|
|
104
|
+
const seen = /* @__PURE__ */ new Set();
|
|
105
|
+
const frames = [];
|
|
106
|
+
let nodes = 0;
|
|
107
|
+
function count(current) {
|
|
108
|
+
if (++nodes > limit) return false;
|
|
109
|
+
if (current === null || typeof current !== "object" || seen.has(current)) return true;
|
|
110
|
+
seen.add(current);
|
|
111
|
+
frames.push(containerValues(current));
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (!count(value)) return nodes;
|
|
115
|
+
while (frames.length > 0) {
|
|
116
|
+
const next = frames[frames.length - 1].next();
|
|
117
|
+
if (next.done) frames.pop();
|
|
118
|
+
else if (!count(next.value)) return nodes;
|
|
119
|
+
}
|
|
120
|
+
return nodes;
|
|
121
|
+
}
|
|
122
|
+
/** Yields a JSON container's children without materialising them. */
|
|
123
|
+
function* containerValues(value) {
|
|
124
|
+
if (Array.isArray(value)) {
|
|
125
|
+
for (let index = 0; index < value.length; index += 1) yield value[index];
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
for (const key in value) if (Object.prototype.hasOwnProperty.call(value, key)) yield Reflect.get(value, key);
|
|
129
|
+
}
|
|
130
|
+
//#endregion
|
|
131
|
+
export { jsonSchemaComplexity as i, MAX_SCHEMA_VALUES as n, countJsonValues as r, MAX_CATALOG_VALUES as t };
|
|
132
|
+
|
|
133
|
+
//# sourceMappingURL=json-schema-dialect-B5B7l9Po.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-schema-dialect-B5B7l9Po.js","names":[],"sources":["../../src/shared/json-schema-dialect.ts"],"sourcesContent":["export const JSON_SCHEMA_2020_12 = 'https://json-schema.org/draft/2020-12/schema';\n\nconst JSON_SCHEMA_2019_09 = /^https?:\\/\\/json-schema\\.org\\/draft\\/2019-09\\/schema#?$/;\n\n/** Keywords whose value is a subschema (`items` may also be an array of subschemas). */\nconst SCHEMA_KEYWORDS = new Set([\n 'items',\n 'additionalItems',\n 'contains',\n 'additionalProperties',\n 'propertyNames',\n 'unevaluatedItems',\n 'unevaluatedProperties',\n 'if',\n 'then',\n 'else',\n 'not',\n]);\n/** Keywords whose value is an array of subschemas. */\nconst SCHEMA_ARRAY_KEYWORDS = new Set(['allOf', 'anyOf', 'oneOf']);\n/** Keywords whose values map arbitrary names to subschemas. */\nconst SCHEMA_MAP_KEYWORDS = new Set(['properties', 'patternProperties', '$defs', 'definitions', 'dependentSchemas']);\n\n/** Complexity limits for untrusted schemas; also enforced by the client before validation. */\nexport const MAX_JSON_SCHEMA_DEPTH = 128;\nexport const MAX_JSON_SCHEMA_NODES = 10_000;\n\n/**\n * Raw JSON values a single schema may contribute before generation widens it, and the total a whole\n * catalogue may contribute before generation widens the remainder. The node limit above counts only\n * schema-bearing keywords, so these bound the annotation and extension values it deliberately\n * ignores. They live here rather than with the generator so the CLI can apply them without loading\n * the converter.\n */\nexport const MAX_SCHEMA_VALUES = MAX_JSON_SCHEMA_NODES * 10;\nexport const MAX_CATALOG_VALUES = MAX_SCHEMA_VALUES * 10;\n\nclass Unconvertible extends Error {}\n\n/**\n * Bounds the work a validator or type generator can be asked to do for an untrusted schema.\n * Only schema-bearing keywords are walked, so deeply nested annotation data such as `default`\n * or `examples` does not count. The counted nodes are returned so callers can also budget\n * across a whole catalogue rather than one schema at a time.\n *\n * The keyword lists are intentionally wider than the conversion sets above: this walk has to\n * reach every position that can carry validation rules, including `dependencies`,\n * `prefixItems`, and `contentSchema`.\n */\nexport function jsonSchemaComplexity(schema: unknown): {\n error?: string;\n limit?: 'depth' | 'nodes';\n nodes: number;\n} {\n const seen = new Set<object>();\n let nodes = 0;\n const stack = [{ value: schema, depth: 0 }];\n const schemaMapKeywords = [\n '$defs',\n 'definitions',\n 'properties',\n 'patternProperties',\n 'dependentSchemas',\n 'dependencies',\n ];\n const schemaArrayKeywords = ['prefixItems', 'allOf', 'anyOf', 'oneOf', 'items'];\n const schemaKeywords = [\n 'additionalProperties',\n 'unevaluatedProperties',\n 'additionalItems',\n 'unevaluatedItems',\n 'items',\n 'contains',\n 'propertyNames',\n 'not',\n 'if',\n 'then',\n 'else',\n 'contentSchema',\n ];\n\n while (stack.length > 0) {\n const { value, depth } = stack.pop()!;\n if (value === null || typeof value !== 'object' || Array.isArray(value) || seen.has(value)) continue;\n seen.add(value);\n\n nodes += 1;\n if (depth > MAX_JSON_SCHEMA_DEPTH) {\n return {\n error: `JSON Schema exceeds the maximum depth of ${MAX_JSON_SCHEMA_DEPTH}`,\n limit: 'depth',\n nodes,\n };\n }\n if (nodes > MAX_JSON_SCHEMA_NODES) {\n return {\n error: `JSON Schema exceeds the maximum node count of ${MAX_JSON_SCHEMA_NODES}`,\n limit: 'nodes',\n nodes,\n };\n }\n\n const record = value as Record<string, unknown>;\n for (const keyword of schemaMapKeywords) {\n const schemas = record[keyword];\n if (schemas && typeof schemas === 'object' && !Array.isArray(schemas)) {\n for (const child of Object.values(schemas)) stack.push({ value: child, depth: depth + 1 });\n }\n }\n for (const keyword of schemaArrayKeywords) {\n const schemas = record[keyword];\n if (Array.isArray(schemas)) {\n for (const child of schemas) stack.push({ value: child, depth: depth + 1 });\n }\n }\n for (const keyword of schemaKeywords) {\n if (record[keyword] !== undefined) stack.push({ value: record[keyword], depth: depth + 1 });\n }\n }\n\n return { nodes };\n}\n\n/**\n * Counts every value in a raw schema and stops once `limit` is exceeded. Unlike\n * {@link jsonSchemaComplexity}, which counts only schema-bearing keywords, this covers\n * annotation and extension data and boolean subschema entries, so callers that copy or transfer\n * the whole value can bound that work rather than the validation work a schema describes.\n *\n * Children are pulled one at a time, so a container wider than the budget is never expanded.\n * Values already seen are counted once, matching how this walk treats cycles.\n */\nexport function countJsonValues(value: unknown, limit: number): number {\n const seen = new Set<object>();\n const frames: Array<Generator<unknown>> = [];\n let nodes = 0;\n\n function count(current: unknown): boolean {\n if (++nodes > limit) return false;\n if (current === null || typeof current !== 'object' || seen.has(current)) return true;\n seen.add(current);\n frames.push(containerValues(current));\n return true;\n }\n\n if (!count(value)) return nodes;\n while (frames.length > 0) {\n const next = frames[frames.length - 1]!.next();\n if (next.done) frames.pop();\n else if (!count(next.value)) return nodes;\n }\n\n return nodes;\n}\n\n/** Yields a JSON container's children without materialising them. */\nfunction* containerValues(value: object): Generator<unknown> {\n if (Array.isArray(value)) {\n for (let index = 0; index < value.length; index += 1) yield value[index];\n return;\n }\n for (const key in value) {\n if (Object.prototype.hasOwnProperty.call(value, key)) yield Reflect.get(value, key);\n }\n}\n\n/**\n * Converts a 2019-09 schema (e.g. from zod v3) into 2020-12 form. Tuples are the\n * only structural difference handled: array-form `items` becomes `prefixItems`,\n * and `additionalItems` becomes `items`.\n *\n * Returns `undefined` when the schema does not declare 2019-09, or relies on\n * features this conversion cannot preserve (`$recursiveRef`/`$recursiveAnchor`,\n * `$ref` pointers into tuple members, embedded `$schema` declarations,\n * `unevaluatedItems`, `prefixItems`, or `contentSchema`), or exceeds the depth/node limits.\n * Callers should then keep the original.\n */\nexport function toJsonSchema2020<T extends { $schema?: string }>(schema: T): T | undefined {\n if (!schema.$schema || !JSON_SCHEMA_2019_09.test(schema.$schema)) return undefined;\n try {\n return { ...(rewrite(schema, 0, { nodes: 0 }, true) as T), $schema: JSON_SCHEMA_2020_12 };\n } catch (error) {\n if (error instanceof Unconvertible) return undefined;\n throw error;\n }\n}\n\nfunction checkBudget(depth: number, budget: { nodes: number }) {\n if (depth > MAX_JSON_SCHEMA_DEPTH || ++budget.nodes > MAX_JSON_SCHEMA_NODES) throw new Unconvertible();\n}\n\n/** Rewrites a value in a subschema position. Only known schema keywords are walked; everything else is copied as-is. */\nfunction rewrite(schema: unknown, depth: number, budget: { nodes: number }, isRoot = false): unknown {\n if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return schema;\n checkBudget(depth, budget);\n\n const out: Record<string, unknown> = { ...schema };\n // Embedded resources may declare their own dialect. unevaluatedItems interacts with contains (including via\n // in-place applicators) differently in 2020-12, prefixItems is an annotation in 2019-09 but an assertion in\n // 2020-12, and contentSchema is not walked. zod v3 emits none of these, so decline rather than risk a change.\n if (!isRoot && '$schema' in out) throw new Unconvertible();\n if ('$recursiveRef' in out || '$recursiveAnchor' in out) throw new Unconvertible();\n if ('unevaluatedItems' in out || 'prefixItems' in out || 'contentSchema' in out) throw new Unconvertible();\n if (typeof out.$ref === 'string' && /\\/(items\\/\\d+|additionalItems)(\\/|$)/.test(out.$ref)) {\n throw new Unconvertible();\n }\n\n for (const [key, child] of Object.entries(out)) {\n if (SCHEMA_KEYWORDS.has(key)) {\n out[key] = Array.isArray(child)\n ? (checkBudget(depth + 1, budget), child.map(item => rewrite(item, depth + 2, budget)))\n : rewrite(child, depth + 1, budget);\n } else if (SCHEMA_ARRAY_KEYWORDS.has(key) && Array.isArray(child)) {\n checkBudget(depth + 1, budget);\n out[key] = child.map(item => rewrite(item, depth + 2, budget));\n } else if (SCHEMA_MAP_KEYWORDS.has(key) && child && typeof child === 'object' && !Array.isArray(child)) {\n checkBudget(depth + 1, budget);\n out[key] = Object.fromEntries(\n Object.entries(child).map(([name, sub]) => [name, rewrite(sub, depth + 2, budget)]),\n );\n }\n }\n if (!Array.isArray(out.items)) return out;\n\n out.prefixItems = out.items;\n if ('additionalItems' in out) out.items = out.additionalItems;\n else delete out.items;\n delete out.additionalItems;\n return out;\n}\n"],"mappings":"AAyBA,MAAa,wBAAwB;;;;;;;;AASrC,MAAa,oBAAoB,wBAAwB;AACzD,MAAa,qBAAqB,oBAAoB;;;;;;;;;;;AActD,SAAgB,qBAAqB,QAInC;CACA,MAAM,uBAAO,IAAI,IAAY;CAC7B,IAAI,QAAQ;CACZ,MAAM,QAAQ,CAAC;EAAE,OAAO;EAAQ,OAAO;CAAE,CAAC;CAC1C,MAAM,oBAAoB;EACxB;EACA;EACA;EACA;EACA;EACA;CACF;CACA,MAAM,sBAAsB;EAAC;EAAe;EAAS;EAAS;EAAS;CAAO;CAC9E,MAAM,iBAAiB;EACrB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACF;CAEA,OAAO,MAAM,SAAS,GAAG;EACvB,MAAM,EAAE,OAAO,UAAU,MAAM,IAAI;EACnC,IAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,MAAM,QAAQ,KAAK,KAAK,KAAK,IAAI,KAAK,GAAG;EAC5F,KAAK,IAAI,KAAK;EAEd,SAAS;EACT,IAAI,QAAA,KACF,OAAO;GACL,OAAO;GACP,OAAO;GACP;EACF;EAEF,IAAI,QAAA,KACF,OAAO;GACL,OAAO,iDAAiD;GACxD,OAAO;GACP;EACF;EAGF,MAAM,SAAS;EACf,KAAK,MAAM,WAAW,mBAAmB;GACvC,MAAM,UAAU,OAAO;GACvB,IAAI,WAAW,OAAO,YAAY,YAAY,CAAC,MAAM,QAAQ,OAAO,GAClE,KAAK,MAAM,SAAS,OAAO,OAAO,OAAO,GAAG,MAAM,KAAK;IAAE,OAAO;IAAO,OAAO,QAAQ;GAAE,CAAC;EAE7F;EACA,KAAK,MAAM,WAAW,qBAAqB;GACzC,MAAM,UAAU,OAAO;GACvB,IAAI,MAAM,QAAQ,OAAO,GACvB,KAAK,MAAM,SAAS,SAAS,MAAM,KAAK;IAAE,OAAO;IAAO,OAAO,QAAQ;GAAE,CAAC;EAE9E;EACA,KAAK,MAAM,WAAW,gBACpB,IAAI,OAAO,aAAa,KAAA,GAAW,MAAM,KAAK;GAAE,OAAO,OAAO;GAAU,OAAO,QAAQ;EAAE,CAAC;CAE9F;CAEA,OAAO,EAAE,MAAM;AACjB;;;;;;;;;;AAWA,SAAgB,gBAAgB,OAAgB,OAAuB;CACrE,MAAM,uBAAO,IAAI,IAAY;CAC7B,MAAM,SAAoC,CAAC;CAC3C,IAAI,QAAQ;CAEZ,SAAS,MAAM,SAA2B;EACxC,IAAI,EAAE,QAAQ,OAAO,OAAO;EAC5B,IAAI,YAAY,QAAQ,OAAO,YAAY,YAAY,KAAK,IAAI,OAAO,GAAG,OAAO;EACjF,KAAK,IAAI,OAAO;EAChB,OAAO,KAAK,gBAAgB,OAAO,CAAC;EACpC,OAAO;CACT;CAEA,IAAI,CAAC,MAAM,KAAK,GAAG,OAAO;CAC1B,OAAO,OAAO,SAAS,GAAG;EACxB,MAAM,OAAO,OAAO,OAAO,SAAS,EAAE,CAAE,KAAK;EAC7C,IAAI,KAAK,MAAM,OAAO,IAAI;OACrB,IAAI,CAAC,MAAM,KAAK,KAAK,GAAG,OAAO;CACtC;CAEA,OAAO;AACT;;AAGA,UAAU,gBAAgB,OAAmC;CAC3D,IAAI,MAAM,QAAQ,KAAK,GAAG;EACxB,KAAK,IAAI,QAAQ,GAAG,QAAQ,MAAM,QAAQ,SAAS,GAAG,MAAM,MAAM;EAClE;CACF;CACA,KAAK,MAAM,OAAO,OAChB,IAAI,OAAO,UAAU,eAAe,KAAK,OAAO,GAAG,GAAG,MAAM,QAAQ,IAAI,OAAO,GAAG;AAEtF"}
|