@rayfold/server 0.1.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.
Files changed (73) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +10 -0
  3. package/README.md +70 -0
  4. package/args.d.ts +15 -0
  5. package/args.js +226 -0
  6. package/args.js.map +1 -0
  7. package/batch.d.ts +54 -0
  8. package/batch.js +483 -0
  9. package/batch.js.map +1 -0
  10. package/bindings.d.ts +37 -0
  11. package/bindings.js +284 -0
  12. package/bindings.js.map +1 -0
  13. package/capability-scope.d.ts +8 -0
  14. package/capability-scope.js +19 -0
  15. package/capability-scope.js.map +1 -0
  16. package/capability.d.ts +56 -0
  17. package/capability.js +112 -0
  18. package/capability.js.map +1 -0
  19. package/context.d.ts +74 -0
  20. package/context.js +109 -0
  21. package/context.js.map +1 -0
  22. package/core.d.ts +18 -0
  23. package/core.js +18 -0
  24. package/core.js.map +1 -0
  25. package/cost.d.ts +15 -0
  26. package/cost.js +110 -0
  27. package/cost.js.map +1 -0
  28. package/executor.d.ts +89 -0
  29. package/executor.js +695 -0
  30. package/executor.js.map +1 -0
  31. package/guard.d.ts +33 -0
  32. package/guard.js +65 -0
  33. package/guard.js.map +1 -0
  34. package/http.d.ts +33 -0
  35. package/http.js +379 -0
  36. package/http.js.map +1 -0
  37. package/index.d.ts +8 -0
  38. package/index.js +9 -0
  39. package/index.js.map +1 -0
  40. package/instrumentation.d.ts +36 -0
  41. package/instrumentation.js +2 -0
  42. package/instrumentation.js.map +1 -0
  43. package/live.d.ts +37 -0
  44. package/live.js +240 -0
  45. package/live.js.map +1 -0
  46. package/mcp.d.ts +55 -0
  47. package/mcp.js +314 -0
  48. package/mcp.js.map +1 -0
  49. package/openapi.d.ts +11 -0
  50. package/openapi.js +124 -0
  51. package/openapi.js.map +1 -0
  52. package/package.json +53 -0
  53. package/policy.d.ts +17 -0
  54. package/policy.js +64 -0
  55. package/policy.js.map +1 -0
  56. package/protocol.d.ts +139 -0
  57. package/protocol.js +98 -0
  58. package/protocol.js.map +1 -0
  59. package/server.d.ts +58 -0
  60. package/server.js +79 -0
  61. package/server.js.map +1 -0
  62. package/usage.d.ts +39 -0
  63. package/usage.js +44 -0
  64. package/usage.js.map +1 -0
  65. package/views.d.ts +33 -0
  66. package/views.js +108 -0
  67. package/views.js.map +1 -0
  68. package/wiring.d.ts +16 -0
  69. package/wiring.js +57 -0
  70. package/wiring.js.map +1 -0
  71. package/ws.d.ts +21 -0
  72. package/ws.js +209 -0
  73. package/ws.js.map +1 -0
package/live.js ADDED
@@ -0,0 +1,240 @@
1
+ export class ChangeBus {
2
+ subs = new Set();
3
+ publish(c) {
4
+ if (!c.keys.size && !c.ops.size)
5
+ return;
6
+ for (const fn of this.subs)
7
+ fn(c);
8
+ }
9
+ subscribe(fn) {
10
+ this.subs.add(fn);
11
+ return () => this.subs.delete(fn);
12
+ }
13
+ get size() {
14
+ return this.subs.size;
15
+ }
16
+ }
17
+ export function changeFromPatch(patch) {
18
+ const keys = new Set();
19
+ const ops = new Set();
20
+ for (const p of patch) {
21
+ if ("set" in p)
22
+ keys.add(p.set);
23
+ else if ("list" in p || "at" in p)
24
+ continue; // result-scoped: names no entity and no operation
25
+ else if ("del" in p)
26
+ keys.add(p.del);
27
+ else if ("inv" in p)
28
+ p.inv.forEach((k) => keys.add(k));
29
+ else if ("invOp" in p)
30
+ p.invOp.forEach((o) => ops.add(o));
31
+ }
32
+ return { keys, ops };
33
+ }
34
+ /** Entities keyed by "$type:id" with nested entities replaced by refs; plus the skeleton. */
35
+ export function normalizeResult(data) {
36
+ const entities = new Map();
37
+ const walk = (v) => {
38
+ if (v === null || typeof v !== "object")
39
+ return v;
40
+ if (Array.isArray(v))
41
+ return v.map(walk);
42
+ const o = v;
43
+ const tn = o["$type"];
44
+ const id = o["id"];
45
+ const out = {};
46
+ for (const [k, x] of Object.entries(o))
47
+ out[k] = walk(x);
48
+ if (typeof tn === "string" && (typeof id === "string" || typeof id === "number")) {
49
+ const key = `${tn}:${id}`;
50
+ entities.set(key, { ...(entities.get(key) ?? {}), ...out });
51
+ return { $ref: key };
52
+ }
53
+ return out;
54
+ };
55
+ const skeleton = walk(data);
56
+ return { entities, skeleton };
57
+ }
58
+ export function readSetOf(data) {
59
+ return new Set(normalizeResult(data).entities.keys());
60
+ }
61
+ /** The key that gives a value its identity in a list: its entity key, or its own content. */
62
+ function identityOf(v) {
63
+ if (v && typeof v === "object" && !Array.isArray(v)) {
64
+ const o = v;
65
+ const tn = o["$type"];
66
+ const id = o["id"];
67
+ if (typeof tn === "string" && (typeof id === "string" || typeof id === "number"))
68
+ return `${tn}:${id}`;
69
+ }
70
+ return `#${JSON.stringify(v)}`;
71
+ }
72
+ const entityAt = (v) => {
73
+ const k = identityOf(v);
74
+ return k.startsWith("#") ? null : k;
75
+ };
76
+ const sameJson = (a, b) => JSON.stringify(a) === JSON.stringify(b);
77
+ const joinPath = (path, part) => (path === "" ? String(part) : `${path}.${part}`);
78
+ const isLeaf = (v) => v === null || typeof v !== "object";
79
+ /**
80
+ * Describes how `b` differs from `a` as ops the client can apply to its stored result, or returns false when the
81
+ * difference cannot be expressed (a reorder, a changed set of fields) and the whole result has to be sent.
82
+ * Entities are not descended into: their fields travel as `set` ops. `carried` collects the entity keys whose
83
+ * values travel inside an insertion, so the caller does not send them twice.
84
+ */
85
+ function structuralDiff(a, b, path, ops, carried) {
86
+ if (sameJson(a, b))
87
+ return true;
88
+ const ea = entityAt(a);
89
+ const eb = entityAt(b);
90
+ if (ea || eb)
91
+ return ea !== null && ea === eb; // same entity: covered by `set`; a different one is structural
92
+ if (Array.isArray(a) && Array.isArray(b))
93
+ return listDiff(a, b, path, ops, carried);
94
+ if (a && b && typeof a === "object" && typeof b === "object" && !Array.isArray(a) && !Array.isArray(b)) {
95
+ const ao = a;
96
+ const bo = b;
97
+ const keys = Object.keys(bo);
98
+ if (keys.length !== Object.keys(ao).length || keys.some((k) => !(k in ao)))
99
+ return false;
100
+ const merge = {};
101
+ for (const k of keys) {
102
+ const av = ao[k];
103
+ const bv = bo[k];
104
+ if (sameJson(av, bv))
105
+ continue;
106
+ if (isLeaf(av) && isLeaf(bv)) {
107
+ merge[k] = bv;
108
+ continue;
109
+ }
110
+ if (!structuralDiff(av, bv, joinPath(path, k), ops, carried))
111
+ return false;
112
+ }
113
+ if (Object.keys(merge).length)
114
+ ops.push({ at: path, value: merge });
115
+ return true;
116
+ }
117
+ return false;
118
+ }
119
+ /** Positions removed and elements inserted, verified by replaying them: anything else (a reorder) is refused. */
120
+ function listDiff(a, b, path, ops, carried) {
121
+ const identified = (xs) => xs.length > 0 && xs.every((x) => entityAt(x) !== null);
122
+ const objects = (xs) => xs.every((x) => x !== null && typeof x === "object" && !Array.isArray(x));
123
+ // Rows with an identity of their own are matched by it. Elements without one (plain objects, such as a board's
124
+ // columns) are matched by position, so a change inside one of them is described in place rather than resent.
125
+ if (!(identified(a) && identified(b)) && a.length === b.length && objects(a) && objects(b)) {
126
+ for (let n = 0; n < b.length; n++)
127
+ if (!structuralDiff(a[n], b[n], joinPath(path, n), ops, carried))
128
+ return false;
129
+ return true;
130
+ }
131
+ const oldKeys = a.map(identityOf);
132
+ const newKeys = b.map(identityOf);
133
+ const del = [];
134
+ const ins = [];
135
+ const pairs = [];
136
+ let i = 0;
137
+ let j = 0;
138
+ while (i < a.length && j < b.length) {
139
+ if (oldKeys[i] === newKeys[j]) {
140
+ pairs.push([i, j]);
141
+ i++;
142
+ j++;
143
+ }
144
+ else if (!newKeys.includes(oldKeys[i], j)) {
145
+ del.push(i);
146
+ i++;
147
+ }
148
+ else {
149
+ ins.push({ at: j, value: b[j] });
150
+ j++;
151
+ }
152
+ }
153
+ while (i < a.length)
154
+ del.push(i++);
155
+ while (j < b.length) {
156
+ ins.push({ at: j, value: b[j] });
157
+ j++;
158
+ }
159
+ // The client removes the old positions, then inserts at the new ones. Refuse anything that does not replay exactly.
160
+ const replay = a.filter((_, n) => !del.includes(n));
161
+ for (const x of ins)
162
+ replay.splice(x.at, 0, x.value);
163
+ if (!sameJson(replay.map(identityOf), newKeys))
164
+ return false;
165
+ for (const [x, y] of pairs)
166
+ if (!structuralDiff(a[x], b[y], joinPath(path, y), ops, carried))
167
+ return false;
168
+ if (del.length || ins.length) {
169
+ for (const x of ins)
170
+ for (const k of normalizeResult(x.value).entities.keys())
171
+ carried.add(k);
172
+ const op = { list: path };
173
+ if (del.length)
174
+ op.del = del;
175
+ if (ins.length)
176
+ op.ins = ins;
177
+ ops.push(op);
178
+ }
179
+ return true;
180
+ }
181
+ /**
182
+ * Compare two results. Returns a `patch` frame body when the difference can be described (changed entity fields,
183
+ * changed fields of a plain object, rows added to or removed from a list), a full `data` replacement when it
184
+ * cannot, or null when nothing changed. A patch that would cost more than the result itself is not worth sending.
185
+ */
186
+ export function diffResults(prev, next) {
187
+ const a = normalizeResult(prev);
188
+ const b = normalizeResult(next);
189
+ const structural = [];
190
+ const carried = new Set();
191
+ if (JSON.stringify(a.skeleton) !== JSON.stringify(b.skeleton) && !structuralDiff(prev, next, "", structural, carried)) {
192
+ return { data: next };
193
+ }
194
+ const patch = [];
195
+ for (const [key, fields] of b.entities) {
196
+ if (carried.has(key))
197
+ continue; // its fields travel inside an insertion
198
+ const before = a.entities.get(key);
199
+ const changed = {};
200
+ for (const [k, v] of Object.entries(fields)) {
201
+ if (!before || JSON.stringify(before[k]) !== JSON.stringify(v))
202
+ changed[k] = v;
203
+ }
204
+ if (Object.keys(changed).length)
205
+ patch.push({ set: key, value: changed });
206
+ }
207
+ // Describing the structure costs more than resending it only when nearly every row changed; then send the result.
208
+ if (structural.length && JSON.stringify(structural).length >= JSON.stringify(next).length)
209
+ return { data: next };
210
+ patch.push(...structural);
211
+ return patch.length ? { patch } : null;
212
+ }
213
+ /** Fold `at` frames into a data value so deferred parts take part in diffs. */
214
+ export function foldFrames(frames) {
215
+ let data;
216
+ for (const f of frames) {
217
+ if ("at" in f) {
218
+ if (f.at === "")
219
+ Object.assign(data, f.data);
220
+ else {
221
+ const target = getPath(data, f.at.split("."));
222
+ if (target && typeof target === "object")
223
+ Object.assign(target, f.data);
224
+ }
225
+ }
226
+ else if ("data" in f)
227
+ data = f.data;
228
+ }
229
+ return data;
230
+ }
231
+ function getPath(v, path) {
232
+ let cur = v;
233
+ for (const p of path) {
234
+ if (cur === null || cur === undefined || typeof cur !== "object")
235
+ return undefined;
236
+ cur = Array.isArray(cur) ? cur[Number(p)] : cur[p];
237
+ }
238
+ return cur;
239
+ }
240
+ //# sourceMappingURL=live.js.map
package/live.js.map ADDED
@@ -0,0 +1 @@
1
+ {"version":3,"file":"live.js","sourceRoot":"","sources":["../src/live.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Live queries (extension `live`, spec/08): the query is the subscription.\n * A live query records its read set (entity keys + op name). Every command's patch and every explicit\n * `changes.publish()` feeds a ChangeBus; when a change intersects a read set the query re-runs and the\n * client receives either a minimal `patch` frame (same result structure, changed fields) or a fresh\n * `data` frame (membership/order changed).\n */\nimport type { Frame, PatchOp } from \"./protocol.ts\";\n\nexport interface Change {\n keys: Set<string>;\n ops: Set<string>;\n}\n\nexport class ChangeBus {\n private readonly subs = new Set<(c: Change) => void>();\n publish(c: Change): void {\n if (!c.keys.size && !c.ops.size) return;\n for (const fn of this.subs) fn(c);\n }\n subscribe(fn: (c: Change) => void): () => void {\n this.subs.add(fn);\n return () => this.subs.delete(fn);\n }\n get size(): number {\n return this.subs.size;\n }\n}\n\nexport function changeFromPatch(patch: PatchOp[]): Change {\n const keys = new Set<string>();\n const ops = new Set<string>();\n for (const p of patch) {\n if (\"set\" in p) keys.add(p.set);\n else if (\"list\" in p || \"at\" in p) continue; // result-scoped: names no entity and no operation\n else if (\"del\" in p) keys.add(p.del);\n else if (\"inv\" in p) p.inv.forEach((k) => keys.add(k));\n else if (\"invOp\" in p) p.invOp.forEach((o) => ops.add(o));\n }\n return { keys, ops };\n}\n\n/** Entities keyed by \"$type:id\" with nested entities replaced by refs; plus the skeleton. */\nexport function normalizeResult(data: unknown): { entities: Map<string, Record<string, unknown>>; skeleton: unknown } {\n const entities = new Map<string, Record<string, unknown>>();\n const walk = (v: unknown): unknown => {\n if (v === null || typeof v !== \"object\") return v;\n if (Array.isArray(v)) return v.map(walk);\n const o = v as Record<string, unknown>;\n const tn = o[\"$type\"];\n const id = o[\"id\"];\n const out: Record<string, unknown> = {};\n for (const [k, x] of Object.entries(o)) out[k] = walk(x);\n if (typeof tn === \"string\" && (typeof id === \"string\" || typeof id === \"number\")) {\n const key = `${tn}:${id}`;\n entities.set(key, { ...(entities.get(key) ?? {}), ...out });\n return { $ref: key };\n }\n return out;\n };\n const skeleton = walk(data);\n return { entities, skeleton };\n}\n\nexport function readSetOf(data: unknown): Set<string> {\n return new Set(normalizeResult(data).entities.keys());\n}\n\n/** The key that gives a value its identity in a list: its entity key, or its own content. */\nfunction identityOf(v: unknown): string {\n if (v && typeof v === \"object\" && !Array.isArray(v)) {\n const o = v as Record<string, unknown>;\n const tn = o[\"$type\"];\n const id = o[\"id\"];\n if (typeof tn === \"string\" && (typeof id === \"string\" || typeof id === \"number\")) return `${tn}:${id}`;\n }\n return `#${JSON.stringify(v)}`;\n}\n\nconst entityAt = (v: unknown): string | null => {\n const k = identityOf(v);\n return k.startsWith(\"#\") ? null : k;\n};\nconst sameJson = (a: unknown, b: unknown): boolean => JSON.stringify(a) === JSON.stringify(b);\nconst joinPath = (path: string, part: string | number): string => (path === \"\" ? String(part) : `${path}.${part}`);\nconst isLeaf = (v: unknown): boolean => v === null || typeof v !== \"object\";\n\ntype StructuralOp = { at: string; value: Record<string, unknown> } | { list: string; del?: number[]; ins?: Array<{ at: number; value: unknown }> };\n\n/**\n * Describes how `b` differs from `a` as ops the client can apply to its stored result, or returns false when the\n * difference cannot be expressed (a reorder, a changed set of fields) and the whole result has to be sent.\n * Entities are not descended into: their fields travel as `set` ops. `carried` collects the entity keys whose\n * values travel inside an insertion, so the caller does not send them twice.\n */\nfunction structuralDiff(a: unknown, b: unknown, path: string, ops: StructuralOp[], carried: Set<string>): boolean {\n if (sameJson(a, b)) return true;\n const ea = entityAt(a);\n const eb = entityAt(b);\n if (ea || eb) return ea !== null && ea === eb; // same entity: covered by `set`; a different one is structural\n if (Array.isArray(a) && Array.isArray(b)) return listDiff(a, b, path, ops, carried);\n if (a && b && typeof a === \"object\" && typeof b === \"object\" && !Array.isArray(a) && !Array.isArray(b)) {\n const ao = a as Record<string, unknown>;\n const bo = b as Record<string, unknown>;\n const keys = Object.keys(bo);\n if (keys.length !== Object.keys(ao).length || keys.some((k) => !(k in ao))) return false;\n const merge: Record<string, unknown> = {};\n for (const k of keys) {\n const av = ao[k];\n const bv = bo[k];\n if (sameJson(av, bv)) continue;\n if (isLeaf(av) && isLeaf(bv)) {\n merge[k] = bv;\n continue;\n }\n if (!structuralDiff(av, bv, joinPath(path, k), ops, carried)) return false;\n }\n if (Object.keys(merge).length) ops.push({ at: path, value: merge });\n return true;\n }\n return false;\n}\n\n/** Positions removed and elements inserted, verified by replaying them: anything else (a reorder) is refused. */\nfunction listDiff(a: unknown[], b: unknown[], path: string, ops: StructuralOp[], carried: Set<string>): boolean {\n const identified = (xs: unknown[]) => xs.length > 0 && xs.every((x) => entityAt(x) !== null);\n const objects = (xs: unknown[]) => xs.every((x) => x !== null && typeof x === \"object\" && !Array.isArray(x));\n // Rows with an identity of their own are matched by it. Elements without one (plain objects, such as a board's\n // columns) are matched by position, so a change inside one of them is described in place rather than resent.\n if (!(identified(a) && identified(b)) && a.length === b.length && objects(a) && objects(b)) {\n for (let n = 0; n < b.length; n++) if (!structuralDiff(a[n], b[n], joinPath(path, n), ops, carried)) return false;\n return true;\n }\n const oldKeys = a.map(identityOf);\n const newKeys = b.map(identityOf);\n const del: number[] = [];\n const ins: Array<{ at: number; value: unknown }> = [];\n const pairs: Array<[number, number]> = [];\n let i = 0;\n let j = 0;\n while (i < a.length && j < b.length) {\n if (oldKeys[i] === newKeys[j]) {\n pairs.push([i, j]);\n i++;\n j++;\n } else if (!newKeys.includes(oldKeys[i]!, j)) {\n del.push(i);\n i++;\n } else {\n ins.push({ at: j, value: b[j] });\n j++;\n }\n }\n while (i < a.length) del.push(i++);\n while (j < b.length) {\n ins.push({ at: j, value: b[j] });\n j++;\n }\n // The client removes the old positions, then inserts at the new ones. Refuse anything that does not replay exactly.\n const replay = a.filter((_, n) => !del.includes(n));\n for (const x of ins) replay.splice(x.at, 0, x.value);\n if (!sameJson(replay.map(identityOf), newKeys)) return false;\n for (const [x, y] of pairs) if (!structuralDiff(a[x], b[y], joinPath(path, y), ops, carried)) return false;\n if (del.length || ins.length) {\n for (const x of ins) for (const k of normalizeResult(x.value).entities.keys()) carried.add(k);\n const op: { list: string; del?: number[]; ins?: Array<{ at: number; value: unknown }> } = { list: path };\n if (del.length) op.del = del;\n if (ins.length) op.ins = ins;\n ops.push(op);\n }\n return true;\n}\n\n/**\n * Compare two results. Returns a `patch` frame body when the difference can be described (changed entity fields,\n * changed fields of a plain object, rows added to or removed from a list), a full `data` replacement when it\n * cannot, or null when nothing changed. A patch that would cost more than the result itself is not worth sending.\n */\nexport function diffResults(prev: unknown, next: unknown): { patch: PatchOp[] } | { data: unknown } | null {\n const a = normalizeResult(prev);\n const b = normalizeResult(next);\n const structural: StructuralOp[] = [];\n const carried = new Set<string>();\n if (JSON.stringify(a.skeleton) !== JSON.stringify(b.skeleton) && !structuralDiff(prev, next, \"\", structural, carried)) {\n return { data: next };\n }\n const patch: PatchOp[] = [];\n for (const [key, fields] of b.entities) {\n if (carried.has(key)) continue; // its fields travel inside an insertion\n const before = a.entities.get(key);\n const changed: Record<string, unknown> = {};\n for (const [k, v] of Object.entries(fields)) {\n if (!before || JSON.stringify(before[k]) !== JSON.stringify(v)) changed[k] = v;\n }\n if (Object.keys(changed).length) patch.push({ set: key, value: changed });\n }\n // Describing the structure costs more than resending it only when nearly every row changed; then send the result.\n if (structural.length && JSON.stringify(structural).length >= JSON.stringify(next).length) return { data: next };\n patch.push(...(structural as PatchOp[]));\n return patch.length ? { patch } : null;\n}\n\n/** Fold `at` frames into a data value so deferred parts take part in diffs. */\nexport function foldFrames(frames: Frame[]): unknown {\n let data: unknown;\n for (const f of frames) {\n if (\"at\" in f) {\n if (f.at === \"\") Object.assign(data as Record<string, unknown>, f.data as Record<string, unknown>);\n else {\n const target = getPath(data, f.at.split(\".\"));\n if (target && typeof target === \"object\") Object.assign(target as Record<string, unknown>, f.data as Record<string, unknown>);\n }\n } else if (\"data\" in f) data = f.data;\n }\n return data;\n}\n\nfunction getPath(v: unknown, path: string[]): unknown {\n let cur = v;\n for (const p of path) {\n if (cur === null || cur === undefined || typeof cur !== \"object\") return undefined;\n cur = Array.isArray(cur) ? cur[Number(p)] : (cur as Record<string, unknown>)[p];\n }\n return cur;\n}\n"]}
package/mcp.d.ts ADDED
@@ -0,0 +1,55 @@
1
+ /**
2
+ * MCP bridge (spec 10): any Rayfold server is an MCP server.
3
+ * Streamable HTTP, stateless (revision 2026-07-28): POST JSON-RPC to /mcp, JSON response.
4
+ * commands -> tools (plus a `simulate` variant), queries -> tools and resources, schema docs -> descriptions.
5
+ */
6
+ import type { IncomingMessage, ServerResponse } from "node:http";
7
+ import { annotation, type RayfoldSchemaIR, type TypeRef } from "@rayfold/schema";
8
+ import type { RayfoldServer } from "./server.js";
9
+ import { type OriginOptions } from "./guard.js";
10
+ export declare const MCP_PROTOCOL_VERSION = "2026-07-28";
11
+ interface JsonRpcRequest {
12
+ jsonrpc: "2.0";
13
+ id?: number | string | null;
14
+ method: string;
15
+ params?: Record<string, unknown>;
16
+ }
17
+ export interface McpTool {
18
+ name: string;
19
+ title?: string;
20
+ description?: string;
21
+ inputSchema: Record<string, unknown>;
22
+ outputSchema?: Record<string, unknown>;
23
+ annotations?: {
24
+ readOnlyHint?: boolean;
25
+ destructiveHint?: boolean;
26
+ idempotentHint?: boolean;
27
+ };
28
+ }
29
+ export interface McpResource {
30
+ uri: string;
31
+ name: string;
32
+ description?: string;
33
+ mimeType: string;
34
+ }
35
+ /** JSON Schema 2020-12 for a Rayfold type reference. */
36
+ export declare function jsonSchemaFor(ir: RayfoldSchemaIR, t: TypeRef, defs: Record<string, unknown>, forInput: boolean): Record<string, unknown>;
37
+ /**
38
+ * @range becomes JSON Schema keywords the validator understands (minimum/maximum on numbers, minLength/maxLength
39
+ * on strings) plus `x-rayfold-range`, which also covers Decimal (a string on the wire).
40
+ */
41
+ export declare function withRange(s: Record<string, unknown>, annotations: {
42
+ name: string;
43
+ args: Record<string, unknown>;
44
+ }[], typeName: string): Record<string, unknown>;
45
+ export declare function mcpTools(server: RayfoldServer): McpTool[];
46
+ export declare function mcpResources(server: RayfoldServer): McpResource[];
47
+ /** Handle one JSON-RPC request (stateless). */
48
+ export declare function handleMcp(server: RayfoldServer, req: JsonRpcRequest, viewer: unknown): Promise<Record<string, unknown> | null>;
49
+ export interface McpHttpOptions extends OriginOptions {
50
+ path?: string;
51
+ viewer?: (req: IncomingMessage) => unknown | Promise<unknown>;
52
+ }
53
+ /** Streamable HTTP endpoint: POST JSON-RPC, JSON reply. */
54
+ export declare function createMcpHandler(server: RayfoldServer, opts?: McpHttpOptions): (req: IncomingMessage, res: ServerResponse) => Promise<boolean>;
55
+ export { annotation as _annotation };
package/mcp.js ADDED
@@ -0,0 +1,314 @@
1
+ import { annotation, baseName } from "@rayfold/schema";
2
+ import { hostProblem, mediaType, originProblem, refuse } from "./guard.js";
3
+ export const MCP_PROTOCOL_VERSION = "2026-07-28";
4
+ /** JSON Schema 2020-12 for a Rayfold type reference. */
5
+ export function jsonSchemaFor(ir, t, defs, forInput) {
6
+ const nullable = (s) => (t.nullable ? { anyOf: [s, { type: "null" }] } : s);
7
+ if (t.kind === "list")
8
+ return nullable({ type: "array", items: jsonSchemaFor(ir, t.of, defs, forInput) });
9
+ const def = ir.types[t.name];
10
+ if (!def)
11
+ return {};
12
+ switch (def.kind) {
13
+ case "scalar": {
14
+ const map = {
15
+ ID: { type: "string" },
16
+ String: { type: "string" },
17
+ Int: { type: "integer" },
18
+ Long: { type: ["integer", "string"] },
19
+ Float: { type: "number" },
20
+ Boolean: { type: "boolean" },
21
+ Decimal: { type: "string", pattern: "^-?\\d+(\\.\\d+)?$" },
22
+ Instant: { type: "string", format: "date-time" },
23
+ Date: { type: "string", format: "date" },
24
+ Duration: { type: ["string", "integer"] },
25
+ Bytes: { type: "string", contentEncoding: "base64url" },
26
+ JSON: {},
27
+ };
28
+ return nullable(map[t.name] ?? { type: ["string", "number"] });
29
+ }
30
+ case "enum":
31
+ return nullable({ type: "string", enum: def.values.map((v) => v.name) });
32
+ case "union":
33
+ return nullable({ anyOf: def.members.map((m) => jsonSchemaFor(ir, { kind: "named", name: m, nullable: false }, defs, forInput)) });
34
+ default: {
35
+ const key = t.name === "Page" && t.args?.[0] ? `Page_${baseName(t.args[0])}` : t.name;
36
+ if (!(key in defs)) {
37
+ defs[key] = {}; // placeholder for recursion
38
+ const fields = def.kind === "object" && def.typeParams?.length && t.args ? substituteFields(ir, def.fields, def.typeParams, t.args) : def.fields;
39
+ const properties = {};
40
+ const required = [];
41
+ if (def.kind === "entity")
42
+ properties["$type"] = { const: def.name };
43
+ for (const f of fields) {
44
+ if (forInput && f.args.length)
45
+ continue;
46
+ const s = withRange(jsonSchemaFor(ir, f.type, defs, forInput), f.annotations, baseName(f.type));
47
+ properties[f.name] = f.description ? { ...s, description: f.description } : s;
48
+ if (!f.type.nullable && f.default === undefined)
49
+ required.push(f.name);
50
+ }
51
+ const schema = { type: "object", properties, additionalProperties: false };
52
+ if (required.length)
53
+ schema["required"] = required;
54
+ if (def.description)
55
+ schema["description"] = def.description;
56
+ defs[key] = schema;
57
+ }
58
+ return nullable({ $ref: `#/$defs/${key}` });
59
+ }
60
+ }
61
+ }
62
+ function substituteFields(ir, fields, params, args) {
63
+ void ir;
64
+ const bind = new Map(params.map((p, i) => [p, args[i]]));
65
+ const sub = (t) => {
66
+ if (t.kind === "list")
67
+ return { kind: "list", of: sub(t.of), nullable: t.nullable };
68
+ const b = bind.get(t.name);
69
+ return b ? { ...b, nullable: t.nullable || b.nullable } : t;
70
+ };
71
+ return fields.map((f) => ({ ...f, type: sub(f.type) }));
72
+ }
73
+ /**
74
+ * @range becomes JSON Schema keywords the validator understands (minimum/maximum on numbers, minLength/maxLength
75
+ * on strings) plus `x-rayfold-range`, which also covers Decimal (a string on the wire).
76
+ */
77
+ export function withRange(s, annotations, typeName) {
78
+ const r = annotations.find((a) => a.name === "range");
79
+ if (!r)
80
+ return s;
81
+ const min = typeof r.args["min"] === "number" ? r.args["min"] : undefined;
82
+ const max = typeof r.args["max"] === "number" ? r.args["max"] : undefined;
83
+ const out = { ...s, "x-rayfold-range": { ...(min !== undefined ? { min } : {}), ...(max !== undefined ? { max } : {}) } };
84
+ const numeric = ["Int", "Long", "Float"].includes(typeName);
85
+ const text = typeName === "String";
86
+ if (numeric || text) {
87
+ if (min !== undefined)
88
+ out[numeric ? "minimum" : "minLength"] = min;
89
+ if (max !== undefined)
90
+ out[numeric ? "maximum" : "maxLength"] = max;
91
+ }
92
+ return out;
93
+ }
94
+ function argsSchema(ir, args) {
95
+ const defs = {};
96
+ const properties = {};
97
+ const required = [];
98
+ for (const a of args) {
99
+ const s = withRange(jsonSchemaFor(ir, a.type, defs, true), a.annotations, baseName(a.type));
100
+ properties[a.name] = a.description ? { ...s, description: a.description } : s;
101
+ if (!a.type.nullable && a.default === undefined)
102
+ required.push(a.name);
103
+ }
104
+ const schema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", properties, additionalProperties: false };
105
+ if (required.length)
106
+ schema["required"] = required;
107
+ if (Object.keys(defs).length)
108
+ schema["$defs"] = defs;
109
+ return schema;
110
+ }
111
+ function resultSchema(ir, t) {
112
+ const defs = {};
113
+ const inner = jsonSchemaFor(ir, t, defs, false);
114
+ const schema = { $schema: "https://json-schema.org/draft/2020-12/schema", type: "object", properties: { result: inner }, required: ["result"] };
115
+ if (Object.keys(defs).length)
116
+ schema["$defs"] = defs;
117
+ return schema;
118
+ }
119
+ export function mcpTools(server) {
120
+ const ir = server.ir;
121
+ const tools = [];
122
+ for (const op of Object.values(ir.ops)) {
123
+ if (op.kind === "stream")
124
+ continue;
125
+ const base = { name: op.name, inputSchema: argsSchema(ir, op.args), outputSchema: resultSchema(ir, op.returns) };
126
+ const desc = [op.description, op.throws.length ? `May fail with: ${op.throws.join(", ")}.` : "", op.kind === "query" ? "Read-only." : "Changes state; idempotent per call key."].filter(Boolean).join(" ");
127
+ if (desc)
128
+ base.description = desc;
129
+ base.annotations = op.kind === "query" ? { readOnlyHint: true, idempotentHint: true } : { readOnlyHint: false, destructiveHint: true, idempotentHint: true };
130
+ tools.push(base);
131
+ // A dry-run tool is offered only where the command declares @simulate: the runtime cannot stop a resolver that ignores ctx.simulate.
132
+ if (op.kind === "command" && op.annotations.some((a) => a.name === "simulate")) {
133
+ tools.push({
134
+ ...base,
135
+ name: `${op.name}.simulate`,
136
+ description: `Dry run of ${op.name}: returns the would-be result and effects without committing.`,
137
+ annotations: { readOnlyHint: true, idempotentHint: true },
138
+ });
139
+ }
140
+ }
141
+ return tools;
142
+ }
143
+ export function mcpResources(server) {
144
+ const out = [{ uri: "rayfold://schema", name: "Rayfold schema (IR)", description: "The full schema as JSON IR", mimeType: "application/json" }];
145
+ for (const op of Object.values(server.ir.ops)) {
146
+ if (op.kind !== "query" || op.args.some((a) => !a.type.nullable && a.default === undefined))
147
+ continue;
148
+ const r = { uri: `rayfold://query/${op.name}`, name: op.name, mimeType: "application/json" };
149
+ if (op.description)
150
+ r.description = op.description;
151
+ out.push(r);
152
+ }
153
+ return out;
154
+ }
155
+ async function callTool(server, name, args, viewer) {
156
+ const simulate = name.endsWith(".simulate");
157
+ const opName = simulate ? name.slice(0, -".simulate".length) : name;
158
+ const op = server.ir.ops[opName];
159
+ if (!op || op.kind === "stream")
160
+ return { isError: true, content: [{ type: "text", text: `Unknown tool ${name}` }] };
161
+ const req = { id: 1, op: opName, args };
162
+ if (op.kind === "command") {
163
+ const optedOut = op.annotations.some((a) => a.name === "idempotent" && a.args["value"] === false);
164
+ if (!optedOut)
165
+ req.key = `mcp-${hashKey(JSON.stringify(args))}`;
166
+ if (simulate)
167
+ req.simulate = true;
168
+ }
169
+ const frames = await server.collect({ ops: [req], meta: { client: "mcp" } }, { viewer });
170
+ const data = foldForMcp(frames);
171
+ if ("error" in data) {
172
+ const e = data.error;
173
+ return { isError: true, content: [{ type: "text", text: `${e.code}${e.type ? ` ${e.type}` : ""}: ${e.message}` }], structuredContent: { error: e } };
174
+ }
175
+ const structured = { result: data.result };
176
+ if (data.patch)
177
+ structured["effects"] = data.patch;
178
+ return { content: [{ type: "text", text: JSON.stringify(structured.result, null, 2) }], structuredContent: structured, resultType: "complete" };
179
+ }
180
+ function foldForMcp(frames) {
181
+ let result;
182
+ let patch;
183
+ for (const f of frames) {
184
+ if ("error" in f)
185
+ return { error: f.error };
186
+ if ("ok" in f) {
187
+ result = f.ok;
188
+ patch = f.patch;
189
+ }
190
+ else if ("data" in f && !("at" in f))
191
+ result = f.data;
192
+ else if ("at" in f && result && typeof result === "object") {
193
+ const target = f.at === "" ? result : getPath(result, f.at.split("."));
194
+ if (target && typeof target === "object")
195
+ Object.assign(target, f.data);
196
+ }
197
+ }
198
+ return patch !== undefined ? { result, patch } : { result };
199
+ }
200
+ function getPath(v, path) {
201
+ let cur = v;
202
+ for (const p of path) {
203
+ if (cur === null || cur === undefined || typeof cur !== "object")
204
+ return undefined;
205
+ cur = Array.isArray(cur) ? cur[Number(p)] : cur[p];
206
+ }
207
+ return cur;
208
+ }
209
+ function hashKey(s) {
210
+ let h = 2166136261;
211
+ for (let i = 0; i < s.length; i++)
212
+ h = Math.imul(h ^ s.charCodeAt(i), 16777619);
213
+ return (h >>> 0).toString(16).padStart(8, "0") + s.length.toString(16).padStart(8, "0");
214
+ }
215
+ /** Handle one JSON-RPC request (stateless). */
216
+ export async function handleMcp(server, req, viewer) {
217
+ const reply = (result) => ({ jsonrpc: "2.0", id: req.id ?? null, result });
218
+ const fail = (code, message) => ({ jsonrpc: "2.0", id: req.id ?? null, error: { code, message } });
219
+ const p = req.params ?? {};
220
+ switch (req.method) {
221
+ case "initialize":
222
+ return reply({ protocolVersion: MCP_PROTOCOL_VERSION, capabilities: { tools: { listChanged: false }, resources: { subscribe: false, listChanged: false } }, serverInfo: { name: "rayfold", version: "0.1", schemaHash: server.hash } });
223
+ case "server/discover":
224
+ return reply({ protocolVersion: MCP_PROTOCOL_VERSION, capabilities: { tools: {}, resources: {} }, serverInfo: { name: "rayfold", version: "0.1", schemaHash: server.hash } });
225
+ case "ping":
226
+ return reply({});
227
+ case "notifications/initialized":
228
+ return null;
229
+ case "tools/list":
230
+ return reply({ tools: mcpTools(server), ttlMs: 300_000, cacheScope: "public" });
231
+ case "tools/call": {
232
+ const name = p["name"];
233
+ if (typeof name !== "string")
234
+ return fail(-32602, "name is required");
235
+ return reply(await callTool(server, name, p["arguments"] ?? {}, viewer));
236
+ }
237
+ case "resources/list":
238
+ return reply({ resources: mcpResources(server), ttlMs: 300_000, cacheScope: "public" });
239
+ case "resources/read": {
240
+ const uri = p["uri"];
241
+ if (typeof uri !== "string")
242
+ return fail(-32602, "uri is required");
243
+ if (uri === "rayfold://schema")
244
+ return reply({ contents: [{ uri, mimeType: "application/json", text: JSON.stringify(server.ir) }] });
245
+ const m = /^rayfold:\/\/query\/([A-Za-z_][A-Za-z0-9_]*)(\?(.*))?$/.exec(uri);
246
+ if (!m)
247
+ return fail(-32602, `Unknown resource ${uri}`);
248
+ const args = {};
249
+ for (const [k, v] of new URLSearchParams(m[3] ?? ""))
250
+ args[k] = v;
251
+ const r = await callTool(server, m[1], args, viewer);
252
+ if (r["isError"])
253
+ return fail(-32000, r["content"][0]?.text ?? "error");
254
+ return reply({ contents: [{ uri, mimeType: "application/json", text: JSON.stringify(r["structuredContent"].result) }] });
255
+ }
256
+ case "prompts/list":
257
+ return reply({ prompts: [] });
258
+ default:
259
+ return fail(-32601, `Method not found: ${req.method}`);
260
+ }
261
+ }
262
+ /** Streamable HTTP endpoint: POST JSON-RPC, JSON reply. */
263
+ export function createMcpHandler(server, opts = {}) {
264
+ const path = opts.path ?? "/mcp";
265
+ return async (req, res) => {
266
+ const url = new URL(req.url ?? "/", "http://localhost");
267
+ if (url.pathname !== path)
268
+ return false;
269
+ if (req.method !== "POST") {
270
+ res.writeHead(405, { Allow: "POST", "X-Content-Type-Options": "nosniff" }).end();
271
+ return true;
272
+ }
273
+ // The MCP transport requires Origin validation: without it any web page could drive a local or intranet server.
274
+ const refused = hostProblem(req, opts) ?? originProblem(req, opts);
275
+ if (refused) {
276
+ refuse(res, 403, "permission_denied", refused);
277
+ return true;
278
+ }
279
+ if (mediaType(req) !== "application/json") {
280
+ refuse(res, 415, "invalid_argument", `Content-Type ${mediaType(req) || "(none)"} is not accepted; send application/json`, "unsupported_media_type");
281
+ return true;
282
+ }
283
+ res.setHeader("X-Content-Type-Options", "nosniff");
284
+ const chunks = [];
285
+ for await (const c of req)
286
+ chunks.push(c);
287
+ let body;
288
+ try {
289
+ body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
290
+ }
291
+ catch {
292
+ res.writeHead(400, { "Content-Type": "application/json" }).end(JSON.stringify({ jsonrpc: "2.0", id: null, error: { code: -32700, message: "Parse error" } }));
293
+ return true;
294
+ }
295
+ const viewer = opts.viewer ? await opts.viewer(req) : null;
296
+ const headerMethod = req.headers["mcp-method"];
297
+ const first = Array.isArray(body) ? body[0] : body;
298
+ if (typeof headerMethod === "string" && first && headerMethod !== first.method) {
299
+ res.writeHead(400, { "Content-Type": "application/json" }).end(JSON.stringify({ jsonrpc: "2.0", id: first.id ?? null, error: { code: -32020, message: "HeaderMismatch" } }));
300
+ return true;
301
+ }
302
+ const results = await Promise.all((Array.isArray(body) ? body : [body]).map((r) => handleMcp(server, r, viewer)));
303
+ const out = Array.isArray(body) ? results.filter(Boolean) : results[0];
304
+ res.setHeader("MCP-Protocol-Version", MCP_PROTOCOL_VERSION);
305
+ if (out === null || out === undefined) {
306
+ res.writeHead(202).end();
307
+ return true;
308
+ }
309
+ res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(out));
310
+ return true;
311
+ };
312
+ }
313
+ export { annotation as _annotation };
314
+ //# sourceMappingURL=mcp.js.map