synartesis 0.2.3 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,18 @@
2
2
 
3
3
  What changed, and why it mattered. Dates are release dates.
4
4
 
5
+ ## 0.2.4 — 2026-08-25
6
+
7
+ ### Changed
8
+
9
+ - **The toy CRM and the demo agent no longer ship.** They exist to make this
10
+ repo's walkthrough work, and that walkthrough is run from a clone — so
11
+ installing put a fixture server, a demo harness and a policy pointing at a
12
+ binary that is not there onto everyone's disk. `manifests/toy-crm.yaml` goes
13
+ with them for the same reason. 21 files down to 16, 162 kB to 152 kB.
14
+ Nothing a user runs is affected: `synartesis`, `synartesis proxy`, the four
15
+ real policies and the HTTP mode are all untouched.
16
+
5
17
  ## 0.2.3 — 2026-08-22
6
18
 
7
19
  ### Fixed
package/README.md CHANGED
@@ -298,7 +298,7 @@ each manifest below says plainly where that runs out.
298
298
  | [`memory.yaml`](manifests/memory.yaml) | `@modelcontextprotocol/server-memory` | the knowledge graph an agent keeps about you |
299
299
  | [`git.yaml`](manifests/git.yaml) | `mcp-server-git` | a real repository's index and history |
300
300
  | [`github.yaml`](manifests/github.yaml) | `github/github-mcp-server` | issues, pull requests, file contents |
301
- | [`toy-crm.yaml`](manifests/toy-crm.yaml) | the fixture in this repo | the worked example of every class |
301
+ | [`toy-crm.yaml`](manifests/toy-crm.yaml) | the fixture in this repo, not in the published package | the worked example of every class |
302
302
 
303
303
  Every one of those but `github.yaml` was checked against the server actually
304
304
  running. Two demos run the whole loop for real:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "synartesis",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "An undo layer for AI agents.",
5
5
  "type": "module",
6
6
  "private": false,
@@ -28,7 +28,10 @@
28
28
  },
29
29
  "files": [
30
30
  "dist",
31
+ "!dist/toy-crm.*",
32
+ "!dist/demo-agent.*",
31
33
  "manifests",
34
+ "!manifests/toy-crm.yaml",
32
35
  "README.md",
33
36
  "CHANGELOG.md",
34
37
  "LICENSE"
@@ -1,67 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- describe
4
- } from "./chunk-K3QIPVBY.js";
5
-
6
- // demo/agent.ts
7
- import { dirname, join } from "path";
8
- import { fileURLToPath } from "url";
9
- import { Client } from "@modelcontextprotocol/sdk/client/index.js";
10
- import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
11
- import { z } from "zod";
12
- var said = z.looseObject({
13
- isError: z.boolean().default(false),
14
- content: z.array(z.looseObject({ type: z.string(), text: z.string().optional() })).default([])
15
- });
16
- function line(name, raw) {
17
- const parsed = said.safeParse(raw);
18
- if (!parsed.success) {
19
- return ` called ${name}`;
20
- }
21
- const text = parsed.data.content.map((block) => block.text ?? "").join(" ").replace(/\s+/g, " ");
22
- return ` ${parsed.data.isError ? "refused " : "called "} ${name} ${text.slice(0, 110)}`;
23
- }
24
- function inherited() {
25
- const env = {};
26
- for (const [key, value] of Object.entries(process.env)) {
27
- if (value !== void 0) {
28
- env[key] = value;
29
- }
30
- }
31
- return env;
32
- }
33
- var toolArgs = z.record(z.string(), z.unknown());
34
- var [journal, manifest, ...calls] = process.argv.slice(2);
35
- if (journal === void 0 || manifest === void 0) {
36
- throw new Error("usage: demo-agent <journal> <manifest> '<tool> <jsonArgs>' ...");
37
- }
38
- var client = new Client({ name: "agent", version: "0" });
39
- await client.connect(
40
- new StdioClientTransport({
41
- command: process.execPath,
42
- args: [
43
- join(dirname(fileURLToPath(import.meta.url)), "proxy.js"),
44
- "--manifest",
45
- manifest,
46
- "--journal",
47
- journal
48
- ],
49
- stderr: "ignore",
50
- env: inherited()
51
- })
52
- );
53
- for (const call of calls) {
54
- const at = call.indexOf(" ");
55
- const name = at === -1 ? call : call.slice(0, at);
56
- const given = at === -1 ? {} : JSON.parse(call.slice(at + 1));
57
- const args = toolArgs.parse(given);
58
- try {
59
- process.stdout.write(`${line(name, await client.callTool({ name, arguments: args }))}
60
- `);
61
- } catch (error) {
62
- process.stdout.write(` blocked ${name} ${describe(error).slice(0, 200)}
63
- `);
64
- }
65
- }
66
- await client.close();
67
- //# sourceMappingURL=demo-agent.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../demo/agent.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * A real MCP client, for the demos.\n *\n * Piping every frame into a server at once is not what a client does, and the\n * difference is not cosmetic: it makes the server run both handlers at the\n * same time. The memory server does a load-modify-save with no lock, so two\n * overlapping calls lose one of the writes outright. Waiting for each reply\n * before sending the next is both what really happens and the only way to\n * demonstrate anything about a server rather than about a race inside it.\n *\n * node dist/demo-agent.js <journal> <manifest> '<toolName> <jsonArgs>' ...\n */\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nimport { Client } from \"@modelcontextprotocol/sdk/client/index.js\";\nimport { StdioClientTransport } from \"@modelcontextprotocol/sdk/client/stdio.js\";\nimport { z } from \"zod\";\n\nimport { describe } from \"../src/errors.js\";\n\nconst said = z.looseObject({\n isError: z.boolean().default(false),\n content: z.array(z.looseObject({ type: z.string(), text: z.string().optional() })).default([]),\n});\n\nfunction line(name: string, raw: unknown): string {\n const parsed = said.safeParse(raw);\n if (!parsed.success) {\n return ` called ${name}`;\n }\n const text = parsed.data.content\n .map((block) => block.text ?? \"\")\n .join(\" \")\n .replace(/\\s+/g, \" \");\n return ` ${parsed.data.isError ? \"refused \" : \"called \"} ${name} ${text.slice(0, 110)}`;\n}\n\n/** process.env types every value as optional; the transport wants only the set ones. */\nfunction inherited(): Record<string, string> {\n const env: Record<string, string> = {};\n for (const [key, value] of Object.entries(process.env)) {\n if (value !== undefined) {\n env[key] = value;\n }\n }\n return env;\n}\n\nconst toolArgs = z.record(z.string(), z.unknown());\n\nconst [journal, manifest, ...calls] = process.argv.slice(2);\nif (journal === undefined || manifest === undefined) {\n throw new Error(\"usage: demo-agent <journal> <manifest> '<tool> <jsonArgs>' ...\");\n}\n\nconst client = new Client({ name: \"agent\", version: \"0\" });\nawait client.connect(\n new StdioClientTransport({\n command: process.execPath,\n args: [\n join(dirname(fileURLToPath(import.meta.url)), \"proxy.js\"),\n \"--manifest\",\n manifest,\n \"--journal\",\n journal,\n ],\n stderr: \"ignore\",\n env: inherited(),\n }),\n);\n\nfor (const call of calls) {\n const at = call.indexOf(\" \");\n const name = at === -1 ? call : call.slice(0, at);\n const given: unknown = at === -1 ? {} : JSON.parse(call.slice(at + 1));\n const args = toolArgs.parse(given);\n try {\n process.stdout.write(`${line(name, await client.callTool({ name, arguments: args }))}\\n`);\n } catch (error: unknown) {\n process.stdout.write(` blocked ${name} ${describe(error).slice(0, 200)}\\n`);\n }\n}\n\nawait client.close();\n"],"mappings":";;;;;;AAaA,SAAS,SAAS,YAAY;AAC9B,SAAS,qBAAqB;AAE9B,SAAS,cAAc;AACvB,SAAS,4BAA4B;AACrC,SAAS,SAAS;AAIlB,IAAM,OAAO,EAAE,YAAY;AAAA,EACzB,SAAS,EAAE,QAAQ,EAAE,QAAQ,KAAK;AAAA,EAClC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;AAC/F,CAAC;AAED,SAAS,KAAK,MAAc,KAAsB;AAChD,QAAM,SAAS,KAAK,UAAU,GAAG;AACjC,MAAI,CAAC,OAAO,SAAS;AACnB,WAAO,cAAc,IAAI;AAAA,EAC3B;AACA,QAAM,OAAO,OAAO,KAAK,QACtB,IAAI,CAAC,UAAU,MAAM,QAAQ,EAAE,EAC/B,KAAK,GAAG,EACR,QAAQ,QAAQ,GAAG;AACtB,SAAO,KAAK,OAAO,KAAK,UAAU,aAAa,UAAU,IAAI,IAAI,KAAK,KAAK,MAAM,GAAG,GAAG,CAAC;AAC1F;AAGA,SAAS,YAAoC;AAC3C,QAAM,MAA8B,CAAC;AACrC,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,QAAQ,GAAG,GAAG;AACtD,QAAI,UAAU,QAAW;AACvB,UAAI,GAAG,IAAI;AAAA,IACb;AAAA,EACF;AACA,SAAO;AACT;AAEA,IAAM,WAAW,EAAE,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC;AAEjD,IAAM,CAAC,SAAS,UAAU,GAAG,KAAK,IAAI,QAAQ,KAAK,MAAM,CAAC;AAC1D,IAAI,YAAY,UAAa,aAAa,QAAW;AACnD,QAAM,IAAI,MAAM,gEAAgE;AAClF;AAEA,IAAM,SAAS,IAAI,OAAO,EAAE,MAAM,SAAS,SAAS,IAAI,CAAC;AACzD,MAAM,OAAO;AAAA,EACX,IAAI,qBAAqB;AAAA,IACvB,SAAS,QAAQ;AAAA,IACjB,MAAM;AAAA,MACJ,KAAK,QAAQ,cAAc,YAAY,GAAG,CAAC,GAAG,UAAU;AAAA,MACxD;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,QAAQ;AAAA,IACR,KAAK,UAAU;AAAA,EACjB,CAAC;AACH;AAEA,WAAW,QAAQ,OAAO;AACxB,QAAM,KAAK,KAAK,QAAQ,GAAG;AAC3B,QAAM,OAAO,OAAO,KAAK,OAAO,KAAK,MAAM,GAAG,EAAE;AAChD,QAAM,QAAiB,OAAO,KAAK,CAAC,IAAI,KAAK,MAAM,KAAK,MAAM,KAAK,CAAC,CAAC;AACrE,QAAM,OAAO,SAAS,MAAM,KAAK;AACjC,MAAI;AACF,YAAQ,OAAO,MAAM,GAAG,KAAK,MAAM,MAAM,OAAO,SAAS,EAAE,MAAM,WAAW,KAAK,CAAC,CAAC,CAAC;AAAA,CAAI;AAAA,EAC1F,SAAS,OAAgB;AACvB,YAAQ,OAAO,MAAM,cAAc,IAAI,KAAK,SAAS,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC;AAAA,CAAI;AAAA,EAC/E;AACF;AAEA,MAAM,OAAO,MAAM;","names":[]}
package/dist/toy-crm.js DELETED
@@ -1,293 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // fixtures/toy-crm/stdio.ts
4
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
5
- import { dirname } from "path";
6
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
7
- import { z as z2 } from "zod";
8
-
9
- // fixtures/toy-crm/server.ts
10
- import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
11
- import { z } from "zod";
12
-
13
- // fixtures/toy-crm/store.ts
14
- var PLANS = ["free", "pro", "enterprise"];
15
- var CustomerNotFoundError = class extends Error {
16
- constructor(customerId) {
17
- super(`no customer with id ${customerId}`);
18
- this.customerId = customerId;
19
- this.name = "CustomerNotFoundError";
20
- }
21
- customerId;
22
- };
23
- var SEED = [
24
- { id: "c_001", name: "Ada Lovelace", email: "ada@example.com", plan: "pro", notes: "founding customer" },
25
- { id: "c_002", name: "Grace Hopper", email: "grace@example.com", plan: "enterprise", notes: "renewal in March" },
26
- { id: "c_003", name: "Alan Turing", email: "alan@example.com", plan: "free", notes: "" }
27
- ];
28
- var ToyCrmStore = class {
29
- #customers = /* @__PURE__ */ new Map();
30
- #outbox = [];
31
- #nextId = SEED.length + 1;
32
- #now;
33
- #beforeWrite;
34
- #afterWrite;
35
- constructor(options2 = {}) {
36
- this.#now = options2.now ?? (() => (/* @__PURE__ */ new Date()).toISOString());
37
- this.#beforeWrite = options2.beforeWrite ?? (() => void 0);
38
- this.#afterWrite = options2.afterWrite ?? (() => void 0);
39
- for (const customer of SEED) {
40
- this.#customers.set(customer.id, customer);
41
- }
42
- }
43
- getCustomer(id) {
44
- const customer = this.#customers.get(id);
45
- if (customer === void 0) {
46
- throw new CustomerNotFoundError(id);
47
- }
48
- return customer;
49
- }
50
- listCustomers() {
51
- return [...this.#customers.keys()].sort().map((id) => this.getCustomer(id));
52
- }
53
- createCustomer(draft) {
54
- this.#beforeWrite();
55
- const id = `c_${String(this.#nextId).padStart(3, "0")}`;
56
- this.#nextId += 1;
57
- const customer = { id, ...draft };
58
- this.#customers.set(id, customer);
59
- this.#afterWrite();
60
- return customer;
61
- }
62
- /**
63
- * Restores a customer under a caller-supplied id. This is what makes
64
- * delete_customer reversible; a plain create would allocate a new id and
65
- * leave every foreign key pointing at nothing.
66
- */
67
- restoreCustomer(customer) {
68
- this.#beforeWrite();
69
- this.#customers.set(customer.id, customer);
70
- this.#afterWrite();
71
- return customer;
72
- }
73
- updateCustomer(id, patch) {
74
- this.#beforeWrite();
75
- const current = this.getCustomer(id);
76
- const updated = {
77
- id: current.id,
78
- name: patch.name ?? current.name,
79
- email: patch.email ?? current.email,
80
- plan: patch.plan ?? current.plan,
81
- notes: patch.notes ?? current.notes
82
- };
83
- this.#customers.set(id, updated);
84
- this.#afterWrite();
85
- return updated;
86
- }
87
- deleteCustomer(id) {
88
- this.#beforeWrite();
89
- const customer = this.getCustomer(id);
90
- this.#customers.delete(id);
91
- this.#afterWrite();
92
- return customer;
93
- }
94
- sendEmail(to, subject, body) {
95
- this.#beforeWrite();
96
- const email = { to, subject, body, sentAt: this.#now() };
97
- this.#outbox = [...this.#outbox, email];
98
- this.#afterWrite();
99
- return email;
100
- }
101
- /**
102
- * Replaces the whole store. Used to hydrate a persisted fixture so that a
103
- * rollback run in a separate process sees the state the agent left behind.
104
- */
105
- __restore(state) {
106
- this.#customers.clear();
107
- let highest = 0;
108
- for (const [id, customer] of Object.entries(state.customers)) {
109
- this.#customers.set(id, { ...customer });
110
- const numeric = Number(/^c_(\d+)$/.exec(id)?.[1] ?? "0");
111
- highest = Math.max(highest, numeric);
112
- }
113
- this.#outbox = state.outbox.map((email) => ({ ...email }));
114
- this.#nextId = highest + 1;
115
- }
116
- /**
117
- * Test helper: a detached deep copy of the whole store. Keys are sorted so
118
- * that two structurally equal stores also serialise identically, which is
119
- * what the Phase 4 rollback assertion actually compares.
120
- */
121
- __snapshot() {
122
- const ids = [...this.#customers.keys()].sort();
123
- return {
124
- customers: Object.fromEntries(ids.map((id) => [id, { ...this.getCustomer(id) }])),
125
- outbox: this.#outbox.map((email) => ({ ...email }))
126
- };
127
- }
128
- };
129
-
130
- // fixtures/toy-crm/server.ts
131
- var planSchema = z.enum(PLANS);
132
- function json(value) {
133
- return { content: [{ type: "text", text: JSON.stringify(value) }] };
134
- }
135
- function toolError(message) {
136
- return { content: [{ type: "text", text: message }], isError: true };
137
- }
138
- function guard(run) {
139
- try {
140
- return run();
141
- } catch (error) {
142
- if (error instanceof CustomerNotFoundError) {
143
- return toolError(error.message);
144
- }
145
- throw error;
146
- }
147
- }
148
- function createToyCrmServer(store2) {
149
- const server2 = new McpServer(
150
- { name: "toy-crm", version: "1.0.0" },
151
- {
152
- capabilities: { tools: {}, resources: {} },
153
- instructions: "A toy CRM. Customer ids look like c_001."
154
- }
155
- );
156
- server2.registerResource(
157
- "customers",
158
- "crm://customers",
159
- { description: "Every customer record.", mimeType: "application/json" },
160
- (uri) => ({
161
- contents: [
162
- {
163
- uri: uri.href,
164
- mimeType: "application/json",
165
- text: JSON.stringify(store2.listCustomers())
166
- }
167
- ]
168
- })
169
- );
170
- server2.registerResource(
171
- "customer",
172
- new ResourceTemplate("crm://customers/{id}", { list: void 0 }),
173
- { description: "A single customer record.", mimeType: "application/json" },
174
- (uri, { id }) => ({
175
- contents: [
176
- {
177
- uri: uri.href,
178
- mimeType: "application/json",
179
- text: JSON.stringify(store2.getCustomer(typeof id === "string" ? id : ""))
180
- }
181
- ]
182
- })
183
- );
184
- server2.registerTool(
185
- "get_customer",
186
- {
187
- description: "Fetch a single customer record by id.",
188
- inputSchema: { id: z.string().describe("Customer id, for example c_001.") },
189
- annotations: { readOnlyHint: true }
190
- },
191
- ({ id }) => guard(() => json(store2.getCustomer(id)))
192
- );
193
- server2.registerTool(
194
- "create_customer",
195
- {
196
- description: "Create a customer and return the created record, including its assigned id.",
197
- inputSchema: {
198
- name: z.string(),
199
- email: z.string(),
200
- plan: planSchema.default("free"),
201
- notes: z.string().default("")
202
- },
203
- annotations: { destructiveHint: false }
204
- },
205
- (draft) => json(store2.createCustomer(draft))
206
- );
207
- server2.registerTool(
208
- "update_customer",
209
- {
210
- description: "Apply a partial patch to a customer. Omitted fields are left unchanged.",
211
- inputSchema: {
212
- id: z.string(),
213
- name: z.string().optional(),
214
- email: z.string().optional(),
215
- plan: planSchema.optional(),
216
- notes: z.string().optional()
217
- },
218
- annotations: { destructiveHint: true, idempotentHint: true }
219
- },
220
- ({ id, ...patch }) => guard(() => json(store2.updateCustomer(id, patch)))
221
- );
222
- server2.registerTool(
223
- "delete_customer",
224
- {
225
- description: "Delete a customer and return the record as it was immediately before deletion.",
226
- inputSchema: { id: z.string() },
227
- annotations: { destructiveHint: true }
228
- },
229
- ({ id }) => guard(() => json(store2.deleteCustomer(id)))
230
- );
231
- server2.registerTool(
232
- "restore_customer",
233
- {
234
- description: "Recreate a previously deleted customer under its original id. Intended as the inverse of delete_customer.",
235
- inputSchema: {
236
- id: z.string(),
237
- name: z.string(),
238
- email: z.string(),
239
- plan: planSchema,
240
- notes: z.string()
241
- },
242
- annotations: { destructiveHint: false, idempotentHint: true }
243
- },
244
- (customer) => json(store2.restoreCustomer(customer))
245
- );
246
- server2.registerTool(
247
- "send_email",
248
- {
249
- description: "Send an email to a customer. This cannot be recalled once sent.",
250
- inputSchema: { to: z.string(), subject: z.string(), body: z.string() },
251
- annotations: { destructiveHint: true, openWorldHint: true }
252
- },
253
- ({ to, subject, body }) => json(store2.sendEmail(to, subject, body))
254
- );
255
- return server2;
256
- }
257
-
258
- // fixtures/toy-crm/stdio.ts
259
- var stateSchema = z2.object({
260
- customers: z2.record(
261
- z2.string(),
262
- z2.object({
263
- id: z2.string(),
264
- name: z2.string(),
265
- email: z2.string(),
266
- plan: z2.enum(PLANS),
267
- notes: z2.string()
268
- })
269
- ),
270
- outbox: z2.array(
271
- z2.object({ to: z2.string(), subject: z2.string(), body: z2.string(), sentAt: z2.string() })
272
- )
273
- });
274
- var at = process.argv.indexOf("--state");
275
- var statePath = at === -1 ? void 0 : process.argv[at + 1];
276
- var options = {};
277
- var store = new ToyCrmStore(
278
- statePath === void 0 ? options : {
279
- afterWrite: () => {
280
- mkdirSync(dirname(statePath), { recursive: true });
281
- writeFileSync(statePath, JSON.stringify(store.__snapshot(), null, 2));
282
- }
283
- }
284
- );
285
- if (statePath !== void 0 && existsSync(statePath)) {
286
- store.__restore(stateSchema.parse(JSON.parse(readFileSync(statePath, "utf8"))));
287
- }
288
- var server = createToyCrmServer(store);
289
- process.stdin.on("end", () => {
290
- process.exit(0);
291
- });
292
- await server.connect(new StdioServerTransport());
293
- //# sourceMappingURL=toy-crm.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../fixtures/toy-crm/stdio.ts","../fixtures/toy-crm/server.ts","../fixtures/toy-crm/store.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { existsSync, mkdirSync, readFileSync, writeFileSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\n\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\n\nimport { createToyCrmServer } from \"./server.js\";\nimport { PLANS, ToyCrmStore, type ToyCrmOptions } from \"./store.js\";\n\n/**\n * Runs the fixture as a real stdio MCP server so the proxy can spawn it the\n * same way it will spawn a production server.\n *\n * toy-crm [--state <path>]\n *\n * Without --state the store lives only for the life of the process. With it,\n * state survives across processes, which is what makes a cross-process undo\n * demonstrable at all: the rollback runs long after the agent's proxy exited.\n */\nconst stateSchema = z.object({\n customers: z.record(\n z.string(),\n z.object({\n id: z.string(),\n name: z.string(),\n email: z.string(),\n plan: z.enum(PLANS),\n notes: z.string(),\n }),\n ),\n outbox: z.array(\n z.object({ to: z.string(), subject: z.string(), body: z.string(), sentAt: z.string() }),\n ),\n});\n\nconst at = process.argv.indexOf(\"--state\");\nconst statePath = at === -1 ? undefined : process.argv[at + 1];\n\nconst options: ToyCrmOptions = {};\nconst store = new ToyCrmStore(\n statePath === undefined\n ? options\n : {\n afterWrite: (): void => {\n mkdirSync(dirname(statePath), { recursive: true });\n writeFileSync(statePath, JSON.stringify(store.__snapshot(), null, 2));\n },\n },\n);\n\nif (statePath !== undefined && existsSync(statePath)) {\n store.__restore(stateSchema.parse(JSON.parse(readFileSync(statePath, \"utf8\"))));\n}\n\nconst server = createToyCrmServer(store);\n\n// Exit when the pipe closes; the SDK's stdio transport does not do this for\n// us, and a fixture that outlives its client makes tests hang.\nprocess.stdin.on(\"end\", () => {\n process.exit(0);\n});\n\nawait server.connect(new StdioServerTransport());\n","import { McpServer, ResourceTemplate } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport type { CallToolResult } from \"@modelcontextprotocol/sdk/types.js\";\nimport { z } from \"zod\";\n\nimport { CustomerNotFoundError, PLANS, ToyCrmStore } from \"./store.js\";\n\n/**\n * Tools are named without a server prefix, the way a real upstream MCP server\n * names them. The `crm.` prefix used in manifest matches is applied by the\n * proxy from the server's key in the manifest, not by the server itself.\n */\n\nconst planSchema = z.enum(PLANS);\n\n/** Real MCP servers overwhelmingly return JSON inside a text block; match that. */\nfunction json(value: unknown): CallToolResult {\n return { content: [{ type: \"text\", text: JSON.stringify(value) }] };\n}\n\nfunction toolError(message: string): CallToolResult {\n return { content: [{ type: \"text\", text: message }], isError: true };\n}\n\n/**\n * A missing customer is a tool-level error, not a protocol error: the agent\n * should be able to read it and recover, exactly as the gate's denial path\n * must behave in Phase 5.\n */\nfunction guard(run: () => CallToolResult): CallToolResult {\n try {\n return run();\n } catch (error: unknown) {\n if (error instanceof CustomerNotFoundError) {\n return toolError(error.message);\n }\n throw error;\n }\n}\n\nexport function createToyCrmServer(store: ToyCrmStore): McpServer {\n const server = new McpServer(\n { name: \"toy-crm\", version: \"1.0.0\" },\n {\n capabilities: { tools: {}, resources: {} },\n instructions: \"A toy CRM. Customer ids look like c_001.\",\n },\n );\n\n // Resources exist so that the proxy's resources/* passthrough is testable;\n // a tools-only fixture could not exercise it.\n server.registerResource(\n \"customers\",\n \"crm://customers\",\n { description: \"Every customer record.\", mimeType: \"application/json\" },\n (uri) => ({\n contents: [\n {\n uri: uri.href,\n mimeType: \"application/json\",\n text: JSON.stringify(store.listCustomers()),\n },\n ],\n }),\n );\n\n server.registerResource(\n \"customer\",\n new ResourceTemplate(\"crm://customers/{id}\", { list: undefined }),\n { description: \"A single customer record.\", mimeType: \"application/json\" },\n (uri, { id }) => ({\n contents: [\n {\n uri: uri.href,\n mimeType: \"application/json\",\n text: JSON.stringify(store.getCustomer(typeof id === \"string\" ? id : \"\")),\n },\n ],\n }),\n );\n\n server.registerTool(\n \"get_customer\",\n {\n description: \"Fetch a single customer record by id.\",\n inputSchema: { id: z.string().describe(\"Customer id, for example c_001.\") },\n annotations: { readOnlyHint: true },\n },\n ({ id }) => guard(() => json(store.getCustomer(id))),\n );\n\n server.registerTool(\n \"create_customer\",\n {\n description: \"Create a customer and return the created record, including its assigned id.\",\n inputSchema: {\n name: z.string(),\n email: z.string(),\n plan: planSchema.default(\"free\"),\n notes: z.string().default(\"\"),\n },\n annotations: { destructiveHint: false },\n },\n (draft) => json(store.createCustomer(draft)),\n );\n\n server.registerTool(\n \"update_customer\",\n {\n description: \"Apply a partial patch to a customer. Omitted fields are left unchanged.\",\n inputSchema: {\n id: z.string(),\n name: z.string().optional(),\n email: z.string().optional(),\n plan: planSchema.optional(),\n notes: z.string().optional(),\n },\n annotations: { destructiveHint: true, idempotentHint: true },\n },\n ({ id, ...patch }) => guard(() => json(store.updateCustomer(id, patch))),\n );\n\n server.registerTool(\n \"delete_customer\",\n {\n description: \"Delete a customer and return the record as it was immediately before deletion.\",\n inputSchema: { id: z.string() },\n annotations: { destructiveHint: true },\n },\n ({ id }) => guard(() => json(store.deleteCustomer(id))),\n );\n\n server.registerTool(\n \"restore_customer\",\n {\n description:\n \"Recreate a previously deleted customer under its original id. Intended as the inverse of delete_customer.\",\n inputSchema: {\n id: z.string(),\n name: z.string(),\n email: z.string(),\n plan: planSchema,\n notes: z.string(),\n },\n annotations: { destructiveHint: false, idempotentHint: true },\n },\n (customer) => json(store.restoreCustomer(customer)),\n );\n\n server.registerTool(\n \"send_email\",\n {\n description: \"Send an email to a customer. This cannot be recalled once sent.\",\n inputSchema: { to: z.string(), subject: z.string(), body: z.string() },\n annotations: { destructiveHint: true, openWorldHint: true },\n },\n ({ to, subject, body }) => json(store.sendEmail(to, subject, body)),\n );\n\n return server;\n}\n","/**\n * In-memory CRM standing in for a real external system.\n *\n * Behaviour is deterministic on purpose: Phase 4 asserts that a store is\n * byte-identical before a run and after undoing it, which is only meaningful\n * if ids and timestamps do not drift between runs.\n */\n\nexport const PLANS = [\"free\", \"pro\", \"enterprise\"] as const;\nexport type Plan = (typeof PLANS)[number];\n\nexport interface Customer {\n readonly id: string;\n readonly name: string;\n readonly email: string;\n readonly plan: Plan;\n readonly notes: string;\n}\n\nexport interface SentEmail {\n readonly to: string;\n readonly subject: string;\n readonly body: string;\n readonly sentAt: string;\n}\n\nexport interface ToyCrmState {\n readonly customers: Readonly<Record<string, Customer>>;\n readonly outbox: readonly SentEmail[];\n}\n\nexport interface CustomerDraft {\n readonly name: string;\n readonly email: string;\n readonly plan: Plan;\n readonly notes: string;\n}\n\nexport type CustomerPatch = {\n readonly [K in keyof CustomerDraft]?: CustomerDraft[K] | undefined;\n};\n\nexport interface ToyCrmOptions {\n /** Injected so tests can pin email timestamps. */\n readonly now?: () => string;\n /**\n * Called before every mutating operation. Exists so a test can inject a\n * failure at a chosen point, which is the only way to exercise an\n * interrupted rollback.\n */\n readonly beforeWrite?: () => void;\n /** Called after every mutating operation, so state can be persisted. */\n readonly afterWrite?: () => void;\n}\n\nexport class CustomerNotFoundError extends Error {\n constructor(public readonly customerId: string) {\n super(`no customer with id ${customerId}`);\n this.name = \"CustomerNotFoundError\";\n }\n}\n\nconst SEED: readonly Customer[] = [\n { id: \"c_001\", name: \"Ada Lovelace\", email: \"ada@example.com\", plan: \"pro\", notes: \"founding customer\" },\n { id: \"c_002\", name: \"Grace Hopper\", email: \"grace@example.com\", plan: \"enterprise\", notes: \"renewal in March\" },\n { id: \"c_003\", name: \"Alan Turing\", email: \"alan@example.com\", plan: \"free\", notes: \"\" },\n];\n\nexport class ToyCrmStore {\n readonly #customers = new Map<string, Customer>();\n #outbox: SentEmail[] = [];\n #nextId = SEED.length + 1;\n readonly #now: () => string;\n readonly #beforeWrite: () => void;\n readonly #afterWrite: () => void;\n\n constructor(options: ToyCrmOptions = {}) {\n this.#now = options.now ?? ((): string => new Date().toISOString());\n this.#beforeWrite = options.beforeWrite ?? ((): void => undefined);\n this.#afterWrite = options.afterWrite ?? ((): void => undefined);\n for (const customer of SEED) {\n this.#customers.set(customer.id, customer);\n }\n }\n\n getCustomer(id: string): Customer {\n const customer = this.#customers.get(id);\n if (customer === undefined) {\n throw new CustomerNotFoundError(id);\n }\n return customer;\n }\n\n listCustomers(): readonly Customer[] {\n return [...this.#customers.keys()].sort().map((id) => this.getCustomer(id));\n }\n\n createCustomer(draft: CustomerDraft): Customer {\n this.#beforeWrite();\n // Ids are never recycled: a rolled-back delete followed by a fresh create\n // must not collide with the id the rollback restored.\n const id = `c_${String(this.#nextId).padStart(3, \"0\")}`;\n this.#nextId += 1;\n const customer: Customer = { id, ...draft };\n this.#customers.set(id, customer);\n this.#afterWrite();\n return customer;\n }\n\n /**\n * Restores a customer under a caller-supplied id. This is what makes\n * delete_customer reversible; a plain create would allocate a new id and\n * leave every foreign key pointing at nothing.\n */\n restoreCustomer(customer: Customer): Customer {\n this.#beforeWrite();\n this.#customers.set(customer.id, customer);\n this.#afterWrite();\n return customer;\n }\n\n updateCustomer(id: string, patch: CustomerPatch): Customer {\n this.#beforeWrite();\n const current = this.getCustomer(id);\n // Merged field by field rather than by spread: an absent optional arrives\n // over the wire as an explicit `undefined`, and spreading that would erase\n // the current value instead of leaving it untouched.\n const updated: Customer = {\n id: current.id,\n name: patch.name ?? current.name,\n email: patch.email ?? current.email,\n plan: patch.plan ?? current.plan,\n notes: patch.notes ?? current.notes,\n };\n this.#customers.set(id, updated);\n this.#afterWrite();\n return updated;\n }\n\n deleteCustomer(id: string): Customer {\n this.#beforeWrite();\n const customer = this.getCustomer(id);\n this.#customers.delete(id);\n this.#afterWrite();\n return customer;\n }\n\n sendEmail(to: string, subject: string, body: string): SentEmail {\n this.#beforeWrite();\n const email: SentEmail = { to, subject, body, sentAt: this.#now() };\n this.#outbox = [...this.#outbox, email];\n this.#afterWrite();\n return email;\n }\n\n /**\n * Replaces the whole store. Used to hydrate a persisted fixture so that a\n * rollback run in a separate process sees the state the agent left behind.\n */\n __restore(state: ToyCrmState): void {\n this.#customers.clear();\n let highest = 0;\n for (const [id, customer] of Object.entries(state.customers)) {\n this.#customers.set(id, { ...customer });\n const numeric = Number(/^c_(\\d+)$/.exec(id)?.[1] ?? \"0\");\n highest = Math.max(highest, numeric);\n }\n this.#outbox = state.outbox.map((email) => ({ ...email }));\n // Ids are never recycled, so the counter resumes past anything restored.\n this.#nextId = highest + 1;\n }\n\n /**\n * Test helper: a detached deep copy of the whole store. Keys are sorted so\n * that two structurally equal stores also serialise identically, which is\n * what the Phase 4 rollback assertion actually compares.\n */\n __snapshot(): ToyCrmState {\n const ids = [...this.#customers.keys()].sort();\n return {\n customers: Object.fromEntries(ids.map((id) => [id, { ...this.getCustomer(id) }])),\n outbox: this.#outbox.map((email) => ({ ...email })),\n };\n }\n}\n"],"mappings":"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","names":["z","options","store","server","z"]}
@@ -1,68 +0,0 @@
1
- # Policy for the toy CRM fixture. Every class in the taxonomy appears here, so
2
- # this doubles as the worked example of the manifest format.
3
- version: 1
4
-
5
- servers:
6
- crm:
7
- command: node
8
- args: ["dist/toy-crm.js"]
9
-
10
- tools:
11
- - match: "crm.get_customer"
12
- class: readonly
13
-
14
- # The id is assigned by the server, so it is only knowable from the result.
15
- - match: "crm.create_customer"
16
- class: compensable
17
- inverse:
18
- tool: "crm.delete_customer"
19
- args:
20
- id: "$result.id"
21
-
22
- # A patch, so the inverse has to restore every field rather than re-apply the
23
- # patch: fields the agent did not touch must still come back unchanged if the
24
- # same record is updated twice in one run.
25
- - match: "crm.update_customer"
26
- class: reversible
27
- snapshot:
28
- tool: "crm.get_customer"
29
- args:
30
- id: "$.id"
31
- absent_when: ["no customer with id"]
32
- inverse:
33
- tool: "crm.update_customer"
34
- args:
35
- id: "$.id"
36
- name: "$snapshot.name"
37
- email: "$snapshot.email"
38
- plan: "$snapshot.plan"
39
- notes: "$snapshot.notes"
40
-
41
- # Reversible rather than irreversible because the pre-read captures the whole
42
- # record and restore_customer puts it back under its original id.
43
- - match: "crm.delete_customer"
44
- class: reversible
45
- snapshot:
46
- tool: "crm.get_customer"
47
- args:
48
- id: "$.id"
49
- absent_when: ["no customer with id"]
50
- inverse:
51
- tool: "crm.restore_customer"
52
- args:
53
- id: "$snapshot.id"
54
- name: "$snapshot.name"
55
- email: "$snapshot.email"
56
- plan: "$snapshot.plan"
57
- notes: "$snapshot.notes"
58
-
59
- - match: "crm.restore_customer"
60
- class: compensable
61
- inverse:
62
- tool: "crm.delete_customer"
63
- args:
64
- id: "$.id"
65
-
66
- - match: "crm.send_email"
67
- class: irreversible
68
- gate: always