@formancy/mcp 0.7.0 → 0.8.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/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as getForm, c as publishForm, i as diffForms, l as validateForm, n as createFormancyMcpServer, o as listForms, r as describeSpec, s as listSubmissions, t as TOOL_DEFINITIONS } from "./server-
|
|
1
|
+
import { a as getForm, c as publishForm, i as diffForms, l as validateForm, n as createFormancyMcpServer, o as listForms, r as describeSpec, s as listSubmissions, t as TOOL_DEFINITIONS } from "./server-C8zOUff6.mjs";
|
|
2
2
|
export { TOOL_DEFINITIONS, createFormancyMcpServer, describeSpec, diffForms, getForm, listForms, listSubmissions, publishForm, validateForm };
|
package/dist/main.mjs
CHANGED
|
@@ -1,19 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { n as createFormancyMcpServer } from "./server-
|
|
2
|
+
import { n as createFormancyMcpServer } from "./server-C8zOUff6.mjs";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
4
|
//#region src/main.ts
|
|
5
|
-
/**
|
|
6
|
-
* The executable. Reads the environment, wires the server, listens on stdio.
|
|
7
|
-
*
|
|
8
|
-
* A server is optional and its absence is a supported state: describe_spec,
|
|
9
|
-
* validate_form and diff_forms need nothing but this process, so an agent can
|
|
10
|
-
* author and check a whole form before anybody has deployed a backend. That is
|
|
11
|
-
* deliberately the useful half.
|
|
12
|
-
*
|
|
13
|
-
* stdio rather than HTTP, because the client launches this process and talks
|
|
14
|
-
* down its pipes. Nothing here listens on a port, so nothing here needs a
|
|
15
|
-
* port's worth of thinking about who can reach it.
|
|
16
|
-
*/
|
|
17
5
|
const baseUrl = process.env["FORMANCY_URL"];
|
|
18
6
|
const apiKey = process.env["FORMANCY_API_KEY"];
|
|
19
7
|
if (baseUrl === void 0 !== (apiKey === void 0)) throw new Error("Set FORMANCY_URL and FORMANCY_API_KEY together, or neither. With neither, the local tools still work.");
|
package/dist/main.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.mjs","names":[],"sources":["../src/main.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { createFormancyMcpServer } from './server.js'\n\n/**\n * The executable. Reads the environment, wires the server, listens on stdio.\n *\n * A server is optional and its absence is a supported state: describe_spec,\n * validate_form and diff_forms need nothing but this process, so an agent can\n * author and check a whole form before anybody has deployed a backend. That is\n * deliberately the useful half.\n *\n * stdio rather than HTTP, because the client launches this process and talks\n * down its pipes. Nothing here listens on a port, so nothing here needs a\n * port's worth of thinking about who can reach it.\n */\nconst baseUrl = process.env['FORMANCY_URL']\nconst apiKey = process.env['FORMANCY_API_KEY']\n\nif ((baseUrl === undefined) !== (apiKey === undefined)) {\n // Half-configured is the state worth refusing: a URL with no key produces a\n // 401 on every call, and a key with no URL produces nothing at all. Both\n // look like the tool being broken rather than unconfigured.\n throw new Error(\n 'Set FORMANCY_URL and FORMANCY_API_KEY together, or neither. With neither, the local tools still work.',\n )\n}\n\nconst server = createFormancyMcpServer(\n baseUrl === undefined || apiKey === undefined ? {} : { access: { baseUrl, apiKey } },\n)\n\nawait server.connect(new StdioServerTransport())\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"main.mjs","names":[],"sources":["../src/main.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'\nimport { createFormancyMcpServer } from './server.js'\n\n/**\n * The executable. Reads the environment, wires the server, listens on stdio.\n *\n * A server is optional and its absence is a supported state: describe_spec,\n * validate_form and diff_forms need nothing but this process, so an agent can\n * author and check a whole form before anybody has deployed a backend. That is\n * deliberately the useful half.\n *\n * stdio rather than HTTP, because the client launches this process and talks\n * down its pipes. Nothing here listens on a port, so nothing here needs a\n * port's worth of thinking about who can reach it.\n */\nconst baseUrl = process.env['FORMANCY_URL']\nconst apiKey = process.env['FORMANCY_API_KEY']\n\nif ((baseUrl === undefined) !== (apiKey === undefined)) {\n // Half-configured is the state worth refusing: a URL with no key produces a\n // 401 on every call, and a key with no URL produces nothing at all. Both\n // look like the tool being broken rather than unconfigured.\n throw new Error(\n 'Set FORMANCY_URL and FORMANCY_API_KEY together, or neither. With neither, the local tools still work.',\n )\n}\n\nconst server = createFormancyMcpServer(\n baseUrl === undefined || apiKey === undefined ? {} : { access: { baseUrl, apiKey } },\n)\n\nawait server.connect(new StdioServerTransport())\n"],"mappings":";;;;AAgBA,MAAM,UAAU,QAAQ,IAAI;AAC5B,MAAM,SAAS,QAAQ,IAAI;AAE3B,IAAK,YAAY,KAAA,OAAgB,WAAW,KAAA,IAI1C,MAAM,IAAI,MACR,uGACF;AAOF,MAJe,wBACb,YAAY,KAAA,KAAa,WAAW,KAAA,IAAY,CAAC,IAAI,EAAE,QAAQ;CAAE;CAAS;AAAO,EAAE,CAG1E,CAAC,CAAC,QAAQ,IAAI,qBAAqB,CAAC"}
|
|
@@ -5,14 +5,6 @@ import { engineRefusal, expressionProblems, runScenarios } from "@formancy/core"
|
|
|
5
5
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
//#region src/tools.ts
|
|
8
|
-
/**
|
|
9
|
-
* Everything the spec allows, so a model does not have to guess.
|
|
10
|
-
*
|
|
11
|
-
* The cheapest tool here and probably the most valuable: without it a model
|
|
12
|
-
* invents `type: "phone"` or `type: "email"` from its memory of other form
|
|
13
|
-
* builders, gets a validation error, and burns a turn finding out that `text`
|
|
14
|
-
* with `format: "email"` is the answer. Told up front, it does not.
|
|
15
|
-
*/
|
|
16
8
|
function describeSpec() {
|
|
17
9
|
const facts = authoringFacts();
|
|
18
10
|
return {
|
|
@@ -21,23 +13,6 @@ function describeSpec() {
|
|
|
21
13
|
data: facts
|
|
22
14
|
};
|
|
23
15
|
}
|
|
24
|
-
/**
|
|
25
|
-
* Check a document without publishing it — the server's publish checks, in the
|
|
26
|
-
* gate's order. Only the analysis of regular-expression `pattern`s stays on
|
|
27
|
-
* the server: it needs an analysis engine this package does not carry, and the
|
|
28
|
-
* server's refusal names the pattern.
|
|
29
|
-
*
|
|
30
|
-
* Schema validity, the engine's compile and expression sanity are different
|
|
31
|
-
* failures with different fixes, and a later check means nothing in a document
|
|
32
|
-
* an earlier one has rejected. So the structure is checked first, then whether
|
|
33
|
-
* the engine would open the document at all, then whether its expressions
|
|
34
|
-
* would ever do anything.
|
|
35
|
-
*
|
|
36
|
-
* The middle check was once missing, and this tool told a model that a form
|
|
37
|
-
* with a misspelled field name was valid: the validator does not read CEL and
|
|
38
|
-
* the expression check skips what the engine refuses, so nothing here asked
|
|
39
|
-
* the engine. The server did, and refused it.
|
|
40
|
-
*/
|
|
41
16
|
function validateForm(document) {
|
|
42
17
|
const result = validateSchema(document);
|
|
43
18
|
if (!result.valid) return {
|
|
@@ -72,12 +47,6 @@ function validateForm(document) {
|
|
|
72
47
|
data: { valid: true }
|
|
73
48
|
};
|
|
74
49
|
}
|
|
75
|
-
/**
|
|
76
|
-
* What a change does to the submissions already collected.
|
|
77
|
-
*
|
|
78
|
-
* The question an agent cannot answer by reading the two documents, and the
|
|
79
|
-
* one whose wrong answer is permanent. `breaking` is not advice.
|
|
80
|
-
*/
|
|
81
50
|
function diffForms(before, after) {
|
|
82
51
|
const first = validateSchema(before);
|
|
83
52
|
const second = validateSchema(after);
|
|
@@ -112,14 +81,6 @@ function severityOf(changes) {
|
|
|
112
81
|
if (changes.length > 0) return "compatible";
|
|
113
82
|
return "none";
|
|
114
83
|
}
|
|
115
|
-
/**
|
|
116
|
-
* Publish, but check first.
|
|
117
|
-
*
|
|
118
|
-
* The local validation is not an optimisation. A model that gets a 422 back
|
|
119
|
-
* has to guess what the server meant from an error shape it has never seen;
|
|
120
|
-
* a model that gets `expressionProblems` back is told which rule, in which
|
|
121
|
-
* words, and what to write instead. And an invalid document never travels.
|
|
122
|
-
*/
|
|
123
84
|
async function publishForm(access, path, document, basedOn) {
|
|
124
85
|
const checked = validateForm(document);
|
|
125
86
|
if (!checked.ok) return {
|
|
@@ -147,20 +108,6 @@ async function publishForm(access, path, document, basedOn) {
|
|
|
147
108
|
})
|
|
148
109
|
});
|
|
149
110
|
}
|
|
150
|
-
/**
|
|
151
|
-
* A form run against examples with their answers written down.
|
|
152
|
-
*
|
|
153
|
-
* The check nothing else here can make. `validate_form` says a document
|
|
154
|
-
* works; it cannot say the condition is backwards, because
|
|
155
|
-
* `kind == 'other'` and `kind != 'other'` are both valid CEL and the
|
|
156
|
-
* difference is between the document and what somebody meant
|
|
157
|
-
* ([0110](../../../docs/decisions/0110-a-form-is-checked-against-examples.md)).
|
|
158
|
-
*
|
|
159
|
-
* An agent is exactly who needs telling: it writes a rule from a sentence,
|
|
160
|
-
* and an example is the one thing that catches it writing the opposite rule.
|
|
161
|
-
* Local — no server, no credentials — so it can run before anything is
|
|
162
|
-
* published.
|
|
163
|
-
*/
|
|
164
111
|
function checkScenarios(document, scenarios) {
|
|
165
112
|
if (scenarios.length === 0) return {
|
|
166
113
|
ok: false,
|
|
@@ -180,18 +127,6 @@ function checkScenarios(document, scenarios) {
|
|
|
180
127
|
data: { results }
|
|
181
128
|
};
|
|
182
129
|
}
|
|
183
|
-
/**
|
|
184
|
-
* An edit held up against what is published, without publishing it.
|
|
185
|
-
*
|
|
186
|
-
* The MCP half of a builder's review step. `validate_form` says a document
|
|
187
|
-
* works; it does not say that the rewrite dropped an option somebody has
|
|
188
|
-
* already chosen, and an agent reading only its own two documents cannot tell
|
|
189
|
-
* either. This fetches what is live, diffs against it, and hands back the
|
|
190
|
-
* change list and the hash — so the agent can put *that* in front of a person
|
|
191
|
-
* rather than "I updated your form", and then publish with the hash so the
|
|
192
|
-
* edit cannot land on a version it was never written against
|
|
193
|
-
* ([0109](../../../docs/decisions/0109-an-ai-edit-is-reviewed-before-it-lands.md)).
|
|
194
|
-
*/
|
|
195
130
|
async function proposeFormEdit(access, path, document) {
|
|
196
131
|
const checked = validateForm(document);
|
|
197
132
|
if (!checked.ok) return {
|
|
@@ -213,7 +148,6 @@ async function proposeFormEdit(access, path, document) {
|
|
|
213
148
|
}
|
|
214
149
|
};
|
|
215
150
|
}
|
|
216
|
-
/** What the server currently has for a path, and its hash. */
|
|
217
151
|
async function currentSchema(access, path) {
|
|
218
152
|
const fetched = await getForm(access, path);
|
|
219
153
|
if (!fetched.ok) return {
|
|
@@ -243,13 +177,6 @@ async function getForm(access, path) {
|
|
|
243
177
|
async function listSubmissions(access, path) {
|
|
244
178
|
return request(access, `/f/${encodeURIComponent(path)}/submissions`);
|
|
245
179
|
}
|
|
246
|
-
/**
|
|
247
|
-
* One request, and one place that knows how a failure is reported.
|
|
248
|
-
*
|
|
249
|
-
* Every error comes back as `ok: false` with the server's own words rather
|
|
250
|
-
* than as a thrown exception, because a thrown exception reaches a model as a
|
|
251
|
-
* stack trace and tells it nothing it can act on.
|
|
252
|
-
*/
|
|
253
180
|
async function request(access, route, init = {}) {
|
|
254
181
|
const send = access.fetch ?? globalThis.fetch;
|
|
255
182
|
const url = `${access.baseUrl.replace(/\/$/, "")}${route}`;
|
|
@@ -292,14 +219,6 @@ function messageFrom(body) {
|
|
|
292
219
|
}
|
|
293
220
|
//#endregion
|
|
294
221
|
//#region src/server.ts
|
|
295
|
-
/**
|
|
296
|
-
* Names and descriptions, apart from the wiring.
|
|
297
|
-
*
|
|
298
|
-
* Exported because the description is the prompt: it is the only thing telling
|
|
299
|
-
* a model when to reach for a tool, and a wrong one means a tool that is never
|
|
300
|
-
* called or called for the wrong reason. Kept where it can be read and tested
|
|
301
|
-
* rather than buried in a registration call.
|
|
302
|
-
*/
|
|
303
222
|
const TOOL_DEFINITIONS = {
|
|
304
223
|
describe_spec: "List every field type, layout kind, rule kind and format the formancy spec defines. Call this BEFORE writing a form document. A type not in the list does not exist, whatever other form builders call it.",
|
|
305
224
|
validate_form: "Run the server's publish checks without publishing: against the spec, through the engine that renders the form (a misspelled field name, a cycle between computed fields, a condition that is not certain to produce a bool), and for expressions that compile but can never evaluate — the failure that is silent at runtime. Each comes back separately, with what to change.",
|
|
@@ -311,41 +230,12 @@ const TOOL_DEFINITIONS = {
|
|
|
311
230
|
get_form: "Fetch one form document from the formancy server, with its version and schema hash.",
|
|
312
231
|
list_submissions: "List submissions for one form."
|
|
313
232
|
};
|
|
314
|
-
/**
|
|
315
|
-
* The version this server reports.
|
|
316
|
-
*
|
|
317
|
-
* From the manifest, not typed. It said `0.1.0` while the package was on 0.3.0 —
|
|
318
|
-
* a wrong statement in the one field a client uses to tell two installations
|
|
319
|
-
* apart, and exactly the hand-written number this repository keeps finding
|
|
320
|
-
* stale.
|
|
321
|
-
*
|
|
322
|
-
* `createRequire` rather than a JSON import: `../package.json` resolves to the
|
|
323
|
-
* package root from both `src/server.ts` and `dist/index.mjs`, so the same line
|
|
324
|
-
* is right in a test and in the published package.
|
|
325
|
-
*/
|
|
326
233
|
const PACKAGE_VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
327
|
-
/**
|
|
328
|
-
* What a client is told a result looks like, before it reads one.
|
|
329
|
-
*
|
|
330
|
-
* One envelope for every tool rather than a schema each. The shape is the same
|
|
331
|
-
* everywhere — did it work, one sentence a person can read, and the part an
|
|
332
|
-
* agent acts on — so a client that has learned it once has learned it for all
|
|
333
|
-
* nine. A schema per tool would be nine places for `data` to drift from what
|
|
334
|
-
* the tool returns.
|
|
335
|
-
*/
|
|
336
234
|
const RESULT_SHAPE = {
|
|
337
235
|
ok: z.boolean().describe("Whether the tool did what was asked."),
|
|
338
236
|
summary: z.string().describe("One or two sentences a person can read."),
|
|
339
237
|
data: z.unknown().optional().describe("The part an agent acts on, when there is one.")
|
|
340
238
|
};
|
|
341
|
-
/**
|
|
342
|
-
* Both readings of the answer: prose and structure.
|
|
343
|
-
*
|
|
344
|
-
* `content` is what a model reads; `structuredContent` is what a client parses
|
|
345
|
-
* without pulling JSON back out of a string. Both travel — prose alone throws
|
|
346
|
-
* away the part an agent acts on, and structure alone makes every refusal a
|
|
347
|
-
* parsing exercise.
|
|
348
|
-
*/
|
|
349
239
|
function respond(result) {
|
|
350
240
|
return {
|
|
351
241
|
content: [{
|
|
@@ -360,15 +250,6 @@ function respond(result) {
|
|
|
360
250
|
...result.ok ? {} : { isError: true }
|
|
361
251
|
};
|
|
362
252
|
}
|
|
363
|
-
/**
|
|
364
|
-
* What a tool will do, for a client deciding whether to ask first.
|
|
365
|
-
*
|
|
366
|
-
* The only thing a host has to tell `validate_form` from `publish_form` before
|
|
367
|
-
* calling either. Without them a tool defaults to `readOnlyHint: false`,
|
|
368
|
-
* `destructiveHint: true`, `openWorldHint: true` — so this server’s local,
|
|
369
|
-
* read-only checks were all advertised as potentially destructive calls into an
|
|
370
|
-
* open world, which is the opposite of true in every case.
|
|
371
|
-
*/
|
|
372
253
|
const LOCAL_CHECK = {
|
|
373
254
|
readOnlyHint: true,
|
|
374
255
|
openWorldHint: false
|
|
@@ -377,37 +258,18 @@ const SERVER_READ = {
|
|
|
377
258
|
readOnlyHint: true,
|
|
378
259
|
openWorldHint: true
|
|
379
260
|
};
|
|
380
|
-
/**
|
|
381
|
-
* Publishing. `destructiveHint: false` is not optimism: a published version is
|
|
382
|
-
* immutable and a publish inserts a new row, so nothing is overwritten
|
|
383
|
-
* ([0025](../../../docs/decisions/0025-immutability-in-the-database.md)).
|
|
384
|
-
*
|
|
385
|
-
* Not idempotent, though — publishing the same document twice makes a second
|
|
386
|
-
* version rather than the same one, and saying otherwise would invite a client
|
|
387
|
-
* to retry it freely.
|
|
388
|
-
*/
|
|
389
261
|
const SERVER_WRITE = {
|
|
390
262
|
readOnlyHint: false,
|
|
391
263
|
destructiveHint: false,
|
|
392
264
|
idempotentHint: false,
|
|
393
265
|
openWorldHint: true
|
|
394
266
|
};
|
|
395
|
-
/** A tool that needs a server, when there is not one. */
|
|
396
267
|
function noServer(name) {
|
|
397
268
|
return {
|
|
398
269
|
ok: false,
|
|
399
270
|
summary: `${name} needs a formancy server, and none is configured. Set FORMANCY_URL and FORMANCY_API_KEY. The local tools — describe_spec, validate_form and diff_forms — work without one, so a form can be written and checked before a server exists.`
|
|
400
271
|
};
|
|
401
272
|
}
|
|
402
|
-
/**
|
|
403
|
-
* The arguments each tool takes.
|
|
404
|
-
*
|
|
405
|
-
* A document arrives as `unknown` on purpose: it is checked by
|
|
406
|
-
* `validateSchema` against the spec's own JSON Schema, which is the one
|
|
407
|
-
* authority on what a formancy document is. Describing its shape a second
|
|
408
|
-
* time here in Zod would be a second authority, and the day the two disagree
|
|
409
|
-
* the tool would reject a document the product accepts.
|
|
410
|
-
*/
|
|
411
273
|
const DOCUMENT = z.unknown().describe("A formancy form document (the JSON schema object).");
|
|
412
274
|
const FORM_PATH = z.string().min(1).describe("The form's path, as it appears in its URL.");
|
|
413
275
|
const SCENARIOS = z.array(z.unknown()).describe("Examples with their answers: each is { name, changes, valid } and may also pin errors, visible, values and absent.");
|
|
@@ -588,4 +450,4 @@ function createFormancyMcpServer(options = {}) {
|
|
|
588
450
|
//#endregion
|
|
589
451
|
export { getForm as a, publishForm as c, diffForms as i, validateForm as l, createFormancyMcpServer as n, listForms as o, describeSpec as r, listSubmissions as s, TOOL_DEFINITIONS as t };
|
|
590
452
|
|
|
591
|
-
//# sourceMappingURL=server-
|
|
453
|
+
//# sourceMappingURL=server-C8zOUff6.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-Cv8ZUKhl.mjs","names":[],"sources":["../src/tools.ts","../src/server.ts"],"sourcesContent":["import { authoringFacts, diffSchemas, schemaHash } from '@formancy/spec'\nimport { validateSchema } from '@formancy/spec/validate'\nimport type { Change, FormSchema } from '@formancy/spec'\nimport { engineRefusal, expressionProblems, runScenarios } from '@formancy/core'\nimport type { Scenario } from '@formancy/core'\n\n/**\n * What formancy can be asked to do by a coding agent, as plain functions.\n *\n * Separated from the MCP server itself for the same reason `server-core` is separated\n * from `server`: a use-case that can only be exercised through a transport is one nobody\n * can test. Everything here is a function from JSON to JSON.\n *\n * **The tools check first and act second.** `publish_form` validates locally before it\n * opens a socket, so an invalid document never reaches the server and the model gets the\n * reason rather than a 422. That is possible because the format has a published JSON\n * Schema and the expressions are statically type-checked — a generated document is\n * checkable before anything is done with it.\n */\n\n/** A tool's answer: what happened, and what the agent should do about it. */\nexport interface ToolResult {\n /** False means the agent must fix something before trying again. */\n readonly ok: boolean\n /** Prose for the model. Written to be acted on, not logged. */\n readonly summary: string\n /** The machine-readable part, when there is one. */\n readonly data?: unknown\n}\n\n/** Where a formancy server is, and how to prove who you are to it. */\nexport interface ServerAccess {\n readonly baseUrl: string\n readonly apiKey: string\n /** Injected so tests do not need a socket. */\n readonly fetch?: typeof globalThis.fetch\n}\n\n// ---------------------------------------------------------------- local tools\n\n/**\n * Everything the spec allows, so a model does not have to guess.\n *\n * The cheapest tool here and probably the most valuable: without it a model\n * invents `type: \"phone\"` or `type: \"email\"` from its memory of other form\n * builders, gets a validation error, and burns a turn finding out that `text`\n * with `format: \"email\"` is the answer. Told up front, it does not.\n */\nexport function describeSpec(): ToolResult {\n const facts = authoringFacts()\n return {\n ok: true,\n summary:\n `formancy speaks spec versions ${facts.specVersions.join(' and ')}; new documents should ` +\n `use \"${facts.currentSpecVersion}\". A field type not in this list does not exist, whatever ` +\n `other form builders call it — an email field is type \"text\" with format \"email\".`,\n data: facts,\n }\n}\n\n/**\n * Check a document without publishing it — the server's publish checks, in the\n * gate's order. Only the analysis of regular-expression `pattern`s stays on\n * the server: it needs an analysis engine this package does not carry, and the\n * server's refusal names the pattern.\n *\n * Schema validity, the engine's compile and expression sanity are different\n * failures with different fixes, and a later check means nothing in a document\n * an earlier one has rejected. So the structure is checked first, then whether\n * the engine would open the document at all, then whether its expressions\n * would ever do anything.\n *\n * The middle check was once missing, and this tool told a model that a form\n * with a misspelled field name was valid: the validator does not read CEL and\n * the expression check skips what the engine refuses, so nothing here asked\n * the engine. The server did, and refused it.\n */\nexport function validateForm(document: unknown): ToolResult {\n const result = validateSchema(document)\n if (!result.valid) {\n return {\n ok: false,\n summary: `Not a valid formancy document: ${String(result.errors.length)} problem(s). Fix these before publishing.`,\n data: { valid: false, errors: result.errors },\n }\n }\n\n const refusal = engineRefusal(document as FormSchema)\n if (refusal !== undefined) {\n return {\n ok: false,\n summary:\n `The document is structurally valid, but the engine refuses to open it — the same engine the ` +\n `server's publish gate and every renderer build, so this form could be neither published nor shown. ` +\n refusal,\n data: { valid: true, engineRefusal: refusal },\n }\n }\n\n const problems = expressionProblems(document as FormSchema)\n if (problems.length > 0) {\n return {\n ok: false,\n summary:\n `The document is structurally valid, but ${String(problems.length)} expression(s) will never ` +\n `do anything. These compile and then fail at evaluation, so nothing would report them at runtime: ` +\n `a computed field would stay empty and a visible rule would show the field it was meant to hide.`,\n data: { valid: true, expressionProblems: problems },\n }\n }\n\n return {\n ok: true,\n summary: 'Valid, and every expression type-checks.',\n data: { valid: true },\n }\n}\n\n/**\n * What a change does to the submissions already collected.\n *\n * The question an agent cannot answer by reading the two documents, and the\n * one whose wrong answer is permanent. `breaking` is not advice.\n */\nexport function diffForms(before: unknown, after: unknown): ToolResult {\n const first = validateSchema(before)\n const second = validateSchema(after)\n if (!first.valid || !second.valid) {\n return {\n ok: false,\n summary: 'Both documents have to be valid before they can be compared. Run validate_form on each.',\n data: { beforeValid: first.valid, afterValid: second.valid },\n }\n }\n\n const changes = diffSchemas(before as FormSchema, after as FormSchema)\n const worst = severityOf(changes)\n\n return {\n ok: true,\n summary:\n changes.length === 0\n ? 'No change: the two documents are identical once canonicalised.'\n : `${String(changes.length)} change(s), worst severity ${worst}. ` + ADVICE[worst],\n data: { severity: worst, changes },\n }\n}\n\nconst ADVICE: Readonly<Record<'none' | 'compatible' | 'lossy' | 'breaking', string>> = {\n none: '',\n compatible: 'Existing drafts and submissions rebind silently.',\n lossy: 'Existing data rebinds, but some answers no longer have a field to live in. They are kept under data.__orphaned rather than deleted.',\n breaking:\n 'Existing drafts CANNOT rebind and will open read-only against the version that produced them. Say so before publishing, and consider a new form instead.',\n}\n\nfunction severityOf(changes: readonly Change[]): 'none' | 'compatible' | 'lossy' | 'breaking' {\n if (changes.some((change) => change.severity === 'breaking')) return 'breaking'\n if (changes.some((change) => change.severity === 'lossy')) return 'lossy'\n if (changes.length > 0) return 'compatible'\n return 'none'\n}\n\n// --------------------------------------------------------------- server tools\n\n/**\n * Publish, but check first.\n *\n * The local validation is not an optimisation. A model that gets a 422 back\n * has to guess what the server meant from an error shape it has never seen;\n * a model that gets `expressionProblems` back is told which rule, in which\n * words, and what to write instead. And an invalid document never travels.\n */\nexport async function publishForm(\n access: ServerAccess,\n path: string,\n document: unknown,\n /**\n * The schema hash the edit was based on, from `propose_form_edit` or\n * `get_form`. Given, the published form is checked against it first.\n *\n * Opt-in, and the reason is the first publish: a new form has nothing to be\n * based on, and requiring this would turn it into a fetch of something that\n * is not there. So the unsafe path still exists, which is why the tool's\n * description points at the safe one rather than leaving an agent to guess.\n */\n basedOn?: string,\n): Promise<ToolResult> {\n const checked = validateForm(document)\n if (!checked.ok) {\n return {\n ok: false,\n summary: `Not published, because the document would not have worked. ${checked.summary}`,\n data: checked.data,\n }\n }\n\n if (basedOn !== undefined) {\n const current = await currentSchema(access, path)\n if (!current.ok) return current.problem\n if (current.hash !== basedOn) {\n /*\n * The lost update, and the one mistake here nobody sees until the form\n * is wrong. A document is the WHOLE form, so publishing an edit based\n * on an older version over a newer one discards that version's change\n * entirely — and the publish succeeds, so nothing reports it.\n */\n return {\n ok: false,\n summary:\n `Not published: \"${path}\" has changed since this edit was based on it, and publishing ` +\n `the whole document would discard that change. Call get_form again, redo the edit ` +\n `against what is there now, and publish with the new basedOn.`,\n data: { basedOn, current: current.hash },\n }\n }\n }\n\n return request(access, '/forms', {\n method: 'POST',\n body: JSON.stringify({ path, schema: document }),\n })\n}\n\n/**\n * A form run against examples with their answers written down.\n *\n * The check nothing else here can make. `validate_form` says a document\n * works; it cannot say the condition is backwards, because\n * `kind == 'other'` and `kind != 'other'` are both valid CEL and the\n * difference is between the document and what somebody meant\n * ([0110](../../../docs/decisions/0110-a-form-is-checked-against-examples.md)).\n *\n * An agent is exactly who needs telling: it writes a rule from a sentence,\n * and an example is the one thing that catches it writing the opposite rule.\n * Local — no server, no credentials — so it can run before anything is\n * published.\n */\nexport function checkScenarios(document: unknown, scenarios: readonly Scenario[]): ToolResult {\n if (scenarios.length === 0) {\n /*\n * The vacuous pass. \"All 0 scenarios hold\" is true, and it is the single\n * most misleading sentence this tool could give an agent about to\n * publish.\n */\n return {\n ok: false,\n summary:\n 'No scenarios to check, so this says nothing about the form. A scenario is a set of ' +\n 'answers and what the form should make of them — whether it validates, which errors ' +\n 'it gives, which fields are visible, and what the submission carries.',\n }\n }\n\n const checked = validateForm(document)\n if (!checked.ok) {\n // One fact, not one failure per example: an agent given twenty identical\n // \"no such field\" lines reads the noise and not the cause.\n return {\n ok: false,\n summary: `Nothing was checked, because the document would not have worked. ${checked.summary}`,\n data: checked.data,\n }\n }\n\n const results = runScenarios(document as FormSchema, scenarios)\n const broken = results.filter((result) => !result.passed)\n\n return {\n ok: broken.length === 0,\n summary:\n broken.length === 0\n ? `All ${String(results.length)} scenarios hold.`\n : `${String(broken.length)} of ${String(results.length)} scenarios no longer hold: ` +\n `${broken.map((result) => `\"${result.name}\"`).join(', ')}. Each one below says what it ` +\n `expected and what happened — a rule that compiles can still be the opposite of the ` +\n `rule that was asked for.`,\n data: { results },\n }\n}\n\n/**\n * An edit held up against what is published, without publishing it.\n *\n * The MCP half of a builder's review step. `validate_form` says a document\n * works; it does not say that the rewrite dropped an option somebody has\n * already chosen, and an agent reading only its own two documents cannot tell\n * either. This fetches what is live, diffs against it, and hands back the\n * change list and the hash — so the agent can put *that* in front of a person\n * rather than \"I updated your form\", and then publish with the hash so the\n * edit cannot land on a version it was never written against\n * ([0109](../../../docs/decisions/0109-an-ai-edit-is-reviewed-before-it-lands.md)).\n */\nexport async function proposeFormEdit(\n access: ServerAccess,\n path: string,\n document: unknown,\n): Promise<ToolResult> {\n const checked = validateForm(document)\n if (!checked.ok) {\n // A change list for a document that cannot be published is a list of\n // things that will not happen.\n return {\n ok: false,\n summary: `Nothing to review, because the document would not have worked. ${checked.summary}`,\n data: checked.data,\n }\n }\n\n const current = await currentSchema(access, path)\n if (!current.ok) return current.problem\n\n const changes = diffSchemas(current.schema, document as FormSchema)\n const worst = severityOf(changes)\n\n return {\n ok: true,\n summary:\n changes.length === 0\n ? `No change: \"${path}\" is already this document. Nothing to publish.`\n : `${String(changes.length)} change(s) to \"${path}\", worst severity ${worst}. ${ADVICE[worst]} ` +\n `Show these to the person before publishing, then publish with basedOn=${current.hash}.`,\n data: { severity: worst, changes, basedOn: current.hash },\n }\n}\n\n/** What the server currently has for a path, and its hash. */\nasync function currentSchema(\n access: ServerAccess,\n path: string,\n): Promise<\n { ok: true; schema: FormSchema; hash: string } | { ok: false; problem: ToolResult }\n> {\n const fetched = await getForm(access, path)\n if (!fetched.ok) return { ok: false, problem: fetched }\n\n const schema = (fetched.data as { schema?: unknown } | undefined)?.schema\n const valid = validateSchema(schema)\n if (!valid.valid) {\n // The published form does not validate against this reader's spec: an\n // older client against a newer document. Said plainly rather than\n // compared anyway, which would report nonsense as a change list.\n return {\n ok: false,\n problem: {\n ok: false,\n summary:\n `The form published at \"${path}\" is not a document this version of the tools can read, ` +\n `so an edit cannot be compared against it. Upgrade the tools to the server's spec version.`,\n },\n }\n }\n\n return { ok: true, schema: schema as FormSchema, hash: schemaHash(schema as FormSchema) }\n}\n\nexport async function listForms(access: ServerAccess): Promise<ToolResult> {\n return request(access, '/forms')\n}\n\nexport async function getForm(access: ServerAccess, path: string): Promise<ToolResult> {\n return request(access, `/f/${encodeURIComponent(path)}`)\n}\n\nexport async function listSubmissions(access: ServerAccess, path: string): Promise<ToolResult> {\n return request(access, `/f/${encodeURIComponent(path)}/submissions`)\n}\n\n/**\n * One request, and one place that knows how a failure is reported.\n *\n * Every error comes back as `ok: false` with the server's own words rather\n * than as a thrown exception, because a thrown exception reaches a model as a\n * stack trace and tells it nothing it can act on.\n */\nasync function request(\n access: ServerAccess,\n route: string,\n init: RequestInit = {},\n): Promise<ToolResult> {\n const send = access.fetch ?? globalThis.fetch\n const url = `${access.baseUrl.replace(/\\/$/, '')}${route}`\n\n let response: Response\n try {\n response = await send(url, {\n ...init,\n headers: {\n ...(init.body === undefined ? {} : { 'content-type': 'application/json' }),\n authorization: `Bearer ${access.apiKey}`,\n ...init.headers,\n },\n })\n } catch (error) {\n return {\n ok: false,\n summary:\n `Could not reach the formancy server at ${access.baseUrl}: ${error instanceof Error ? error.message : String(error)}. ` +\n `Check FORMANCY_URL, and that the server is running.`,\n }\n }\n\n const body: unknown = await response.json().catch(() => undefined)\n\n if (!response.ok) {\n const detail = messageFrom(body)\n return {\n ok: false,\n summary:\n response.status === 401 || response.status === 403\n ? `The server refused the request (${String(response.status)}). Check FORMANCY_API_KEY.`\n : `The server refused the request (${String(response.status)})${detail === undefined ? '.' : `: ${detail}`}`,\n ...(body === undefined ? {} : { data: body }),\n }\n }\n\n return { ok: true, summary: 'Done.', data: body }\n}\n\nfunction messageFrom(body: unknown): string | undefined {\n if (body === null || typeof body !== 'object') return undefined\n const record = body as { message?: unknown; error?: unknown }\n if (typeof record.message === 'string') return record.message\n if (typeof record.error === 'string') return record.error\n return undefined\n}\n","import { createRequire } from 'node:module'\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'\nimport { z } from 'zod'\nimport {\n checkScenarios,\n describeSpec,\n diffForms,\n getForm,\n listForms,\n listSubmissions,\n proposeFormEdit,\n publishForm,\n validateForm,\n} from './tools.js'\nimport type { ServerAccess, ToolResult } from './tools.js'\n\n/**\n * The tools, wired to the protocol.\n *\n * Thin on purpose: every one of these is a call into `tools.ts` and a\n * conversion of the answer into MCP's content shape. The logic is tested\n * without a protocol, because a use-case that can only be exercised through a\n * transport is a use-case nobody tests properly.\n */\n\nexport interface McpServerOptions {\n /**\n * Where a formancy server is, if there is one.\n *\n * Absent is a supported state and not a degraded one: the four local tools\n * — describe_spec, validate_form, diff_forms and the checking half of\n * publishing — need no server at all, which means an agent can author and\n * verify a whole form before anybody has deployed anything.\n */\n readonly access?: ServerAccess\n}\n\n/**\n * Names and descriptions, apart from the wiring.\n *\n * Exported because the description is the prompt: it is the only thing telling\n * a model when to reach for a tool, and a wrong one means a tool that is never\n * called or called for the wrong reason. Kept where it can be read and tested\n * rather than buried in a registration call.\n */\nexport const TOOL_DEFINITIONS = {\n describe_spec:\n 'List every field type, layout kind, rule kind and format the formancy spec defines. ' +\n 'Call this BEFORE writing a form document. A type not in the list does not exist, whatever ' +\n 'other form builders call it.',\n validate_form:\n \"Run the server's publish checks without publishing: against the spec, through the engine \" +\n 'that renders the form (a misspelled field name, a cycle between computed fields, a ' +\n 'condition that is not certain to produce a bool), and for expressions that compile but ' +\n 'can never evaluate — the failure that is silent at runtime. Each comes back separately, ' +\n 'with what to change.',\n diff_forms:\n 'Compare two form documents and report what the change would do to submissions already ' +\n 'collected: compatible, lossy, or breaking. Call this before publishing over an existing form.',\n check_scenarios:\n 'Run a form against examples with their answers written down, and report which stopped ' +\n 'holding. The ONE check that catches a condition which compiles and is the opposite of ' +\n 'what was asked for — validate_form cannot, because both spellings are valid CEL. Needs no ' +\n 'server. Write a scenario for every rule you add: a set of answers, whether the form is ' +\n 'valid, which errors it gives, which fields are visible, and what the submission carries.',\n propose_form_edit:\n 'Hold an edit up against the form that is published, WITHOUT publishing it. Answers with ' +\n 'what the edit would do to submissions already collected, and with a basedOn hash. ' +\n 'Call this instead of publish_form whenever you are changing a form that already exists: ' +\n 'show the changes to the person, get their agreement, then publish_form with the basedOn ' +\n 'it gave you.',\n publish_form:\n 'Publish a form document to a formancy server. Validates locally first and refuses to send ' +\n 'an invalid document, so the reason comes back as something to fix rather than as a 422. ' +\n 'Pass basedOn when changing an existing form: a document is the WHOLE form, so publishing ' +\n 'one based on an older version silently discards whatever was published in between, and ' +\n 'the publish succeeds so nothing reports it.',\n list_forms: 'List the forms on the formancy server, with their current version.',\n get_form: 'Fetch one form document from the formancy server, with its version and schema hash.',\n list_submissions: 'List submissions for one form.',\n} as const\n\n/**\n * The version this server reports.\n *\n * From the manifest, not typed. It said `0.1.0` while the package was on 0.3.0 —\n * a wrong statement in the one field a client uses to tell two installations\n * apart, and exactly the hand-written number this repository keeps finding\n * stale.\n *\n * `createRequire` rather than a JSON import: `../package.json` resolves to the\n * package root from both `src/server.ts` and `dist/index.mjs`, so the same line\n * is right in a test and in the published package.\n */\nexport const PACKAGE_VERSION: string = (\n createRequire(import.meta.url)('../package.json') as { version: string }\n).version\n\n/**\n * What a client is told a result looks like, before it reads one.\n *\n * One envelope for every tool rather than a schema each. The shape is the same\n * everywhere — did it work, one sentence a person can read, and the part an\n * agent acts on — so a client that has learned it once has learned it for all\n * nine. A schema per tool would be nine places for `data` to drift from what\n * the tool returns.\n */\nconst RESULT_SHAPE = {\n ok: z.boolean().describe('Whether the tool did what was asked.'),\n summary: z.string().describe('One or two sentences a person can read.'),\n data: z.unknown().optional().describe('The part an agent acts on, when there is one.'),\n}\n\n/**\n * Both readings of the answer: prose and structure.\n *\n * `content` is what a model reads; `structuredContent` is what a client parses\n * without pulling JSON back out of a string. Both travel — prose alone throws\n * away the part an agent acts on, and structure alone makes every refusal a\n * parsing exercise.\n */\nfunction respond(result: ToolResult): {\n content: { type: 'text'; text: string }[]\n structuredContent: Record<string, unknown>\n isError?: boolean\n} {\n const text =\n result.data === undefined\n ? result.summary\n : `${result.summary}\\n\\n${JSON.stringify(result.data, null, 2)}`\n return {\n content: [{ type: 'text', text }],\n structuredContent: {\n ok: result.ok,\n summary: result.summary,\n ...(result.data === undefined ? {} : { data: result.data }),\n },\n // `isError` rather than a thrown exception: a refusal is an answer the model\n // should read and act on, not a transport failure.\n ...(result.ok ? {} : { isError: true }),\n }\n}\n\n/**\n * What a tool will do, for a client deciding whether to ask first.\n *\n * The only thing a host has to tell `validate_form` from `publish_form` before\n * calling either. Without them a tool defaults to `readOnlyHint: false`,\n * `destructiveHint: true`, `openWorldHint: true` — so this server’s local,\n * read-only checks were all advertised as potentially destructive calls into an\n * open world, which is the opposite of true in every case.\n */\nconst LOCAL_CHECK: ToolAnnotations = {\n readOnlyHint: true,\n // No network and no server: these are the ones that work before anything is\n // deployed, and saying so is what lets a host run them without asking.\n openWorldHint: false,\n}\n\nconst SERVER_READ: ToolAnnotations = { readOnlyHint: true, openWorldHint: true }\n\n/**\n * Publishing. `destructiveHint: false` is not optimism: a published version is\n * immutable and a publish inserts a new row, so nothing is overwritten\n * ([0025](../../../docs/decisions/0025-immutability-in-the-database.md)).\n *\n * Not idempotent, though — publishing the same document twice makes a second\n * version rather than the same one, and saying otherwise would invite a client\n * to retry it freely.\n */\nconst SERVER_WRITE: ToolAnnotations = {\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: true,\n}\n\n/** A tool that needs a server, when there is not one. */\nfunction noServer(name: string): ToolResult {\n return {\n ok: false,\n summary:\n `${name} needs a formancy server, and none is configured. Set FORMANCY_URL and ` +\n `FORMANCY_API_KEY. The local tools — describe_spec, validate_form and diff_forms — work ` +\n `without one, so a form can be written and checked before a server exists.`,\n }\n}\n\n/**\n * The arguments each tool takes.\n *\n * A document arrives as `unknown` on purpose: it is checked by\n * `validateSchema` against the spec's own JSON Schema, which is the one\n * authority on what a formancy document is. Describing its shape a second\n * time here in Zod would be a second authority, and the day the two disagree\n * the tool would reject a document the product accepts.\n */\nconst DOCUMENT = z.unknown().describe('A formancy form document (the JSON schema object).')\nconst FORM_PATH = z.string().min(1).describe(\"The form's path, as it appears in its URL.\")\n/*\n * The scenarios arrive loosely typed for the reason a document does: their\n * shape belongs to `@formancy/core`, and describing it a second time in Zod\n * would be a second authority that eventually disagrees with the first.\n */\nconst SCENARIOS = z\n .array(z.unknown())\n .describe(\n 'Examples with their answers: each is { name, changes, valid } and may also pin errors, ' +\n 'visible, values and absent.',\n )\nconst BASED_ON = z\n .string()\n .optional()\n .describe(\n 'The schema hash this edit was based on, from propose_form_edit or get_form. Given, the ' +\n 'publish is refused if the form has changed since.',\n )\n\nexport function createFormancyMcpServer(options: McpServerOptions = {}): McpServer {\n const server = new McpServer({ name: 'formancy', version: PACKAGE_VERSION })\n const { access } = options\n\n server.registerTool('describe_spec', { description: TOOL_DEFINITIONS.describe_spec, outputSchema: RESULT_SHAPE, annotations: LOCAL_CHECK }, () =>\n respond(describeSpec()),\n )\n\n server.registerTool(\n 'validate_form',\n { description: TOOL_DEFINITIONS.validate_form,\n outputSchema: RESULT_SHAPE,\n annotations: LOCAL_CHECK, inputSchema: { document: DOCUMENT } },\n ({ document }) => respond(validateForm(document)),\n )\n\n server.registerTool(\n 'diff_forms',\n {\n description: TOOL_DEFINITIONS.diff_forms,\n outputSchema: RESULT_SHAPE,\n annotations: LOCAL_CHECK,\n inputSchema: { before: DOCUMENT, after: DOCUMENT },\n },\n ({ before, after }) => respond(diffForms(before, after)),\n )\n\n server.registerTool(\n 'check_scenarios',\n {\n description: TOOL_DEFINITIONS.check_scenarios,\n outputSchema: RESULT_SHAPE,\n annotations: LOCAL_CHECK,\n inputSchema: { document: DOCUMENT, scenarios: SCENARIOS },\n },\n ({ document, scenarios }) => respond(checkScenarios(document, scenarios as never)),\n )\n\n server.registerTool(\n 'propose_form_edit',\n {\n description: TOOL_DEFINITIONS.propose_form_edit,\n outputSchema: RESULT_SHAPE,\n annotations: SERVER_READ,\n inputSchema: { path: FORM_PATH, document: DOCUMENT },\n },\n async ({ path, document }) =>\n respond(\n access === undefined\n ? noServer('propose_form_edit')\n : await proposeFormEdit(access, path, document),\n ),\n )\n\n server.registerTool(\n 'publish_form',\n {\n description: TOOL_DEFINITIONS.publish_form,\n outputSchema: RESULT_SHAPE,\n annotations: SERVER_WRITE,\n inputSchema: { path: FORM_PATH, document: DOCUMENT, basedOn: BASED_ON },\n },\n async ({ path, document, basedOn }) =>\n respond(\n access === undefined\n ? noServer('publish_form')\n : await publishForm(access, path, document, basedOn),\n ),\n )\n\n server.registerTool('list_forms', { description: TOOL_DEFINITIONS.list_forms, outputSchema: RESULT_SHAPE, annotations: SERVER_READ }, async () =>\n respond(access === undefined ? noServer('list_forms') : await listForms(access)),\n )\n\n server.registerTool(\n 'get_form',\n { description: TOOL_DEFINITIONS.get_form,\n outputSchema: RESULT_SHAPE,\n annotations: SERVER_READ, inputSchema: { path: FORM_PATH } },\n async ({ path }) =>\n respond(access === undefined ? noServer('get_form') : await getForm(access, path)),\n )\n\n server.registerTool(\n 'list_submissions',\n { description: TOOL_DEFINITIONS.list_submissions,\n outputSchema: RESULT_SHAPE,\n annotations: SERVER_READ, inputSchema: { path: FORM_PATH } },\n async ({ path }) =>\n respond(\n access === undefined ? noServer('list_submissions') : await listSubmissions(access, path),\n ),\n )\n\n /*\n * The three things somebody asks an agent to do with a form.\n *\n * A tool says what a model CAN call; a prompt says what to do with the set\n * of them, in order. That order is most of the value: a model that writes\n * the document first and calls `describe_spec` afterwards has already\n * invented `type: \"email\"`, and the correction costs a turn. Scenarios\n * written after the rules they check tend to agree with whatever the rules\n * happen to say.\n *\n * MCP's own answer to a skill pack, so it arrives with the server rather\n * than being documentation somebody has to find and paste\n * ([0112](../../../docs/decisions/0112-the-mcp-server-says-what-its-tools-do.md)).\n */\n server.registerPrompt(\n 'build_a_form',\n {\n title: 'Build a form',\n description:\n 'Write a new formancy form from a description, in the order that gets it right ' +\n 'the first time: the spec, then the document, then examples that check the rules.',\n argsSchema: {\n description: z.string().min(1).describe('What the form is for, in a sentence or two.'),\n },\n },\n ({ description }) => ({\n messages: [\n {\n role: 'user' as const,\n content: {\n type: 'text' as const,\n text: [\n `Write a formancy form for: ${description}`,\n '',\n 'Work in this order, and do not skip the first step:',\n '',\n '1. Call `describe_spec`. It lists every field type, widget, rule kind and',\n ' format the document format has. A type that is not in that list does not',\n ' exist, whatever other form builders call it — an email field is `text`',\n ' with `format: \"email\"`.',\n '2. Write the document.',\n '3. Call `validate_form`. It runs the schema check, the engine compile, and',\n ' the check for expressions that compile and can never evaluate. Fix what',\n ' it names and call it again.',\n '4. Write a scenario for every rule, and call `check_scenarios`. This is the',\n ' only check that catches a condition written backwards: `a == b` and',\n ' `a != b` are both valid CEL and both pass step 3. Write the scenarios',\n ' from the description above rather than from the rules you just wrote, or',\n ' they will agree with whatever the rules happen to say.',\n '5. Show the person the form and what the scenarios checked, before',\n ' publishing anything.',\n ].join('\\n'),\n },\n },\n ],\n }),\n )\n\n server.registerPrompt(\n 'change_a_form',\n {\n title: 'Change a published form',\n description:\n 'Edit a form that already exists, without discarding somebody else’s work and ' +\n 'without publishing a change nobody has read.',\n argsSchema: {\n path: z.string().min(1).describe(\"The form's path, as it appears in its URL.\"),\n change: z.string().min(1).describe('What should be different, in a sentence.'),\n },\n },\n ({ path, change }) => ({\n messages: [\n {\n role: 'user' as const,\n content: {\n type: 'text' as const,\n text: [\n `Change the form at \"${path}\": ${change}`,\n '',\n 'A formancy document is the WHOLE form, so an edit based on an older version',\n 'silently discards whatever was published in between — and the publish',\n 'succeeds, so nothing reports it. Work this way:',\n '',\n '1. Call `get_form` for the current document.',\n '2. Make the change to it.',\n '3. Call `propose_form_edit`. It publishes nothing, and answers with what the',\n ' edit would cost submissions already collected, plus a `basedOn` hash.',\n '4. Show the person that list. A change marked lossy means stored answers lose',\n ' the field they live in; say so plainly before going further.',\n '5. Call `publish_form` with `basedOn` set to the hash from step 3. If it',\n ' refuses, the form moved — start again from step 1 rather than publishing',\n ' over it.',\n ].join('\\n'),\n },\n },\n ],\n }),\n )\n\n server.registerPrompt(\n 'embed_a_form',\n {\n title: 'Embed a form in an application',\n description: 'Render a formancy document in a React or Angular application.',\n argsSchema: {\n framework: z.enum(['react', 'angular']).describe('Which renderer the application uses.'),\n },\n },\n ({ framework }) => ({\n messages: [\n {\n role: 'user' as const,\n content: {\n type: 'text' as const,\n // One framework's instructions, never both: a prompt that lists\n // the alternative makes the model choose again, having just been\n // told which one this application uses.\n text:\n framework === 'angular'\n ? [\n 'Render a formancy form in an Angular application.',\n '',\n '- Install `@formancy/angular` and `@formancy/core`.',\n '- Build the engine once with `createFormEngine({ schema })` and provide',\n ' it with `provideFormancy(engine)`.',\n '- Render `<formancy-form />`. It is zoneless and OnPush, and the engine',\n ' is the state — there is nothing to copy into a component.',\n '- Styling is yours: the renderer ships no CSS. A theme from',\n ' `@formancy/themes` is one stylesheet import and a',\n ' `data-formancy-theme` attribute, or write your own against the',\n ' `data-formancy-part` hooks.',\n '- The submitted value is `engine.value()`. Validate it on the server',\n ' too: the same engine runs there, so the two cannot disagree.',\n ].join('\\n')\n : [\n 'Render a formancy form in a React application.',\n '',\n '- Install `@formancy/react` and `@formancy/core`.',\n '- Build the engine once with `createFormEngine({ schema })` and put it',\n ' in `<FormancyProvider engine={engine}>`.',\n '- Render `<FormancyForm />`. The engine is the state; there is nothing',\n ' to copy into component state and nothing to memoise.',\n '- Styling is yours: the renderer ships no CSS. A theme from',\n ' `@formancy/themes` is one stylesheet import and a',\n ' `data-formancy-theme` attribute, or write your own against the',\n ' `data-formancy-part` hooks.',\n '- The submitted value is `engine.value()`. Validate it on the server',\n ' too: the same engine runs there, so the two cannot disagree.',\n ].join('\\n'),\n },\n },\n ],\n }),\n )\n\n return server\n}\n"],"mappings":"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,CACR;EACE,MAAM;EACN,SAAS;GACP,MAAM;GACN,MAAM;IACJ,8BAA8B;IAC9B;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;GACF,CAAC,CAAC,KAAK,IAAI;EACb;CACF,CACF,EACF,EACF;CAEA,OAAO,eACL,iBACA;EACE,OAAO;EACP,aACE;EAEF,YAAY;GACV,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,4CAA4C;GAC7E,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,0CAA0C;EAC/E;CACF,IACC,EAAE,MAAM,cAAc,EACrB,UAAU,CACR;EACE,MAAM;EACN,SAAS;GACP,MAAM;GACN,MAAM;IACJ,uBAAuB,KAAK,KAAK;IACjC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;GACF,CAAC,CAAC,KAAK,IAAI;EACb;CACF,CACF,EACF,EACF;CAEA,OAAO,eACL,gBACA;EACE,OAAO;EACP,aAAa;EACb,YAAY,EACV,WAAW,EAAE,KAAK,CAAC,SAAS,SAAS,CAAC,CAAC,CAAC,SAAS,sCAAsC,EACzF;CACF,IACC,EAAE,iBAAiB,EAClB,UAAU,CACR;EACE,MAAM;EACN,SAAS;GACP,MAAM;GAIN,MACE,cAAc,YACV;IACE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;GACF,CAAC,CAAC,KAAK,IAAI,IACX;IACE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;GACF,CAAC,CAAC,KAAK,IAAI;EACnB;CACF,CACF,EACF,EACF;CAEA,OAAO;AACT"}
|
|
1
|
+
{"version":3,"file":"server-C8zOUff6.mjs","names":[],"sources":["../src/tools.ts","../src/server.ts"],"sourcesContent":["import { authoringFacts, diffSchemas, schemaHash } from '@formancy/spec'\nimport { validateSchema } from '@formancy/spec/validate'\nimport type { Change, FormSchema } from '@formancy/spec'\nimport { engineRefusal, expressionProblems, runScenarios } from '@formancy/core'\nimport type { Scenario } from '@formancy/core'\n\n/**\n * What formancy can be asked to do by a coding agent, as plain functions.\n *\n * Separated from the MCP server itself for the same reason `server-core` is separated\n * from `server`: a use-case that can only be exercised through a transport is one nobody\n * can test. Everything here is a function from JSON to JSON.\n *\n * **The tools check first and act second.** `publish_form` validates locally before it\n * opens a socket, so an invalid document never reaches the server and the model gets the\n * reason rather than a 422. That is possible because the format has a published JSON\n * Schema and the expressions are statically type-checked — a generated document is\n * checkable before anything is done with it.\n */\n\n/** A tool's answer: what happened, and what the agent should do about it. */\nexport interface ToolResult {\n /** False means the agent must fix something before trying again. */\n readonly ok: boolean\n /** Prose for the model. Written to be acted on, not logged. */\n readonly summary: string\n /** The machine-readable part, when there is one. */\n readonly data?: unknown\n}\n\n/** Where a formancy server is, and how to prove who you are to it. */\nexport interface ServerAccess {\n readonly baseUrl: string\n readonly apiKey: string\n /** Injected so tests do not need a socket. */\n readonly fetch?: typeof globalThis.fetch\n}\n\n// ---------------------------------------------------------------- local tools\n\n/**\n * Everything the spec allows, so a model does not have to guess.\n *\n * The cheapest tool here and probably the most valuable: without it a model\n * invents `type: \"phone\"` or `type: \"email\"` from its memory of other form\n * builders, gets a validation error, and burns a turn finding out that `text`\n * with `format: \"email\"` is the answer. Told up front, it does not.\n */\nexport function describeSpec(): ToolResult {\n const facts = authoringFacts()\n return {\n ok: true,\n summary:\n `formancy speaks spec versions ${facts.specVersions.join(' and ')}; new documents should ` +\n `use \"${facts.currentSpecVersion}\". A field type not in this list does not exist, whatever ` +\n `other form builders call it — an email field is type \"text\" with format \"email\".`,\n data: facts,\n }\n}\n\n/**\n * Check a document without publishing it — the server's publish checks, in the\n * gate's order. Only the analysis of regular-expression `pattern`s stays on\n * the server: it needs an analysis engine this package does not carry, and the\n * server's refusal names the pattern.\n *\n * Schema validity, the engine's compile and expression sanity are different\n * failures with different fixes, and a later check means nothing in a document\n * an earlier one has rejected. So the structure is checked first, then whether\n * the engine would open the document at all, then whether its expressions\n * would ever do anything.\n *\n * The middle check was once missing, and this tool told a model that a form\n * with a misspelled field name was valid: the validator does not read CEL and\n * the expression check skips what the engine refuses, so nothing here asked\n * the engine. The server did, and refused it.\n */\nexport function validateForm(document: unknown): ToolResult {\n const result = validateSchema(document)\n if (!result.valid) {\n return {\n ok: false,\n summary: `Not a valid formancy document: ${String(result.errors.length)} problem(s). Fix these before publishing.`,\n data: { valid: false, errors: result.errors },\n }\n }\n\n const refusal = engineRefusal(document as FormSchema)\n if (refusal !== undefined) {\n return {\n ok: false,\n summary:\n `The document is structurally valid, but the engine refuses to open it — the same engine the ` +\n `server's publish gate and every renderer build, so this form could be neither published nor shown. ` +\n refusal,\n data: { valid: true, engineRefusal: refusal },\n }\n }\n\n const problems = expressionProblems(document as FormSchema)\n if (problems.length > 0) {\n return {\n ok: false,\n summary:\n `The document is structurally valid, but ${String(problems.length)} expression(s) will never ` +\n `do anything. These compile and then fail at evaluation, so nothing would report them at runtime: ` +\n `a computed field would stay empty and a visible rule would show the field it was meant to hide.`,\n data: { valid: true, expressionProblems: problems },\n }\n }\n\n return {\n ok: true,\n summary: 'Valid, and every expression type-checks.',\n data: { valid: true },\n }\n}\n\n/**\n * What a change does to the submissions already collected.\n *\n * The question an agent cannot answer by reading the two documents, and the\n * one whose wrong answer is permanent. `breaking` is not advice.\n */\nexport function diffForms(before: unknown, after: unknown): ToolResult {\n const first = validateSchema(before)\n const second = validateSchema(after)\n if (!first.valid || !second.valid) {\n return {\n ok: false,\n summary: 'Both documents have to be valid before they can be compared. Run validate_form on each.',\n data: { beforeValid: first.valid, afterValid: second.valid },\n }\n }\n\n const changes = diffSchemas(before as FormSchema, after as FormSchema)\n const worst = severityOf(changes)\n\n return {\n ok: true,\n summary:\n changes.length === 0\n ? 'No change: the two documents are identical once canonicalised.'\n : `${String(changes.length)} change(s), worst severity ${worst}. ` + ADVICE[worst],\n data: { severity: worst, changes },\n }\n}\n\nconst ADVICE: Readonly<Record<'none' | 'compatible' | 'lossy' | 'breaking', string>> = {\n none: '',\n compatible: 'Existing drafts and submissions rebind silently.',\n lossy: 'Existing data rebinds, but some answers no longer have a field to live in. They are kept under data.__orphaned rather than deleted.',\n breaking:\n 'Existing drafts CANNOT rebind and will open read-only against the version that produced them. Say so before publishing, and consider a new form instead.',\n}\n\nfunction severityOf(changes: readonly Change[]): 'none' | 'compatible' | 'lossy' | 'breaking' {\n if (changes.some((change) => change.severity === 'breaking')) return 'breaking'\n if (changes.some((change) => change.severity === 'lossy')) return 'lossy'\n if (changes.length > 0) return 'compatible'\n return 'none'\n}\n\n// --------------------------------------------------------------- server tools\n\n/**\n * Publish, but check first.\n *\n * The local validation is not an optimisation. A model that gets a 422 back\n * has to guess what the server meant from an error shape it has never seen;\n * a model that gets `expressionProblems` back is told which rule, in which\n * words, and what to write instead. And an invalid document never travels.\n */\nexport async function publishForm(\n access: ServerAccess,\n path: string,\n document: unknown,\n /**\n * The schema hash the edit was based on, from `propose_form_edit` or\n * `get_form`. Given, the published form is checked against it first.\n *\n * Opt-in, and the reason is the first publish: a new form has nothing to be\n * based on, and requiring this would turn it into a fetch of something that\n * is not there. So the unsafe path still exists, which is why the tool's\n * description points at the safe one rather than leaving an agent to guess.\n */\n basedOn?: string,\n): Promise<ToolResult> {\n const checked = validateForm(document)\n if (!checked.ok) {\n return {\n ok: false,\n summary: `Not published, because the document would not have worked. ${checked.summary}`,\n data: checked.data,\n }\n }\n\n if (basedOn !== undefined) {\n const current = await currentSchema(access, path)\n if (!current.ok) return current.problem\n if (current.hash !== basedOn) {\n /*\n * The lost update, and the one mistake here nobody sees until the form\n * is wrong. A document is the WHOLE form, so publishing an edit based\n * on an older version over a newer one discards that version's change\n * entirely — and the publish succeeds, so nothing reports it.\n */\n return {\n ok: false,\n summary:\n `Not published: \"${path}\" has changed since this edit was based on it, and publishing ` +\n `the whole document would discard that change. Call get_form again, redo the edit ` +\n `against what is there now, and publish with the new basedOn.`,\n data: { basedOn, current: current.hash },\n }\n }\n }\n\n return request(access, '/forms', {\n method: 'POST',\n body: JSON.stringify({ path, schema: document }),\n })\n}\n\n/**\n * A form run against examples with their answers written down.\n *\n * The check nothing else here can make. `validate_form` says a document\n * works; it cannot say the condition is backwards, because\n * `kind == 'other'` and `kind != 'other'` are both valid CEL and the\n * difference is between the document and what somebody meant\n * ([0110](../../../docs/decisions/0110-a-form-is-checked-against-examples.md)).\n *\n * An agent is exactly who needs telling: it writes a rule from a sentence,\n * and an example is the one thing that catches it writing the opposite rule.\n * Local — no server, no credentials — so it can run before anything is\n * published.\n */\nexport function checkScenarios(document: unknown, scenarios: readonly Scenario[]): ToolResult {\n if (scenarios.length === 0) {\n /*\n * The vacuous pass. \"All 0 scenarios hold\" is true, and it is the single\n * most misleading sentence this tool could give an agent about to\n * publish.\n */\n return {\n ok: false,\n summary:\n 'No scenarios to check, so this says nothing about the form. A scenario is a set of ' +\n 'answers and what the form should make of them — whether it validates, which errors ' +\n 'it gives, which fields are visible, and what the submission carries.',\n }\n }\n\n const checked = validateForm(document)\n if (!checked.ok) {\n // One fact, not one failure per example: an agent given twenty identical\n // \"no such field\" lines reads the noise and not the cause.\n return {\n ok: false,\n summary: `Nothing was checked, because the document would not have worked. ${checked.summary}`,\n data: checked.data,\n }\n }\n\n const results = runScenarios(document as FormSchema, scenarios)\n const broken = results.filter((result) => !result.passed)\n\n return {\n ok: broken.length === 0,\n summary:\n broken.length === 0\n ? `All ${String(results.length)} scenarios hold.`\n : `${String(broken.length)} of ${String(results.length)} scenarios no longer hold: ` +\n `${broken.map((result) => `\"${result.name}\"`).join(', ')}. Each one below says what it ` +\n `expected and what happened — a rule that compiles can still be the opposite of the ` +\n `rule that was asked for.`,\n data: { results },\n }\n}\n\n/**\n * An edit held up against what is published, without publishing it.\n *\n * The MCP half of a builder's review step. `validate_form` says a document\n * works; it does not say that the rewrite dropped an option somebody has\n * already chosen, and an agent reading only its own two documents cannot tell\n * either. This fetches what is live, diffs against it, and hands back the\n * change list and the hash — so the agent can put *that* in front of a person\n * rather than \"I updated your form\", and then publish with the hash so the\n * edit cannot land on a version it was never written against\n * ([0109](../../../docs/decisions/0109-an-ai-edit-is-reviewed-before-it-lands.md)).\n */\nexport async function proposeFormEdit(\n access: ServerAccess,\n path: string,\n document: unknown,\n): Promise<ToolResult> {\n const checked = validateForm(document)\n if (!checked.ok) {\n // A change list for a document that cannot be published is a list of\n // things that will not happen.\n return {\n ok: false,\n summary: `Nothing to review, because the document would not have worked. ${checked.summary}`,\n data: checked.data,\n }\n }\n\n const current = await currentSchema(access, path)\n if (!current.ok) return current.problem\n\n const changes = diffSchemas(current.schema, document as FormSchema)\n const worst = severityOf(changes)\n\n return {\n ok: true,\n summary:\n changes.length === 0\n ? `No change: \"${path}\" is already this document. Nothing to publish.`\n : `${String(changes.length)} change(s) to \"${path}\", worst severity ${worst}. ${ADVICE[worst]} ` +\n `Show these to the person before publishing, then publish with basedOn=${current.hash}.`,\n data: { severity: worst, changes, basedOn: current.hash },\n }\n}\n\n/** What the server currently has for a path, and its hash. */\nasync function currentSchema(\n access: ServerAccess,\n path: string,\n): Promise<\n { ok: true; schema: FormSchema; hash: string } | { ok: false; problem: ToolResult }\n> {\n const fetched = await getForm(access, path)\n if (!fetched.ok) return { ok: false, problem: fetched }\n\n const schema = (fetched.data as { schema?: unknown } | undefined)?.schema\n const valid = validateSchema(schema)\n if (!valid.valid) {\n // The published form does not validate against this reader's spec: an\n // older client against a newer document. Said plainly rather than\n // compared anyway, which would report nonsense as a change list.\n return {\n ok: false,\n problem: {\n ok: false,\n summary:\n `The form published at \"${path}\" is not a document this version of the tools can read, ` +\n `so an edit cannot be compared against it. Upgrade the tools to the server's spec version.`,\n },\n }\n }\n\n return { ok: true, schema: schema as FormSchema, hash: schemaHash(schema as FormSchema) }\n}\n\nexport async function listForms(access: ServerAccess): Promise<ToolResult> {\n return request(access, '/forms')\n}\n\nexport async function getForm(access: ServerAccess, path: string): Promise<ToolResult> {\n return request(access, `/f/${encodeURIComponent(path)}`)\n}\n\nexport async function listSubmissions(access: ServerAccess, path: string): Promise<ToolResult> {\n return request(access, `/f/${encodeURIComponent(path)}/submissions`)\n}\n\n/**\n * One request, and one place that knows how a failure is reported.\n *\n * Every error comes back as `ok: false` with the server's own words rather\n * than as a thrown exception, because a thrown exception reaches a model as a\n * stack trace and tells it nothing it can act on.\n */\nasync function request(\n access: ServerAccess,\n route: string,\n init: RequestInit = {},\n): Promise<ToolResult> {\n const send = access.fetch ?? globalThis.fetch\n const url = `${access.baseUrl.replace(/\\/$/, '')}${route}`\n\n let response: Response\n try {\n response = await send(url, {\n ...init,\n headers: {\n ...(init.body === undefined ? {} : { 'content-type': 'application/json' }),\n authorization: `Bearer ${access.apiKey}`,\n ...init.headers,\n },\n })\n } catch (error) {\n return {\n ok: false,\n summary:\n `Could not reach the formancy server at ${access.baseUrl}: ${error instanceof Error ? error.message : String(error)}. ` +\n `Check FORMANCY_URL, and that the server is running.`,\n }\n }\n\n const body: unknown = await response.json().catch(() => undefined)\n\n if (!response.ok) {\n const detail = messageFrom(body)\n return {\n ok: false,\n summary:\n response.status === 401 || response.status === 403\n ? `The server refused the request (${String(response.status)}). Check FORMANCY_API_KEY.`\n : `The server refused the request (${String(response.status)})${detail === undefined ? '.' : `: ${detail}`}`,\n ...(body === undefined ? {} : { data: body }),\n }\n }\n\n return { ok: true, summary: 'Done.', data: body }\n}\n\nfunction messageFrom(body: unknown): string | undefined {\n if (body === null || typeof body !== 'object') return undefined\n const record = body as { message?: unknown; error?: unknown }\n if (typeof record.message === 'string') return record.message\n if (typeof record.error === 'string') return record.error\n return undefined\n}\n","import { createRequire } from 'node:module'\nimport { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport type { ToolAnnotations } from '@modelcontextprotocol/sdk/types.js'\nimport { z } from 'zod'\nimport {\n checkScenarios,\n describeSpec,\n diffForms,\n getForm,\n listForms,\n listSubmissions,\n proposeFormEdit,\n publishForm,\n validateForm,\n} from './tools.js'\nimport type { ServerAccess, ToolResult } from './tools.js'\n\n/**\n * The tools, wired to the protocol.\n *\n * Thin on purpose: every one of these is a call into `tools.ts` and a\n * conversion of the answer into MCP's content shape. The logic is tested\n * without a protocol, because a use-case that can only be exercised through a\n * transport is a use-case nobody tests properly.\n */\n\nexport interface McpServerOptions {\n /**\n * Where a formancy server is, if there is one.\n *\n * Absent is a supported state and not a degraded one: the four local tools\n * — describe_spec, validate_form, diff_forms and the checking half of\n * publishing — need no server at all, which means an agent can author and\n * verify a whole form before anybody has deployed anything.\n */\n readonly access?: ServerAccess\n}\n\n/**\n * Names and descriptions, apart from the wiring.\n *\n * Exported because the description is the prompt: it is the only thing telling\n * a model when to reach for a tool, and a wrong one means a tool that is never\n * called or called for the wrong reason. Kept where it can be read and tested\n * rather than buried in a registration call.\n */\nexport const TOOL_DEFINITIONS = {\n describe_spec:\n 'List every field type, layout kind, rule kind and format the formancy spec defines. ' +\n 'Call this BEFORE writing a form document. A type not in the list does not exist, whatever ' +\n 'other form builders call it.',\n validate_form:\n \"Run the server's publish checks without publishing: against the spec, through the engine \" +\n 'that renders the form (a misspelled field name, a cycle between computed fields, a ' +\n 'condition that is not certain to produce a bool), and for expressions that compile but ' +\n 'can never evaluate — the failure that is silent at runtime. Each comes back separately, ' +\n 'with what to change.',\n diff_forms:\n 'Compare two form documents and report what the change would do to submissions already ' +\n 'collected: compatible, lossy, or breaking. Call this before publishing over an existing form.',\n check_scenarios:\n 'Run a form against examples with their answers written down, and report which stopped ' +\n 'holding. The ONE check that catches a condition which compiles and is the opposite of ' +\n 'what was asked for — validate_form cannot, because both spellings are valid CEL. Needs no ' +\n 'server. Write a scenario for every rule you add: a set of answers, whether the form is ' +\n 'valid, which errors it gives, which fields are visible, and what the submission carries.',\n propose_form_edit:\n 'Hold an edit up against the form that is published, WITHOUT publishing it. Answers with ' +\n 'what the edit would do to submissions already collected, and with a basedOn hash. ' +\n 'Call this instead of publish_form whenever you are changing a form that already exists: ' +\n 'show the changes to the person, get their agreement, then publish_form with the basedOn ' +\n 'it gave you.',\n publish_form:\n 'Publish a form document to a formancy server. Validates locally first and refuses to send ' +\n 'an invalid document, so the reason comes back as something to fix rather than as a 422. ' +\n 'Pass basedOn when changing an existing form: a document is the WHOLE form, so publishing ' +\n 'one based on an older version silently discards whatever was published in between, and ' +\n 'the publish succeeds so nothing reports it.',\n list_forms: 'List the forms on the formancy server, with their current version.',\n get_form: 'Fetch one form document from the formancy server, with its version and schema hash.',\n list_submissions: 'List submissions for one form.',\n} as const\n\n/**\n * The version this server reports.\n *\n * From the manifest, not typed. It said `0.1.0` while the package was on 0.3.0 —\n * a wrong statement in the one field a client uses to tell two installations\n * apart, and exactly the hand-written number this repository keeps finding\n * stale.\n *\n * `createRequire` rather than a JSON import: `../package.json` resolves to the\n * package root from both `src/server.ts` and `dist/index.mjs`, so the same line\n * is right in a test and in the published package.\n */\nexport const PACKAGE_VERSION: string = (\n createRequire(import.meta.url)('../package.json') as { version: string }\n).version\n\n/**\n * What a client is told a result looks like, before it reads one.\n *\n * One envelope for every tool rather than a schema each. The shape is the same\n * everywhere — did it work, one sentence a person can read, and the part an\n * agent acts on — so a client that has learned it once has learned it for all\n * nine. A schema per tool would be nine places for `data` to drift from what\n * the tool returns.\n */\nconst RESULT_SHAPE = {\n ok: z.boolean().describe('Whether the tool did what was asked.'),\n summary: z.string().describe('One or two sentences a person can read.'),\n data: z.unknown().optional().describe('The part an agent acts on, when there is one.'),\n}\n\n/**\n * Both readings of the answer: prose and structure.\n *\n * `content` is what a model reads; `structuredContent` is what a client parses\n * without pulling JSON back out of a string. Both travel — prose alone throws\n * away the part an agent acts on, and structure alone makes every refusal a\n * parsing exercise.\n */\nfunction respond(result: ToolResult): {\n content: { type: 'text'; text: string }[]\n structuredContent: Record<string, unknown>\n isError?: boolean\n} {\n const text =\n result.data === undefined\n ? result.summary\n : `${result.summary}\\n\\n${JSON.stringify(result.data, null, 2)}`\n return {\n content: [{ type: 'text', text }],\n structuredContent: {\n ok: result.ok,\n summary: result.summary,\n ...(result.data === undefined ? {} : { data: result.data }),\n },\n // `isError` rather than a thrown exception: a refusal is an answer the model\n // should read and act on, not a transport failure.\n ...(result.ok ? {} : { isError: true }),\n }\n}\n\n/**\n * What a tool will do, for a client deciding whether to ask first.\n *\n * The only thing a host has to tell `validate_form` from `publish_form` before\n * calling either. Without them a tool defaults to `readOnlyHint: false`,\n * `destructiveHint: true`, `openWorldHint: true` — so this server’s local,\n * read-only checks were all advertised as potentially destructive calls into an\n * open world, which is the opposite of true in every case.\n */\nconst LOCAL_CHECK: ToolAnnotations = {\n readOnlyHint: true,\n // No network and no server: these are the ones that work before anything is\n // deployed, and saying so is what lets a host run them without asking.\n openWorldHint: false,\n}\n\nconst SERVER_READ: ToolAnnotations = { readOnlyHint: true, openWorldHint: true }\n\n/**\n * Publishing. `destructiveHint: false` is not optimism: a published version is\n * immutable and a publish inserts a new row, so nothing is overwritten\n * ([0025](../../../docs/decisions/0025-immutability-in-the-database.md)).\n *\n * Not idempotent, though — publishing the same document twice makes a second\n * version rather than the same one, and saying otherwise would invite a client\n * to retry it freely.\n */\nconst SERVER_WRITE: ToolAnnotations = {\n readOnlyHint: false,\n destructiveHint: false,\n idempotentHint: false,\n openWorldHint: true,\n}\n\n/** A tool that needs a server, when there is not one. */\nfunction noServer(name: string): ToolResult {\n return {\n ok: false,\n summary:\n `${name} needs a formancy server, and none is configured. Set FORMANCY_URL and ` +\n `FORMANCY_API_KEY. The local tools — describe_spec, validate_form and diff_forms — work ` +\n `without one, so a form can be written and checked before a server exists.`,\n }\n}\n\n/**\n * The arguments each tool takes.\n *\n * A document arrives as `unknown` on purpose: it is checked by\n * `validateSchema` against the spec's own JSON Schema, which is the one\n * authority on what a formancy document is. Describing its shape a second\n * time here in Zod would be a second authority, and the day the two disagree\n * the tool would reject a document the product accepts.\n */\nconst DOCUMENT = z.unknown().describe('A formancy form document (the JSON schema object).')\nconst FORM_PATH = z.string().min(1).describe(\"The form's path, as it appears in its URL.\")\n/*\n * The scenarios arrive loosely typed for the reason a document does: their\n * shape belongs to `@formancy/core`, and describing it a second time in Zod\n * would be a second authority that eventually disagrees with the first.\n */\nconst SCENARIOS = z\n .array(z.unknown())\n .describe(\n 'Examples with their answers: each is { name, changes, valid } and may also pin errors, ' +\n 'visible, values and absent.',\n )\nconst BASED_ON = z\n .string()\n .optional()\n .describe(\n 'The schema hash this edit was based on, from propose_form_edit or get_form. Given, the ' +\n 'publish is refused if the form has changed since.',\n )\n\nexport function createFormancyMcpServer(options: McpServerOptions = {}): McpServer {\n const server = new McpServer({ name: 'formancy', version: PACKAGE_VERSION })\n const { access } = options\n\n server.registerTool('describe_spec', { description: TOOL_DEFINITIONS.describe_spec, outputSchema: RESULT_SHAPE, annotations: LOCAL_CHECK }, () =>\n respond(describeSpec()),\n )\n\n server.registerTool(\n 'validate_form',\n { description: TOOL_DEFINITIONS.validate_form,\n outputSchema: RESULT_SHAPE,\n annotations: LOCAL_CHECK, inputSchema: { document: DOCUMENT } },\n ({ document }) => respond(validateForm(document)),\n )\n\n server.registerTool(\n 'diff_forms',\n {\n description: TOOL_DEFINITIONS.diff_forms,\n outputSchema: RESULT_SHAPE,\n annotations: LOCAL_CHECK,\n inputSchema: { before: DOCUMENT, after: DOCUMENT },\n },\n ({ before, after }) => respond(diffForms(before, after)),\n )\n\n server.registerTool(\n 'check_scenarios',\n {\n description: TOOL_DEFINITIONS.check_scenarios,\n outputSchema: RESULT_SHAPE,\n annotations: LOCAL_CHECK,\n inputSchema: { document: DOCUMENT, scenarios: SCENARIOS },\n },\n ({ document, scenarios }) => respond(checkScenarios(document, scenarios as never)),\n )\n\n server.registerTool(\n 'propose_form_edit',\n {\n description: TOOL_DEFINITIONS.propose_form_edit,\n outputSchema: RESULT_SHAPE,\n annotations: SERVER_READ,\n inputSchema: { path: FORM_PATH, document: DOCUMENT },\n },\n async ({ path, document }) =>\n respond(\n access === undefined\n ? noServer('propose_form_edit')\n : await proposeFormEdit(access, path, document),\n ),\n )\n\n server.registerTool(\n 'publish_form',\n {\n description: TOOL_DEFINITIONS.publish_form,\n outputSchema: RESULT_SHAPE,\n annotations: SERVER_WRITE,\n inputSchema: { path: FORM_PATH, document: DOCUMENT, basedOn: BASED_ON },\n },\n async ({ path, document, basedOn }) =>\n respond(\n access === undefined\n ? noServer('publish_form')\n : await publishForm(access, path, document, basedOn),\n ),\n )\n\n server.registerTool('list_forms', { description: TOOL_DEFINITIONS.list_forms, outputSchema: RESULT_SHAPE, annotations: SERVER_READ }, async () =>\n respond(access === undefined ? noServer('list_forms') : await listForms(access)),\n )\n\n server.registerTool(\n 'get_form',\n { description: TOOL_DEFINITIONS.get_form,\n outputSchema: RESULT_SHAPE,\n annotations: SERVER_READ, inputSchema: { path: FORM_PATH } },\n async ({ path }) =>\n respond(access === undefined ? noServer('get_form') : await getForm(access, path)),\n )\n\n server.registerTool(\n 'list_submissions',\n { description: TOOL_DEFINITIONS.list_submissions,\n outputSchema: RESULT_SHAPE,\n annotations: SERVER_READ, inputSchema: { path: FORM_PATH } },\n async ({ path }) =>\n respond(\n access === undefined ? noServer('list_submissions') : await listSubmissions(access, path),\n ),\n )\n\n /*\n * The three things somebody asks an agent to do with a form.\n *\n * A tool says what a model CAN call; a prompt says what to do with the set\n * of them, in order. That order is most of the value: a model that writes\n * the document first and calls `describe_spec` afterwards has already\n * invented `type: \"email\"`, and the correction costs a turn. Scenarios\n * written after the rules they check tend to agree with whatever the rules\n * happen to say.\n *\n * MCP's own answer to a skill pack, so it arrives with the server rather\n * than being documentation somebody has to find and paste\n * ([0112](../../../docs/decisions/0112-the-mcp-server-says-what-its-tools-do.md)).\n */\n server.registerPrompt(\n 'build_a_form',\n {\n title: 'Build a form',\n description:\n 'Write a new formancy form from a description, in the order that gets it right ' +\n 'the first time: the spec, then the document, then examples that check the rules.',\n argsSchema: {\n description: z.string().min(1).describe('What the form is for, in a sentence or two.'),\n },\n },\n ({ description }) => ({\n messages: [\n {\n role: 'user' as const,\n content: {\n type: 'text' as const,\n text: [\n `Write a formancy form for: ${description}`,\n '',\n 'Work in this order, and do not skip the first step:',\n '',\n '1. Call `describe_spec`. It lists every field type, widget, rule kind and',\n ' format the document format has. A type that is not in that list does not',\n ' exist, whatever other form builders call it — an email field is `text`',\n ' with `format: \"email\"`.',\n '2. Write the document.',\n '3. Call `validate_form`. It runs the schema check, the engine compile, and',\n ' the check for expressions that compile and can never evaluate. Fix what',\n ' it names and call it again.',\n '4. Write a scenario for every rule, and call `check_scenarios`. This is the',\n ' only check that catches a condition written backwards: `a == b` and',\n ' `a != b` are both valid CEL and both pass step 3. Write the scenarios',\n ' from the description above rather than from the rules you just wrote, or',\n ' they will agree with whatever the rules happen to say.',\n '5. Show the person the form and what the scenarios checked, before',\n ' publishing anything.',\n ].join('\\n'),\n },\n },\n ],\n }),\n )\n\n server.registerPrompt(\n 'change_a_form',\n {\n title: 'Change a published form',\n description:\n 'Edit a form that already exists, without discarding somebody else’s work and ' +\n 'without publishing a change nobody has read.',\n argsSchema: {\n path: z.string().min(1).describe(\"The form's path, as it appears in its URL.\"),\n change: z.string().min(1).describe('What should be different, in a sentence.'),\n },\n },\n ({ path, change }) => ({\n messages: [\n {\n role: 'user' as const,\n content: {\n type: 'text' as const,\n text: [\n `Change the form at \"${path}\": ${change}`,\n '',\n 'A formancy document is the WHOLE form, so an edit based on an older version',\n 'silently discards whatever was published in between — and the publish',\n 'succeeds, so nothing reports it. Work this way:',\n '',\n '1. Call `get_form` for the current document.',\n '2. Make the change to it.',\n '3. Call `propose_form_edit`. It publishes nothing, and answers with what the',\n ' edit would cost submissions already collected, plus a `basedOn` hash.',\n '4. Show the person that list. A change marked lossy means stored answers lose',\n ' the field they live in; say so plainly before going further.',\n '5. Call `publish_form` with `basedOn` set to the hash from step 3. If it',\n ' refuses, the form moved — start again from step 1 rather than publishing',\n ' over it.',\n ].join('\\n'),\n },\n },\n ],\n }),\n )\n\n server.registerPrompt(\n 'embed_a_form',\n {\n title: 'Embed a form in an application',\n description: 'Render a formancy document in a React or Angular application.',\n argsSchema: {\n framework: z.enum(['react', 'angular']).describe('Which renderer the application uses.'),\n },\n },\n ({ framework }) => ({\n messages: [\n {\n role: 'user' as const,\n content: {\n type: 'text' as const,\n // One framework's instructions, never both: a prompt that lists\n // the alternative makes the model choose again, having just been\n // told which one this application uses.\n text:\n framework === 'angular'\n ? [\n 'Render a formancy form in an Angular application.',\n '',\n '- Install `@formancy/angular` and `@formancy/core`.',\n '- Build the engine once with `createFormEngine({ schema })` and provide',\n ' it with `provideFormancy(engine)`.',\n '- Render `<formancy-form />`. It is zoneless and OnPush, and the engine',\n ' is the state — there is nothing to copy into a component.',\n '- Styling is yours: the renderer ships no CSS. A theme from',\n ' `@formancy/themes` is one stylesheet import and a',\n ' `data-formancy-theme` attribute, or write your own against the',\n ' `data-formancy-part` hooks.',\n '- The submitted value is `engine.value()`. Validate it on the server',\n ' too: the same engine runs there, so the two cannot disagree.',\n ].join('\\n')\n : [\n 'Render a formancy form in a React application.',\n '',\n '- Install `@formancy/react` and `@formancy/core`.',\n '- Build the engine once with `createFormEngine({ schema })` and put it',\n ' in `<FormancyProvider engine={engine}>`.',\n '- Render `<FormancyForm />`. The engine is the state; there is nothing',\n ' to copy into component state and nothing to memoise.',\n '- Styling is yours: the renderer ships no CSS. A theme from',\n ' `@formancy/themes` is one stylesheet import and a',\n ' `data-formancy-theme` attribute, or write your own against the',\n ' `data-formancy-part` hooks.',\n '- The submitted value is `engine.value()`. Validate it on the server',\n ' too: the same engine runs there, so the two cannot disagree.',\n ].join('\\n'),\n },\n },\n ],\n }),\n )\n\n return server\n}\n"],"mappings":";;;;;;;AAgDA,SAAgB,eAA2B;CACzC,MAAM,QAAQ,eAAe;CAC7B,OAAO;EACL,IAAI;EACJ,SACE,iCAAiC,MAAM,aAAa,KAAK,OAAO,EAAE,8BAC1D,MAAM,mBAAmB;EAEnC,MAAM;CACR;AACF;AAmBA,SAAgB,aAAa,UAA+B;CAC1D,MAAM,SAAS,eAAe,QAAQ;CACtC,IAAI,CAAC,OAAO,OACV,OAAO;EACL,IAAI;EACJ,SAAS,kCAAkC,OAAO,OAAO,OAAO,MAAM,EAAE;EACxE,MAAM;GAAE,OAAO;GAAO,QAAQ,OAAO;EAAO;CAC9C;CAGF,MAAM,UAAU,cAAc,QAAsB;CACpD,IAAI,YAAY,KAAA,GACd,OAAO;EACL,IAAI;EACJ,SACE,oMAEA;EACF,MAAM;GAAE,OAAO;GAAM,eAAe;EAAQ;CAC9C;CAGF,MAAM,WAAW,mBAAmB,QAAsB;CAC1D,IAAI,SAAS,SAAS,GACpB,OAAO;EACL,IAAI;EACJ,SACE,2CAA2C,OAAO,SAAS,MAAM,EAAE;EAGrE,MAAM;GAAE,OAAO;GAAM,oBAAoB;EAAS;CACpD;CAGF,OAAO;EACL,IAAI;EACJ,SAAS;EACT,MAAM,EAAE,OAAO,KAAK;CACtB;AACF;AAQA,SAAgB,UAAU,QAAiB,OAA4B;CACrE,MAAM,QAAQ,eAAe,MAAM;CACnC,MAAM,SAAS,eAAe,KAAK;CACnC,IAAI,CAAC,MAAM,SAAS,CAAC,OAAO,OAC1B,OAAO;EACL,IAAI;EACJ,SAAS;EACT,MAAM;GAAE,aAAa,MAAM;GAAO,YAAY,OAAO;EAAM;CAC7D;CAGF,MAAM,UAAU,YAAY,QAAsB,KAAmB;CACrE,MAAM,QAAQ,WAAW,OAAO;CAEhC,OAAO;EACL,IAAI;EACJ,SACE,QAAQ,WAAW,IACf,mEACA,GAAG,OAAO,QAAQ,MAAM,EAAE,6BAA6B,MAAM,MAAM,OAAO;EAChF,MAAM;GAAE,UAAU;GAAO;EAAQ;CACnC;AACF;AAEA,MAAM,SAAiF;CACrF,MAAM;CACN,YAAY;CACZ,OAAO;CACP,UACE;AACJ;AAEA,SAAS,WAAW,SAA0E;CAC5F,IAAI,QAAQ,MAAM,WAAW,OAAO,aAAa,UAAU,GAAG,OAAO;CACrE,IAAI,QAAQ,MAAM,WAAW,OAAO,aAAa,OAAO,GAAG,OAAO;CAClE,IAAI,QAAQ,SAAS,GAAG,OAAO;CAC/B,OAAO;AACT;AAYA,eAAsB,YACpB,QACA,MACA,UAUA,SACqB;CACrB,MAAM,UAAU,aAAa,QAAQ;CACrC,IAAI,CAAC,QAAQ,IACX,OAAO;EACL,IAAI;EACJ,SAAS,8DAA8D,QAAQ;EAC/E,MAAM,QAAQ;CAChB;CAGF,IAAI,YAAY,KAAA,GAAW;EACzB,MAAM,UAAU,MAAM,cAAc,QAAQ,IAAI;EAChD,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ;EAChC,IAAI,QAAQ,SAAS,SAOnB,OAAO;GACL,IAAI;GACJ,SACE,mBAAmB,KAAK;GAG1B,MAAM;IAAE;IAAS,SAAS,QAAQ;GAAK;EACzC;CAEJ;CAEA,OAAO,QAAQ,QAAQ,UAAU;EAC/B,QAAQ;EACR,MAAM,KAAK,UAAU;GAAE;GAAM,QAAQ;EAAS,CAAC;CACjD,CAAC;AACH;AAgBA,SAAgB,eAAe,UAAmB,WAA4C;CAC5F,IAAI,UAAU,WAAW,GAMvB,OAAO;EACL,IAAI;EACJ,SACE;CAGJ;CAGF,MAAM,UAAU,aAAa,QAAQ;CACrC,IAAI,CAAC,QAAQ,IAGX,OAAO;EACL,IAAI;EACJ,SAAS,oEAAoE,QAAQ;EACrF,MAAM,QAAQ;CAChB;CAGF,MAAM,UAAU,aAAa,UAAwB,SAAS;CAC9D,MAAM,SAAS,QAAQ,QAAQ,WAAW,CAAC,OAAO,MAAM;CAExD,OAAO;EACL,IAAI,OAAO,WAAW;EACtB,SACE,OAAO,WAAW,IACd,OAAO,OAAO,QAAQ,MAAM,EAAE,oBAC9B,GAAG,OAAO,OAAO,MAAM,EAAE,MAAM,OAAO,QAAQ,MAAM,EAAE,6BACnD,OAAO,KAAK,WAAW,IAAI,OAAO,KAAK,EAAE,CAAC,CAAC,KAAK,IAAI,EAAE;EAG/D,MAAM,EAAE,QAAQ;CAClB;AACF;AAcA,eAAsB,gBACpB,QACA,MACA,UACqB;CACrB,MAAM,UAAU,aAAa,QAAQ;CACrC,IAAI,CAAC,QAAQ,IAGX,OAAO;EACL,IAAI;EACJ,SAAS,kEAAkE,QAAQ;EACnF,MAAM,QAAQ;CAChB;CAGF,MAAM,UAAU,MAAM,cAAc,QAAQ,IAAI;CAChD,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ;CAEhC,MAAM,UAAU,YAAY,QAAQ,QAAQ,QAAsB;CAClE,MAAM,QAAQ,WAAW,OAAO;CAEhC,OAAO;EACL,IAAI;EACJ,SACE,QAAQ,WAAW,IACf,eAAe,KAAK,mDACpB,GAAG,OAAO,QAAQ,MAAM,EAAE,iBAAiB,KAAK,oBAAoB,MAAM,IAAI,OAAO,OAAO,yEACnB,QAAQ,KAAK;EAC5F,MAAM;GAAE,UAAU;GAAO;GAAS,SAAS,QAAQ;EAAK;CAC1D;AACF;AAGA,eAAe,cACb,QACA,MAGA;CACA,MAAM,UAAU,MAAM,QAAQ,QAAQ,IAAI;CAC1C,IAAI,CAAC,QAAQ,IAAI,OAAO;EAAE,IAAI;EAAO,SAAS;CAAQ;CAEtD,MAAM,SAAU,QAAQ,MAA2C;CAEnE,IAAI,CADU,eAAe,MACpB,CAAC,CAAC,OAIT,OAAO;EACL,IAAI;EACJ,SAAS;GACP,IAAI;GACJ,SACE,0BAA0B,KAAK;EAEnC;CACF;CAGF,OAAO;EAAE,IAAI;EAAc;EAAsB,MAAM,WAAW,MAAoB;CAAE;AAC1F;AAEA,eAAsB,UAAU,QAA2C;CACzE,OAAO,QAAQ,QAAQ,QAAQ;AACjC;AAEA,eAAsB,QAAQ,QAAsB,MAAmC;CACrF,OAAO,QAAQ,QAAQ,MAAM,mBAAmB,IAAI,GAAG;AACzD;AAEA,eAAsB,gBAAgB,QAAsB,MAAmC;CAC7F,OAAO,QAAQ,QAAQ,MAAM,mBAAmB,IAAI,EAAE,aAAa;AACrE;AASA,eAAe,QACb,QACA,OACA,OAAoB,CAAC,GACA;CACrB,MAAM,OAAO,OAAO,SAAS,WAAW;CACxC,MAAM,MAAM,GAAG,OAAO,QAAQ,QAAQ,OAAO,EAAE,IAAI;CAEnD,IAAI;CACJ,IAAI;EACF,WAAW,MAAM,KAAK,KAAK;GACzB,GAAG;GACH,SAAS;IACP,GAAI,KAAK,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,gBAAgB,mBAAmB;IACxE,eAAe,UAAU,OAAO;IAChC,GAAG,KAAK;GACV;EACF,CAAC;CACH,SAAS,OAAO;EACd,OAAO;GACL,IAAI;GACJ,SACE,0CAA0C,OAAO,QAAQ,IAAI,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,EAAE;EAExH;CACF;CAEA,MAAM,OAAgB,MAAM,SAAS,KAAK,CAAC,CAAC,YAAY,KAAA,CAAS;CAEjE,IAAI,CAAC,SAAS,IAAI;EAChB,MAAM,SAAS,YAAY,IAAI;EAC/B,OAAO;GACL,IAAI;GACJ,SACE,SAAS,WAAW,OAAO,SAAS,WAAW,MAC3C,mCAAmC,OAAO,SAAS,MAAM,EAAE,8BAC3D,mCAAmC,OAAO,SAAS,MAAM,EAAE,GAAG,WAAW,KAAA,IAAY,MAAM,KAAK;GACtG,GAAI,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM,KAAK;EAC7C;CACF;CAEA,OAAO;EAAE,IAAI;EAAM,SAAS;EAAS,MAAM;CAAK;AAClD;AAEA,SAAS,YAAY,MAAmC;CACtD,IAAI,SAAS,QAAQ,OAAO,SAAS,UAAU,OAAO,KAAA;CACtD,MAAM,SAAS;CACf,IAAI,OAAO,OAAO,YAAY,UAAU,OAAO,OAAO;CACtD,IAAI,OAAO,OAAO,UAAU,UAAU,OAAO,OAAO;AAEtD;;;AC3XA,MAAa,mBAAmB;CAC9B,eACE;CAGF,eACE;CAKF,YACE;CAEF,iBACE;CAKF,mBACE;CAKF,cACE;CAKF,YAAY;CACZ,UAAU;CACV,kBAAkB;AACpB;AAcA,MAAa,kBACX,cAAc,YAAY,GAAG,CAAC,CAAC,iBAAiB,CAAC,CACjD;AAWF,MAAM,eAAe;CACnB,IAAI,EAAE,QAAQ,CAAC,CAAC,SAAS,sCAAsC;CAC/D,SAAS,EAAE,OAAO,CAAC,CAAC,SAAS,yCAAyC;CACtE,MAAM,EAAE,QAAQ,CAAC,CAAC,SAAS,CAAC,CAAC,SAAS,+CAA+C;AACvF;AAUA,SAAS,QAAQ,QAIf;CAKA,OAAO;EACL,SAAS,CAAC;GAAE,MAAM;GAAQ,MAJ1B,OAAO,SAAS,KAAA,IACZ,OAAO,UACP,GAAG,OAAO,QAAQ,MAAM,KAAK,UAAU,OAAO,MAAM,MAAM,CAAC;EAEhC,CAAC;EAChC,mBAAmB;GACjB,IAAI,OAAO;GACX,SAAS,OAAO;GAChB,GAAI,OAAO,SAAS,KAAA,IAAY,CAAC,IAAI,EAAE,MAAM,OAAO,KAAK;EAC3D;EAGA,GAAI,OAAO,KAAK,CAAC,IAAI,EAAE,SAAS,KAAK;CACvC;AACF;AAWA,MAAM,cAA+B;CACnC,cAAc;CAGd,eAAe;AACjB;AAEA,MAAM,cAA+B;CAAE,cAAc;CAAM,eAAe;AAAK;AAW/E,MAAM,eAAgC;CACpC,cAAc;CACd,iBAAiB;CACjB,gBAAgB;CAChB,eAAe;AACjB;AAGA,SAAS,SAAS,MAA0B;CAC1C,OAAO;EACL,IAAI;EACJ,SACE,GAAG,KAAK;CAGZ;AACF;AAWA,MAAM,WAAW,EAAE,QAAQ,CAAC,CAAC,SAAS,oDAAoD;AAC1F,MAAM,YAAY,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,4CAA4C;AAMzF,MAAM,YAAY,EACf,MAAM,EAAE,QAAQ,CAAC,CAAC,CAClB,SACC,oHAEF;AACF,MAAM,WAAW,EACd,OAAO,CAAC,CACR,SAAS,CAAC,CACV,SACC,0IAEF;AAEF,SAAgB,wBAAwB,UAA4B,CAAC,GAAc;CACjF,MAAM,SAAS,IAAI,UAAU;EAAE,MAAM;EAAY,SAAS;CAAgB,CAAC;CAC3E,MAAM,EAAE,WAAW;CAEnB,OAAO,aAAa,iBAAiB;EAAE,aAAa,iBAAiB;EAAe,cAAc;EAAc,aAAa;CAAY,SACvI,QAAQ,aAAa,CAAC,CACxB;CAEA,OAAO,aACL,iBACA;EAAE,aAAa,iBAAiB;EAC9B,cAAc;EACd,aAAa;EAAa,aAAa,EAAE,UAAU,SAAS;CAAE,IAC/D,EAAE,eAAe,QAAQ,aAAa,QAAQ,CAAC,CAClD;CAEA,OAAO,aACL,cACA;EACE,aAAa,iBAAiB;EAC9B,cAAc;EACd,aAAa;EACb,aAAa;GAAE,QAAQ;GAAU,OAAO;EAAS;CACnD,IACC,EAAE,QAAQ,YAAY,QAAQ,UAAU,QAAQ,KAAK,CAAC,CACzD;CAEA,OAAO,aACL,mBACA;EACE,aAAa,iBAAiB;EAC9B,cAAc;EACd,aAAa;EACb,aAAa;GAAE,UAAU;GAAU,WAAW;EAAU;CAC1D,IACC,EAAE,UAAU,gBAAgB,QAAQ,eAAe,UAAU,SAAkB,CAAC,CACnF;CAEA,OAAO,aACL,qBACA;EACE,aAAa,iBAAiB;EAC9B,cAAc;EACd,aAAa;EACb,aAAa;GAAE,MAAM;GAAW,UAAU;EAAS;CACrD,GACA,OAAO,EAAE,MAAM,eACb,QACE,WAAW,KAAA,IACP,SAAS,mBAAmB,IAC5B,MAAM,gBAAgB,QAAQ,MAAM,QAAQ,CAClD,CACJ;CAEA,OAAO,aACL,gBACA;EACE,aAAa,iBAAiB;EAC9B,cAAc;EACd,aAAa;EACb,aAAa;GAAE,MAAM;GAAW,UAAU;GAAU,SAAS;EAAS;CACxE,GACA,OAAO,EAAE,MAAM,UAAU,cACvB,QACE,WAAW,KAAA,IACP,SAAS,cAAc,IACvB,MAAM,YAAY,QAAQ,MAAM,UAAU,OAAO,CACvD,CACJ;CAEA,OAAO,aAAa,cAAc;EAAE,aAAa,iBAAiB;EAAY,cAAc;EAAc,aAAa;CAAY,GAAG,YACpI,QAAQ,WAAW,KAAA,IAAY,SAAS,YAAY,IAAI,MAAM,UAAU,MAAM,CAAC,CACjF;CAEA,OAAO,aACL,YACA;EAAE,aAAa,iBAAiB;EAC9B,cAAc;EACd,aAAa;EAAa,aAAa,EAAE,MAAM,UAAU;CAAE,GAC7D,OAAO,EAAE,WACP,QAAQ,WAAW,KAAA,IAAY,SAAS,UAAU,IAAI,MAAM,QAAQ,QAAQ,IAAI,CAAC,CACrF;CAEA,OAAO,aACL,oBACA;EAAE,aAAa,iBAAiB;EAC9B,cAAc;EACd,aAAa;EAAa,aAAa,EAAE,MAAM,UAAU;CAAE,GAC7D,OAAO,EAAE,WACP,QACE,WAAW,KAAA,IAAY,SAAS,kBAAkB,IAAI,MAAM,gBAAgB,QAAQ,IAAI,CAC1F,CACJ;CAgBA,OAAO,eACL,gBACA;EACE,OAAO;EACP,aACE;EAEF,YAAY,EACV,aAAa,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,6CAA6C,EACvF;CACF,IACC,EAAE,mBAAmB,EACpB,UAAU,CACR;EACE,MAAM;EACN,SAAS;GACP,MAAM;GACN,MAAM;IACJ,8BAA8B;IAC9B;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;GACF,CAAC,CAAC,KAAK,IAAI;EACb;CACF,CACF,EACF,EACF;CAEA,OAAO,eACL,iBACA;EACE,OAAO;EACP,aACE;EAEF,YAAY;GACV,MAAM,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,4CAA4C;GAC7E,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,0CAA0C;EAC/E;CACF,IACC,EAAE,MAAM,cAAc,EACrB,UAAU,CACR;EACE,MAAM;EACN,SAAS;GACP,MAAM;GACN,MAAM;IACJ,uBAAuB,KAAK,KAAK;IACjC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;GACF,CAAC,CAAC,KAAK,IAAI;EACb;CACF,CACF,EACF,EACF;CAEA,OAAO,eACL,gBACA;EACE,OAAO;EACP,aAAa;EACb,YAAY,EACV,WAAW,EAAE,KAAK,CAAC,SAAS,SAAS,CAAC,CAAC,CAAC,SAAS,sCAAsC,EACzF;CACF,IACC,EAAE,iBAAiB,EAClB,UAAU,CACR;EACE,MAAM;EACN,SAAS;GACP,MAAM;GAIN,MACE,cAAc,YACV;IACE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;GACF,CAAC,CAAC,KAAK,IAAI,IACX;IACE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;GACF,CAAC,CAAC,KAAK,IAAI;EACnB;CACF,CACF,EACF,EACF;CAEA,OAAO;AACT"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@formancy/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "formancy as tools for a coding agent: author a form, have it checked before it exists, and publish it.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"NOTICE"
|
|
21
21
|
],
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@formancy/core": "0.
|
|
24
|
-
"@formancy/spec": "0.
|
|
23
|
+
"@formancy/core": "0.8.0",
|
|
24
|
+
"@formancy/spec": "0.8.0",
|
|
25
25
|
"@modelcontextprotocol/sdk": "^1.30.1",
|
|
26
26
|
"zod": "^4.6.5"
|
|
27
27
|
},
|