@afora/mcp 0.0.0-stage → 0.0.4

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/index.js ADDED
@@ -0,0 +1,1087 @@
1
+ // src/guide.ts
2
+ var GUIDE = `# afora.json guide
3
+
4
+ \`afora.json\` describes a domain. \`afora_generate\` turns it into TypeScript on \`@afora/core\`
5
+ (one file per kind of declaration, under \`output.dir\`, default \`src/domain\`). The generated files
6
+ start with "// Generated by @afora/codegen from afora.json. Do not edit." and are overwritten on
7
+ every run, so change the JSON, never those files. To add behavior, extend the generated class in
8
+ a file of your own.
9
+
10
+ ## Workflow
11
+
12
+ 1. \`afora_catalog\`: what you can use (Std properties, record plugins, rules).
13
+ 2. \`afora_spec_read\`: the current spec (or the one extracted from the code).
14
+ 3. Edit with \`afora_spec_upsert\` / \`afora_spec_remove\` (one declaration at a time; the whole
15
+ spec must stay valid, so declare what a declaration uses first) or \`afora_spec_write\`.
16
+ 4. \`afora_spec_validate\`, then \`afora_generate\` (\`dryRun: true\` to preview). The result has the
17
+ TypeScript diagnostics of the generated files: fix the spec until there are none.
18
+
19
+ ## Shape
20
+
21
+ \`\`\`json
22
+ {
23
+ "version": 1,
24
+ "name": "blog",
25
+ "output": { "dir": "src/domain" },
26
+ "provider": "inMemory",
27
+ "properties": { "Age": { "base": "integer", "with": { "min": 0, "max": 150 } },
28
+ "Status": { "enum": ["draft", "published"] } },
29
+ "blueprints": { "Address": { "fields": { "street": "string", "zip": "string" } } },
30
+ "records": { "Money": { "fields": { "amount": "number", "currency": "string" } } },
31
+ "entities": { "User": { "fields": { "name": "text", "email": "email", "password": "password",
32
+ "age": "Age?", "address": "Address", "tags": "string[]" },
33
+ "unique": ["email"], "change": ["name"] },
34
+ "Post": { "fields": { "title": "text", "status": "Status",
35
+ "author": "User", "coauthors": "User[]" } } },
36
+ "environments": { "Database": { "fields": { "DATABASE_URL": "url", "PORT": "integer" } } },
37
+ "repositories": { "Users": { "entity": "User", "actions": ["findById", "findByEmail", "create", "update"],
38
+ "environments": ["Database"] },
39
+ "Posts": { "entity": "Post", "actions": ["findById", "findManyByAuthor", "create"],
40
+ "relations": ["Users"] } }
41
+ }
42
+ \`\`\`
43
+
44
+ ## Fields
45
+
46
+ - Shorthand string: \`"email"\`, \`"Age?"\` (optional), \`"string[]"\` (list), \`"User[]?"\` (optional list).
47
+ - Lowercase names are Std properties (see \`afora_catalog\`: id, slug, email, phone, url, ip, password,
48
+ string, text, number, integer, boolean, date, dateTime...). PascalCase names are declarations of
49
+ the spec (properties, blueprints, records, entities).
50
+ - Object form: \`{ "type": "string", "optional": true, "list": { "minItems": 1, "maxItems": 5 }, "with": { "pattern": "^a" } }\`.
51
+ \`with\` applies to each item of a list and only takes options valid for the type (min/max/pattern for strings,
52
+ min/max/exclusiveMin/exclusiveMax/multipleOf for numbers, plus title/description/examples/deprecated/readOnly/writeOnly/default).
53
+ - Enum: \`{ "enum": ["a", "b"] }\`. Nested object in place: \`{ "fields": { "lat": "number", "lng": "number" } }\`.
54
+ - A property of your own: \`{ "base": "integer", "with": {...} }\`, \`{ "enum": [...] }\`, or
55
+ \`{ "opaque": true, "import": "./my-property" }\` for a hand-written one (imported as it is).
56
+
57
+ ## Rules the validator enforces
58
+
59
+ - Declaration names are PascalCase and unique across every kind; names exported by @afora/core are taken.
60
+ - No circular references between declarations (store an id instead of the entity).
61
+ - An entity cannot declare \`id\`, \`createdAt\`, \`updatedAt\`, \`deletedAt\` (it gets them). No record member names
62
+ (\`toJSON\`, \`set\`, \`onSet\`...); see \`reservedRecordKeys\` in the catalog. \`blueprint\` and \`options\` are valid fields.
63
+ - \`unique\`, \`sensitive\`, \`change\` must name fields (or keys of its plugins); \`change\` cannot name an identity key.
64
+ \`password\` is always sensitive and \`slug\` always unique; \`id\` is always unique.
65
+ - \`plugins\`: Std record plugins (publishable, archivable, taggable, ownable, authenticatable); their keys cannot be declared again.
66
+ - Repository actions: \`findById\`; \`findBy<Key>\` only for unique keys (id, \`unique\`, slug fields, unique plugin keys such as
67
+ \`email\` of authenticatable); \`findManyBy<Key>\` only for keys that are not unique; \`create\`, \`update\`, \`delete\`, \`count\`, and the
68
+ queries \`findMany\`, \`exists\`, \`findPage\` (no key needed; \`where\`, \`orderBy\`, \`limit\`, \`offset\` at runtime).
69
+ - \`provider\` (top level for the application, and optionally on a repository, which then uses its own): \`"inMemory"\`,
70
+ \`{ "use": "<name>", "options": {...} }\` with a name from the \`providers\` of \`afora_catalog\` (external ones are given
71
+ to the server with \`--providers\`), or \`{ "import": "./db.ts#provider" }\` for one created by hand. Options are JSON or
72
+ \`{ "import": "./db.ts#db" }\` references (any depth); imports are relative to \`output.dir\` and never run by the validator.
73
+ Omitting \`provider\` is not the same as \`"inMemory"\`: a repository without one uses the application's. For a provider
74
+ not created with \`providerOf\`, \`afora_extract\` needs \`imports\` (\`provider\` / \`repositories.<Name>.provider\`).
75
+ - Repository \`deletion\`: \`"hard"\` (default) physically removes rows; \`"soft"\` retains \`deletedAt\` and hides deleted rows from reads, counts and relations.
76
+ - \`relations\` must list exactly one repository for each entity the repository's entity holds, at any depth (inside
77
+ records, blueprints and nested objects), not through another entity.
78
+ - Environments hold properties only (no lists, no objects).
79
+ - \`extends\`: other specs (relative path to their afora.json, or a package) whose declarations you can reference;
80
+ the same name for different declarations is a conflict.
81
+
82
+ ## Exceptions and use cases
83
+
84
+ \`\`\`json
85
+ {
86
+ "exceptions": { "UserAlreadyExists": { "layer": "use-case", "message": "A user with this email already exists." } },
87
+ "useCases": {
88
+ "Register": {
89
+ "description": "Signs a user up.", "tags": ["users"],
90
+ "input": { "fields": { "name": "text", "email": "email", "plainPassword": "string" } },
91
+ "response": { "fields": { "user": "User" } },
92
+ "dependencies": { "users": "Users" },
93
+ "errors": ["UserAlreadyExists", "RepositoryUniqueViolation"],
94
+ "implementation": { "steps": [
95
+ { "find": "users.findByEmail", "args": ["$input.email"], "as": "existing" },
96
+ { "ensure": "absent", "value": "$existing", "error": "UserAlreadyExists" },
97
+ { "run": "hashPassword", "fn": { "import": "../hash#hashPassword" }, "args": ["$input.plainPassword"], "as": "hash" },
98
+ { "build": "User", "values": { "name": "$input.name", "email": "$input.email", "password": "$hash" }, "as": "user" },
99
+ { "create": "users", "value": "$user", "as": "saved" },
100
+ { "return": { "user": "$saved" } }
101
+ ] }
102
+ }
103
+ }
104
+ }
105
+ \`\`\`
106
+
107
+ - \`exceptions\`: \`{ layer, message? }\`; \`layer\` is one of \`exceptionLayers\` of \`afora_catalog\`; the message is "<Name>." when omitted.
108
+ - \`useCases\`: \`input\`, \`response\` and \`context\` (optional) are a blueprint name or \`{ "fields": {...} }\`. \`dependencies\` map a key to a use case, repository
109
+ or environment, or to \`"Logger"\` for the logger of the application (an \`ILogger\`; no step reads it). \`errors\` are declared exceptions or exceptions exported by @afora/core (\`coreExceptions\` of \`afora_catalog\`).
110
+ - \`implementation\` is \`{ "steps": [...] }\` (generated complete, no hand-written file) or \`{ "import": "../handlers/x.ts#Name" }\`:
111
+ the generated file has \`abstract class <Name>Base\`; write \`class Name extends <Name>Base\` with \`execute\` at that path.
112
+ A use case cannot list an imported (hand-written) use case as a dependency.
113
+ - Steps run in order and \`return\` is last, exactly once: \`find\` ("<repo>.<read action>"), \`ensure\` (exists | absent | equals | notEquals | true | false;
114
+ \`other\` for the comparisons, \`error\` a declared exception name or a core one, listed in \`errors\`; optional \`details\`), \`build\` (a record or
115
+ entity name, \`values\`, \`as\`), \`change\` ($ref, \`key\` in the class's change keys, \`value\`), \`invoke\` ($ref, \`method\`, \`args\`),
116
+ \`create\` / \`update\` / \`delete\` (repository key, \`value\`), \`call\` (use case key, \`input\`) and \`run\` (a unique name, \`fn\` is
117
+ \`{ "import": "../x.ts#fn" }\`, \`args\`; the function gets (...args, { context })). \`as\` binds the result.
118
+ - Values: \`"$input.key"\`, \`"$context.key"\`, \`"$env.<dependency>.VAR"\`, \`"$name.path.0"\` (a bound result); a literal; \`{ "literal": "$5" }\` for a string that
119
+ starts with $; or an object/array of values. No expressions: compute with \`run\`.
120
+ - \`afora_extract\` needs \`imports\` for \`useCases.<Name>\` (hand-written execute) and \`useCases.<Name>.run.<name>\` (run functions).
121
+
122
+ ## Commands and outputs
123
+
124
+ \`\`\`json
125
+ {
126
+ "commands": {
127
+ "Users": {
128
+ "description": "Accounts.", "tags": ["public"],
129
+ "context": { "fields": { "userId": "id?" } },
130
+ "useCases": { "register": "Register" },
131
+ "transaction": true
132
+ }
133
+ },
134
+ "outputs": {
135
+ "Api": {
136
+ "commands": ["Users"],
137
+ "statuses": { "UserAlreadyExists": 409 },
138
+ "pipe": [
139
+ { "transform": "select", "options": { "tags": ["public"] } },
140
+ { "sink": "openapi", "options": { "info": { "title": "Blog", "version": "1.0.0" } } }
141
+ ]
142
+ }
143
+ }
144
+ }
145
+ \`\`\`
146
+
147
+ - \`commands\`: \`useCases\` maps an operation name (an identifier) to a declared use case; \`context\` is a blueprint name or \`{ "fields": {...} }\`.
148
+ \`transaction: true\` runs each call in a transaction of the repositories' provider: it is checked when the code runs, not by the validator
149
+ (the provider is unknown then). \`events\` is \`"after-commit"\` (default), \`"manual"\` or \`{ "import": "./events.ts#dispatch" }\` (a handler);
150
+ \`onEventError\` is an import and only with \`after-commit\`. Defaults (\`transaction: false\`, \`events: "after-commit"\`, no tags) are not written.
151
+ - \`outputs\`: \`commands\` are declared commands; \`statuses\` maps an exception name (declared or exported by @afora/core) to a status from 400 to 599;
152
+ \`pipe\` is transforms and, last, at most one sink, each \`{ "transform" | "sink": name, "options"?: {...JSON} }\` with a name from the \`stages\` of
153
+ \`afora_catalog\` (\`select\`, \`openapi\`, plus the ones given to the server with \`--stages\`), or \`{ "import": "./stages.ts#auth" }\` for a stage written by hand.
154
+ - \`afora_extract\` needs \`imports\` for \`commands.<Name>.events\`, \`commands.<Name>.onEventError\` and \`outputs.<Name>.pipe.<index>\` (hand-written ones).
155
+ `;
156
+ // src/server.ts
157
+ import {
158
+ catalog as catalog2,
159
+ specSchema as specSchema2
160
+ } from "@afora/codegen";
161
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
162
+ import {
163
+ CallToolRequestSchema,
164
+ ListResourcesRequestSchema,
165
+ ListToolsRequestSchema,
166
+ ReadResourceRequestSchema
167
+ } from "@modelcontextprotocol/sdk/types.js";
168
+
169
+ // src/extract.ts
170
+ import { existsSync } from "node:fs";
171
+ import { realpath } from "node:fs/promises";
172
+ import { fileURLToPath } from "node:url";
173
+ import { confinePath } from "@afora/codegen";
174
+
175
+ // src/process.ts
176
+ import { spawn } from "node:child_process";
177
+ function run(command, args, options) {
178
+ return new Promise((resolve) => {
179
+ let output = "";
180
+ let timedOut = false;
181
+ const child = spawn(command, [...args], {
182
+ cwd: options.cwd,
183
+ stdio: ["ignore", "pipe", "pipe"]
184
+ });
185
+ const timer = setTimeout(() => {
186
+ timedOut = true;
187
+ child.kill("SIGKILL");
188
+ }, options.timeoutMs);
189
+ child.stdout.on("data", (chunk) => {
190
+ output += chunk;
191
+ });
192
+ child.stderr.on("data", (chunk) => {
193
+ output += chunk;
194
+ });
195
+ child.on("error", () => {
196
+ clearTimeout(timer);
197
+ resolve({ code: null, output, missing: true, timedOut });
198
+ });
199
+ child.on("close", (code) => {
200
+ clearTimeout(timer);
201
+ resolve({ code, output, missing: false, timedOut });
202
+ });
203
+ });
204
+ }
205
+
206
+ // src/extract.ts
207
+ var START = `
208
+ <<<AFORA_SPEC>>>
209
+ `;
210
+ var END = `
211
+ <<<END_AFORA_SPEC>>>
212
+ `;
213
+
214
+ class ToolError extends Error {
215
+ name = "ToolError";
216
+ }
217
+ function childPath() {
218
+ for (const extension of ["js", "ts"]) {
219
+ const url = new URL(`./extract-child.${extension}`, import.meta.url);
220
+ const path = fileURLToPath(url);
221
+ if (existsSync(path))
222
+ return path;
223
+ }
224
+ throw new ToolError("The extract helper is missing from the installation.");
225
+ }
226
+ function parseAppReference(reference) {
227
+ const [file, name = "App"] = reference.split("#");
228
+ if (!file) {
229
+ throw new ToolError('Expected "path/to/file.ts#ExportName", e.g. "src/app.ts#App".');
230
+ }
231
+ return { file, name };
232
+ }
233
+ async function extractFromApp(root, reference, outputDir, imports) {
234
+ const { file, name } = parseAppReference(reference);
235
+ const confined = await confinePath(root, file);
236
+ if (!confined.ok) {
237
+ throw new ToolError(`The application file ${file} is ${confined.reason}.`);
238
+ }
239
+ if (!existsSync(confined.absolute)) {
240
+ throw new ToolError(`The application file ${file} does not exist.`);
241
+ }
242
+ const real = await realpath(confined.absolute);
243
+ const bun = process.versions.bun ? process.execPath : "bun";
244
+ const result = await run(bun, [
245
+ childPath(),
246
+ real,
247
+ name,
248
+ outputDir ?? "",
249
+ ...imports ? [JSON.stringify(imports)] : []
250
+ ], { cwd: root, timeoutMs: 60000 });
251
+ if (result.missing) {
252
+ throw new ToolError("Bun is needed to import the application and was not found on PATH.");
253
+ }
254
+ if (result.timedOut)
255
+ throw new ToolError("Importing the application timed out.");
256
+ const start = result.output.indexOf(START);
257
+ const end = result.output.indexOf(END, start);
258
+ if (result.code !== 0 || start === -1 || end === -1) {
259
+ throw new ToolError(`Could not extract the application:
260
+ ${result.output.trim().slice(-2000)}`);
261
+ }
262
+ return JSON.parse(result.output.slice(start + START.length, end));
263
+ }
264
+
265
+ // src/spec-file.ts
266
+ import { existsSync as existsSync2 } from "node:fs";
267
+ import { mkdir, readFile, realpath as realpath2, writeFile } from "node:fs/promises";
268
+ import { dirname, join } from "node:path";
269
+ import {
270
+ COLLECTION_KEYS,
271
+ confinePath as confinePath2,
272
+ resolveExtended,
273
+ SpecLoadError,
274
+ validateSpec
275
+ } from "@afora/codegen";
276
+ var SPEC_FILE = "afora.json";
277
+ function codegenOptionsOf(project) {
278
+ return {
279
+ ...project.providers ? { providers: project.providers } : {},
280
+ ...project.stages ? { stages: project.stages } : {}
281
+ };
282
+ }
283
+ async function openProject(directory, app, providers, stages) {
284
+ let root;
285
+ try {
286
+ root = await realpath2(directory);
287
+ } catch {
288
+ throw new Error(`The project directory ${directory} does not exist.`);
289
+ }
290
+ return {
291
+ root,
292
+ ...app ? { app } : {},
293
+ ...providers ? { providers } : {},
294
+ ...stages ? { stages } : {}
295
+ };
296
+ }
297
+ async function readSpec(project) {
298
+ const path = join(project.root, SPEC_FILE);
299
+ if (!existsSync2(path))
300
+ return;
301
+ const confined = await confinePath2(project.root, SPEC_FILE);
302
+ if (!confined.ok) {
303
+ throw new ToolError(`${SPEC_FILE} is ${confined.reason}.`);
304
+ }
305
+ try {
306
+ return JSON.parse(await readFile(confined.absolute, "utf8"));
307
+ } catch (error) {
308
+ throw new ToolError(`${SPEC_FILE} is not valid JSON: ${error instanceof Error ? error.message : String(error)}`);
309
+ }
310
+ }
311
+ async function writeSpec(project, spec) {
312
+ const confined = await confinePath2(project.root, SPEC_FILE);
313
+ if (!confined.ok) {
314
+ throw new ToolError(`${SPEC_FILE} is ${confined.reason}; it was not written.`);
315
+ }
316
+ await mkdir(dirname(confined.absolute), { recursive: true });
317
+ await writeFile(confined.absolute, `${JSON.stringify(ordered(spec), null, "\t")}
318
+ `);
319
+ }
320
+ var TOP_ORDER = [
321
+ "$schema",
322
+ "version",
323
+ "name",
324
+ "extends",
325
+ "output",
326
+ "provider",
327
+ ...COLLECTION_KEYS
328
+ ];
329
+ function ordered(spec) {
330
+ const source = spec;
331
+ const keys = [
332
+ ...TOP_ORDER.filter((key) => (key in source)),
333
+ ...Object.keys(source).filter((key) => !TOP_ORDER.includes(key))
334
+ ];
335
+ return Object.fromEntries(keys.map((key) => [key, source[key]]));
336
+ }
337
+ async function extendedOf(project, spec) {
338
+ try {
339
+ return {
340
+ extended: await resolveExtended(spec, join(project.root, SPEC_FILE)),
341
+ issues: []
342
+ };
343
+ } catch (error) {
344
+ if (!(error instanceof SpecLoadError))
345
+ throw error;
346
+ return {
347
+ extended: {},
348
+ issues: [
349
+ {
350
+ path: ["extends"],
351
+ code: "unresolved_extends",
352
+ message: error.message
353
+ }
354
+ ]
355
+ };
356
+ }
357
+ }
358
+ async function validateInProject(project, spec) {
359
+ const loaded = typeof spec === "object" && spec !== null ? await extendedOf(project, spec) : { extended: {}, issues: [] };
360
+ const issues = validateSpec(spec, loaded.extended, codegenOptionsOf(project));
361
+ const known = new Set(issues.map((issue) => `${issue.code}@${issue.path.join(".")}`));
362
+ return {
363
+ extended: loaded.extended,
364
+ issues: [
365
+ ...issues,
366
+ ...loaded.issues.filter((issue) => !known.has(`${issue.code}@${issue.path.join(".")}`) && !issues.some((other) => other.code === "unresolved_extends"))
367
+ ]
368
+ };
369
+ }
370
+
371
+ // src/tools.ts
372
+ import {
373
+ catalog,
374
+ declarationSchemas,
375
+ fieldDefinition,
376
+ generate,
377
+ InvalidSpecError,
378
+ specSchema,
379
+ writeGenerated
380
+ } from "@afora/codegen";
381
+ import { Type } from "typebox";
382
+
383
+ // src/typecheck.ts
384
+ import { existsSync as existsSync3 } from "node:fs";
385
+ import { join as join2, relative, resolve } from "node:path";
386
+ var DIAGNOSTIC = /^(.+?)\((\d+),(\d+)\): error (TS\d+): (.*)$/;
387
+ function parseDiagnostics(output) {
388
+ const found = [];
389
+ for (const text of output.split(/\r?\n/)) {
390
+ const match = DIAGNOSTIC.exec(text);
391
+ if (match) {
392
+ found.push({
393
+ path: match[1],
394
+ file: match[1],
395
+ line: Number(match[2]),
396
+ column: Number(match[3]),
397
+ code: match[4],
398
+ message: match[5]
399
+ });
400
+ } else if (/^\s+\S/.test(text) && found.length > 0) {
401
+ const last = found[found.length - 1];
402
+ last.message += `
403
+ ${text.trim()}`;
404
+ }
405
+ }
406
+ return found;
407
+ }
408
+ async function typecheck(root, files) {
409
+ if (!existsSync3(join2(root, "tsconfig.json"))) {
410
+ return { ran: false, reason: "The project has no tsconfig.json." };
411
+ }
412
+ const mine = new Set(files.map((file) => resolve(root, file)));
413
+ const commands = [
414
+ ["bunx", ["tsc", "--noEmit", "-p", root]],
415
+ ["npx", ["--no-install", "tsc", "--noEmit", "-p", root]]
416
+ ];
417
+ for (const [command, args] of commands) {
418
+ const result = await run(command, args, { cwd: root, timeoutMs: 180000 });
419
+ if (result.missing)
420
+ continue;
421
+ if (result.timedOut) {
422
+ return { ran: false, reason: "tsc timed out after 180 seconds." };
423
+ }
424
+ const all = parseDiagnostics(result.output);
425
+ if (all.length === 0 && result.code !== 0) {
426
+ return {
427
+ ran: false,
428
+ reason: `tsc failed without diagnostics: ${result.output.trim().slice(0, 500)}`
429
+ };
430
+ }
431
+ const diagnostics = all.filter((item) => mine.has(resolve(root, item.path))).map(({ path, ...item }) => ({
432
+ ...item,
433
+ file: relative(root, resolve(root, path))
434
+ }));
435
+ return {
436
+ ran: true,
437
+ ok: diagnostics.length === 0,
438
+ diagnostics,
439
+ otherErrors: all.length - diagnostics.length
440
+ };
441
+ }
442
+ return {
443
+ ran: false,
444
+ reason: "Neither bunx nor npx is available to run tsc."
445
+ };
446
+ }
447
+
448
+ // src/tools.ts
449
+ var json = (schema) => JSON.parse(JSON.stringify(schema));
450
+ var KINDS = [
451
+ "property",
452
+ "blueprint",
453
+ "record",
454
+ "entity",
455
+ "environment",
456
+ "repository",
457
+ "exception",
458
+ "useCase",
459
+ "command",
460
+ "output"
461
+ ];
462
+ var COLLECTION_OF = {
463
+ property: "properties",
464
+ blueprint: "blueprints",
465
+ record: "records",
466
+ entity: "entities",
467
+ environment: "environments",
468
+ repository: "repositories",
469
+ exception: "exceptions",
470
+ useCase: "useCases",
471
+ command: "commands",
472
+ output: "outputs"
473
+ };
474
+ var specInput = () => {
475
+ const { $defs: _defs, ...schema } = json(specSchema);
476
+ return schema;
477
+ };
478
+ var FIELD_DEFS = { Field: json(fieldDefinition) };
479
+ function issuesResult(issues) {
480
+ return { valid: issues.length === 0, issues };
481
+ }
482
+ async function currentSpec(project) {
483
+ const spec = await readSpec(project);
484
+ if (spec)
485
+ return { spec, source: "file" };
486
+ if (project.app) {
487
+ return {
488
+ spec: await extractFromApp(project.root, project.app),
489
+ source: "app"
490
+ };
491
+ }
492
+ return { spec: { version: 1 }, source: "new" };
493
+ }
494
+ function asSpec(value) {
495
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
496
+ throw new ToolError("`spec` must be a JSON object (the contents of afora.json).");
497
+ }
498
+ return value;
499
+ }
500
+ function kindOf(value) {
501
+ if (!KINDS.includes(value)) {
502
+ throw new ToolError(`\`kind\` must be one of: ${KINDS.join(", ")}.`);
503
+ }
504
+ return value;
505
+ }
506
+ function nameOf(value) {
507
+ if (typeof value !== "string" || value === "") {
508
+ throw new ToolError("`name` must be the PascalCase name of the declaration.");
509
+ }
510
+ return value;
511
+ }
512
+ async function saveIfValid(project, spec, extra = {}) {
513
+ const { issues } = await validateInProject(project, spec);
514
+ if (issues.length > 0) {
515
+ return {
516
+ written: false,
517
+ ...issuesResult(issues),
518
+ hint: "Nothing was written. Fix the issues (declare what a declaration uses first) and retry.",
519
+ ...extra
520
+ };
521
+ }
522
+ await writeSpec(project, spec);
523
+ return { written: true, path: "afora.json", ...issuesResult([]), ...extra };
524
+ }
525
+ var tools = [
526
+ {
527
+ name: "afora_catalog",
528
+ description: "What an afora.json can use, read from @afora/core: the Std properties (type, flags sensitive/unique/optional, and the `with` options each accepts), factories (enum), the Std record plugins (keys they add, unique/sensitive/change keys, methods), reserved record/entity keys, identity keys and the repository action rules. The `providers` list is what `provider.use` may name; `exceptionLayers` and `coreExceptions` are what an exception's `layer` and a use case's `errors` may use; `stages` (select, openapi and the ones given to the server with --stages) are what an output's `pipe` may name with `transform` or `sink`. Call this first; do not guess property names.",
529
+ inputSchema: {
530
+ type: "object",
531
+ properties: {},
532
+ additionalProperties: false
533
+ },
534
+ readOnly: true,
535
+ handle: async (_args, project) => ({
536
+ ...catalog(),
537
+ providers: [
538
+ {
539
+ name: "inMemory",
540
+ builtin: true,
541
+ note: 'provider: "inMemory" (or { "use": "inMemory" }), no options.'
542
+ },
543
+ ...project.providers ?? []
544
+ ],
545
+ stages: [
546
+ ...catalog().stages,
547
+ ...(project.stages ?? []).map((stage) => ({
548
+ ...stage,
549
+ builtin: false
550
+ }))
551
+ ]
552
+ })
553
+ },
554
+ {
555
+ name: "afora_spec_read",
556
+ description: "Reads the project's afora.json, with its validation issues. If there is none and the server was started with --app, returns the spec extracted from that application's code (not written). Use it before editing.",
557
+ inputSchema: {
558
+ type: "object",
559
+ properties: {},
560
+ additionalProperties: false
561
+ },
562
+ readOnly: true,
563
+ async handle(_args, project) {
564
+ const spec = await readSpec(project);
565
+ if (spec) {
566
+ const { issues } = await validateInProject(project, spec);
567
+ return {
568
+ source: "file",
569
+ path: "afora.json",
570
+ spec,
571
+ ...issuesResult(issues)
572
+ };
573
+ }
574
+ if (project.app) {
575
+ const extracted = await extractFromApp(project.root, project.app);
576
+ return {
577
+ source: "app",
578
+ app: project.app,
579
+ spec: extracted,
580
+ note: "Extracted from the code; nothing was written. afora_spec_write or afora_spec_upsert saves a spec as afora.json."
581
+ };
582
+ }
583
+ return {
584
+ source: "none",
585
+ spec: null,
586
+ note: 'There is no afora.json yet. Start with afora_spec_upsert (it creates one) or afora_spec_write with { "version": 1, ... }.'
587
+ };
588
+ }
589
+ },
590
+ {
591
+ name: "afora_extract",
592
+ description: "Extracts the spec from an existing @afora/core Application in the project, by importing the module in a Bun subprocess (this runs the module's code). Does not write anything: review it, then save with afora_spec_write. Properties that cannot be expressed in JSON become opaque imports.",
593
+ inputSchema: json(Type.Object({
594
+ app: Type.Optional(Type.String({
595
+ description: 'Path inside the project and export name, e.g. "src/app.ts#App". Defaults to the --app the server was started with.'
596
+ })),
597
+ outputDir: Type.Optional(Type.String({
598
+ description: "output.dir for the spec; src/domain by default."
599
+ })),
600
+ imports: Type.Optional(Type.Record(Type.String(), Type.String(), {
601
+ description: 'Where what cannot be described is imported from: a property name -> module, and "./file.ts#export" (relative to output.dir) for a provider not created with providerOf ("provider" or "repositories.<Name>.provider"), a use case with a hand-written execute ("useCases.<Name>") and a run function of a steps use case ("useCases.<Name>.run.<name>"), a command handler ("commands.<Name>.events", "commands.<Name>.onEventError") and a stage written by hand ("outputs.<Name>.pipe.<index>"). Without it extraction fails with an error naming the key.'
602
+ }))
603
+ }, { additionalProperties: false })),
604
+ readOnly: true,
605
+ async handle(args, project) {
606
+ const app = typeof args.app === "string" ? args.app : project.app;
607
+ if (!app) {
608
+ throw new ToolError('Pass `app` ("src/app.ts#App") or start the server with --app.');
609
+ }
610
+ const outputDir = typeof args.outputDir === "string" ? args.outputDir : undefined;
611
+ const imports = typeof args.imports === "object" && args.imports !== null && !Array.isArray(args.imports) ? args.imports : undefined;
612
+ const spec = await extractFromApp(project.root, app, outputDir, imports);
613
+ const { issues } = await validateInProject(project, spec);
614
+ return { source: "app", app, spec, ...issuesResult(issues) };
615
+ }
616
+ },
617
+ {
618
+ name: "afora_spec_validate",
619
+ description: "Validates a spec without writing anything and returns the issues ({ path, code, message }). Pass `spec` to check a draft, or omit it to check the afora.json on disk. Rules covered (use cases: references, dependency kinds, errors registered or core, every step rule with the codes unknown_step, invalid_reference, unbound_reference, unknown_key, unknown_dependency, unknown_action, write_in_find, undeclared_error, missing_return, misplaced_return, invalid_step_name, invalid_class, invalid_change_key, invalid_value, invalid_import; commands: unknown_use_case, invalid_use_cases, invalid_operation, invalid_events, event_error_requires_after_commit; outputs: unknown_command, invalid_commands, invalid_pipe, invalid_stage, unknown_stage, stage_kind_mismatch, sink_not_last, invalid_stage_options, invalid_statuses, invalid_status, unknown_exception): unknown references, name collisions, cycles, reserved and identity keys, unique/sensitive/change naming missing keys, repository action rules (findBy only for unique keys, findManyBy only for non-unique; findMany, exists, findPage need no key), providers (unknown_provider, invalid_provider, invalid_import), relations covering the entities held, invalid `with` options.",
620
+ inputSchema: {
621
+ type: "object",
622
+ properties: {
623
+ spec: {
624
+ ...specInput(),
625
+ description: "A draft spec. Omit to validate afora.json on disk."
626
+ }
627
+ },
628
+ additionalProperties: false,
629
+ $defs: FIELD_DEFS
630
+ },
631
+ readOnly: true,
632
+ async handle(args, project) {
633
+ const spec = args.spec ?? await readSpec(project);
634
+ if (spec === undefined) {
635
+ throw new ToolError("There is no afora.json to validate; pass `spec`.");
636
+ }
637
+ const { issues } = await validateInProject(project, spec);
638
+ return issuesResult(issues);
639
+ }
640
+ },
641
+ {
642
+ name: "afora_spec_upsert",
643
+ description: "Adds or replaces ONE declaration in afora.json (creating the file if needed), validates the whole spec and writes only if it is valid. Cheaper than resending the spec. `kind` is property | blueprint | record | entity | environment | repository | exception | useCase | command | output; `declaration` is its JSON (see the schema). Declare what it references first (e.g. a blueprint before the entity that uses it, the entity before its repository). If invalid, nothing is written and the issues are returned.",
644
+ inputSchema: {
645
+ type: "object",
646
+ properties: {
647
+ kind: {
648
+ type: "string",
649
+ enum: [...KINDS],
650
+ description: "Which collection the declaration belongs to."
651
+ },
652
+ name: {
653
+ type: "string",
654
+ pattern: "^[A-Z][A-Za-z0-9]*$",
655
+ description: "PascalCase name, unique across all kinds."
656
+ },
657
+ declaration: {
658
+ description: 'The declaration. property: { base, with? } | { enum } | { opaque, import }. blueprint/environment: { fields }. record/entity: { fields, unique?, sensitive?, change?, plugins? }. repository: { entity, actions, provider?, deletion?: hard | soft, relations?, environments? }. exception: { layer, message? }. useCase: { description?, tags?, deprecated?, input, response, context?, dependencies?, errors?, implementation: { steps: [...] } | { import: "../handlers/x.ts#Name" } } (see the guide for steps). command: { description?, tags?, context?, useCases: { op: "UseCaseName" }, transaction?, events?: "after-commit" | "manual" | { import }, onEventError?: { import } }. output: { commands: [names], statuses?: { ExceptionName: 400-599 }, pipe: [{ transform, options? } | { sink, options? } | { import }] }. provider: "inMemory" | { use, options? } | { import: "./file.ts#name" } (the application provider is the top-level `provider` of the spec: afora_spec_write). A use case dependency can also be "Logger"; the top-level `log` is { level?, providers: ["json" | "console" | { import }] }.',
659
+ anyOf: Object.values(declarationSchemas).map((schema) => json(schema))
660
+ }
661
+ },
662
+ required: ["kind", "name", "declaration"],
663
+ additionalProperties: false,
664
+ $defs: FIELD_DEFS
665
+ },
666
+ readOnly: false,
667
+ async handle(args, project) {
668
+ const kind = kindOf(args.kind);
669
+ const name = nameOf(args.name);
670
+ const declaration = args.declaration;
671
+ if (typeof declaration !== "object" || declaration === null || Array.isArray(declaration)) {
672
+ throw new ToolError("`declaration` must be a JSON object.");
673
+ }
674
+ const { spec, source } = await currentSpec(project);
675
+ const collection = COLLECTION_OF[kind];
676
+ const existing = spec[collection];
677
+ const next = {
678
+ ...spec,
679
+ [collection]: { ...existing ?? {}, [name]: declaration }
680
+ };
681
+ return saveIfValid(project, next, {
682
+ action: existing && name in existing ? "replaced" : "added",
683
+ kind,
684
+ name,
685
+ ...source === "app" ? {
686
+ note: "There was no afora.json: started from the spec extracted from the application."
687
+ } : {}
688
+ });
689
+ }
690
+ },
691
+ {
692
+ name: "afora_spec_remove",
693
+ description: "Removes ONE declaration from afora.json, validates the whole spec and writes only if it stays valid (so remove what depends on it first: a repository before its entity, an entity before the blueprint it uses is not needed, but anything referencing it must go or change).",
694
+ inputSchema: json(Type.Object({
695
+ kind: Type.Union(KINDS.map((kind) => Type.Literal(kind))),
696
+ name: Type.String()
697
+ }, { additionalProperties: false })),
698
+ readOnly: false,
699
+ async handle(args, project) {
700
+ const kind = kindOf(args.kind);
701
+ const name = nameOf(args.name);
702
+ const { spec } = await currentSpec(project);
703
+ const collection = COLLECTION_OF[kind];
704
+ const existing = spec[collection];
705
+ if (!existing || !(name in existing)) {
706
+ throw new ToolError(`There is no ${kind} named "${name}" in afora.json.`);
707
+ }
708
+ const { [name]: _removed, ...rest } = existing;
709
+ const next = { ...spec };
710
+ if (Object.keys(rest).length > 0)
711
+ next[collection] = rest;
712
+ else
713
+ delete next[collection];
714
+ return saveIfValid(project, next, {
715
+ action: "removed",
716
+ kind,
717
+ name
718
+ });
719
+ }
720
+ },
721
+ {
722
+ name: "afora_spec_write",
723
+ description: "Replaces the whole afora.json with `spec` (validated first; invalid specs are not written). Use it to create the spec in one go or to save one from afora_extract.",
724
+ inputSchema: {
725
+ type: "object",
726
+ properties: { spec: specInput() },
727
+ required: ["spec"],
728
+ additionalProperties: false,
729
+ $defs: FIELD_DEFS
730
+ },
731
+ readOnly: false,
732
+ async handle(args, project) {
733
+ return saveIfValid(project, asSpec(args.spec), { action: "replaced" });
734
+ }
735
+ },
736
+ {
737
+ name: "afora_generate",
738
+ description: "Generates the TypeScript from afora.json. With dryRun: true returns the files (path and contents) and writes nothing. Otherwise writes them under output.dir (overwriting or removing only files that carry the 'Generated by @afora/codegen' banner; anything else makes it refuse and write nothing), then runs `bunx tsc --noEmit -p <project>` if the project has a tsconfig.json and returns only the diagnostics of the generated files. Fix the spec, not the generated files.",
739
+ inputSchema: json(Type.Object({
740
+ dryRun: Type.Optional(Type.Boolean({ description: "Return the files without writing." })),
741
+ typecheck: Type.Optional(Type.Boolean({
742
+ description: "Run tsc after writing. Default true."
743
+ }))
744
+ }, { additionalProperties: false })),
745
+ readOnly: false,
746
+ async handle(args, project) {
747
+ const spec = await readSpec(project);
748
+ if (!spec) {
749
+ throw new ToolError("There is no afora.json to generate from. Create it with afora_spec_write or afora_spec_upsert.");
750
+ }
751
+ const { extended, issues } = await validateInProject(project, spec);
752
+ if (issues.length > 0)
753
+ return { generated: false, ...issuesResult(issues) };
754
+ let files;
755
+ try {
756
+ files = generate(spec, extended, codegenOptionsOf(project));
757
+ } catch (error) {
758
+ if (error instanceof InvalidSpecError) {
759
+ return { generated: false, ...issuesResult([...error.issues]) };
760
+ }
761
+ throw error;
762
+ }
763
+ if (args.dryRun === true) {
764
+ const preview = await writeGenerated(project.root, files, {
765
+ dryRun: true
766
+ });
767
+ return {
768
+ generated: true,
769
+ dryRun: true,
770
+ files,
771
+ wouldRemove: preview.removed,
772
+ refused: preview.refused
773
+ };
774
+ }
775
+ const result = await writeGenerated(project.root, files);
776
+ if (result.refused.length > 0) {
777
+ return {
778
+ generated: false,
779
+ refused: result.refused,
780
+ hint: "Nothing was written. These files exist and are not generated ones (no banner), or are outside the project. Change output.dir or move them."
781
+ };
782
+ }
783
+ const check = args.typecheck === false ? { ran: false, reason: "Skipped (typecheck: false)." } : await typecheck(project.root, files.map((file) => file.path));
784
+ return {
785
+ generated: true,
786
+ written: result.written,
787
+ unchanged: result.unchanged,
788
+ removed: result.removed,
789
+ typecheck: check
790
+ };
791
+ }
792
+ }
793
+ ];
794
+
795
+ // src/server.ts
796
+ var RESOURCES = [
797
+ {
798
+ uri: "afora://spec/schema",
799
+ name: "afora.json JSON Schema",
800
+ description: "The JSON Schema of afora.json (version 1).",
801
+ mimeType: "application/json",
802
+ read: () => JSON.stringify(specSchema2, null, 2)
803
+ },
804
+ {
805
+ uri: "afora://catalog",
806
+ name: "afora catalog",
807
+ description: "Std properties and their `with` options, record plugins, reserved keys and repository rules, read from @afora/core.",
808
+ mimeType: "application/json",
809
+ read: () => JSON.stringify(catalog2(), null, 2)
810
+ },
811
+ {
812
+ uri: "afora://spec/guide",
813
+ name: "afora.json guide",
814
+ description: "A short guide to writing afora.json: workflow, shape, fields and the rules the validator enforces.",
815
+ mimeType: "text/markdown",
816
+ read: () => GUIDE
817
+ }
818
+ ];
819
+ async function createServer(options = {}) {
820
+ const project = await openProject(options.project ?? process.cwd(), options.app, options.providers, options.stages);
821
+ const server = new Server({ name: "afora", version: "0.0.1" }, {
822
+ capabilities: { tools: {}, resources: {} },
823
+ instructions: "Builds a domain from an afora.json: afora_catalog to see what exists, afora_spec_read, then afora_spec_upsert (one declaration at a time) or afora_spec_write, afora_spec_validate and afora_generate. Read the resource afora://spec/guide for the format and rules. Never edit the generated files; change the spec."
824
+ });
825
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({
826
+ tools: tools.map((tool) => ({
827
+ name: tool.name,
828
+ description: tool.description,
829
+ inputSchema: tool.inputSchema,
830
+ annotations: {
831
+ readOnlyHint: tool.readOnly,
832
+ destructiveHint: false,
833
+ openWorldHint: false
834
+ }
835
+ }))
836
+ }));
837
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
838
+ const tool = tools.find((candidate) => candidate.name === request.params.name);
839
+ if (!tool) {
840
+ return {
841
+ isError: true,
842
+ content: [
843
+ { type: "text", text: `Unknown tool "${request.params.name}".` }
844
+ ]
845
+ };
846
+ }
847
+ try {
848
+ const result = await tool.handle(request.params.arguments ?? {}, project);
849
+ return {
850
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
851
+ };
852
+ } catch (error) {
853
+ const message = error instanceof ToolError || error instanceof Error ? error.message : String(error);
854
+ return { isError: true, content: [{ type: "text", text: message }] };
855
+ }
856
+ });
857
+ server.setRequestHandler(ListResourcesRequestSchema, async () => ({
858
+ resources: RESOURCES.map(({ uri, name, description, mimeType }) => ({
859
+ uri,
860
+ name,
861
+ description,
862
+ mimeType
863
+ }))
864
+ }));
865
+ server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
866
+ const resource = RESOURCES.find((candidate) => candidate.uri === request.params.uri);
867
+ if (!resource)
868
+ throw new Error(`Unknown resource ${request.params.uri}.`);
869
+ return {
870
+ contents: [
871
+ {
872
+ uri: resource.uri,
873
+ mimeType: resource.mimeType,
874
+ text: resource.read()
875
+ }
876
+ ]
877
+ };
878
+ });
879
+ return server;
880
+ }
881
+ // src/sink.ts
882
+ import {
883
+ InvalidOutputStage,
884
+ outputValueOf,
885
+ problemOf,
886
+ sinkOf,
887
+ stageCatalogOf,
888
+ statusOf,
889
+ toOpenAPI
890
+ } from "@afora/core";
891
+ import { Server as Server2 } from "@modelcontextprotocol/sdk/server/index.js";
892
+ import {
893
+ CallToolRequestSchema as CallToolRequestSchema2,
894
+ ListToolsRequestSchema as ListToolsRequestSchema2
895
+ } from "@modelcontextprotocol/sdk/types.js";
896
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
897
+ function validate(options) {
898
+ const given = options;
899
+ for (const key of Object.keys(given)) {
900
+ if (!["name", "version", "prefix", "include", "context"].includes(key)) {
901
+ return `mcpTools has no option "${key}".`;
902
+ }
903
+ }
904
+ for (const key of ["name", "version"]) {
905
+ if (typeof given[key] !== "string" || given[key] === "") {
906
+ return `Expected mcpTools.${key} to be a non-empty string.`;
907
+ }
908
+ }
909
+ const { prefix, include, context } = given;
910
+ if (prefix !== undefined && (typeof prefix !== "string" || !/^[A-Za-z0-9_.-]*$/.test(prefix))) {
911
+ return "Expected mcpTools.prefix to use only letters, digits, _, . and -.";
912
+ }
913
+ if (include !== undefined && (!Array.isArray(include) || include.some((id) => typeof id !== "string" || id === ""))) {
914
+ return "Expected mcpTools.include to be a list of operation ids.";
915
+ }
916
+ if (context !== undefined) {
917
+ if (!isObject(context))
918
+ return "Expected mcpTools.context to be an object.";
919
+ for (const [key, name] of Object.entries(context)) {
920
+ if (typeof name !== "string" || name === "") {
921
+ return `Expected mcpTools.context["${key}"] to be a value name.`;
922
+ }
923
+ }
924
+ }
925
+ return;
926
+ }
927
+ var kebab = (text) => text.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9_.-]+/g, "_").toLowerCase();
928
+ function scalarTypesOf(blueprint) {
929
+ const schema = toOpenAPI(blueprint);
930
+ const types = {};
931
+ for (const [key, property] of Object.entries(schema.properties ?? {})) {
932
+ types[key] = [property.type ?? []].flat();
933
+ }
934
+ return types;
935
+ }
936
+ function coerce(text, types) {
937
+ if (!types || types.includes("string"))
938
+ return text;
939
+ const trimmed = text.trim();
940
+ if (types.includes("boolean") && (trimmed === "true" || trimmed === "false")) {
941
+ return trimmed === "true";
942
+ }
943
+ if (types.includes("integer") || types.includes("number")) {
944
+ const number = trimmed === "" ? Number.NaN : Number(trimmed);
945
+ if (Number.isFinite(number))
946
+ return number;
947
+ }
948
+ return text;
949
+ }
950
+ var text = (value, isError) => ({
951
+ ...isError ? { isError } : {},
952
+ content: [{ type: "text", text: JSON.stringify(value) }]
953
+ });
954
+ var mcpTools = sinkOf({
955
+ name: "mcpTools",
956
+ validate,
957
+ build: (operations, options, { output }) => {
958
+ const prefix = options.prefix ?? "";
959
+ const mapping = options.context ?? {};
960
+ const include = options.include;
961
+ if (include) {
962
+ const known = new Set(operations.map((operation) => operation.id));
963
+ const unknown = include.find((id) => !known.has(id));
964
+ if (unknown !== undefined) {
965
+ throw new InvalidOutputStage({
966
+ source: "mcpTools",
967
+ details: {
968
+ reason: `mcpTools.include names "${unknown}", which is not an operation.`
969
+ }
970
+ });
971
+ }
972
+ }
973
+ const entries = new Map;
974
+ for (const operation of operations) {
975
+ if (include && !include.includes(operation.id))
976
+ continue;
977
+ const name = `${prefix}${kebab(operation.command)}_${kebab(operation.name)}`;
978
+ if (name.length > 128 || entries.has(name)) {
979
+ throw new InvalidOutputStage({
980
+ source: "mcpTools",
981
+ details: {
982
+ reason: `mcpTools tool name "${name}" of ${operation.id} is duplicated or longer than 128 characters.`
983
+ }
984
+ });
985
+ }
986
+ const schema = toOpenAPI(operation.input);
987
+ const description = [
988
+ operation.description ?? `${operation.command}.${operation.name}`,
989
+ operation.deprecated ? "Deprecated." : undefined
990
+ ].filter(Boolean).join(" ");
991
+ entries.set(name, {
992
+ operation,
993
+ definition: Object.freeze({
994
+ name,
995
+ description,
996
+ inputSchema: {
997
+ ...schema,
998
+ type: "object"
999
+ },
1000
+ operation: operation.id
1001
+ })
1002
+ });
1003
+ }
1004
+ const tools = [...entries.values()].map((entry) => entry.definition);
1005
+ const call = async (name, args, extra) => {
1006
+ const entry = entries.get(name);
1007
+ if (!entry) {
1008
+ return text({
1009
+ type: "urn:afora:exception:NotFound",
1010
+ title: "NotFound",
1011
+ status: 404,
1012
+ detail: `Unknown tool "${name}".`
1013
+ }, true);
1014
+ }
1015
+ const { operation } = entry;
1016
+ const options_ = {};
1017
+ if (operation.context) {
1018
+ const types = scalarTypesOf(operation.context);
1019
+ const context = {};
1020
+ for (const key of Object.keys(operation.context.properties)) {
1021
+ const source = mapping[key];
1022
+ const value = source === undefined ? undefined : extra?.values?.[source];
1023
+ if (value !== undefined)
1024
+ context[key] = coerce(value, types[key]);
1025
+ }
1026
+ options_.context = context;
1027
+ }
1028
+ try {
1029
+ const response = await operation.invoke(args ?? {}, options_);
1030
+ return text(outputValueOf(response) ?? null);
1031
+ } catch (error) {
1032
+ const status = statusOf(error, operation, output.statuses);
1033
+ return text(problemOf(error, { status }), true);
1034
+ }
1035
+ };
1036
+ const register = (server) => {
1037
+ server.setRequestHandler(ListToolsRequestSchema2, async () => ({
1038
+ tools: tools.map(({ name, description, inputSchema }) => ({
1039
+ name,
1040
+ description,
1041
+ inputSchema
1042
+ }))
1043
+ }));
1044
+ server.setRequestHandler(CallToolRequestSchema2, async (request, extra) => {
1045
+ const values = {};
1046
+ const meta = request.params._meta;
1047
+ for (const [key, value] of Object.entries(meta ?? {})) {
1048
+ if (typeof value === "string")
1049
+ values[key] = value;
1050
+ }
1051
+ const headers = extra.requestInfo?.headers ?? {};
1052
+ for (const [key, value] of Object.entries(headers)) {
1053
+ const one = Array.isArray(value) ? value[0] : value;
1054
+ if (one !== undefined)
1055
+ values[key.toLowerCase()] = one;
1056
+ }
1057
+ const result = await call(request.params.name, request.params.arguments, { values });
1058
+ return { ...result, content: [...result.content] };
1059
+ });
1060
+ return server;
1061
+ };
1062
+ return {
1063
+ tools,
1064
+ call,
1065
+ register,
1066
+ server: () => register(new Server2({ name: options.name, version: options.version }, { capabilities: { tools: {} } }))
1067
+ };
1068
+ }
1069
+ });
1070
+ var McpStages = stageCatalogOf(mcpTools);
1071
+ var stages = [
1072
+ {
1073
+ name: "mcpTools",
1074
+ module: "@afora/mcp",
1075
+ export: "mcpTools",
1076
+ kind: "sink"
1077
+ }
1078
+ ];
1079
+ export {
1080
+ GUIDE,
1081
+ McpStages,
1082
+ createServer,
1083
+ mcpTools,
1084
+ parseDiagnostics,
1085
+ stages,
1086
+ tools
1087
+ };