@tensor-cad/mcp 0.1.2 → 0.1.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/serve.js CHANGED
@@ -310,7 +310,12 @@ Content-Length: 0\r
310
310
  setTimeout(() => socket.destroy(), 50).unref();
311
311
  return;
312
312
  }
313
- this.wss.handleUpgrade(req, socket, head, (ws) => this.attach(ws));
313
+ this.wss.handleUpgrade(req, socket, head, (ws) => {
314
+ this.attach(ws).catch((e) => {
315
+ this.log(`tensorcad bridge: could not greet a client: ${e.message}`);
316
+ this.send(ws, { type: "error", message: e.message });
317
+ });
318
+ });
314
319
  });
315
320
  }
316
321
  get port() {
@@ -384,14 +389,14 @@ Content-Length: 0\r
384
389
  }
385
390
  return;
386
391
  }
387
- attach(ws) {
392
+ async attach(ws) {
388
393
  this.send(ws, {
389
394
  type: "hello",
390
395
  protocol: BRIDGE_PROTOCOL,
391
396
  server: this.options.name,
392
397
  version: this.options.version,
393
398
  root: this.options.root,
394
- designs: this.options.store.list()
399
+ designs: await this.options.store.list()
395
400
  });
396
401
  ws.on("message", (raw) => {
397
402
  let message;
@@ -401,23 +406,21 @@ Content-Length: 0\r
401
406
  this.send(ws, { type: "error", message: `not JSON: ${e.message}` });
402
407
  return;
403
408
  }
404
- try {
405
- this.handle(ws, message);
406
- } catch (e) {
409
+ this.handle(ws, message).catch((e) => {
407
410
  this.send(ws, { type: "error", message: e.message, about: message?.type });
408
- }
411
+ });
409
412
  });
410
413
  }
411
- handle(ws, message) {
414
+ async handle(ws, message) {
412
415
  switch (message?.type) {
413
416
  case "publish": {
414
417
  const doc = asDocument(message.doc);
415
- const record = this.during(ws, () => this.options.store.adopt(doc));
418
+ const record = await this.during(ws, () => this.options.store.adopt(doc));
416
419
  this.send(ws, designMessage(record, "published"));
417
420
  return;
418
421
  }
419
422
  case "attach": {
420
- const record = this.options.store.get(message.design_id);
423
+ const record = await this.options.store.get(message.design_id);
421
424
  this.send(ws, designMessage(record, "requested"));
422
425
  return;
423
426
  }
@@ -425,12 +428,12 @@ Content-Length: 0\r
425
428
  case "replace": {
426
429
  const write = message.type === "ops" ? () => this.options.store.apply(message.design_id, parseOps(message.ops), message.revision) : () => this.options.store.replace(message.design_id, asDocument(message.doc), message.revision);
427
430
  try {
428
- const { record } = this.during(ws, write);
431
+ const { record } = await this.during(ws, write);
429
432
  this.send(ws, designMessage(record, message.type === "ops" ? "applied" : "replaced"));
430
433
  } catch (e) {
431
434
  if (e instanceof RevisionConflictError) {
432
435
  this.send(ws, { type: "error", message: e.message, about: message.type });
433
- this.send(ws, designMessage(this.options.store.get(message.design_id), "requested"));
436
+ this.send(ws, designMessage(await this.options.store.get(message.design_id), "requested"));
434
437
  return;
435
438
  }
436
439
  throw e;
@@ -460,13 +463,11 @@ Content-Length: 0\r
460
463
  }
461
464
  }
462
465
  }
463
- during(ws, work) {
466
+ async during(ws, work) {
464
467
  this.acting = ws;
465
- try {
466
- return work();
467
- } finally {
468
- this.acting = undefined;
469
- }
468
+ const running = work();
469
+ this.acting = undefined;
470
+ return await running;
470
471
  }
471
472
  send(ws, message) {
472
473
  if (ws.readyState !== ws.OPEN)
@@ -549,7 +550,7 @@ import { McpServer as McpServer2 } from "@modelcontextprotocol/server";
549
550
  // packages/mcp/package.json
550
551
  var package_default = {
551
552
  name: "@tensor-cad/mcp",
552
- version: "0.1.2",
553
+ version: "0.1.4",
553
554
  description: "Model Context Protocol server for TensorCAD: design, validate, analyze and generate LLM architectures from an agent",
554
555
  mcpName: "io.github.filip-pajalic/tensorcad",
555
556
  type: "module",
@@ -824,7 +825,7 @@ class FileStore {
824
825
  }
825
826
  }
826
827
  }
827
- list() {
828
+ async list() {
828
829
  return [...this.entries.values()].map((e) => summaryOf(e.record)).sort((a, b) => b.updated_at.localeCompare(a.updated_at));
829
830
  }
830
831
  async listFiles() {
@@ -850,13 +851,13 @@ class FileStore {
850
851
  await walk(this.root, this.depth);
851
852
  return out.sort();
852
853
  }
853
- get(id) {
854
+ async get(id) {
854
855
  const entry = this.entries.get(id);
855
856
  if (!entry)
856
857
  throw new UnknownDesignError(id, [...this.entries.keys()]);
857
858
  return entry.record;
858
859
  }
859
- create(options) {
860
+ async create(options) {
860
861
  let doc;
861
862
  let source;
862
863
  if (options.preset) {
@@ -873,7 +874,7 @@ class FileStore {
873
874
  doc.meta.name = options.name;
874
875
  return this.register(doc, source, undefined, true);
875
876
  }
876
- adopt(doc) {
877
+ async adopt(doc) {
877
878
  return this.register(doc, "derived", undefined, true);
878
879
  }
879
880
  async open(path) {
@@ -915,7 +916,7 @@ class FileStore {
915
916
  this.emit({ kind: "registered", record });
916
917
  return record;
917
918
  }
918
- apply(id, ops, expectedRevision) {
919
+ async apply(id, ops, expectedRevision) {
919
920
  const entry = this.entry(id);
920
921
  const { record } = entry;
921
922
  if (expectedRevision !== undefined && expectedRevision !== record.revision) {
@@ -933,7 +934,7 @@ class FileStore {
933
934
  this.emit({ kind: "applied", record, ops });
934
935
  return { record, applied, previousRevision };
935
936
  }
936
- replace(id, doc, expectedRevision) {
937
+ async replace(id, doc, expectedRevision) {
937
938
  const entry = this.entry(id);
938
939
  const { record } = entry;
939
940
  if (expectedRevision !== undefined && expectedRevision !== record.revision) {
@@ -950,7 +951,7 @@ class FileStore {
950
951
  return { record, applied: ["replaced the document"], previousRevision };
951
952
  }
952
953
  async save(id, path) {
953
- const record = this.get(id);
954
+ const record = await this.get(id);
954
955
  const target = path ? this.resolvePath(path) : record.path ?? join2(this.root, `${slug(record.name)}.tensorcad.json`);
955
956
  const text = `${JSON.stringify(record.doc, null, 2)}
956
957
  `;
@@ -962,7 +963,7 @@ class FileStore {
962
963
  this.emit({ kind: "saved", record });
963
964
  return { record, path: target, bytes: Buffer.byteLength(text, "utf8") };
964
965
  }
965
- checkpoint(id, label) {
966
+ async checkpoint(id, label) {
966
967
  const entry = this.entry(id);
967
968
  const info = {
968
969
  checkpoint_id: `ckpt_${this.nextCheckpoint++}`,
@@ -973,11 +974,11 @@ class FileStore {
973
974
  entry.checkpoints.set(info.checkpoint_id, { ...info, doc: structuredClone(entry.record.doc) });
974
975
  return info;
975
976
  }
976
- checkpoints(id) {
977
+ async checkpoints(id) {
977
978
  const entry = this.entry(id);
978
979
  return [...entry.checkpoints.values()].map(({ doc: _doc, ...info }) => info);
979
980
  }
980
- restore(id, checkpointId) {
981
+ async restore(id, checkpointId) {
981
982
  const entry = this.entry(id);
982
983
  const { record } = entry;
983
984
  let doc;
@@ -1034,6 +1035,25 @@ function slug(name) {
1034
1035
  return name.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "design";
1035
1036
  }
1036
1037
 
1038
+ // packages/mcp/src/store/disk-artifacts.ts
1039
+ import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
1040
+ import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "node:path";
1041
+
1042
+ class DiskSink {
1043
+ root;
1044
+ label = "disk";
1045
+ constructor(root) {
1046
+ this.root = root;
1047
+ }
1048
+ async write(prefix, path, contents) {
1049
+ const base = isAbsolute2(prefix) ? prefix : resolve2(this.root, prefix);
1050
+ const target = join3(base, path);
1051
+ await mkdir3(dirname3(target), { recursive: true });
1052
+ await writeFile3(target, contents, "utf8");
1053
+ return { location: target };
1054
+ }
1055
+ }
1056
+
1037
1057
  // packages/mcp/src/prompts.ts
1038
1058
  import * as z2 from "zod";
1039
1059
  import { HARDWARE, PRESET_NAMES as PRESET_NAMES2 } from "@tensor-cad/engine/node";
@@ -1500,6 +1520,7 @@ function catalogEntry(def) {
1500
1520
  type: def.type,
1501
1521
  kind: def.kind,
1502
1522
  category: def.category,
1523
+ name: def.docs.name ?? def.type,
1503
1524
  summary: def.docs.summary ?? "",
1504
1525
  refs: def.docs.refs ?? [],
1505
1526
  params: def.paramOrder.map((name) => catalogParam(name, def.params[name])),
@@ -1538,7 +1559,10 @@ function allCatalogEntries() {
1538
1559
  }
1539
1560
  function catalogText(entries) {
1540
1561
  return entries.map((e) => {
1541
- const lines = [`${e.type} (${e.kind}/${e.category})`, ` ${e.summary}`];
1562
+ const lines = [
1563
+ e.name && e.name !== e.type ? `${e.type} — ${e.name} (${e.kind}/${e.category})` : `${e.type} (${e.kind}/${e.category})`,
1564
+ ` ${e.summary}`
1565
+ ];
1542
1566
  if (e.formula)
1543
1567
  lines.push(` formula: ${e.formula}`);
1544
1568
  if (e.params.length > 0) {
@@ -1696,8 +1720,8 @@ function registerResources(server, store) {
1696
1720
  title: "Design rule report",
1697
1721
  description: "Every finding for a design: shape errors, memory fit, kernel constraints, Chinchilla sanity.",
1698
1722
  mimeType: JSON_MIME
1699
- }, (uri, { id }) => {
1700
- const record = store.get(String(id));
1723
+ }, async (uri, { id }) => {
1724
+ const record = await store.get(String(id));
1701
1725
  const report = validate(record.doc);
1702
1726
  return json(uri, {
1703
1727
  design_id: record.design_id,
@@ -1712,14 +1736,14 @@ function registerResources(server, store) {
1712
1736
  title: "Design analysis",
1713
1737
  description: "Parameters, FLOPs, KV cache, memory, throughput and cost at the document's own defaults.",
1714
1738
  mimeType: JSON_MIME
1715
- }, (uri, { id }) => {
1716
- const record = store.get(String(id));
1739
+ }, async (uri, { id }) => {
1740
+ const record = await store.get(String(id));
1717
1741
  const result = analyze(record.doc);
1718
1742
  return json(uri, { design_id: record.design_id, revision: record.revision, ...analysisJson(result) });
1719
1743
  });
1720
1744
  server.registerResource("design", new ResourceTemplate("tensorcad://designs/{id}", {
1721
- list: () => ({
1722
- resources: store.list().map((d) => ({
1745
+ list: async () => ({
1746
+ resources: (await store.list()).map((d) => ({
1723
1747
  uri: `tensorcad://designs/${d.design_id}`,
1724
1748
  name: d.name,
1725
1749
  title: `${d.name} (revision ${d.revision})`,
@@ -1732,8 +1756,8 @@ function registerResources(server, store) {
1732
1756
  title: "Design document",
1733
1757
  description: "The literal .tensorcad.json document, with a compact outline beside it.",
1734
1758
  mimeType: JSON_MIME
1735
- }, (uri, { id }) => {
1736
- const record = store.get(String(id));
1759
+ }, async (uri, { id }) => {
1760
+ const record = await store.get(String(id));
1737
1761
  return json(uri, {
1738
1762
  design_id: record.design_id,
1739
1763
  revision: record.revision,
@@ -1747,7 +1771,7 @@ function registerResources(server, store) {
1747
1771
  title: "Block catalog",
1748
1772
  description: "Every block type with its parameter schema, port patterns and documentation. " + "Primitives carry the formulas; composites expand into primitives; repeat is the only container.",
1749
1773
  mimeType: JSON_MIME
1750
- }, (uri) => {
1774
+ }, async (uri) => {
1751
1775
  const blocks = allCatalogEntries();
1752
1776
  return json(uri, {
1753
1777
  count: blocks.length,
@@ -1756,7 +1780,7 @@ function registerResources(server, store) {
1756
1780
  });
1757
1781
  });
1758
1782
  server.registerResource("catalog-block", new ResourceTemplate("tensorcad://catalog/{type}", {
1759
- list: () => ({
1783
+ list: async () => ({
1760
1784
  resources: Object.keys(CATALOG).sort().map((type) => ({
1761
1785
  uri: `tensorcad://catalog/${type}`,
1762
1786
  name: type,
@@ -1791,12 +1815,10 @@ function registerResources(server, store) {
1791
1815
  }));
1792
1816
  }
1793
1817
  function completeDesignId(store) {
1794
- return (value) => store.list().map((d) => d.design_id).filter((id) => id.startsWith(value));
1818
+ return async (value) => (await store.list()).map((d) => d.design_id).filter((id) => id.startsWith(value));
1795
1819
  }
1796
1820
 
1797
1821
  // packages/mcp/src/tools.ts
1798
- import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
1799
- import { dirname as dirname3, isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "node:path";
1800
1822
  import * as z3 from "zod";
1801
1823
  import { formatBytes as formatBytes2, formatCount as formatCount2 } from "@tensor-cad/engine";
1802
1824
  import {
@@ -1873,7 +1895,7 @@ function toAnalysisOptions(input) {
1873
1895
  var READ = { readOnlyHint: true, idempotentHint: true, openWorldHint: false };
1874
1896
  var WRITE = { readOnlyHint: false, idempotentHint: false, openWorldHint: false };
1875
1897
  var DESTRUCTIVE = { readOnlyHint: false, idempotentHint: false, destructiveHint: true, openWorldHint: false };
1876
- function registerTools(server, store) {
1898
+ function registerTools(server, store, artifacts) {
1877
1899
  server.registerTool("tensorcad_list_designs", {
1878
1900
  title: "List designs",
1879
1901
  description: "List the designs this server has open, the built-in reference architectures you can start from, " + "and the .tensorcad.json files it can see on disk. Start here when you do not already hold a design_id.",
@@ -1892,7 +1914,7 @@ function registerTools(server, store) {
1892
1914
  }),
1893
1915
  annotations: { ...READ, title: "List designs" }
1894
1916
  }, async ({ include_files }) => guard(async () => {
1895
- const designs = store.list();
1917
+ const designs = await store.list();
1896
1918
  const presets = PRESET_NAMES3.map((name) => {
1897
1919
  const doc = getPreset2(name);
1898
1920
  const p = { name };
@@ -1933,8 +1955,8 @@ ${designs.map((d) => ` ${d.design_id} ${d.name} rev ${d.revision}${d.dirty ?
1933
1955
  outline: Outline
1934
1956
  }),
1935
1957
  annotations: { ...WRITE, title: "New design" }
1936
- }, async (args) => guard(() => {
1937
- const record = store.create(args);
1958
+ }, async (args) => guard(async () => {
1959
+ const record = await store.create(args);
1938
1960
  const outline = outlineOf(record.doc);
1939
1961
  return ok(`${record.design_id} (revision ${record.revision})
1940
1962
 
@@ -2019,8 +2041,8 @@ ${formatCount2(report.analysis.params.total)} parameters, ` + `${report.counts.e
2019
2041
  document: z3.record(z3.string(), z3.unknown()).optional().describe("The literal design document.")
2020
2042
  }),
2021
2043
  annotations: { ...READ, title: "Get design" }
2022
- }, async ({ design_id, format }) => guard(() => {
2023
- const record = store.get(design_id);
2044
+ }, async ({ design_id, format }) => guard(async () => {
2045
+ const record = await store.get(design_id);
2024
2046
  const outline = outlineOf(record.doc);
2025
2047
  const mode = format ?? "outline";
2026
2048
  const base = {
@@ -2074,8 +2096,8 @@ ${outlineText(outline)}`, {
2074
2096
  children: z3.array(z3.string())
2075
2097
  }),
2076
2098
  annotations: { ...READ, title: "Get block" }
2077
- }, async ({ design_id, path }) => guard(() => {
2078
- const record = store.get(design_id);
2099
+ }, async ({ design_id, path }) => guard(async () => {
2100
+ const record = await store.get(design_id);
2079
2101
  const detail = blockDetail(record.doc, path);
2080
2102
  return ok(blockText(detail), {
2081
2103
  design_id: record.design_id,
@@ -2099,6 +2121,7 @@ ${outlineText(outline)}`, {
2099
2121
  type: z3.string(),
2100
2122
  kind: z3.string(),
2101
2123
  category: z3.string(),
2124
+ name: z3.string().describe("What a drawing calls this block."),
2102
2125
  summary: z3.string(),
2103
2126
  formula: z3.string().optional(),
2104
2127
  refs: z3.array(z3.string()),
@@ -2115,7 +2138,7 @@ ${outlineText(outline)}`, {
2115
2138
  }))
2116
2139
  }),
2117
2140
  annotations: { ...READ, title: "Search catalog" }
2118
- }, async ({ query, category, kind, limit }) => guard(() => {
2141
+ }, async ({ query, category, kind, limit }) => guard(async () => {
2119
2142
  const all = allCatalogEntries();
2120
2143
  const q = query?.toLowerCase();
2121
2144
  const matched = all.filter((e) => {
@@ -2125,7 +2148,7 @@ ${outlineText(outline)}`, {
2125
2148
  return false;
2126
2149
  if (!q)
2127
2150
  return true;
2128
- const hay = `${e.type} ${e.category} ${e.summary} ${e.formula ?? ""}`.toLowerCase();
2151
+ const hay = `${e.type} ${e.name} ${e.category} ${e.summary} ${e.formula ?? ""}`.toLowerCase();
2129
2152
  return hay.includes(q);
2130
2153
  });
2131
2154
  const blocks = matched.slice(0, limit ?? 20);
@@ -2159,10 +2182,10 @@ ${catalogText(blocks)}`, {
2159
2182
  validation: ValidationSummary
2160
2183
  }),
2161
2184
  annotations: { ...WRITE, title: "Apply edits" }
2162
- }, async ({ design_id, expected_revision, ops }) => guard(() => {
2163
- const before = store.get(design_id);
2185
+ }, async ({ design_id, expected_revision, ops }) => guard(async () => {
2186
+ const before = await store.get(design_id);
2164
2187
  const paramsBefore = outlineOf(before.doc).params_total;
2165
- const outcome = store.apply(design_id, ops, expected_revision);
2188
+ const outcome = await store.apply(design_id, ops, expected_revision);
2166
2189
  const report = validate2(outcome.record.doc);
2167
2190
  const total = report.analysis.params.total;
2168
2191
  const delta = total - paramsBefore;
@@ -2205,8 +2228,8 @@ ${catalogText(blocks)}`, {
2205
2228
  params_total: z3.number()
2206
2229
  }),
2207
2230
  annotations: { ...READ, title: "Validate design" }
2208
- }, async ({ design_id, severity, ...rest }) => guard(() => {
2209
- const record = store.get(design_id);
2231
+ }, async ({ design_id, severity, ...rest }) => guard(async () => {
2232
+ const record = await store.get(design_id);
2210
2233
  const report = validate2(record.doc, toAnalysisOptions(rest));
2211
2234
  const rank = { error: 0, warning: 1, info: 2 };
2212
2235
  const findings = findingsJson(report).filter((f) => severity === undefined || rank[f.severity] <= rank[severity]);
@@ -2233,8 +2256,8 @@ ${catalogText(blocks)}`, {
2233
2256
  inputSchema: z3.object({ design_id: DESIGN_ID, ...analysisOptionsShape }),
2234
2257
  outputSchema: AnalysisOutput,
2235
2258
  annotations: { ...READ, title: "Analyze design" }
2236
- }, async ({ design_id, ...rest }) => guard(() => {
2237
- const record = store.get(design_id);
2259
+ }, async ({ design_id, ...rest }) => guard(async () => {
2260
+ const record = await store.get(design_id);
2238
2261
  const result = analyze2(record.doc, toAnalysisOptions(rest));
2239
2262
  return ok(analysisText(result), {
2240
2263
  design_id: record.design_id,
@@ -2267,30 +2290,36 @@ ${catalogText(blocks)}`, {
2267
2290
  }),
2268
2291
  annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false, title: "Generate code" }
2269
2292
  }, async ({ design_id, class_name, include_smoke_test, out_dir }) => guard(async () => {
2270
- const record = store.get(design_id);
2293
+ const record = await store.get(design_id);
2271
2294
  const generated = generateTorch(record.doc, {
2272
2295
  ...class_name ? { className: class_name } : {},
2273
2296
  includeSmokeTest: include_smoke_test ?? false
2274
2297
  });
2275
- const root = out_dir ? isAbsolute2(out_dir) ? out_dir : resolve2(process.cwd(), out_dir) : undefined;
2276
2298
  const files = [];
2277
2299
  for (const file of generated.files) {
2278
- const bytes = Buffer.byteLength(file.contents, "utf8");
2300
+ const bytes = new TextEncoder().encode(file.contents).length;
2279
2301
  const lines = file.contents.split(`
2280
2302
  `).length;
2281
- if (root) {
2282
- const target = join3(root, file.path);
2283
- await mkdir3(dirname3(target), { recursive: true });
2284
- await writeFile3(target, file.contents, "utf8");
2285
- files.push({ path: file.path, bytes, lines, written_to: target });
2286
- } else {
2303
+ if (out_dir === undefined) {
2287
2304
  files.push({ path: file.path, bytes, lines, contents: file.contents });
2305
+ continue;
2288
2306
  }
2307
+ const written = await artifacts.write(out_dir, file.path, file.contents);
2308
+ files.push({
2309
+ path: file.path,
2310
+ bytes,
2311
+ lines,
2312
+ written_to: written.location,
2313
+ ...written.url ? { url: written.url } : {}
2314
+ });
2289
2315
  }
2290
- const text = root ? [`wrote ${files.length} file(s) to ${root}`, ...files.map((f) => ` ${f.path} ${f.bytes} bytes`)].join(`
2291
- `) : generated.files.map((f) => `# ${f.path}
2316
+ const text = out_dir === undefined ? generated.files.map((f) => `# ${f.path}
2292
2317
  ${f.contents}`).join(`
2293
2318
 
2319
+ `) : [
2320
+ `wrote ${files.length} file(s) to ${artifacts.label}`,
2321
+ ...files.map((f) => ` ${f.path} ${f.bytes} bytes ${f.url ?? f.written_to}`)
2322
+ ].join(`
2294
2323
  `);
2295
2324
  return ok(generated.warnings.length > 0 ? `${text}
2296
2325
 
@@ -2299,8 +2328,8 @@ ${generated.warnings.map((w) => ` ${w}`).join(`
2299
2328
  `)}` : text, {
2300
2329
  design_id: record.design_id,
2301
2330
  revision: record.revision,
2302
- wrote: Boolean(root),
2303
- ...root ? { out_dir: root } : {},
2331
+ wrote: out_dir !== undefined,
2332
+ ...out_dir !== undefined ? { out_dir } : {},
2304
2333
  files,
2305
2334
  warnings: generated.warnings
2306
2335
  });
@@ -2326,12 +2355,12 @@ ${generated.warnings.map((w) => ` ${w}`).join(`
2326
2355
  }))
2327
2356
  }),
2328
2357
  annotations: { ...WRITE, title: "Checkpoint design" }
2329
- }, async ({ design_id, label }) => guard(() => {
2330
- const info = store.checkpoint(design_id, label);
2358
+ }, async ({ design_id, label }) => guard(async () => {
2359
+ const info = await store.checkpoint(design_id, label);
2331
2360
  return ok(`${info.checkpoint_id} at revision ${info.revision}: ${info.label}`, {
2332
2361
  design_id,
2333
2362
  ...info,
2334
- checkpoints: store.checkpoints(design_id)
2363
+ checkpoints: await store.checkpoints(design_id)
2335
2364
  });
2336
2365
  }));
2337
2366
  server.registerTool("tensorcad_restore", {
@@ -2350,8 +2379,8 @@ ${generated.warnings.map((w) => ` ${w}`).join(`
2350
2379
  validation: ValidationSummary
2351
2380
  }),
2352
2381
  annotations: { ...DESTRUCTIVE, title: "Restore design" }
2353
- }, async ({ design_id, checkpoint_id }) => guard(() => {
2354
- const { record, restoredFrom } = store.restore(design_id, checkpoint_id);
2382
+ }, async ({ design_id, checkpoint_id }) => guard(async () => {
2383
+ const { record, restoredFrom } = await store.restore(design_id, checkpoint_id);
2355
2384
  const report = validate2(record.doc);
2356
2385
  return ok(`${record.design_id} restored from ${restoredFrom}; now revision ${record.revision}, ` + `${formatCount2(report.analysis.params.total)} parameters`, {
2357
2386
  design_id: record.design_id,
@@ -2394,8 +2423,8 @@ ${generated.warnings.map((w) => ` ${w}`).join(`
2394
2423
  })
2395
2424
  }),
2396
2425
  annotations: { ...READ, title: "Explain a block" }
2397
- }, async ({ design_id, path, ...rest }) => guard(() => {
2398
- const record = store.get(design_id);
2426
+ }, async ({ design_id, path, ...rest }) => guard(async () => {
2427
+ const record = await store.get(design_id);
2399
2428
  const e = explain(record.doc, path, toAnalysisOptions(rest));
2400
2429
  const lines = [
2401
2430
  `${path} ${e.type} (${e.kind})`,
@@ -2456,8 +2485,8 @@ ${generated.warnings.map((w) => ` ${w}`).join(`
2456
2485
  notes: z3.array(z3.string())
2457
2486
  }),
2458
2487
  annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false, title: "Scale a design" }
2459
- }, async ({ design_id, target_params, target_basis, vocab, tie_head, keep_depth }) => guard(() => {
2460
- const record = store.get(design_id);
2488
+ }, async ({ design_id, target_params, target_basis, vocab, tie_head, keep_depth }) => guard(async () => {
2489
+ const record = await store.get(design_id);
2461
2490
  const result = scaleDesign(record.doc, {
2462
2491
  targetParams: target_params,
2463
2492
  ...target_basis ? { targetBasis: target_basis } : {},
@@ -2465,7 +2494,7 @@ ${generated.warnings.map((w) => ` ${w}`).join(`
2465
2494
  ...tie_head !== undefined ? { tieHead: tie_head } : {},
2466
2495
  ...keep_depth !== undefined ? { keepDepth: keep_depth } : {}
2467
2496
  });
2468
- const saved = store.adopt(result.doc);
2497
+ const saved = await store.adopt(result.doc);
2469
2498
  const changes = Object.entries(result.changes).map(([symbol, c]) => ({
2470
2499
  symbol,
2471
2500
  from: c.from,
@@ -2518,28 +2547,32 @@ ${generated.warnings.map((w) => ` ${w}`).join(`
2518
2547
  notes: z3.array(z3.string())
2519
2548
  }),
2520
2549
  annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false, title: "Build a width ladder" }
2521
- }, async ({ design_id, widths, base_width }) => guard(() => {
2522
- const record = store.get(design_id);
2550
+ }, async ({ design_id, widths, base_width }) => guard(async () => {
2551
+ const record = await store.get(design_id);
2523
2552
  const ladder = mupLadder(record.doc, {
2524
2553
  ...widths ? { widths } : {},
2525
2554
  ...base_width !== undefined ? { baseWidth: base_width } : {}
2526
2555
  });
2527
- const rungs = ladder.rungs.map((rung) => ({
2528
- design_id: store.adopt(rung.doc).design_id,
2529
- width: rung.width,
2530
- multiplier: rung.multiplier,
2531
- heads: rung.heads,
2532
- params: rung.params,
2533
- base: rung.base,
2534
- scaling: rung.scaling.map((s) => ({
2535
- class: s.class,
2536
- init_std: s.initStd,
2537
- adam_lr: s.adamLr,
2538
- paths: s.paths,
2539
- why: s.why
2540
- })),
2541
- notes: rung.notes
2542
- }));
2556
+ const rungs = [];
2557
+ for (const rung of ladder.rungs) {
2558
+ const adopted = await store.adopt(rung.doc);
2559
+ rungs.push({
2560
+ design_id: adopted.design_id,
2561
+ width: rung.width,
2562
+ multiplier: rung.multiplier,
2563
+ heads: rung.heads,
2564
+ params: rung.params,
2565
+ base: rung.base,
2566
+ scaling: rung.scaling.map((s) => ({
2567
+ class: s.class,
2568
+ init_std: s.initStd,
2569
+ adam_lr: s.adamLr,
2570
+ paths: s.paths,
2571
+ why: s.why
2572
+ })),
2573
+ notes: rung.notes
2574
+ });
2575
+ }
2543
2576
  const text = [
2544
2577
  `${record.doc.meta.name} laddered by ${ladder.widthSymbol}, tuned at ${ladder.baseWidth}, ` + `heads of ${ladder.headDim} throughout`,
2545
2578
  ...rungs.map((r) => ` ${r.base ? "base " : " "}${r.width} wide, ${r.heads} heads, ${formatCount2(r.params)}` + ` ${r.scaling.filter((s) => s.class !== "input").map((s) => `${s.class} init x${s.init_std.toPrecision(4)} rate x${s.adam_lr.toPrecision(4)}`).join(", ")}`),
@@ -2588,8 +2621,8 @@ ${generated.warnings.map((w) => ` ${w}`).join(`
2588
2621
  notes: z3.array(z3.string())
2589
2622
  }),
2590
2623
  annotations: { ...READ, title: "Plan a cluster" }
2591
- }, async ({ design_id, gpus, gpus_per_node, headroom, limit, ...rest }) => guard(() => {
2592
- const record = store.get(design_id);
2624
+ }, async ({ design_id, gpus, gpus_per_node, headroom, limit, ...rest }) => guard(async () => {
2625
+ const record = await store.get(design_id);
2593
2626
  const result = planCluster(record.doc, toAnalysisOptions(rest), {
2594
2627
  gpus,
2595
2628
  ...gpus_per_node !== undefined ? { gpusPerNode: gpus_per_node } : {},
@@ -2665,9 +2698,9 @@ ${generated.warnings.map((w) => ` ${w}`).join(`
2665
2698
  }))
2666
2699
  }),
2667
2700
  annotations: { ...READ, title: "Compare two designs" }
2668
- }, async ({ a, b, ...rest }) => guard(() => {
2669
- const left = store.get(a);
2670
- const right = store.get(b);
2701
+ }, async ({ a, b, ...rest }) => guard(async () => {
2702
+ const left = await store.get(a);
2703
+ const right = await store.get(b);
2671
2704
  const d = diffDesigns(left.doc, right.doc, toAnalysisOptions(rest));
2672
2705
  const brief = (v) => {
2673
2706
  if (v === undefined || v === null)
@@ -2733,9 +2766,9 @@ ${generated.warnings.map((w) => ` ${w}`).join(`
2733
2766
  warnings: z3.array(z3.string())
2734
2767
  }),
2735
2768
  annotations: { readOnlyHint: false, idempotentHint: true, openWorldHint: false, title: "Import a config" }
2736
- }, async ({ config, name }) => guard(() => {
2769
+ }, async ({ config, name }) => guard(async () => {
2737
2770
  const result = importHfConfig(config, name);
2738
- const record = store.adopt(result.doc);
2771
+ const record = await store.adopt(result.doc);
2739
2772
  const total = analyze2(result.doc).params.total;
2740
2773
  const text = [
2741
2774
  `${result.doc.meta.name}: ${formatCount2(total)} parameters`,
@@ -2798,13 +2831,14 @@ var INSTRUCTIONS = [
2798
2831
  ].join(`
2799
2832
  `);
2800
2833
  function createServer2(options = {}) {
2801
- const { store: given, bridge, ...storeOptions } = options;
2834
+ const { store: given, bridge, artifacts: givenArtifacts, ...storeOptions } = options;
2802
2835
  const store = given ?? new FileStore(storeOptions);
2836
+ const artifacts = givenArtifacts ?? new DiskSink(storeOptions.root ?? process.cwd());
2803
2837
  const server = new McpServer2({ name: SERVER_NAME, version: SERVER_VERSION }, {
2804
2838
  capabilities: { tools: {}, resources: {}, prompts: {}, completions: {} },
2805
2839
  instructions: INSTRUCTIONS
2806
2840
  });
2807
- registerTools(server, store);
2841
+ registerTools(server, store, artifacts);
2808
2842
  registerResources(server, store);
2809
2843
  registerPrompts(server);
2810
2844
  if (bridge)
package/server.d.ts CHANGED
@@ -9,6 +9,7 @@
9
9
  import { McpServer } from "@modelcontextprotocol/server";
10
10
  import { type FileStoreOptions } from "./store/file-store.js";
11
11
  import type { DocumentStore } from "./store/types.js";
12
+ import type { ArtifactSink } from "./artifacts.js";
12
13
  import type { BridgeServer } from "./bridge/server.js";
13
14
  export declare const SERVER_NAME = "tensorcad";
14
15
  /**
@@ -21,6 +22,14 @@ export declare const SERVER_VERSION: string;
21
22
  export interface ServerOptions extends FileStoreOptions {
22
23
  /** Defaults to a `FileStore` rooted at the working directory. */
23
24
  store?: DocumentStore;
25
+ /**
26
+ * Where `tensorcad_generate_code` puts what it emits.
27
+ *
28
+ * Defaults to writing into a directory. A hosted server has no filesystem
29
+ * and supplies object storage instead, which is the difference between an
30
+ * agent being handed a model it can run and one it can only read.
31
+ */
32
+ artifacts?: ArtifactSink;
24
33
  /**
25
34
  * The live editor bridge, when one is running. Given one, the server tells
26
35
  * its client that a design's resources changed whenever the *human* changed
package/server.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
3
  "name": "io.github.filip-pajalic/tensorcad",
4
4
  "description": "Design transformer LLM architectures and report their parameters, FLOPs, memory and cost",
5
- "version": "0.1.2",
5
+ "version": "0.1.4",
6
6
  "repository": {
7
7
  "url": "https://github.com/Filip-Pajalic/TensorCAD",
8
8
  "source": "github",
@@ -14,7 +14,7 @@
14
14
  "registryType": "npm",
15
15
  "registryBaseUrl": "https://registry.npmjs.org",
16
16
  "identifier": "@tensor-cad/mcp",
17
- "version": "0.1.2",
17
+ "version": "0.1.4",
18
18
  "runtimeHint": "npx",
19
19
  "transport": {
20
20
  "type": "stdio"