@papyruslabsai/seshat-mcp 0.2.0 → 0.3.1

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/graph.js CHANGED
@@ -21,7 +21,10 @@ export function buildCallGraph(entities) {
21
21
  callers.set(entity.id, new Set());
22
22
  callees.set(entity.id, new Set());
23
23
  if (entity.context?.module) {
24
- entityByModule.set(entity.context.module, entity);
24
+ const mod = entity.context.module;
25
+ if (!entityByModule.has(mod))
26
+ entityByModule.set(mod, []);
27
+ entityByModule.get(mod).push(entity);
25
28
  }
26
29
  }
27
30
  // Build call edges from ε dimension
@@ -42,12 +45,27 @@ export function buildCallGraph(entities) {
42
45
  }
43
46
  // Strategy 2: Method match (target = "module.method")
44
47
  else if (target.includes('.')) {
45
- const [modulePart, methodPart] = target.split('.');
46
- const moduleEntity = entityByModule.get(modulePart);
47
- if (moduleEntity) {
48
- calleeId = moduleEntity.id;
48
+ const dotIdx = target.indexOf('.');
49
+ const modulePart = target.substring(0, dotIdx);
50
+ const methodPart = target.substring(dotIdx + 1);
51
+ // First try "module.method" as a full entity ID
52
+ if (entityById.has(target)) {
53
+ calleeId = target;
49
54
  }
50
- // Also try "module.method" as a full ID
55
+ // Search within the module's entities for one named methodPart
56
+ if (!calleeId) {
57
+ const moduleEntities = entityByModule.get(modulePart);
58
+ if (moduleEntities) {
59
+ const match = moduleEntities.find(e => {
60
+ const name = typeof e.struct === 'string' ? e.struct : e.struct?.name;
61
+ return e.id === methodPart || name === methodPart;
62
+ });
63
+ if (match) {
64
+ calleeId = match.id;
65
+ }
66
+ }
67
+ }
68
+ // Also try "module.method" concatenated as an ID
51
69
  if (!calleeId && entityById.has(`${modulePart}.${methodPart}`)) {
52
70
  calleeId = `${modulePart}.${methodPart}`;
53
71
  }
package/dist/index.d.ts CHANGED
@@ -3,20 +3,27 @@
3
3
  * @papyruslabs/seshat-mcp — Semantic MCP Server
4
4
  *
5
5
  * Exposes a codebase's 9D JSTF-T coordinate space as queryable MCP tools.
6
- * Reads .seshat/_bundle.json from the current working directory.
6
+ * Reads .seshat/_bundle.json from project directories.
7
7
  *
8
- * Tools:
9
- * query_entities — Search entities by name, layer, module, language
10
- * get_entity — Full 9D coordinate dump for one entity
11
- * get_dependencies — ε edge traversal (callers/callees with depth control)
12
- * get_data_flow — δ dimension: inputs, outputs, mutations
13
- * find_by_constraint — κ dimension search (AUTH, THROWS, DB_ACCESS, etc.)
14
- * get_blast_radius — Theorem 9.4: affected set for given entity IDs
15
- * list_modules — Group entities by layer, module, file, or language
16
- * get_topology — API topology (routes, plugins, auth, tables)
8
+ * Multi-project mode:
9
+ * Set SESHAT_PROJECTS env var to comma-separated paths or a glob pattern.
10
+ * In multi-project mode, all tools require a `project` parameter.
11
+ * Use `list_projects` to see available projects.
17
12
  *
18
- * Interpretation Functors (composite analysis):
19
- * find_dead_code — Unreachable entities via ε-graph BFS from entry points
13
+ * Single-project mode (default):
14
+ * When SESHAT_PROJECTS is not set, loads from CWD. No `project` param needed.
15
+ *
16
+ * Tools (8 core + 7 interpretation functors + 1 meta):
17
+ * list_projects — Show loaded projects with entity counts
18
+ * query_entities — Search entities by name, layer, module, language
19
+ * get_entity — Full 9D coordinate dump for one entity
20
+ * get_dependencies — ε edge traversal (callers/callees with depth control)
21
+ * get_data_flow — δ dimension: inputs, outputs, mutations
22
+ * find_by_constraint — κ dimension search (AUTH, THROWS, DB_ACCESS, etc.)
23
+ * get_blast_radius — Theorem 9.4: affected set for given entity IDs
24
+ * list_modules — Group entities by layer, module, file, or language
25
+ * get_topology — API topology (routes, plugins, auth, tables)
26
+ * find_dead_code — Unreachable entities via ε-graph BFS
20
27
  * find_layer_violations — ε edges violating architectural layer ordering
21
28
  * get_coupling_metrics — Module coupling/cohesion/instability from ε-graph
22
29
  * get_auth_matrix — Auth coverage across API-facing entities from κ
@@ -25,9 +32,8 @@
25
32
  * get_optimal_context — Greedy knapsack: max relevance per token for LLM context
26
33
  *
27
34
  * Usage:
28
- * npx @papyruslabs/seshat-mcp
29
- *
30
- * Configure in Claude Code (~/.claude/settings.json):
31
- * { "mcpServers": { "seshat": { "command": "npx", "args": ["@papyruslabs/seshat-mcp"] } } }
35
+ * npx @papyruslabs/seshat-mcp # single project (CWD)
36
+ * SESHAT_PROJECTS=/path/a,/path/b npx @papyruslabs/seshat-mcp # multi-project
37
+ * SESHAT_PROJECTS="/home/user/projects/*" npx @papyruslabs/seshat-mcp # glob
32
38
  */
33
39
  export {};
package/dist/index.js CHANGED
@@ -3,20 +3,27 @@
3
3
  * @papyruslabs/seshat-mcp — Semantic MCP Server
4
4
  *
5
5
  * Exposes a codebase's 9D JSTF-T coordinate space as queryable MCP tools.
6
- * Reads .seshat/_bundle.json from the current working directory.
6
+ * Reads .seshat/_bundle.json from project directories.
7
7
  *
8
- * Tools:
9
- * query_entities — Search entities by name, layer, module, language
10
- * get_entity — Full 9D coordinate dump for one entity
11
- * get_dependencies — ε edge traversal (callers/callees with depth control)
12
- * get_data_flow — δ dimension: inputs, outputs, mutations
13
- * find_by_constraint — κ dimension search (AUTH, THROWS, DB_ACCESS, etc.)
14
- * get_blast_radius — Theorem 9.4: affected set for given entity IDs
15
- * list_modules — Group entities by layer, module, file, or language
16
- * get_topology — API topology (routes, plugins, auth, tables)
8
+ * Multi-project mode:
9
+ * Set SESHAT_PROJECTS env var to comma-separated paths or a glob pattern.
10
+ * In multi-project mode, all tools require a `project` parameter.
11
+ * Use `list_projects` to see available projects.
17
12
  *
18
- * Interpretation Functors (composite analysis):
19
- * find_dead_code — Unreachable entities via ε-graph BFS from entry points
13
+ * Single-project mode (default):
14
+ * When SESHAT_PROJECTS is not set, loads from CWD. No `project` param needed.
15
+ *
16
+ * Tools (8 core + 7 interpretation functors + 1 meta):
17
+ * list_projects — Show loaded projects with entity counts
18
+ * query_entities — Search entities by name, layer, module, language
19
+ * get_entity — Full 9D coordinate dump for one entity
20
+ * get_dependencies — ε edge traversal (callers/callees with depth control)
21
+ * get_data_flow — δ dimension: inputs, outputs, mutations
22
+ * find_by_constraint — κ dimension search (AUTH, THROWS, DB_ACCESS, etc.)
23
+ * get_blast_radius — Theorem 9.4: affected set for given entity IDs
24
+ * list_modules — Group entities by layer, module, file, or language
25
+ * get_topology — API topology (routes, plugins, auth, tables)
26
+ * find_dead_code — Unreachable entities via ε-graph BFS
20
27
  * find_layer_violations — ε edges violating architectural layer ordering
21
28
  * get_coupling_metrics — Module coupling/cohesion/instability from ε-graph
22
29
  * get_auth_matrix — Auth coverage across API-facing entities from κ
@@ -25,25 +32,80 @@
25
32
  * get_optimal_context — Greedy knapsack: max relevance per token for LLM context
26
33
  *
27
34
  * Usage:
28
- * npx @papyruslabs/seshat-mcp
29
- *
30
- * Configure in Claude Code (~/.claude/settings.json):
31
- * { "mcpServers": { "seshat": { "command": "npx", "args": ["@papyruslabs/seshat-mcp"] } } }
35
+ * npx @papyruslabs/seshat-mcp # single project (CWD)
36
+ * SESHAT_PROJECTS=/path/a,/path/b npx @papyruslabs/seshat-mcp # multi-project
37
+ * SESHAT_PROJECTS="/home/user/projects/*" npx @papyruslabs/seshat-mcp # glob
32
38
  */
39
+ import fs from 'fs';
40
+ import path from 'path';
33
41
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
34
42
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
35
43
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
36
- import { BundleLoader } from './loader.js';
44
+ import { MultiLoader } from './loader.js';
37
45
  import { initTools, queryEntities, getEntity, getDependencies, getDataFlow, findByConstraint, getBlastRadius, listModules, getTopology, } from './tools/index.js';
38
46
  import { findDeadCode, findLayerViolations, getCouplingMetrics, getAuthMatrix, findErrorGaps, getTestCoverage, getOptimalContext, } from './tools/functors.js';
47
+ // ─── Project Discovery ───────────────────────────────────────────
48
+ /**
49
+ * Discover project directories from SESHAT_PROJECTS env var.
50
+ *
51
+ * - Not set → [process.cwd()] (backward compat)
52
+ * - Comma-separated paths → resolve each
53
+ * - Glob pattern (contains *) → scan parent dir for subdirs with .seshat/_bundle.json
54
+ */
55
+ function discoverProjects() {
56
+ const env = process.env.SESHAT_PROJECTS;
57
+ if (!env)
58
+ return [process.cwd()];
59
+ const trimmed = env.trim();
60
+ // Glob pattern: /path/to/projects/*
61
+ if (trimmed.includes('*')) {
62
+ const globDir = trimmed.replace(/\/?\*$/, '');
63
+ const resolved = path.resolve(globDir);
64
+ if (!fs.existsSync(resolved)) {
65
+ process.stderr.write(`SESHAT_PROJECTS glob dir not found: ${resolved}\n`);
66
+ return [process.cwd()];
67
+ }
68
+ const dirs = [];
69
+ for (const entry of fs.readdirSync(resolved, { withFileTypes: true })) {
70
+ if (!entry.isDirectory())
71
+ continue;
72
+ const candidate = path.join(resolved, entry.name);
73
+ const bundlePath = path.join(candidate, '.seshat', '_bundle.json');
74
+ if (fs.existsSync(bundlePath)) {
75
+ dirs.push(candidate);
76
+ }
77
+ }
78
+ if (dirs.length === 0) {
79
+ process.stderr.write(`SESHAT_PROJECTS: no dirs with .seshat/_bundle.json found in ${resolved}\n`);
80
+ return [process.cwd()];
81
+ }
82
+ return dirs;
83
+ }
84
+ // Comma-separated paths
85
+ return trimmed.split(',').map(p => path.resolve(p.trim())).filter(p => p.length > 0);
86
+ }
87
+ // ─── Project param definition (injected into all tool schemas) ───
88
+ const projectParam = {
89
+ type: 'string',
90
+ description: 'Project name (required in multi-project mode). Use list_projects to see available projects.',
91
+ };
39
92
  // ─── Tool Definitions ─────────────────────────────────────────────
40
93
  const TOOLS = [
94
+ {
95
+ name: 'list_projects',
96
+ description: 'List all loaded projects with entity counts, languages, and metadata. Call this first to see what projects are available, then pass the project name to other tools.',
97
+ inputSchema: {
98
+ type: 'object',
99
+ properties: {},
100
+ },
101
+ },
41
102
  {
42
103
  name: 'query_entities',
43
104
  description: 'Search entities in the 9D semantic coordinate space. Filter by name, architectural layer (route/service/repository/component), module, or source language. Returns entity summaries with constraint tags.',
44
105
  inputSchema: {
45
106
  type: 'object',
46
107
  properties: {
108
+ project: projectParam,
47
109
  query: {
48
110
  type: 'string',
49
111
  description: 'Search term — matches against entity name, ID, source file, and module',
@@ -73,6 +135,7 @@ const TOOLS = [
73
135
  inputSchema: {
74
136
  type: 'object',
75
137
  properties: {
138
+ project: projectParam,
76
139
  id: {
77
140
  type: 'string',
78
141
  description: 'Entity ID or name',
@@ -87,6 +150,7 @@ const TOOLS = [
87
150
  inputSchema: {
88
151
  type: 'object',
89
152
  properties: {
153
+ project: projectParam,
90
154
  entity_id: {
91
155
  type: 'string',
92
156
  description: 'Entity ID or name',
@@ -110,6 +174,7 @@ const TOOLS = [
110
174
  inputSchema: {
111
175
  type: 'object',
112
176
  properties: {
177
+ project: projectParam,
113
178
  entity_id: {
114
179
  type: 'string',
115
180
  description: 'Entity ID or name',
@@ -124,6 +189,7 @@ const TOOLS = [
124
189
  inputSchema: {
125
190
  type: 'object',
126
191
  properties: {
192
+ project: projectParam,
127
193
  constraint: {
128
194
  type: 'string',
129
195
  description: 'Constraint tag to search for: AUTH, VALIDATED, PURE, THROWS, DB_ACCESS, NETWORK_IO, IMP, etc.',
@@ -138,6 +204,7 @@ const TOOLS = [
138
204
  inputSchema: {
139
205
  type: 'object',
140
206
  properties: {
207
+ project: projectParam,
141
208
  entity_ids: {
142
209
  type: 'array',
143
210
  items: { type: 'string' },
@@ -153,6 +220,7 @@ const TOOLS = [
153
220
  inputSchema: {
154
221
  type: 'object',
155
222
  properties: {
223
+ project: projectParam,
156
224
  group_by: {
157
225
  type: 'string',
158
226
  enum: ['layer', 'module', 'file', 'language'],
@@ -166,7 +234,9 @@ const TOOLS = [
166
234
  description: 'Get the API topology: routes, plugins, auth patterns, and database tables. Built from the χ (context) and ε (edges) dimensions across all entities.',
167
235
  inputSchema: {
168
236
  type: 'object',
169
- properties: {},
237
+ properties: {
238
+ project: projectParam,
239
+ },
170
240
  },
171
241
  },
172
242
  // ─── Interpretation Functor Tools ────────────────────────────────
@@ -176,6 +246,7 @@ const TOOLS = [
176
246
  inputSchema: {
177
247
  type: 'object',
178
248
  properties: {
249
+ project: projectParam,
179
250
  include_tests: {
180
251
  type: 'boolean',
181
252
  description: 'Include test entities in dead code results (default: false)',
@@ -188,7 +259,9 @@ const TOOLS = [
188
259
  description: 'Detect architectural layer violations in the ε call graph. Finds backward calls (lower layer calling higher layer, e.g. repository → route) and skip-layer calls (jumping over multiple layers). Uses χ.layer for classification.',
189
260
  inputSchema: {
190
261
  type: 'object',
191
- properties: {},
262
+ properties: {
263
+ project: projectParam,
264
+ },
192
265
  },
193
266
  },
194
267
  {
@@ -197,6 +270,7 @@ const TOOLS = [
197
270
  inputSchema: {
198
271
  type: 'object',
199
272
  properties: {
273
+ project: projectParam,
200
274
  group_by: {
201
275
  type: 'string',
202
276
  enum: ['module', 'layer'],
@@ -210,7 +284,9 @@ const TOOLS = [
210
284
  description: 'Analyze authentication coverage across all API-facing entities. Shows which routes/controllers have auth requirements (from κ.auth) and which don\'t. Detects inconsistencies like DB access without auth.',
211
285
  inputSchema: {
212
286
  type: 'object',
213
- properties: {},
287
+ properties: {
288
+ project: projectParam,
289
+ },
214
290
  },
215
291
  },
216
292
  {
@@ -218,7 +294,9 @@ const TOOLS = [
218
294
  description: 'Find error handling gaps: fallible entities (κ.throws, network/db side effects) whose callers lack try/catch (κ.errorHandling). These are crash risk points where exceptions can propagate unhandled.',
219
295
  inputSchema: {
220
296
  type: 'object',
221
- properties: {},
297
+ properties: {
298
+ project: projectParam,
299
+ },
222
300
  },
223
301
  },
224
302
  {
@@ -227,6 +305,7 @@ const TOOLS = [
227
305
  inputSchema: {
228
306
  type: 'object',
229
307
  properties: {
308
+ project: projectParam,
230
309
  weight_by_blast_radius: {
231
310
  type: 'boolean',
232
311
  description: 'Rank uncovered entities by blast radius to prioritize testing (default: false, slower)',
@@ -240,6 +319,7 @@ const TOOLS = [
240
319
  inputSchema: {
241
320
  type: 'object',
242
321
  properties: {
322
+ project: projectParam,
243
323
  target_entity: {
244
324
  type: 'string',
245
325
  description: 'Entity ID or name to build context around',
@@ -260,22 +340,34 @@ const TOOLS = [
260
340
  ];
261
341
  // ─── Server Setup ─────────────────────────────────────────────────
262
342
  async function main() {
263
- // Load the JSTF-T bundle
264
- const loader = new BundleLoader();
343
+ // Discover and load projects
344
+ const projectDirs = discoverProjects();
345
+ const loader = new MultiLoader(projectDirs);
265
346
  try {
266
347
  loader.load();
267
348
  }
268
349
  catch (err) {
269
- // Server will start but tools will return helpful errors
270
350
  process.stderr.write(`Warning: ${err.message}\n`);
271
351
  }
272
352
  initTools(loader);
273
- const manifest = loader.isLoaded() ? loader.getManifest() : null;
274
- const projectName = manifest?.projectName || 'unknown';
275
- const entityCount = manifest?.entityCount || 0;
353
+ // Build server name
354
+ const projectNames = loader.getProjectNames();
355
+ const totalEntities = loader.totalEntities();
356
+ const isMulti = loader.isMultiProject();
357
+ let serverLabel;
358
+ if (!loader.isLoaded()) {
359
+ serverLabel = 'seshat-mcp (no projects loaded)';
360
+ }
361
+ else if (isMulti) {
362
+ serverLabel = `seshat-mcp (${projectNames.length} projects, ${totalEntities} entities)`;
363
+ }
364
+ else {
365
+ const manifest = loader.getManifest();
366
+ serverLabel = `seshat-mcp (${manifest?.projectName || projectNames[0] || 'unknown'})`;
367
+ }
276
368
  const server = new Server({
277
- name: `seshat-mcp (${projectName})`,
278
- version: '0.2.0',
369
+ name: serverLabel,
370
+ version: '0.3.1',
279
371
  }, {
280
372
  capabilities: {
281
373
  tools: {},
@@ -294,13 +386,25 @@ async function main() {
294
386
  content: [{
295
387
  type: 'text',
296
388
  text: JSON.stringify({
297
- error: 'No .seshat/_bundle.json found in the current working directory. Run the Seshat extraction pipeline first.',
389
+ error: 'No projects loaded. Ensure .seshat/_bundle.json exists or set SESHAT_PROJECTS env var.',
298
390
  }, null, 2),
299
391
  }],
300
392
  };
301
393
  }
302
394
  let result;
303
395
  switch (name) {
396
+ // Meta
397
+ case 'list_projects':
398
+ result = {
399
+ multiProject: isMulti,
400
+ projects: loader.getProjectInfo(),
401
+ totalEntities,
402
+ hint: isMulti
403
+ ? 'Pass the project name as the "project" parameter to all other tools.'
404
+ : 'Single project mode — the "project" parameter is optional.',
405
+ };
406
+ break;
407
+ // Core tools
304
408
  case 'query_entities':
305
409
  result = queryEntities(args);
306
410
  break;
@@ -323,23 +427,23 @@ async function main() {
323
427
  result = listModules(args);
324
428
  break;
325
429
  case 'get_topology':
326
- result = getTopology();
430
+ result = getTopology(args);
327
431
  break;
328
432
  // Interpretation Functors
329
433
  case 'find_dead_code':
330
434
  result = findDeadCode(args);
331
435
  break;
332
436
  case 'find_layer_violations':
333
- result = findLayerViolations();
437
+ result = findLayerViolations(args);
334
438
  break;
335
439
  case 'get_coupling_metrics':
336
440
  result = getCouplingMetrics(args);
337
441
  break;
338
442
  case 'get_auth_matrix':
339
- result = getAuthMatrix();
443
+ result = getAuthMatrix(args);
340
444
  break;
341
445
  case 'find_error_gaps':
342
- result = findErrorGaps();
446
+ result = findErrorGaps(args);
343
447
  break;
344
448
  case 'get_test_coverage':
345
449
  result = getTestCoverage(args);
@@ -372,7 +476,13 @@ async function main() {
372
476
  // Start stdio transport
373
477
  const transport = new StdioServerTransport();
374
478
  await server.connect(transport);
375
- process.stderr.write(`Seshat MCP server started: ${projectName} (${entityCount} entities)\n`);
479
+ if (isMulti) {
480
+ process.stderr.write(`Seshat MCP server started: ${projectNames.length} projects (${totalEntities} entities) — ${projectNames.join(', ')}\n`);
481
+ }
482
+ else {
483
+ const manifest = loader.getManifest();
484
+ process.stderr.write(`Seshat MCP server started: ${manifest?.projectName || 'unknown'} (${totalEntities} entities)\n`);
485
+ }
376
486
  }
377
487
  main().catch((err) => {
378
488
  process.stderr.write(`Fatal: ${err.message}\n`);
package/dist/loader.d.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * Discovers and loads .seshat/_bundle.json from the current working directory.
5
5
  * Falls back to checking common alternative locations.
6
6
  */
7
- import type { JstfEntity, Topology, Manifest } from './types.js';
7
+ import type { JstfEntity, Topology, Manifest, ProjectInfo } from './types.js';
8
8
  export declare class BundleLoader {
9
9
  private entities;
10
10
  private topology;
@@ -20,3 +20,31 @@ export declare class BundleLoader {
20
20
  getEntityByName(name: string): JstfEntity | undefined;
21
21
  isLoaded(): boolean;
22
22
  }
23
+ /**
24
+ * Wraps N BundleLoader instances for multi-project access.
25
+ * In single-project mode (1 dir), behaves identically to BundleLoader.
26
+ * In multi-project mode, `project` param is required on all accessors.
27
+ */
28
+ export declare class MultiLoader {
29
+ private projects;
30
+ private projectPaths;
31
+ private projectEntities;
32
+ private loaded;
33
+ private dirs;
34
+ constructor(projectDirs: string[]);
35
+ load(): void;
36
+ /** Get the resolved project name when only one project is loaded. */
37
+ private defaultProject;
38
+ private resolveProject;
39
+ getEntities(project?: string): JstfEntity[];
40
+ getTopology(project?: string): Topology | null;
41
+ getManifest(project?: string): Manifest | null;
42
+ getEntityById(id: string, project?: string): JstfEntity | undefined;
43
+ getEntityByName(name: string, project?: string): JstfEntity | undefined;
44
+ getProjectNames(): string[];
45
+ getProjectInfo(): ProjectInfo[];
46
+ isMultiProject(): boolean;
47
+ isLoaded(): boolean;
48
+ hasProject(name: string): boolean;
49
+ totalEntities(): number;
50
+ }
package/dist/loader.js CHANGED
@@ -27,6 +27,18 @@ export class BundleLoader {
27
27
  const raw = fs.readFileSync(bundlePath, 'utf-8');
28
28
  const bundle = JSON.parse(raw);
29
29
  this.entities = bundle.entities || [];
30
+ // Remap bundle field names to internal _ prefixed names.
31
+ // The extraction pipeline outputs `sourceFile`, `sourceLanguage`, `_jstfFilename`
32
+ // but our JstfEntity type expects `_sourceFile`, `_sourceLanguage`.
33
+ for (const e of this.entities) {
34
+ const raw = e;
35
+ if (raw.sourceFile && !e._sourceFile) {
36
+ e._sourceFile = raw.sourceFile;
37
+ }
38
+ if (raw.sourceLanguage && !e._sourceLanguage) {
39
+ e._sourceLanguage = raw.sourceLanguage;
40
+ }
41
+ }
30
42
  // Load topology if available
31
43
  const topoPath = path.join(this.seshatDir, '_topology.json');
32
44
  if (fs.existsSync(topoPath)) {
@@ -67,3 +79,122 @@ export class BundleLoader {
67
79
  return this.loaded;
68
80
  }
69
81
  }
82
+ // ─── Multi-Project Loader ─────────────────────────────────────────
83
+ /**
84
+ * Wraps N BundleLoader instances for multi-project access.
85
+ * In single-project mode (1 dir), behaves identically to BundleLoader.
86
+ * In multi-project mode, `project` param is required on all accessors.
87
+ */
88
+ export class MultiLoader {
89
+ projects = new Map();
90
+ projectPaths = new Map();
91
+ projectEntities = new Map();
92
+ loaded = false;
93
+ dirs;
94
+ constructor(projectDirs) {
95
+ this.dirs = projectDirs.length > 0 ? projectDirs : [process.cwd()];
96
+ }
97
+ load() {
98
+ for (const dir of this.dirs) {
99
+ const loader = new BundleLoader(dir);
100
+ try {
101
+ loader.load();
102
+ const manifest = loader.getManifest();
103
+ const name = manifest?.projectName || path.basename(dir);
104
+ this.projects.set(name, loader);
105
+ this.projectPaths.set(name, dir);
106
+ // Stamp entities with _project
107
+ const entities = loader.getEntities();
108
+ for (const e of entities) {
109
+ e._project = name;
110
+ }
111
+ this.projectEntities.set(name, entities);
112
+ }
113
+ catch (err) {
114
+ process.stderr.write(`Warning: Skipping ${dir}: ${err.message}\n`);
115
+ }
116
+ }
117
+ this.loaded = true;
118
+ }
119
+ /** Get the resolved project name when only one project is loaded. */
120
+ defaultProject() {
121
+ if (this.projects.size === 1)
122
+ return [...this.projects.keys()][0];
123
+ return undefined;
124
+ }
125
+ resolveProject(project) {
126
+ return project || this.defaultProject();
127
+ }
128
+ getEntities(project) {
129
+ if (!this.loaded)
130
+ this.load();
131
+ const p = this.resolveProject(project);
132
+ if (p)
133
+ return this.projectEntities.get(p) || [];
134
+ return []; // multi-project with no project specified
135
+ }
136
+ getTopology(project) {
137
+ if (!this.loaded)
138
+ this.load();
139
+ const p = this.resolveProject(project);
140
+ if (p)
141
+ return this.projects.get(p)?.getTopology() || null;
142
+ return null;
143
+ }
144
+ getManifest(project) {
145
+ if (!this.loaded)
146
+ this.load();
147
+ const p = this.resolveProject(project);
148
+ if (p)
149
+ return this.projects.get(p)?.getManifest() || null;
150
+ return null;
151
+ }
152
+ getEntityById(id, project) {
153
+ return this.getEntities(project).find(e => e.id === id);
154
+ }
155
+ getEntityByName(name, project) {
156
+ return this.getEntities(project).find(e => {
157
+ const structName = typeof e.struct === 'string' ? e.struct : e.struct?.name;
158
+ return e.id === name || structName === name;
159
+ });
160
+ }
161
+ getProjectNames() {
162
+ if (!this.loaded)
163
+ this.load();
164
+ return [...this.projects.keys()];
165
+ }
166
+ getProjectInfo() {
167
+ if (!this.loaded)
168
+ this.load();
169
+ const result = [];
170
+ for (const [name, loader] of this.projects) {
171
+ const manifest = loader.getManifest();
172
+ result.push({
173
+ name,
174
+ path: this.projectPaths.get(name) || '',
175
+ entityCount: loader.getEntities().length,
176
+ languages: manifest?.languages || [],
177
+ commitSha: manifest?.commitSha || '',
178
+ extractedAt: manifest?.extractedAt || '',
179
+ layers: manifest?.layers || {},
180
+ });
181
+ }
182
+ return result;
183
+ }
184
+ isMultiProject() {
185
+ return this.projects.size > 1;
186
+ }
187
+ isLoaded() {
188
+ return this.loaded && this.projects.size > 0;
189
+ }
190
+ hasProject(name) {
191
+ return this.projects.has(name);
192
+ }
193
+ totalEntities() {
194
+ let total = 0;
195
+ for (const entities of this.projectEntities.values()) {
196
+ total += entities.length;
197
+ }
198
+ return total;
199
+ }
200
+ }
@@ -7,18 +7,28 @@
7
7
  */
8
8
  export declare function findDeadCode(args: {
9
9
  include_tests?: boolean;
10
+ project?: string;
11
+ }): unknown;
12
+ export declare function findLayerViolations(args?: {
13
+ project?: string;
10
14
  }): unknown;
11
- export declare function findLayerViolations(): unknown;
12
15
  export declare function getCouplingMetrics(args: {
13
16
  group_by?: 'module' | 'layer';
17
+ project?: string;
18
+ }): unknown;
19
+ export declare function getAuthMatrix(args?: {
20
+ project?: string;
21
+ }): unknown;
22
+ export declare function findErrorGaps(args?: {
23
+ project?: string;
14
24
  }): unknown;
15
- export declare function getAuthMatrix(): unknown;
16
- export declare function findErrorGaps(): unknown;
17
25
  export declare function getTestCoverage(args: {
18
26
  weight_by_blast_radius?: boolean;
27
+ project?: string;
19
28
  }): unknown;
20
29
  export declare function getOptimalContext(args: {
21
30
  target_entity: string;
22
31
  max_tokens?: number;
23
32
  strategy?: 'bfs' | 'blast_radius';
33
+ project?: string;
24
34
  }): unknown;
@@ -6,7 +6,7 @@
6
6
  * from the primitive dimensions (sigma, epsilon, delta, kappa, chi, tau, rho).
7
7
  */
8
8
  import { computeBlastRadius } from '../graph.js';
9
- import { getLoader, getGraph, entityLayer, entitySummary, } from './index.js';
9
+ import { getLoader, getGraph, validateProject, entityLayer, entitySummary, } from './index.js';
10
10
  // ─── Layer ordering for violation detection ──────────────────────
11
11
  const LAYER_ORDER = {
12
12
  route: 0,
@@ -22,10 +22,13 @@ const LAYER_ORDER = {
22
22
  };
23
23
  // ─── Functor 1: find_dead_code ───────────────────────────────────
24
24
  export function findDeadCode(args) {
25
+ const projErr = validateProject(args.project);
26
+ if (projErr)
27
+ return { error: projErr };
25
28
  const { include_tests = false } = args;
26
29
  const loader = getLoader();
27
- const g = getGraph();
28
- const entities = loader.getEntities();
30
+ const g = getGraph(args.project);
31
+ const entities = loader.getEntities(args.project);
29
32
  // Entry points: routes, exported functions, test files, plugin registrations
30
33
  const entryPointIds = new Set();
31
34
  for (const e of entities) {
@@ -88,8 +91,11 @@ export function findDeadCode(args) {
88
91
  };
89
92
  }
90
93
  // ─── Functor 2: find_layer_violations ────────────────────────────
91
- export function findLayerViolations() {
92
- const g = getGraph();
94
+ export function findLayerViolations(args) {
95
+ const projErr = validateProject(args?.project);
96
+ if (projErr)
97
+ return { error: projErr };
98
+ const g = getGraph(args?.project);
93
99
  const violations = [];
94
100
  for (const [callerId, calleeIds] of g.callees) {
95
101
  const callerEntity = g.entityById.get(callerId);
@@ -142,10 +148,13 @@ export function findLayerViolations() {
142
148
  }
143
149
  // ─── Functor 3: get_coupling_metrics ─────────────────────────────
144
150
  export function getCouplingMetrics(args) {
151
+ const projErr = validateProject(args.project);
152
+ if (projErr)
153
+ return { error: projErr };
145
154
  const { group_by = 'module' } = args;
146
155
  const loader = getLoader();
147
- const g = getGraph();
148
- const entities = loader.getEntities();
156
+ const g = getGraph(args.project);
157
+ const entities = loader.getEntities(args.project);
149
158
  // Group entities
150
159
  const groups = new Map();
151
160
  for (const e of entities) {
@@ -209,9 +218,12 @@ export function getCouplingMetrics(args) {
209
218
  };
210
219
  }
211
220
  // ─── Functor 4: get_auth_matrix ──────────────────────────────────
212
- export function getAuthMatrix() {
221
+ export function getAuthMatrix(args) {
222
+ const projErr = validateProject(args?.project);
223
+ if (projErr)
224
+ return { error: projErr };
213
225
  const loader = getLoader();
214
- const entities = loader.getEntities();
226
+ const entities = loader.getEntities(args?.project);
215
227
  const apiEntities = entities.filter(e => {
216
228
  const layer = entityLayer(e);
217
229
  return layer === 'route' || layer === 'controller' ||
@@ -264,10 +276,13 @@ export function getAuthMatrix() {
264
276
  };
265
277
  }
266
278
  // ─── Functor 5: find_error_gaps ──────────────────────────────────
267
- export function findErrorGaps() {
279
+ export function findErrorGaps(args) {
280
+ const projErr = validateProject(args?.project);
281
+ if (projErr)
282
+ return { error: projErr };
268
283
  const loader = getLoader();
269
- const g = getGraph();
270
- const entities = loader.getEntities();
284
+ const g = getGraph(args?.project);
285
+ const entities = loader.getEntities(args?.project);
271
286
  // Find all fallible entities (throws === true or has THROWS tag)
272
287
  const fallibleIds = new Set();
273
288
  for (const e of entities) {
@@ -328,10 +343,13 @@ export function findErrorGaps() {
328
343
  }
329
344
  // ─── Functor 6: get_test_coverage ────────────────────────────────
330
345
  export function getTestCoverage(args) {
346
+ const projErr = validateProject(args.project);
347
+ if (projErr)
348
+ return { error: projErr };
331
349
  const { weight_by_blast_radius = false } = args;
332
350
  const loader = getLoader();
333
- const g = getGraph();
334
- const entities = loader.getEntities();
351
+ const g = getGraph(args.project);
352
+ const entities = loader.getEntities(args.project);
335
353
  // Partition into test and non-test entities
336
354
  const testIds = new Set();
337
355
  const productionEntities = [];
@@ -402,10 +420,13 @@ export function getTestCoverage(args) {
402
420
  }
403
421
  // ─── Functor 7: get_optimal_context ──────────────────────────────
404
422
  export function getOptimalContext(args) {
423
+ const projErr = validateProject(args.project);
424
+ if (projErr)
425
+ return { error: projErr };
405
426
  const { target_entity, max_tokens = 8000, strategy = 'bfs' } = args;
406
427
  const loader = getLoader();
407
- const g = getGraph();
408
- const entity = loader.getEntityById(target_entity) || loader.getEntityByName(target_entity);
428
+ const g = getGraph(args.project);
429
+ const entity = loader.getEntityById(target_entity, args.project) || loader.getEntityByName(target_entity, args.project);
409
430
  if (!entity) {
410
431
  return { error: `Entity not found: ${target_entity}` };
411
432
  }
@@ -3,13 +3,22 @@
3
3
  *
4
4
  * Each tool exposes a dimension or computation over the 9D JSTF-T coordinate space.
5
5
  * Tools operate on the in-memory entity bundle loaded from .seshat/_bundle.json.
6
+ *
7
+ * Multi-project: when multiple projects are loaded, each tool accepts an optional
8
+ * `project` parameter. In multi-project mode, `project` is required.
6
9
  */
7
10
  import type { JstfEntity } from '../types.js';
8
- import { BundleLoader } from '../loader.js';
11
+ import { MultiLoader } from '../loader.js';
9
12
  import { type CallGraph } from '../graph.js';
10
- export declare function initTools(bundleLoader: BundleLoader): void;
11
- export declare function getLoader(): BundleLoader;
12
- export declare function getGraph(): CallGraph;
13
+ export declare function initTools(multiLoader: MultiLoader): void;
14
+ export declare function getLoader(): MultiLoader;
15
+ export declare function getGraph(project?: string): CallGraph;
16
+ /**
17
+ * Validate project param. Returns error string if invalid, null if OK.
18
+ * In single-project mode, project is optional (defaults to the only project).
19
+ * In multi-project mode, project is required.
20
+ */
21
+ export declare function validateProject(project?: string): string | null;
13
22
  export declare function entityName(e: JstfEntity): string;
14
23
  export declare function entityLayer(e: JstfEntity): string;
15
24
  export declare function entitySummary(e: JstfEntity): Record<string, unknown>;
@@ -20,6 +29,7 @@ export declare function normalizeConstraints(constraints: JstfEntity['constraint
20
29
  */
21
30
  export declare function constraintMatches(constraints: JstfEntity['constraints'], target: string): boolean;
22
31
  export declare function queryEntities(args: {
32
+ project?: string;
23
33
  query?: string;
24
34
  layer?: string;
25
35
  module?: string;
@@ -28,23 +38,31 @@ export declare function queryEntities(args: {
28
38
  }): unknown;
29
39
  export declare function getEntity(args: {
30
40
  id: string;
41
+ project?: string;
31
42
  }): unknown;
32
43
  export declare function getDependencies(args: {
33
44
  entity_id: string;
34
45
  direction?: 'callers' | 'callees' | 'both';
35
46
  depth?: number;
47
+ project?: string;
36
48
  }): unknown;
37
49
  export declare function collectTransitive(adjacency: Map<string, Set<string>>, startId: string, maxDepth: number): string[];
38
50
  export declare function getDataFlow(args: {
39
51
  entity_id: string;
52
+ project?: string;
40
53
  }): unknown;
41
54
  export declare function findByConstraint(args: {
42
55
  constraint: string;
56
+ project?: string;
43
57
  }): unknown;
44
58
  export declare function getBlastRadius(args: {
45
59
  entity_ids: string[];
60
+ project?: string;
46
61
  }): unknown;
47
62
  export declare function listModules(args: {
48
63
  group_by?: 'layer' | 'module' | 'file' | 'language';
64
+ project?: string;
65
+ }): unknown;
66
+ export declare function getTopology(args?: {
67
+ project?: string;
49
68
  }): unknown;
50
- export declare function getTopology(): unknown;
@@ -3,23 +3,43 @@
3
3
  *
4
4
  * Each tool exposes a dimension or computation over the 9D JSTF-T coordinate space.
5
5
  * Tools operate on the in-memory entity bundle loaded from .seshat/_bundle.json.
6
+ *
7
+ * Multi-project: when multiple projects are loaded, each tool accepts an optional
8
+ * `project` parameter. In multi-project mode, `project` is required.
6
9
  */
7
10
  import { buildCallGraph, computeBlastRadius } from '../graph.js';
8
11
  // Lazy-initialized shared state
9
12
  let loader;
10
- let graph = null;
11
- export function initTools(bundleLoader) {
12
- loader = bundleLoader;
13
- graph = null; // Reset graph cache when loader changes
13
+ let graphCache = new Map();
14
+ export function initTools(multiLoader) {
15
+ loader = multiLoader;
16
+ graphCache = new Map();
14
17
  }
15
18
  export function getLoader() {
16
19
  return loader;
17
20
  }
18
- export function getGraph() {
19
- if (!graph) {
20
- graph = buildCallGraph(loader.getEntities());
21
+ export function getGraph(project) {
22
+ const key = project || '__default__';
23
+ if (!graphCache.has(key)) {
24
+ graphCache.set(key, buildCallGraph(loader.getEntities(project)));
25
+ }
26
+ return graphCache.get(key);
27
+ }
28
+ /**
29
+ * Validate project param. Returns error string if invalid, null if OK.
30
+ * In single-project mode, project is optional (defaults to the only project).
31
+ * In multi-project mode, project is required.
32
+ */
33
+ export function validateProject(project) {
34
+ if (!loader.isMultiProject())
35
+ return null; // single project, always OK
36
+ if (!project) {
37
+ return `Multiple projects loaded. You must specify a "project" parameter. Available: ${loader.getProjectNames().join(', ')}`;
38
+ }
39
+ if (!loader.hasProject(project)) {
40
+ return `Project "${project}" not found. Available: ${loader.getProjectNames().join(', ')}`;
21
41
  }
22
- return graph;
42
+ return null;
23
43
  }
24
44
  // ─── Helper: Extract entity name from struct ─────────────────────
25
45
  export function entityName(e) {
@@ -54,7 +74,8 @@ export function entityLayer(e) {
54
74
  return 'model';
55
75
  if (src.includes('/schema') || explicit === 'schema')
56
76
  return 'schema';
57
- if (src.includes('/test') || src.includes('.test.') || src.includes('.spec.'))
77
+ if (src.includes('/test/') || src.includes('/tests/') || src.includes('/__tests__/') ||
78
+ src.includes('.test.') || src.includes('.spec.'))
58
79
  return 'test';
59
80
  return explicit || 'other';
60
81
  }
@@ -63,6 +84,7 @@ export function entitySummary(e) {
63
84
  return {
64
85
  id: e.id,
65
86
  name: entityName(e),
87
+ ...(e._project ? { project: e._project } : {}),
66
88
  layer: entityLayer(e),
67
89
  module: e.context?.module || null,
68
90
  sourceFile: e._sourceFile || null,
@@ -70,7 +92,10 @@ export function entitySummary(e) {
70
92
  async: typeof e.struct !== 'string' ? e.struct?.async : undefined,
71
93
  exported: typeof e.struct !== 'string' ? e.struct?.exported : undefined,
72
94
  constraints: constraintTags.length > 0 ? constraintTags : undefined,
73
- callCount: Array.isArray(e.edges?.calls) ? e.edges.calls.length : 0,
95
+ callExpressions: Array.isArray(e.edges?.calls) ? e.edges.calls.length : 0,
96
+ uniqueCallees: Array.isArray(e.edges?.calls)
97
+ ? new Set(e.edges.calls.map(c => c.target)).size
98
+ : 0,
74
99
  };
75
100
  }
76
101
  export function normalizeConstraints(constraints) {
@@ -142,8 +167,11 @@ export function constraintMatches(constraints, target) {
142
167
  }
143
168
  // ─── Tool: query_entities ─────────────────────────────────────────
144
169
  export function queryEntities(args) {
170
+ const projErr = validateProject(args.project);
171
+ if (projErr)
172
+ return { error: projErr };
145
173
  const { query, layer, module, language, limit = 50 } = args;
146
- let results = loader.getEntities();
174
+ let results = loader.getEntities(args.project);
147
175
  if (layer) {
148
176
  results = results.filter(e => entityLayer(e).toLowerCase() === layer.toLowerCase());
149
177
  }
@@ -173,7 +201,10 @@ export function queryEntities(args) {
173
201
  }
174
202
  // ─── Tool: get_entity ─────────────────────────────────────────────
175
203
  export function getEntity(args) {
176
- const entity = loader.getEntityById(args.id) || loader.getEntityByName(args.id);
204
+ const projErr = validateProject(args.project);
205
+ if (projErr)
206
+ return { error: projErr };
207
+ const entity = loader.getEntityById(args.id, args.project) || loader.getEntityByName(args.id, args.project);
177
208
  if (!entity) {
178
209
  return { error: `Entity not found: ${args.id}` };
179
210
  }
@@ -181,6 +212,7 @@ export function getEntity(args) {
181
212
  return {
182
213
  id: entity.id,
183
214
  name: entityName(entity),
215
+ ...(entity._project ? { project: entity._project } : {}),
184
216
  sourceFile: entity._sourceFile,
185
217
  sourceLanguage: entity._sourceLanguage,
186
218
  jstfFilename: entity._jstfFilename,
@@ -199,9 +231,12 @@ export function getEntity(args) {
199
231
  }
200
232
  // ─── Tool: get_dependencies ───────────────────────────────────────
201
233
  export function getDependencies(args) {
234
+ const projErr = validateProject(args.project);
235
+ if (projErr)
236
+ return { error: projErr };
202
237
  const { entity_id, direction = 'both', depth = 2 } = args;
203
- const g = getGraph();
204
- const entity = loader.getEntityById(entity_id) || loader.getEntityByName(entity_id);
238
+ const g = getGraph(args.project);
239
+ const entity = loader.getEntityById(entity_id, args.project) || loader.getEntityByName(entity_id, args.project);
205
240
  if (!entity) {
206
241
  return { error: `Entity not found: ${entity_id}` };
207
242
  }
@@ -252,7 +287,10 @@ export function collectTransitive(adjacency, startId, maxDepth) {
252
287
  }
253
288
  // ─── Tool: get_data_flow ──────────────────────────────────────────
254
289
  export function getDataFlow(args) {
255
- const entity = loader.getEntityById(args.entity_id) || loader.getEntityByName(args.entity_id);
290
+ const projErr = validateProject(args.project);
291
+ if (projErr)
292
+ return { error: projErr };
293
+ const entity = loader.getEntityById(args.entity_id, args.project) || loader.getEntityByName(args.entity_id, args.project);
256
294
  if (!entity) {
257
295
  return { error: `Entity not found: ${args.entity_id}` };
258
296
  }
@@ -265,8 +303,11 @@ export function getDataFlow(args) {
265
303
  }
266
304
  // ─── Tool: find_by_constraint ─────────────────────────────────────
267
305
  export function findByConstraint(args) {
306
+ const projErr = validateProject(args.project);
307
+ if (projErr)
308
+ return { error: projErr };
268
309
  const target = args.constraint;
269
- const results = loader.getEntities().filter(e => constraintMatches(e.constraints, target));
310
+ const results = loader.getEntities(args.project).filter(e => constraintMatches(e.constraints, target));
270
311
  return {
271
312
  constraint: args.constraint,
272
313
  total: results.length,
@@ -275,12 +316,15 @@ export function findByConstraint(args) {
275
316
  }
276
317
  // ─── Tool: get_blast_radius ───────────────────────────────────────
277
318
  export function getBlastRadius(args) {
278
- const g = getGraph();
319
+ const projErr = validateProject(args.project);
320
+ if (projErr)
321
+ return { error: projErr };
322
+ const g = getGraph(args.project);
279
323
  // Resolve names to IDs
280
324
  const resolvedIds = new Set();
281
325
  const notFound = [];
282
326
  for (const nameOrId of args.entity_ids) {
283
- const entity = loader.getEntityById(nameOrId) || loader.getEntityByName(nameOrId);
327
+ const entity = loader.getEntityById(nameOrId, args.project) || loader.getEntityByName(nameOrId, args.project);
284
328
  if (entity) {
285
329
  resolvedIds.add(entity.id);
286
330
  }
@@ -309,8 +353,11 @@ export function getBlastRadius(args) {
309
353
  }
310
354
  // ─── Tool: list_modules ───────────────────────────────────────────
311
355
  export function listModules(args) {
356
+ const projErr = validateProject(args.project);
357
+ if (projErr)
358
+ return { error: projErr };
312
359
  const { group_by = 'layer' } = args;
313
- const entities = loader.getEntities();
360
+ const entities = loader.getEntities(args.project);
314
361
  const groups = new Map();
315
362
  for (const e of entities) {
316
363
  let key;
@@ -342,8 +389,11 @@ export function listModules(args) {
342
389
  };
343
390
  }
344
391
  // ─── Tool: get_topology ───────────────────────────────────────────
345
- export function getTopology() {
346
- const topology = loader.getTopology();
392
+ export function getTopology(args) {
393
+ const projErr = validateProject(args?.project);
394
+ if (projErr)
395
+ return { error: projErr };
396
+ const topology = loader.getTopology(args?.project);
347
397
  if (!topology) {
348
398
  return { error: 'No topology data found. The _topology.json file may not have been generated.' };
349
399
  }
package/dist/types.d.ts CHANGED
@@ -11,6 +11,7 @@ export interface JstfEntity {
11
11
  _jstfFilename?: string;
12
12
  _sourceFile?: string | null;
13
13
  _sourceLanguage?: string;
14
+ _project?: string;
14
15
  /** σ — Structure: function shape, signature, modifiers */
15
16
  struct?: {
16
17
  name?: string;
@@ -145,3 +146,12 @@ export interface Manifest {
145
146
  languages: string[];
146
147
  layers: Record<string, number>;
147
148
  }
149
+ export interface ProjectInfo {
150
+ name: string;
151
+ path: string;
152
+ entityCount: number;
153
+ languages: string[];
154
+ commitSha: string;
155
+ extractedAt: string;
156
+ layers: Record<string, number>;
157
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papyruslabsai/seshat-mcp",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Semantic MCP server — exposes a codebase's 9D JSTF-T coordinate space as queryable tools",
5
5
  "type": "module",
6
6
  "bin": {