@totemsdk/mcp-server 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/indexer.js CHANGED
@@ -72,6 +72,24 @@ function detectRepoRoot() {
72
72
  }
73
73
  return null;
74
74
  }
75
+ /** Read the checked-out Git revision without spawning a process. */
76
+ function readGitRevision(root) {
77
+ try {
78
+ const git = path.join(root, '.git');
79
+ let head = fs.readFileSync(path.join(git, 'HEAD'), 'utf-8').trim();
80
+ if (head.startsWith('ref: ')) {
81
+ const ref = head.slice(5).trim();
82
+ const refPath = path.join(git, ref);
83
+ head = fs.existsSync(refPath)
84
+ ? fs.readFileSync(refPath, 'utf-8').trim()
85
+ : '';
86
+ }
87
+ return /^[0-9a-f]{40,64}$/.test(head) ? head : undefined;
88
+ }
89
+ catch {
90
+ return undefined;
91
+ }
92
+ }
75
93
  function loadRepoData(root) {
76
94
  const topPkgDir = path.join(root, 'packages');
77
95
  const manifest = new Map();
@@ -97,13 +115,29 @@ function loadRepoData(root) {
97
115
  catch {
98
116
  // Optional artifact — fall back to the regex scan.
99
117
  }
100
- return { topPkgDir, manifest, ast };
118
+ const maturity = new Map();
119
+ try {
120
+ const gates = JSON.parse(fs.readFileSync(path.join(root, 'scripts', 'workspace-gates.config.json'), 'utf-8'));
121
+ for (const [dir, meta] of Object.entries(gates.packages ?? {})) {
122
+ if (!meta?.maturity)
123
+ continue;
124
+ try {
125
+ const pj = JSON.parse(fs.readFileSync(path.join(root, dir, 'package.json'), 'utf-8'));
126
+ if (pj.name)
127
+ maturity.set(pj.name, meta.maturity);
128
+ }
129
+ catch { /* unreadable package */ }
130
+ }
131
+ }
132
+ catch { /* optional */ }
133
+ return { topPkgDir, manifest, maturity, ast };
101
134
  }
102
135
  function loadBundledIndex() {
103
136
  try {
104
137
  const idx = JSON.parse(fs.readFileSync(BUNDLED_INDEX_PATH, 'utf-8'));
105
- if (idx && typeof idx === 'object' && idx.packages && typeof idx.packages === 'object')
106
- return idx;
138
+ if (idx && typeof idx === 'object' && idx.packages && typeof idx.packages === 'object') {
139
+ return { ...idx, schemaVersion: idx.schemaVersion ?? 2, source: idx.source ?? 'bundled' };
140
+ }
107
141
  return null;
108
142
  }
109
143
  catch {
@@ -310,10 +344,24 @@ function collectExports(file, symbols, visited) {
310
344
  const braceMatch = block.match(/\{([^}]+)\}/);
311
345
  if (braceMatch) {
312
346
  for (const part of braceMatch[1].split(',')) {
313
- const name = part.trim().split(/\s+as\s+/).pop()?.trim() || '';
314
- if (!name)
347
+ let clause = part.trim();
348
+ if (!clause)
315
349
  continue;
316
- const kind = isType || /^[A-Z]/.test(name) ? 'type' : 'function';
350
+ // Inline type modifiers: `export { type Foo }` / `export { type Foo as Bar }`.
351
+ // Strip the leading `type` keyword before resolving the exposed name,
352
+ // otherwise the symbol is recorded literally as "type Foo".
353
+ let inlineType = false;
354
+ const typeKw = clause.match(/^type\s+([\s\S]+)$/);
355
+ if (typeKw) {
356
+ inlineType = true;
357
+ clause = typeKw[1].trim();
358
+ }
359
+ const name = clause.split(/\s+as\s+/).pop()?.trim() || '';
360
+ // Reject anything that isn't a plain identifier (guards against
361
+ // malformed fragments leaking in as fake symbols).
362
+ if (!/^[A-Za-z_$][\w$]*$/.test(name))
363
+ continue;
364
+ const kind = isType || inlineType || /^[A-Z]/.test(name) ? 'type' : 'function';
317
365
  setSymbol(symbols, name, { kind });
318
366
  }
319
367
  }
@@ -348,15 +396,18 @@ function mergeAst(symbols, ast) {
348
396
  }
349
397
  /** Scan a monorepo root into an `SdkIndex`. */
350
398
  function buildFromMonorepo(root, now = Date.now()) {
351
- const { topPkgDir, manifest: MANIFEST, ast: AST } = loadRepoData(root);
399
+ const { topPkgDir, manifest: MANIFEST, maturity: MATURITY, ast: AST } = loadRepoData(root);
352
400
  const packages = {};
353
401
  const symbolIndex = {};
354
402
  const domainMap = {};
403
+ const manifestNames = new Set(MANIFEST.keys());
404
+ const discoveredNames = new Set();
355
405
  for (const dir of findPackageDirs(topPkgDir)) {
356
406
  const pkg = readPackageJson(dir);
357
407
  if (!pkg || !pkg.name)
358
408
  continue;
359
409
  const manifest = MANIFEST.get(pkg.name);
410
+ discoveredNames.add(pkg.name);
360
411
  if (MANIFEST.size > 0 && !manifest)
361
412
  continue;
362
413
  const dirName = path.basename(dir);
@@ -372,12 +423,20 @@ function buildFromMonorepo(root, now = Date.now()) {
372
423
  ...(Array.isArray(pkg.keywords) ? { keywords: pkg.keywords } : {}),
373
424
  dependencies: Object.keys(pkg.dependencies || {}),
374
425
  devDependencies: Object.keys(pkg.devDependencies || {}),
426
+ dependencyGroups: {
427
+ runtime: { ...(pkg.dependencies || {}) },
428
+ peer: { ...(pkg.peerDependencies || {}) },
429
+ optional: { ...(pkg.optionalDependencies || {}) },
430
+ dev: { ...(pkg.devDependencies || {}) },
431
+ },
432
+ ...(pkg.exports && typeof pkg.exports === 'object' ? { exportsMap: pkg.exports } : {}),
375
433
  hasRust: hasSubdir(dir, 'rust') || hasSubdir(dir, 'rust-toolchain'),
376
434
  hasGo: hasSubdir(dir, 'go'),
377
435
  hasTests: hasTestDir(dir),
378
436
  exports,
379
437
  symbols: Object.fromEntries(symbols),
380
438
  domain,
439
+ ...(MATURITY.get(pkg.name) ? { maturity: MATURITY.get(pkg.name) } : {}),
381
440
  };
382
441
  packages[pkg.name] = idx;
383
442
  if (!domainMap[domain])
@@ -393,7 +452,39 @@ function buildFromMonorepo(root, now = Date.now()) {
393
452
  });
394
453
  }
395
454
  }
396
- return { generatedAt: now, packages, symbolIndex, domainMap };
455
+ // Normalise workspace protocol ranges to the concrete workspace version so
456
+ // graph output is actionable outside pnpm. External ranges are preserved.
457
+ for (const pkg of Object.values(packages)) {
458
+ for (const kind of ['runtime', 'peer', 'optional', 'dev']) {
459
+ const group = pkg.dependencyGroups?.[kind];
460
+ if (!group)
461
+ continue;
462
+ for (const [dep, range] of Object.entries(group)) {
463
+ if (String(range).startsWith('workspace:')) {
464
+ const version = packages[dep]?.version;
465
+ group[dep] = version ? `^${version}` : String(range);
466
+ }
467
+ }
468
+ }
469
+ }
470
+ const missingFromIndex = [...manifestNames].filter((name) => !packages[name]).sort();
471
+ const extraInIndex = [...discoveredNames].filter((name) => !manifestNames.has(name)).sort();
472
+ const sourceRevision = readGitRevision(root);
473
+ return {
474
+ schemaVersion: 2,
475
+ source: 'monorepo',
476
+ ...(sourceRevision ? { sourceRevision } : {}),
477
+ coverage: {
478
+ manifestCount: manifestNames.size,
479
+ discoveredCount: discoveredNames.size,
480
+ missingFromIndex,
481
+ extraInIndex,
482
+ },
483
+ generatedAt: now,
484
+ packages,
485
+ symbolIndex,
486
+ domainMap,
487
+ };
397
488
  }
398
489
  /**
399
490
  * Build the SDK index. Prefers a live monorepo scan; falls back to the bundled
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Optional verified recipe accelerators. They are fixtures for the shared
3
+ * planner/scaffolder, not special-cased branches and not the taxonomy of
4
+ * supported applications.
5
+ */
6
+ import type { PlanCompositionInput, CompositionPlan } from './composition.js';
7
+ import type { SdkIndex } from './types.js';
8
+ export interface RecipeDefinition {
9
+ id: string;
10
+ title: string;
11
+ boundary: string;
12
+ input: Omit<PlanCompositionInput, 'brief'>;
13
+ }
14
+ export declare const RECIPES: RecipeDefinition[];
15
+ export declare function planRecipe(recipe: RecipeDefinition, index: SdkIndex): CompositionPlan;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ /**
3
+ * Optional verified recipe accelerators. They are fixtures for the shared
4
+ * planner/scaffolder, not special-cased branches and not the taxonomy of
5
+ * supported applications.
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.RECIPES = void 0;
9
+ exports.planRecipe = planRecipe;
10
+ const composition_js_1 = require("./composition.js");
11
+ exports.RECIPES = [
12
+ {
13
+ id: 'digital-product-passport',
14
+ title: 'Digital Product Passport',
15
+ boundary: 'Passport assembles attested event evidence; generated summaries are not certified digital twins.',
16
+ input: {
17
+ runtime: 'node-esm',
18
+ requiredPackages: ['@totemsdk/edge-mqtt', '@totemsdk/core', '@totemsdk/proof', '@totemsdk/proofgraph'],
19
+ suggestedPackages: ['@totemsdk/manifest', '@totemsdk/identity', '@totemsdk/authority', '@totemsdk/intelligence', '@totemsdk/qvac', '@totemsdk/storage'],
20
+ requiredCapabilities: ['attested event evidence', 'manifest identity', 'proof graph', 'local AI summary'],
21
+ },
22
+ },
23
+ {
24
+ id: 'machinepay-edge',
25
+ title: 'MachinePay Edge Gateway',
26
+ boundary: 'The gateway executes only within a signed policy envelope; payment receipts do not certify the physical outcome.',
27
+ input: {
28
+ runtime: 'node-esm',
29
+ requiredPackages: ['@totemsdk/edge', '@totemsdk/edge-mqtt', '@totemsdk/omnia', '@totemsdk/agent-policy'],
30
+ suggestedPackages: ['@totemsdk/manifest', '@totemsdk/proofgraph', '@totemsdk/root-identity'],
31
+ requiredCapabilities: ['edge device runtime', 'mqtt transport', 'payment channel', 'agent policy'],
32
+ },
33
+ },
34
+ ];
35
+ function planRecipe(recipe, index) {
36
+ return (0, composition_js_1.planComposition)({ ...recipe.input, brief: `${recipe.title}. ${recipe.boundary}` }, index);
37
+ }
package/dist/resources.js CHANGED
@@ -54,7 +54,7 @@ const PAPERS = [
54
54
  {
55
55
  uri: 'totemsdk://papers/yellow',
56
56
  name: 'Core Yellow Paper',
57
- description: 'WOTS+ signatures, TreeKey hierarchy, MMR proofs, byte-exact serialization, lease/watermark coordination, Rust/WASM engine',
57
+ description: 'WOTS signatures, TreeKey hierarchy, MMR proofs, byte-exact serialization, lease/watermark coordination, Rust/WASM engine',
58
58
  file: 'TOTEM_CORE_YELLOW_PAPER.md',
59
59
  },
60
60
  {
@@ -108,7 +108,12 @@ function docMeta(file, fallbackName) {
108
108
  description = t.replace(/[*_`]/g, '').trim();
109
109
  break;
110
110
  }
111
- return { name: name || fallbackName, description: description || name || fallbackName };
111
+ const clean = description || name || fallbackName;
112
+ // Cap at the first sentence / ~160 chars. Discovery must stay cheap: an
113
+ // unbounded first paragraph bloats every client's resource list.
114
+ const firstSentence = clean.split(/(?<=\.)\s/)[0];
115
+ const bounded = (firstSentence.length <= 160 ? firstSentence : clean.slice(0, 160)).trim();
116
+ return { name: name || fallbackName, description: bounded.length > 0 ? bounded : name || fallbackName };
112
117
  }
113
118
  /**
114
119
  * RFCs and audits are discovered from disk, so a new `docs/rfc/RFC-*.md` or
@@ -125,7 +130,14 @@ function discoverDocs() {
125
130
  continue;
126
131
  const file = path.join('docs', 'rfc', f);
127
132
  const meta = docMeta(file, `RFC-${m[1]}`);
128
- docs.push({ uri: `totemsdk://rfc/${m[1]}`, ...meta, file });
133
+ // Amendments share an RFC number with the original; give them a distinct
134
+ // URI (`totemsdk://rfc/007/amendment-a`) so a reader never retrieves an
135
+ // amendment when they asked for the base specification.
136
+ const amend = m[2].match(/^AMENDMENT-([A-Z])/i);
137
+ const uri = amend
138
+ ? `totemsdk://rfc/${m[1]}/amendment-${amend[1].toLowerCase()}`
139
+ : `totemsdk://rfc/${m[1]}`;
140
+ docs.push({ uri, ...meta, file });
129
141
  }
130
142
  }
131
143
  const auditDir = path.join(REPO_ROOT, 'docs', 'audits');
@@ -140,6 +152,19 @@ function discoverDocs() {
140
152
  docs.push({ uri: `totemsdk://audit/${slug}`, ...meta, file });
141
153
  }
142
154
  }
155
+ // Worked, runnable end-to-end scenarios (usually backed by a test under
156
+ // packages/sdk-tests). Discovered from disk like RFCs/audits.
157
+ const exampleDir = path.join(REPO_ROOT, 'docs', 'examples');
158
+ if (fs.existsSync(exampleDir)) {
159
+ for (const f of fs.readdirSync(exampleDir).sort()) {
160
+ if (!f.endsWith('.md'))
161
+ continue;
162
+ const slug = f.replace(/\.md$/, '');
163
+ const file = path.join('docs', 'examples', f);
164
+ const meta = docMeta(file, slug);
165
+ docs.push({ uri: `totemsdk://example/${slug}`, ...meta, file });
166
+ }
167
+ }
143
168
  return docs;
144
169
  }
145
170
  const DOCS = discoverDocs();
@@ -202,7 +227,7 @@ function resourceMimeType(uri) {
202
227
  const base = uri.split('#')[0];
203
228
  if (base === 'totemsdk://conventions' || base === 'totemsdk://templates')
204
229
  return 'text/markdown';
205
- if (/^totemsdk:\/\/(papers|rfc|audit)\/[^/]+$/.test(base))
230
+ if (/^totemsdk:\/\/(papers|rfc|audit|example)\/[^/]+(\/[^/]+)?$/.test(base))
206
231
  return 'text/markdown';
207
232
  if (base === 'totemsdk://packages' || base === 'totemsdk://domain-map' || base === 'totemsdk://rfc')
208
233
  return 'application/json';
@@ -253,6 +278,17 @@ const CONVENTIONS = `# Totem SDK Conventions
253
278
  ## Async
254
279
  - Promise<T> everywhere, typed event emitters, no Observables
255
280
  - Action lifecycle: propose -> reserve -> execute -> confirm/fail/unknown`;
281
+ function decodeResourceSegment(value) {
282
+ try {
283
+ const decoded = decodeURIComponent(value);
284
+ if (!decoded || decoded.includes('..') || decoded.includes('\\') || decoded.includes('\0'))
285
+ return null;
286
+ return decoded;
287
+ }
288
+ catch {
289
+ return null;
290
+ }
291
+ }
256
292
  function handleResourceRead(uri, index) {
257
293
  if (uri === 'totemsdk://connect/methods')
258
294
  return JSON.stringify(connectMethods(), null, 2);
@@ -275,6 +311,16 @@ function handleResourceRead(uri, index) {
275
311
  }
276
312
  return content;
277
313
  }
314
+ if (uri === 'totemsdk://index') {
315
+ return JSON.stringify({
316
+ schemaVersion: index.schemaVersion ?? null,
317
+ source: index.source ?? 'unknown',
318
+ sourceRevision: index.sourceRevision ?? null,
319
+ generatedAt: index.generatedAt,
320
+ packageCount: Object.keys(index.packages).length,
321
+ coverage: index.coverage ?? null,
322
+ }, null, 2);
323
+ }
278
324
  if (uri === 'totemsdk://packages') {
279
325
  return JSON.stringify(Object.values(index.packages).map(p => ({
280
326
  name: p.name,
@@ -306,29 +352,61 @@ function handleResourceRead(uri, index) {
306
352
  }
307
353
  const pkgMatch = uri.match(/^totemsdk:\/\/packages\/([^/]+)$/);
308
354
  if (pkgMatch) {
309
- const pkg = index.packages[pkgMatch[1]];
355
+ const name = decodeResourceSegment(pkgMatch[1]);
356
+ if (!name)
357
+ return null;
358
+ const pkg = index.packages[name];
310
359
  return pkg ? JSON.stringify(pkg, null, 2) : null;
311
360
  }
361
+ const pkgContractsMatch = uri.match(/^totemsdk:\/\/packages\/([^/]+)\/contracts$/);
362
+ if (pkgContractsMatch) {
363
+ const name = decodeResourceSegment(pkgContractsMatch[1]);
364
+ if (!name)
365
+ return null;
366
+ const pkg = index.packages[name];
367
+ if (!pkg)
368
+ return null;
369
+ return JSON.stringify({
370
+ contentKind: 'contract',
371
+ package: pkg.name,
372
+ version: pkg.version,
373
+ origin: index.source ?? 'unknown',
374
+ sourceRevision: index.sourceRevision ?? null,
375
+ symbols: pkg.symbols ?? {},
376
+ }, null, 2);
377
+ }
312
378
  const pkgExportsMatch = uri.match(/^totemsdk:\/\/packages\/([^/]+)\/exports$/);
313
379
  if (pkgExportsMatch) {
314
- const pkg = index.packages[pkgExportsMatch[1]];
380
+ const name = decodeResourceSegment(pkgExportsMatch[1]);
381
+ if (!name)
382
+ return null;
383
+ const pkg = index.packages[name];
315
384
  return pkg ? JSON.stringify(pkg.exports, null, 2) : null;
316
385
  }
317
386
  const pkgDepsMatch = uri.match(/^totemsdk:\/\/packages\/([^/]+)\/dependencies$/);
318
387
  if (pkgDepsMatch) {
319
- const pkg = index.packages[pkgDepsMatch[1]];
388
+ const name = decodeResourceSegment(pkgDepsMatch[1]);
389
+ if (!name)
390
+ return null;
391
+ const pkg = index.packages[name];
320
392
  if (!pkg)
321
393
  return null;
322
394
  return JSON.stringify({ dependencies: pkg.dependencies, devDependencies: pkg.devDependencies }, null, 2);
323
395
  }
324
396
  const domainMatch = uri.match(/^totemsdk:\/\/packages\/by-domain\/(.+)$/);
325
397
  if (domainMatch) {
326
- const pkgs = index.domainMap[domainMatch[1]];
398
+ const domain = decodeResourceSegment(domainMatch[1]);
399
+ if (!domain)
400
+ return null;
401
+ const pkgs = index.domainMap[domain];
327
402
  return pkgs ? JSON.stringify(pkgs.map(n => index.packages[n]).filter(Boolean), null, 2) : null;
328
403
  }
329
404
  const symbolMatch = uri.match(/^totemsdk:\/\/symbol\/(.+)$/);
330
405
  if (symbolMatch) {
331
- const entries = index.symbolIndex[symbolMatch[1]];
406
+ const symbol = decodeResourceSegment(symbolMatch[1]);
407
+ if (!symbol)
408
+ return null;
409
+ const entries = index.symbolIndex[symbol];
332
410
  return entries ? JSON.stringify(entries, null, 2) : null;
333
411
  }
334
412
  return null;
@@ -337,6 +415,7 @@ function listResources(index) {
337
415
  const resources = [
338
416
  { uri: 'totemsdk://packages', name: 'All Packages', description: `List of all ${Object.keys(index.packages).length} SDK packages with metadata` },
339
417
  { uri: 'totemsdk://templates', name: 'All KISSVM Templates', description: `All ${(0, template_catalog_js_1.getAllTemplates)().length} KISSVM script templates with import paths and descriptions` },
418
+ { uri: 'totemsdk://index', name: 'Index Provenance', description: 'Index schema, source revision, package count and coverage reconciliation' },
340
419
  { uri: 'totemsdk://conventions', name: 'Coding Conventions', description: 'Totem SDK coding conventions and patterns' },
341
420
  { uri: 'totemsdk://domain-map', name: 'Domain Map', description: 'Packages grouped by domain layer' },
342
421
  ...PAPERS.map(p => ({ uri: p.uri, name: p.name, description: p.description })),
@@ -366,11 +445,22 @@ function listResources(index) {
366
445
  name: `Dependencies: ${name}`,
367
446
  description: `Dependencies of ${name}`,
368
447
  });
448
+ resources.push({
449
+ uri: `totemsdk://packages/${encodeURIComponent(name)}/contracts`,
450
+ name: `Contracts: ${name}`,
451
+ description: `Exact exported contract metadata for ${name} (contentKind: contract)`,
452
+ });
369
453
  }
370
- const topSymbols = Object.entries(index.symbolIndex)
371
- .filter(([_, v]) => v.length <= 3)
372
- .slice(0, 200);
373
- for (const [symbol, entries] of topSymbols) {
454
+ // Symbol lookups are served by the `search-symbol` tool (query-driven), which
455
+ // is far cheaper than enumerating ~3.7k symbols as resources. Only surface a
456
+ // small set of the most unambiguous symbols (exported by exactly one package)
457
+ // so `totemsdk://symbol/{name}` remains discoverable without bloating
458
+ // discovery to hundreds of entries.
459
+ const uniqueSymbols = Object.entries(index.symbolIndex)
460
+ .filter(([_, v]) => v.length === 1)
461
+ .sort(([a], [b]) => a.localeCompare(b))
462
+ .slice(0, 50);
463
+ for (const [symbol, entries] of uniqueSymbols) {
374
464
  resources.push({
375
465
  uri: `totemsdk://symbol/${symbol}`,
376
466
  name: `Symbol: ${symbol}`,