@papyruslabsai/seshat-mcp 0.11.0 → 0.12.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/index.d.ts CHANGED
@@ -1,29 +1 @@
1
- #!/usr/bin/env node
2
- /**
3
- * @papyruslabs/seshat-mcp — Semantic Code Analysis MCP Server
4
- *
5
- * Exposes a codebase's structure, dependencies, and constraints as queryable
6
- * MCP tools. Reads pre-extracted analysis data from .seshat/_bundle.json.
7
- *
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.
12
- *
13
- * Single-project mode (default):
14
- * When SESHAT_PROJECTS is not set, loads from CWD. No `project` param needed.
15
- *
16
- * 20 tools across 4 categories:
17
- * Discovery: list_projects, query_entities, get_entity, list_modules, get_topology
18
- * Graph: get_dependencies, get_data_flow, find_by_constraint, get_blast_radius
19
- * Analysis: find_dead_code, find_layer_violations, get_coupling_metrics,
20
- * get_auth_matrix, find_error_gaps, get_test_coverage,
21
- * get_optimal_context, estimate_task_cost, report_actual_burn
22
- * Diff: diff_bundle, conflict_matrix
23
- *
24
- * Usage:
25
- * npx @papyruslabs/seshat-mcp # single project (CWD)
26
- * SESHAT_PROJECTS=/path/a,/path/b npx @papyruslabs/seshat-mcp # multi-project
27
- * SESHAT_PROJECTS="/home/user/projects/*" npx @papyruslabs/seshat-mcp # glob
28
- */
29
1
  export {};
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
- #!/usr/bin/env node
1
+ !/usr/bin / env;
2
+ node;
2
3
  /**
3
4
  * @papyruslabs/seshat-mcp — Semantic Code Analysis MCP Server
4
5
  *
@@ -33,7 +34,8 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
33
34
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
34
35
  import { MultiLoader } from './loader.js';
35
36
  import { bootstrap } from './bootstrap.js';
36
- import { initTools, queryEntities, getEntity, getDependencies, getDataFlow, findByConstraint, getBlastRadius, listModules, getTopology, } from './tools/index.js';
37
+ import { logTelemetry, isSupabaseConfigured } from './supabase.js';
38
+ import { queryEntities, getEntity, getDependencies, getDataFlow, findByConstraint, getBlastRadius, listModules, getTopology, } from './tools/index.js';
37
39
  import { findDeadCode, findLayerViolations, getCouplingMetrics, getAuthMatrix, findErrorGaps, getTestCoverage, getOptimalContext, estimateTaskCost, reportActualBurn, find_runtime_violations, find_ownership_violations, query_traits, simulate_mutation, query_data_targets, find_exposure_leaks, find_semantic_clones, create_symbol, } from './tools/functors.js';
38
40
  import { diffBundle, conflictMatrix, } from './tools/diff.js';
39
41
  // ─── Project Discovery ───────────────────────────────────────────
@@ -647,6 +649,7 @@ async function main() {
647
649
  // Handle tool calls
648
650
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
649
651
  const { name, arguments: args } = request.params;
652
+ const startTime = Date.now();
650
653
  try {
651
654
  if (!loader.isLoaded()) {
652
655
  return {
@@ -659,8 +662,7 @@ async function main() {
659
662
  };
660
663
  }
661
664
  let result;
662
- switch (name) {
663
- // Meta
665
+ switch (name) { // Meta
664
666
  case 'list_projects':
665
667
  result = {
666
668
  multiProject: isMulti,
@@ -759,6 +761,21 @@ async function main() {
759
761
  default:
760
762
  result = { error: `Unknown tool: ${name}` };
761
763
  }
764
+ // Log telemetry for the tool execution
765
+ if (isSupabaseConfigured() && name !== 'list_projects') {
766
+ const executionMs = Date.now() - startTime;
767
+ const projectHash = args && typeof args === 'object' && 'project' in args
768
+ ? String(args.project)
769
+ : loader.getProjectNames()[0] || 'unknown';
770
+ // Fire-and-forget telemetry log
771
+ logTelemetry({
772
+ tool_name: name,
773
+ project_hash: projectHash,
774
+ execution_ms: executionMs,
775
+ // user_id will be added by the Ptah API gateway if passing through there,
776
+ // or stay null if running strictly local CLI without an injected token.
777
+ }).catch(() => { });
778
+ }
762
779
  return {
763
780
  content: [{
764
781
  type: 'text',
package/dist/loader.d.ts CHANGED
@@ -4,14 +4,20 @@
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, ProjectInfo } from './types.js';
7
+ import type { JstfEntity, Topology, Manifest, ProjectInfo, ProjectLoader } from './types.js';
8
8
  export declare class BundleLoader {
9
9
  private entities;
10
10
  private topology;
11
11
  private manifest;
12
12
  private loaded;
13
13
  private seshatDir;
14
+ private bundleMtime;
14
15
  constructor(cwd?: string);
16
+ /**
17
+ * Check if the bundle file on disk has changed since last load.
18
+ * If so, reload transparently. Called by all accessors.
19
+ */
20
+ private ensureFresh;
15
21
  load(): void;
16
22
  getEntities(): JstfEntity[];
17
23
  getTopology(): Topology | null;
@@ -25,7 +31,7 @@ export declare class BundleLoader {
25
31
  * In single-project mode (1 dir), behaves identically to BundleLoader.
26
32
  * In multi-project mode, `project` param is required on all accessors.
27
33
  */
28
- export declare class MultiLoader {
34
+ export declare class MultiLoader implements ProjectLoader {
29
35
  private projects;
30
36
  private projectPaths;
31
37
  private projectEntities;
@@ -33,6 +39,7 @@ export declare class MultiLoader {
33
39
  private dirs;
34
40
  constructor(projectDirs: string[]);
35
41
  load(): void;
42
+ private loadProject;
36
43
  /** Get the resolved project name when only one project is loaded. */
37
44
  private defaultProject;
38
45
  private resolveProject;
package/dist/loader.js CHANGED
@@ -12,10 +12,32 @@ export class BundleLoader {
12
12
  manifest = null;
13
13
  loaded = false;
14
14
  seshatDir;
15
+ bundleMtime = 0;
15
16
  constructor(cwd) {
16
17
  const root = cwd || process.cwd();
17
18
  this.seshatDir = path.join(root, '.seshat');
18
19
  }
20
+ /**
21
+ * Check if the bundle file on disk has changed since last load.
22
+ * If so, reload transparently. Called by all accessors.
23
+ */
24
+ ensureFresh() {
25
+ if (!this.loaded) {
26
+ this.load();
27
+ return;
28
+ }
29
+ const bundlePath = path.join(this.seshatDir, '_bundle.json');
30
+ try {
31
+ const currentMtime = fs.statSync(bundlePath).mtimeMs;
32
+ if (currentMtime !== this.bundleMtime) {
33
+ process.stderr.write(`[seshat] Bundle changed on disk, reloading…\n`);
34
+ this.load();
35
+ }
36
+ }
37
+ catch {
38
+ // File may have been deleted — keep stale data rather than crash
39
+ }
40
+ }
19
41
  load() {
20
42
  const bundlePath = path.join(this.seshatDir, '_bundle.json');
21
43
  if (!fs.existsSync(bundlePath)) {
@@ -24,6 +46,7 @@ export class BundleLoader {
24
46
  ` node ci/extract-and-generate.mjs <repo-path> .seshat <project-name>\n` +
25
47
  `Or add the CI workflow to auto-generate on push to main.`);
26
48
  }
49
+ this.bundleMtime = fs.statSync(bundlePath).mtimeMs;
27
50
  const raw = fs.readFileSync(bundlePath, 'utf-8');
28
51
  const bundle = JSON.parse(raw);
29
52
  this.entities = bundle.entities || [];
@@ -52,18 +75,15 @@ export class BundleLoader {
52
75
  this.loaded = true;
53
76
  }
54
77
  getEntities() {
55
- if (!this.loaded)
56
- this.load();
78
+ this.ensureFresh();
57
79
  return this.entities;
58
80
  }
59
81
  getTopology() {
60
- if (!this.loaded)
61
- this.load();
82
+ this.ensureFresh();
62
83
  return this.topology;
63
84
  }
64
85
  getManifest() {
65
- if (!this.loaded)
66
- this.load();
86
+ this.ensureFresh();
67
87
  return this.manifest;
68
88
  }
69
89
  getEntityById(id) {
@@ -96,26 +116,29 @@ export class MultiLoader {
96
116
  }
97
117
  load() {
98
118
  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
- }
119
+ this.loadProject(dir);
116
120
  }
117
121
  this.loaded = true;
118
122
  }
123
+ loadProject(dir) {
124
+ const loader = new BundleLoader(dir);
125
+ try {
126
+ loader.load();
127
+ const manifest = loader.getManifest();
128
+ const name = manifest?.projectName || path.basename(dir);
129
+ this.projects.set(name, loader);
130
+ this.projectPaths.set(name, dir);
131
+ // Stamp entities with _project
132
+ const entities = loader.getEntities();
133
+ for (const e of entities) {
134
+ e._project = name;
135
+ }
136
+ this.projectEntities.set(name, entities);
137
+ }
138
+ catch (err) {
139
+ process.stderr.write(`Warning: Skipping ${dir}: ${err.message}\n`);
140
+ }
141
+ }
119
142
  /** Get the resolved project name when only one project is loaded. */
120
143
  defaultProject() {
121
144
  if (this.projects.size === 1)
@@ -129,9 +152,22 @@ export class MultiLoader {
129
152
  if (!this.loaded)
130
153
  this.load();
131
154
  const p = this.resolveProject(project);
132
- if (p)
133
- return this.projectEntities.get(p) || [];
134
- return []; // multi-project with no project specified
155
+ if (!p)
156
+ return [];
157
+ // Delegate to BundleLoader (which checks mtime via ensureFresh),
158
+ // then re-stamp and cache if the reference changed.
159
+ const loader = this.projects.get(p);
160
+ if (!loader)
161
+ return [];
162
+ const fresh = loader.getEntities();
163
+ const cached = this.projectEntities.get(p);
164
+ if (fresh !== cached) {
165
+ for (const e of fresh) {
166
+ e._project = p;
167
+ }
168
+ this.projectEntities.set(p, fresh);
169
+ }
170
+ return fresh;
135
171
  }
136
172
  getTopology(project) {
137
173
  if (!this.loaded)
@@ -55,6 +55,31 @@ export interface TokenPredictionRow {
55
55
  session_id: string | null;
56
56
  notes: string | null;
57
57
  }
58
+ export interface McpTelemetryLog {
59
+ user_id?: string;
60
+ tool_name: string;
61
+ project_hash: string;
62
+ execution_ms: number;
63
+ }
64
+ export interface JstfSnapshotInsert {
65
+ user_id?: string;
66
+ project_hash: string;
67
+ commit_sha: string;
68
+ language_primary: string;
69
+ total_entities: number;
70
+ metric_typeless_interfaces: number;
71
+ metric_max_in_degree: number;
72
+ metric_circular_dependencies: number;
73
+ metric_untracked_data_transformations: number;
74
+ metric_exposure_leaks: number;
75
+ metric_layer_violations: number;
76
+ metric_ownership_violations: number;
77
+ metric_impure_ratio: number;
78
+ metric_runtime_violations: number;
79
+ metric_semantic_clones: number;
80
+ metric_core_fragility_score: number;
81
+ bundle_payload: unknown;
82
+ }
58
83
  /**
59
84
  * Insert a prediction row. Returns the row ID or null on failure.
60
85
  */
@@ -70,4 +95,5 @@ export declare function abandonPrediction(predictionId: string): Promise<boolean
70
95
  /**
71
96
  * List recent predictions for a project (for calibration analysis).
72
97
  */
73
- export declare function listPredictions(project?: string, limit?: number): Promise<TokenPredictionRow[]>;
98
+ export declare function logTelemetry(log: McpTelemetryLog): Promise<void>;
99
+ export declare function insertJstfSnapshot(snapshot: JstfSnapshotInsert): Promise<boolean>;
package/dist/supabase.js CHANGED
@@ -18,7 +18,9 @@ export function isSupabaseConfigured() {
18
18
  return SUPABASE_URL.length > 0 && SUPABASE_KEY.length > 0;
19
19
  }
20
20
  // ─── REST helpers ────────────────────────────────────────────────
21
- const TABLE = 'mcp_token_predictions';
21
+ const TABLE_PREDICTIONS = 'mcp_token_predictions';
22
+ const TABLE_TELEMETRY = 'mcp_telemetry_logs';
23
+ const TABLE_SNAPSHOTS = 'jstf_snapshots';
22
24
  async function supabaseRequest(method, path, body, headers) {
23
25
  if (!isSupabaseConfigured()) {
24
26
  return { ok: false, status: 0, error: 'Supabase not configured' };
@@ -59,7 +61,7 @@ async function supabaseRequest(method, path, body, headers) {
59
61
  * Insert a prediction row. Returns the row ID or null on failure.
60
62
  */
61
63
  export async function insertPrediction(row) {
62
- const result = await supabaseRequest('POST', TABLE, row);
64
+ const result = await supabaseRequest('POST', TABLE_PREDICTIONS, row);
63
65
  if (!result.ok) {
64
66
  process.stderr.write(`[seshat] Prediction log failed: ${result.error}\n`);
65
67
  return null;
@@ -72,7 +74,7 @@ export async function insertPrediction(row) {
72
74
  */
73
75
  export async function updateActualBurn(predictionId, actual) {
74
76
  // First fetch the prediction to compute drift
75
- const fetchResult = await supabaseRequest('GET', `${TABLE}?id=eq.${predictionId}&select=predicted_total,status`);
77
+ const fetchResult = await supabaseRequest('GET', `${TABLE_PREDICTIONS}?id=eq.${predictionId}&select=predicted_total,status`);
76
78
  if (!fetchResult.ok) {
77
79
  process.stderr.write(`[seshat] Fetch prediction failed: ${fetchResult.error}\n`);
78
80
  return null;
@@ -98,7 +100,7 @@ export async function updateActualBurn(predictionId, actual) {
98
100
  status: 'completed',
99
101
  ...(actual.notes ? { notes: actual.notes } : {}),
100
102
  };
101
- const updateResult = await supabaseRequest('PATCH', `${TABLE}?id=eq.${predictionId}`, updateBody);
103
+ const updateResult = await supabaseRequest('PATCH', `${TABLE_PREDICTIONS}?id=eq.${predictionId}`, updateBody);
102
104
  if (!updateResult.ok) {
103
105
  process.stderr.write(`[seshat] Update actual burn failed: ${updateResult.error}\n`);
104
106
  return null;
@@ -109,16 +111,22 @@ export async function updateActualBurn(predictionId, actual) {
109
111
  * Abandon a prediction (task was cancelled or not completed).
110
112
  */
111
113
  export async function abandonPrediction(predictionId) {
112
- const result = await supabaseRequest('PATCH', `${TABLE}?id=eq.${predictionId}&status=eq.predicted`, { status: 'abandoned' });
114
+ const result = await supabaseRequest('PATCH', `${TABLE_PREDICTIONS}?id=eq.${predictionId}&status=eq.predicted`, { status: 'abandoned' });
113
115
  return result.ok;
114
116
  }
115
117
  /**
116
118
  * List recent predictions for a project (for calibration analysis).
117
119
  */
118
- export async function listPredictions(project, limit = 20) {
119
- const filter = project ? `&project=eq.${encodeURIComponent(project)}` : '';
120
- const result = await supabaseRequest('GET', `${TABLE}?select=*${filter}&order=created_at.desc&limit=${limit}`);
121
- if (!result.ok)
122
- return [];
123
- return result.data || [];
120
+ export async function logTelemetry(log) {
121
+ const result = await supabaseRequest('POST', TABLE_TELEMETRY, log);
122
+ if (!result.ok) {
123
+ process.stderr.write(`[seshat] Telemetry log failed: ${result.error}\n`);
124
+ }
125
+ }
126
+ export async function insertJstfSnapshot(snapshot) {
127
+ const result = await supabaseRequest('POST', TABLE_SNAPSHOTS, snapshot);
128
+ if (!result.ok) {
129
+ process.stderr.write(`[seshat] JSTF snapshot failed: ${result.error}\n`);
130
+ }
131
+ return result.ok;
124
132
  }
@@ -11,7 +11,7 @@ export declare function diffBundle(args: {
11
11
  worktree_path: string;
12
12
  project?: string;
13
13
  include_unchanged?: boolean;
14
- }): Promise<unknown>;
14
+ }, loader: ProjectLoader): Promise<unknown>;
15
15
  export declare function conflictMatrix(args: {
16
16
  tasks: Array<{
17
17
  id: string;
@@ -20,4 +20,4 @@ export declare function conflictMatrix(args: {
20
20
  expand_blast_radius?: boolean;
21
21
  }>;
22
22
  project?: string;
23
- }): unknown;
23
+ }, loader: ProjectLoader): unknown;
@@ -11,7 +11,7 @@ import fs from 'fs';
11
11
  import path from 'path';
12
12
  import { bootstrap } from '../bootstrap.js';
13
13
  import { computeBlastRadius } from '../graph.js';
14
- import { getLoader, getGraph, validateProject, entityName, entityLayer, } from './index.js';
14
+ import { getGraph, validateProject, entityName, entityLayer, } from './index.js';
15
15
  // ─── Entity Identity ─────────────────────────────────────────────
16
16
  // Ported from api-v2/translator/seshat-pipeline/src/incremental/diff-engine.mjs
17
17
  /**
@@ -145,11 +145,10 @@ function compareEntityFields(baseEntity, branchEntity) {
145
145
  return changed;
146
146
  }
147
147
  // ─── Tool: diff_bundle ───────────────────────────────────────────
148
- export async function diffBundle(args) {
149
- const projErr = validateProject(args.project);
148
+ export async function diffBundle(args, loader) {
149
+ const projErr = validateProject(args.project, loader);
150
150
  if (projErr)
151
151
  return { error: projErr };
152
- const loader = getLoader();
153
152
  const baseEntities = loader.getEntities(args.project);
154
153
  const baseManifest = loader.getManifest(args.project);
155
154
  if (baseEntities.length === 0) {
@@ -366,11 +365,10 @@ function buildExecutionPlan(taskIds, tier3Edges) {
366
365
  };
367
366
  }
368
367
  // ─── Tool: conflict_matrix ────────────────────────────────────────
369
- export function conflictMatrix(args) {
370
- const projErr = validateProject(args.project);
368
+ export function conflictMatrix(args, loader) {
369
+ const projErr = validateProject(args.project, loader);
371
370
  if (projErr)
372
371
  return { error: projErr };
373
- const loader = getLoader();
374
372
  const { tasks } = args;
375
373
  if (!tasks || tasks.length < 2) {
376
374
  return { error: 'At least 2 tasks are required to compute a conflict matrix.' };
@@ -402,7 +400,7 @@ export function conflictMatrix(args) {
402
400
  }
403
401
  // Expand blast radius if requested
404
402
  if (task.expand_blast_radius && entityIds.size > 0) {
405
- const g = getGraph(args.project);
403
+ const g = getGraph(args.project, loader);
406
404
  const blastResult = computeBlastRadius(g, entityIds);
407
405
  for (const affectedId of blastResult.affected) {
408
406
  if (!entityIds.has(affectedId)) {
@@ -15,14 +15,14 @@ export declare function estimateTokens(e: JstfEntity): number;
15
15
  export declare function findDeadCode(args: {
16
16
  include_tests?: boolean;
17
17
  project?: string;
18
- }): unknown;
18
+ }, loader: ProjectLoader): unknown;
19
19
  export declare function findLayerViolations(args?: {
20
20
  project?: string;
21
21
  }): unknown;
22
22
  export declare function getCouplingMetrics(args: {
23
23
  group_by?: 'module' | 'layer';
24
24
  project?: string;
25
- }): unknown;
25
+ }, loader: ProjectLoader): unknown;
26
26
  export declare function getAuthMatrix(args?: {
27
27
  project?: string;
28
28
  }): unknown;
@@ -32,13 +32,13 @@ export declare function findErrorGaps(args?: {
32
32
  export declare function getTestCoverage(args: {
33
33
  weight_by_blast_radius?: boolean;
34
34
  project?: string;
35
- }): unknown;
35
+ }, loader: ProjectLoader): unknown;
36
36
  export declare function getOptimalContext(args: {
37
37
  target_entity: string;
38
38
  max_tokens?: number;
39
39
  strategy?: 'bfs' | 'blast_radius';
40
40
  project?: string;
41
- }): unknown;
41
+ }, loader: ProjectLoader): unknown;
42
42
  /**
43
43
  * Estimate token cost of a code change BEFORE starting work.
44
44
  * Computes blast radius, sums source token counts, and projects total burn.
@@ -48,7 +48,7 @@ export declare function estimateTaskCost(args: {
48
48
  target_entities: string[];
49
49
  context_budget?: number;
50
50
  project?: string;
51
- }): Promise<unknown>;
51
+ }, loader: ProjectLoader): Promise<unknown>;
52
52
  /**
53
53
  * Close the calibration feedback loop by reporting actual token usage
54
54
  * against a prior prediction from estimate_task_cost.
@@ -65,7 +65,7 @@ export declare function reportActualBurn(args: {
65
65
  action?: 'complete' | 'abandon' | 'list';
66
66
  project?: string;
67
67
  notes?: string;
68
- }): Promise<unknown>;
68
+ }, loader: ProjectLoader): Promise<unknown>;
69
69
  export declare function find_runtime_violations(args?: {
70
70
  project?: string;
71
71
  }): unknown;
@@ -75,7 +75,7 @@ export declare function find_ownership_violations(args?: {
75
75
  export declare function query_traits(args: {
76
76
  trait: string;
77
77
  project?: string;
78
- }): unknown;
78
+ }, loader: ProjectLoader): unknown;
79
79
  export declare function simulate_mutation(args: {
80
80
  entity_id: string;
81
81
  mutation: {
@@ -86,11 +86,11 @@ export declare function simulate_mutation(args: {
86
86
  };
87
87
  };
88
88
  project?: string;
89
- }): unknown;
89
+ }, loader: ProjectLoader): unknown;
90
90
  export declare function query_data_targets(args: {
91
91
  target_name: string;
92
92
  project?: string;
93
- }): unknown;
93
+ }, loader: ProjectLoader): unknown;
94
94
  export declare function find_exposure_leaks(args?: {
95
95
  project?: string;
96
96
  }): unknown;
@@ -104,4 +104,4 @@ export declare function create_symbol(args: {
104
104
  layer?: string;
105
105
  project?: string;
106
106
  description?: string;
107
- }): unknown;
107
+ }, loader: ProjectLoader): unknown;
@@ -7,8 +7,8 @@
7
7
  */
8
8
  import path from 'path';
9
9
  import { computeBlastRadius } from '../graph.js';
10
- import { getLoader, getGraph, validateProject, entityName, entityLayer, entitySummary, } from './index.js';
11
- import { isSupabaseConfigured, insertPrediction, updateActualBurn, abandonPrediction, listPredictions, } from '../supabase.js';
10
+ import { getGraph, validateProject, entityName, entityLayer, entitySummary, } from './index.js';
11
+ import { isSupabaseConfigured, insertPrediction, updateActualBurn, abandonPrediction, } from '../supabase.js';
12
12
  // ─── Layer ordering for violation detection ──────────────────────
13
13
  const LAYER_ORDER = {
14
14
  route: 0,
@@ -82,13 +82,12 @@ function dominantEstimator(entities) {
82
82
  return 'heuristic';
83
83
  }
84
84
  // ─── Tool: find_dead_code ────────────────────────────────────────
85
- export function findDeadCode(args) {
86
- const projErr = validateProject(args.project);
85
+ export function findDeadCode(args, loader) {
86
+ const projErr = validateProject(args.project, loader);
87
87
  if (projErr)
88
88
  return { error: projErr };
89
89
  const { include_tests = false } = args;
90
- const loader = getLoader();
91
- const g = getGraph(args.project);
90
+ const g = getGraph(args.project, loader);
92
91
  const entities = loader.getEntities(args.project);
93
92
  // Entry points: routes, exported functions, test files, plugin registrations
94
93
  const entryPointIds = new Set();
@@ -132,9 +131,9 @@ export function findDeadCode(args) {
132
131
  }
133
132
  }
134
133
  // Unreachable = dead code candidates
135
- let deadEntities = entities.filter(e => e.id && !reachable.has(e.id));
134
+ let deadEntities = entities.filter((e) => e.id && !reachable.has(e.id));
136
135
  if (!include_tests) {
137
- deadEntities = deadEntities.filter(e => entityLayer(e) !== 'test');
136
+ deadEntities = deadEntities.filter((e) => entityLayer(e) !== 'test');
138
137
  }
139
138
  // Group by layer for overview
140
139
  const byLayer = new Map();
@@ -153,10 +152,10 @@ export function findDeadCode(args) {
153
152
  }
154
153
  // ─── Tool: find_layer_violations ─────────────────────────────────
155
154
  export function findLayerViolations(args) {
156
- const projErr = validateProject(args?.project);
155
+ const projErr = validateProject(args?.project, loader);
157
156
  if (projErr)
158
157
  return { error: projErr };
159
- const g = getGraph(args?.project);
158
+ const g = getGraph(args?.project, loader);
160
159
  const violations = [];
161
160
  for (const [callerId, calleeIds] of g.callees) {
162
161
  const callerEntity = g.entityById.get(callerId);
@@ -208,13 +207,12 @@ export function findLayerViolations(args) {
208
207
  };
209
208
  }
210
209
  // ─── Tool: get_coupling_metrics ──────────────────────────────────
211
- export function getCouplingMetrics(args) {
212
- const projErr = validateProject(args.project);
210
+ export function getCouplingMetrics(args, loader) {
211
+ const projErr = validateProject(args.project, loader);
213
212
  if (projErr)
214
213
  return { error: projErr };
215
214
  const { group_by = 'module' } = args;
216
- const loader = getLoader();
217
- const g = getGraph(args.project);
215
+ const g = getGraph(args.project, loader);
218
216
  const entities = loader.getEntities(args.project);
219
217
  // Group entities
220
218
  const groups = new Map();
@@ -281,12 +279,11 @@ export function getCouplingMetrics(args) {
281
279
  }
282
280
  // ─── Tool: get_auth_matrix ───────────────────────────────────────
283
281
  export function getAuthMatrix(args) {
284
- const projErr = validateProject(args?.project);
282
+ const projErr = validateProject(args?.project, loader);
285
283
  if (projErr)
286
284
  return { error: projErr };
287
- const loader = getLoader();
288
285
  const entities = loader.getEntities(args?.project);
289
- const apiEntities = entities.filter(e => {
286
+ const apiEntities = entities.filter((e) => {
290
287
  const layer = entityLayer(e);
291
288
  return layer === 'route' || layer === 'controller' ||
292
289
  e.context?.exposure === 'api';
@@ -339,11 +336,10 @@ export function getAuthMatrix(args) {
339
336
  }
340
337
  // ─── Tool: find_error_gaps ───────────────────────────────────────
341
338
  export function findErrorGaps(args) {
342
- const projErr = validateProject(args?.project);
339
+ const projErr = validateProject(args?.project, loader);
343
340
  if (projErr)
344
341
  return { error: projErr };
345
- const loader = getLoader();
346
- const g = getGraph(args?.project);
342
+ const g = getGraph(args?.project, loader);
347
343
  const entities = loader.getEntities(args?.project);
348
344
  // Find all fallible entities (throws === true or has THROWS tag)
349
345
  const fallibleIds = new Set();
@@ -405,13 +401,12 @@ export function findErrorGaps(args) {
405
401
  };
406
402
  }
407
403
  // ─── Tool: get_test_coverage ─────────────────────────────────────
408
- export function getTestCoverage(args) {
409
- const projErr = validateProject(args.project);
404
+ export function getTestCoverage(args, loader) {
405
+ const projErr = validateProject(args.project, loader);
410
406
  if (projErr)
411
407
  return { error: projErr };
412
408
  const { weight_by_blast_radius = false } = args;
413
- const loader = getLoader();
414
- const g = getGraph(args.project);
409
+ const g = getGraph(args.project, loader);
415
410
  const entities = loader.getEntities(args.project);
416
411
  // Partition into test and non-test entities
417
412
  const testIds = new Set();
@@ -445,8 +440,8 @@ export function getTestCoverage(args) {
445
440
  }
446
441
  }
447
442
  }
448
- const covered = productionEntities.filter(e => exercised.has(e.id));
449
- const uncovered = productionEntities.filter(e => !exercised.has(e.id));
443
+ const covered = productionEntities.filter((e) => exercised.has(e.id));
444
+ const uncovered = productionEntities.filter((e) => !exercised.has(e.id));
450
445
  const coveragePercent = productionEntities.length > 0
451
446
  ? Math.round((covered.length / productionEntities.length) * 1000) / 10
452
447
  : 0;
@@ -484,13 +479,12 @@ export function getTestCoverage(args) {
484
479
  return result;
485
480
  }
486
481
  // ─── Tool: get_optimal_context ───────────────────────────────────
487
- export function getOptimalContext(args) {
488
- const projErr = validateProject(args.project);
482
+ export function getOptimalContext(args, loader) {
483
+ const projErr = validateProject(args.project, loader);
489
484
  if (projErr)
490
485
  return { error: projErr };
491
486
  const { target_entity, max_tokens = 8000, strategy = 'bfs' } = args;
492
- const loader = getLoader();
493
- const g = getGraph(args.project);
487
+ const g = getGraph(args.project, loader);
494
488
  const entity = loader.getEntityById(target_entity, args.project) || loader.getEntityByName(target_entity, args.project);
495
489
  if (!entity) {
496
490
  return { error: `Entity not found: ${target_entity}` };
@@ -605,13 +599,12 @@ export function getOptimalContext(args) {
605
599
  * Computes blast radius, sums source token counts, and projects total burn.
606
600
  * Logs prediction to Supabase when configured (for calibration feedback loop).
607
601
  */
608
- export async function estimateTaskCost(args) {
609
- const projErr = validateProject(args.project);
602
+ export async function estimateTaskCost(args, loader) {
603
+ const projErr = validateProject(args.project, loader);
610
604
  if (projErr)
611
605
  return { error: projErr };
612
606
  const { target_entities, context_budget = 200000 } = args;
613
- const loader = getLoader();
614
- const g = getGraph(args.project);
607
+ const g = getGraph(args.project, loader);
615
608
  // Resolve target entities
616
609
  const resolvedTargets = [];
617
610
  const unresolvedNames = [];
@@ -674,7 +667,7 @@ export async function estimateTaskCost(args) {
674
667
  let outgoing = 0;
675
668
  let incoming = 0;
676
669
  const entities = loader.getEntities(args.project);
677
- const modIds = new Set(entities.filter(e => e.context?.module === mod).map(e => e.id));
670
+ const modIds = new Set(entities.filter((e) => e.context?.module === mod).map(e => e.id));
678
671
  for (const id of modIds) {
679
672
  const calleeSet = g.callees.get(id);
680
673
  if (calleeSet) {
@@ -772,7 +765,7 @@ export async function estimateTaskCost(args) {
772
765
  * Can also abandon a prediction (task was cancelled/not completed),
773
766
  * or list recent predictions for calibration analysis.
774
767
  */
775
- export async function reportActualBurn(args) {
768
+ export async function reportActualBurn(args, loader) {
776
769
  if (!isSupabaseConfigured()) {
777
770
  return {
778
771
  error: 'Supabase not configured. Set SESHAT_SUPABASE_URL and SESHAT_SUPABASE_KEY env vars.',
@@ -782,48 +775,9 @@ export async function reportActualBurn(args) {
782
775
  const { action = 'complete' } = args;
783
776
  // List mode: show recent predictions for calibration analysis
784
777
  if (action === 'list') {
785
- const rows = await listPredictions(args.project);
786
- if (rows.length === 0) {
787
- return { message: 'No predictions found.', predictions: [] };
788
- }
789
- const summary = rows.map((r) => ({
790
- id: r.id,
791
- project: r.project,
792
- targets: r.target_entities,
793
- predicted: r.predicted_total,
794
- actual: r.actual_total_tokens,
795
- drift: r.drift_ratio,
796
- estimator: r.estimator_used,
797
- status: r.status,
798
- createdAt: r.created_at,
799
- }));
800
- // Compute aggregate calibration stats for completed predictions
801
- const completed = rows.filter((r) => r.status === 'completed' && r.drift_ratio != null);
802
- let calibration;
803
- if (completed.length >= 3) {
804
- const drifts = completed.map((r) => r.drift_ratio);
805
- const meanDrift = drifts.reduce((a, b) => a + b, 0) / drifts.length;
806
- const sortedDrifts = [...drifts].sort((a, b) => a - b);
807
- const medianDrift = sortedDrifts[Math.floor(sortedDrifts.length / 2)];
808
- const maxOvershoot = Math.max(...drifts);
809
- const maxUndershoot = Math.min(...drifts);
810
- calibration = {
811
- completedSamples: completed.length,
812
- meanDrift: Math.round(meanDrift * 1000) / 1000,
813
- medianDrift: Math.round(medianDrift * 1000) / 1000,
814
- maxOvershoot: Math.round(maxOvershoot * 1000) / 1000,
815
- maxUndershoot: Math.round(maxUndershoot * 1000) / 1000,
816
- _interpretation: meanDrift > 0.2
817
- ? 'Predictions underestimate — consider increasing iteration multiplier.'
818
- : meanDrift < -0.2
819
- ? 'Predictions overestimate — consider decreasing iteration multiplier.'
820
- : 'Predictions are well-calibrated (within 20% mean drift).',
821
- };
822
- }
823
778
  return {
824
- total: rows.length,
825
- predictions: summary,
826
- ...(calibration ? { calibration } : {}),
779
+ message: 'List predictions has been migrated to the Ptah Dashboard.',
780
+ predictions: []
827
781
  };
828
782
  }
829
783
  // Complete or abandon requires prediction_id
@@ -869,10 +823,10 @@ export async function reportActualBurn(args) {
869
823
  }
870
824
  // ─── Tool: find_runtime_violations (Runtime Context) ────────────
871
825
  export function find_runtime_violations(args) {
872
- const projErr = validateProject(args?.project);
826
+ const projErr = validateProject(args?.project, loader);
873
827
  if (projErr)
874
828
  return { error: projErr };
875
- const g = getGraph(args?.project);
829
+ const g = getGraph(args?.project, loader);
876
830
  const violations = [];
877
831
  for (const [callerId, calleeIds] of g.callees) {
878
832
  const callerEntity = g.entityById.get(callerId);
@@ -910,10 +864,9 @@ export function find_runtime_violations(args) {
910
864
  }
911
865
  // ─── Tool: find_ownership_violations (Ownership & Lifetimes) ─────
912
866
  export function find_ownership_violations(args) {
913
- const projErr = validateProject(args?.project);
867
+ const projErr = validateProject(args?.project, loader);
914
868
  if (projErr)
915
869
  return { error: projErr };
916
- const loader = getLoader();
917
870
  const entities = loader.getEntities(args?.project);
918
871
  const violations = [];
919
872
  for (const e of entities) {
@@ -950,19 +903,18 @@ export function find_ownership_violations(args) {
950
903
  };
951
904
  }
952
905
  // ─── Tool: query_traits (Trait Search) ───────────────────────────
953
- export function query_traits(args) {
954
- const projErr = validateProject(args.project);
906
+ export function query_traits(args, loader) {
907
+ const projErr = validateProject(args.project, loader);
955
908
  if (projErr)
956
909
  return { error: projErr };
957
910
  const target = args.trait.toLowerCase();
958
- const loader = getLoader();
959
911
  const entities = loader.getEntities(args.project);
960
- const results = entities.filter(e => {
912
+ const results = entities.filter((e) => {
961
913
  if (!e.traits)
962
914
  return false;
963
915
  // Handle array of strings [ 'asyncContext', 'generator' ]
964
916
  if (Array.isArray(e.traits)) {
965
- return e.traits.some(t => t.toLowerCase().includes(target));
917
+ return e.traits.some((t) => t.toLowerCase().includes(target));
966
918
  }
967
919
  // Handle structured traits { self: { fallible: true } }
968
920
  if (typeof e.traits === 'object') {
@@ -994,12 +946,11 @@ export function query_traits(args) {
994
946
  };
995
947
  }
996
948
  // ─── Tool: simulate_mutation (Impact Simulator) ──────────────────
997
- export function simulate_mutation(args) {
998
- const projErr = validateProject(args.project);
949
+ export function simulate_mutation(args, loader) {
950
+ const projErr = validateProject(args.project, loader);
999
951
  if (projErr)
1000
952
  return { error: projErr };
1001
- const loader = getLoader();
1002
- const g = getGraph(args.project);
953
+ const g = getGraph(args.project, loader);
1003
954
  const targetEntity = loader.getEntityById(args.entity_id, args.project)
1004
955
  || loader.getEntityByName(args.entity_id, args.project);
1005
956
  if (!targetEntity) {
@@ -1075,11 +1026,10 @@ export function simulate_mutation(args) {
1075
1026
  };
1076
1027
  }
1077
1028
  // ─── Tool: query_data_targets (Data Flow Analysis) ──────────────
1078
- export function query_data_targets(args) {
1079
- const projErr = validateProject(args.project);
1029
+ export function query_data_targets(args, loader) {
1030
+ const projErr = validateProject(args.project, loader);
1080
1031
  if (projErr)
1081
1032
  return { error: projErr };
1082
- const loader = getLoader();
1083
1033
  const entities = loader.getEntities(args.project);
1084
1034
  const target = args.target_name.toLowerCase();
1085
1035
  const readers = [];
@@ -1090,24 +1040,24 @@ export function query_data_targets(args) {
1090
1040
  let isReader = false;
1091
1041
  let isWriter = false;
1092
1042
  // Check tables
1093
- if (Array.isArray(e.data.tables) && e.data.tables.some(t => String(t).toLowerCase() === target)) {
1043
+ if (Array.isArray(e.data.tables) && e.data.tables.some((t) => String(t).toLowerCase() === target)) {
1094
1044
  // By default, if they touch a table but don't specify mutation, we assume read
1095
1045
  isReader = true;
1096
1046
  }
1097
1047
  // Check inputs (sources)
1098
1048
  if (Array.isArray(e.data.inputs)) {
1099
- if (e.data.inputs.some(i => i && typeof i === 'object' && String(i.name || i.source).toLowerCase() === target)) {
1049
+ if (e.data.inputs.some((i) => i && typeof i === 'object' && String(i.name || i.source).toLowerCase() === target)) {
1100
1050
  isReader = true;
1101
1051
  }
1102
1052
  }
1103
1053
  else if (Array.isArray(e.data.sources)) {
1104
- if (e.data.sources.some(s => String(s).toLowerCase() === target)) {
1054
+ if (e.data.sources.some((s) => String(s).toLowerCase() === target)) {
1105
1055
  isReader = true;
1106
1056
  }
1107
1057
  }
1108
1058
  // Check mutations
1109
1059
  if (Array.isArray(e.data.mutations)) {
1110
- if (e.data.mutations.some(m => m && typeof m === 'object' && String(m.target).toLowerCase() === target)) {
1060
+ if (e.data.mutations.some((m) => m && typeof m === 'object' && String(m.target).toLowerCase() === target)) {
1111
1061
  isWriter = true;
1112
1062
  }
1113
1063
  }
@@ -1130,10 +1080,10 @@ export function query_data_targets(args) {
1130
1080
  }
1131
1081
  // ─── Tool: find_exposure_leaks (Architectural Visibility) ───────
1132
1082
  export function find_exposure_leaks(args) {
1133
- const projErr = validateProject(args?.project);
1083
+ const projErr = validateProject(args?.project, loader);
1134
1084
  if (projErr)
1135
1085
  return { error: projErr };
1136
- const g = getGraph(args?.project);
1086
+ const g = getGraph(args?.project, loader);
1137
1087
  const leaks = [];
1138
1088
  for (const [callerId, calleeIds] of g.callees) {
1139
1089
  const callerEntity = g.entityById.get(callerId);
@@ -1165,10 +1115,9 @@ export function find_exposure_leaks(args) {
1165
1115
  // ─── Tool: find_semantic_clones (Logic Analysis) ────────────────
1166
1116
  import { createHash } from 'crypto';
1167
1117
  export function find_semantic_clones(args) {
1168
- const projErr = validateProject(args?.project);
1118
+ const projErr = validateProject(args?.project, loader);
1169
1119
  if (projErr)
1170
1120
  return { error: projErr };
1171
- const loader = getLoader();
1172
1121
  const entities = loader.getEntities(args?.project);
1173
1122
  const { min_complexity = 5 } = args || {};
1174
1123
  const logicHashes = new Map();
@@ -1212,11 +1161,10 @@ export function find_semantic_clones(args) {
1212
1161
  };
1213
1162
  }
1214
1163
  // ─── Tool: create_symbol (Virtual Entry) ─────────────────────────
1215
- export function create_symbol(args) {
1216
- const projErr = validateProject(args.project);
1164
+ export function create_symbol(args, loader) {
1165
+ const projErr = validateProject(args.project, loader);
1217
1166
  if (projErr)
1218
1167
  return { error: projErr };
1219
- const loader = getLoader();
1220
1168
  // Create a skeleton JSTF-T entity
1221
1169
  const virtualEntity = {
1222
1170
  id: args.id,
@@ -8,17 +8,14 @@
8
8
  * `project` parameter. In multi-project mode, `project` is required.
9
9
  */
10
10
  import type { JstfEntity } from '../types.js';
11
- import { MultiLoader } from '../loader.js';
12
11
  import { type CallGraph } from '../graph.js';
13
- export declare function initTools(multiLoader: MultiLoader): void;
14
- export declare function getLoader(): MultiLoader;
15
- export declare function getGraph(project?: string): CallGraph;
12
+ export declare function getGraph(project: string | undefined, loader: ProjectLoader): CallGraph;
16
13
  /**
17
14
  * Validate project param. Returns error string if invalid, null if OK.
18
15
  * In single-project mode, project is optional (defaults to the only project).
19
16
  * In multi-project mode, project is required.
20
17
  */
21
- export declare function validateProject(project?: string): string | null;
18
+ export declare function validateProject(project: string | undefined, loader: ProjectLoader): string | null;
22
19
  export declare function entityName(e: JstfEntity): string;
23
20
  export declare function entityLayer(e: JstfEntity): string;
24
21
  export declare function entitySummary(e: JstfEntity): Record<string, unknown>;
@@ -35,34 +32,34 @@ export declare function queryEntities(args: {
35
32
  module?: string;
36
33
  language?: string;
37
34
  limit?: number;
38
- }): unknown;
35
+ }, loader: ProjectLoader): Promise<unknown>;
39
36
  export declare function getEntity(args: {
40
37
  id: string;
41
38
  project?: string;
42
- }): unknown;
39
+ }, loader: ProjectLoader): unknown;
43
40
  export declare function getDependencies(args: {
44
41
  entity_id: string;
45
42
  direction?: 'callers' | 'callees' | 'both';
46
43
  depth?: number;
47
44
  project?: string;
48
- }): unknown;
45
+ }, loader: ProjectLoader): unknown;
49
46
  export declare function collectTransitive(adjacency: Map<string, Set<string>>, startId: string, maxDepth: number): string[];
50
47
  export declare function getDataFlow(args: {
51
48
  entity_id: string;
52
49
  project?: string;
53
- }): unknown;
50
+ }, loader: ProjectLoader): unknown;
54
51
  export declare function findByConstraint(args: {
55
52
  constraint: string;
56
53
  project?: string;
57
- }): unknown;
54
+ }, loader: ProjectLoader): unknown;
58
55
  export declare function getBlastRadius(args: {
59
56
  entity_ids: string[];
60
57
  project?: string;
61
- }): unknown;
58
+ }, loader: ProjectLoader): unknown;
62
59
  export declare function listModules(args: {
63
60
  group_by?: 'layer' | 'module' | 'file' | 'language';
64
61
  project?: string;
65
- }): unknown;
66
- export declare function getTopology(args?: {
62
+ }, loader: ProjectLoader): unknown;
63
+ export declare function getTopology(args: {
67
64
  project?: string;
68
- }): unknown;
65
+ } | undefined, loader: ProjectLoader): unknown;
@@ -8,29 +8,15 @@
8
8
  * `project` parameter. In multi-project mode, `project` is required.
9
9
  */
10
10
  import { buildCallGraph, computeBlastRadius } from '../graph.js';
11
- // Lazy-initialized shared state
12
- let loader;
13
- let graphCache = new Map();
14
- export function initTools(multiLoader) {
15
- loader = multiLoader;
16
- graphCache = new Map();
17
- }
18
- export function getLoader() {
19
- return loader;
20
- }
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);
11
+ export function getGraph(project, loader) {
12
+ return buildCallGraph(loader.getEntities(project));
27
13
  }
28
14
  /**
29
15
  * Validate project param. Returns error string if invalid, null if OK.
30
16
  * In single-project mode, project is optional (defaults to the only project).
31
17
  * In multi-project mode, project is required.
32
18
  */
33
- export function validateProject(project) {
19
+ export function validateProject(project, loader) {
34
20
  if (!loader.isMultiProject())
35
21
  return null; // single project, always OK
36
22
  if (!project) {
@@ -155,7 +141,7 @@ export function normalizeConstraints(constraints) {
155
141
  export function constraintMatches(constraints, target) {
156
142
  // First check normalized tags
157
143
  const tags = normalizeConstraints(constraints);
158
- if (tags.some(t => t.toUpperCase().includes(target.toUpperCase())))
144
+ if (tags.some((t) => t.toUpperCase().includes(target.toUpperCase())))
159
145
  return true;
160
146
  // Also search raw constraint values for substring match
161
147
  if (constraints && typeof constraints === 'object' && !Array.isArray(constraints)) {
@@ -166,42 +152,40 @@ export function constraintMatches(constraints, target) {
166
152
  return false;
167
153
  }
168
154
  // ─── Tool: query_entities ─────────────────────────────────────────
169
- export function queryEntities(args) {
170
- const projErr = validateProject(args.project);
171
- if (projErr)
172
- return { error: projErr };
173
- const { query, layer, module, language, limit = 50 } = args;
174
- let results = loader.getEntities(args.project);
175
- if (layer) {
176
- results = results.filter(e => entityLayer(e).toLowerCase() === layer.toLowerCase());
177
- }
178
- if (module) {
179
- results = results.filter(e => e.context?.module?.toLowerCase().includes(module.toLowerCase()));
155
+ export async function queryEntities(args, loader) {
156
+ const apiKey = process.env.SESHAT_API_KEY;
157
+ if (!apiKey) {
158
+ return { error: 'SESHAT_API_KEY environment variable is missing. You must provide an API key to use Ptah Cloud tools.' };
180
159
  }
181
- if (language) {
182
- results = results.filter(e => e._sourceLanguage?.toLowerCase() === language.toLowerCase());
183
- }
184
- if (query) {
185
- const q = query.toLowerCase();
186
- results = results.filter(e => {
187
- const name = entityName(e).toLowerCase();
188
- const file = (e._sourceFile || '').toLowerCase();
189
- const mod = (e.context?.module || '').toLowerCase();
190
- const id = (e.id || '').toLowerCase();
191
- return name.includes(q) || file.includes(q) || mod.includes(q) || id.includes(q);
160
+ // We are hardcoding the project_hash to 'seshat-fabric' for this local test,
161
+ // but in production the MCP would pass args.project or read from the manifest.
162
+ const project_hash = args.project || 'seshat-fabric';
163
+ try {
164
+ const res = await fetch('http://localhost:3001/api/mcp/execute', {
165
+ method: 'POST',
166
+ headers: {
167
+ 'Content-Type': 'application/json',
168
+ 'x-api-key': apiKey
169
+ },
170
+ body: JSON.stringify({
171
+ tool: 'query_entities',
172
+ project_hash: project_hash,
173
+ args
174
+ })
192
175
  });
176
+ if (!res.ok) {
177
+ const errorText = await res.text();
178
+ return { error: `Cloud API Error (${res.status}): ${errorText}` };
179
+ }
180
+ return await res.json();
181
+ }
182
+ catch (error) {
183
+ return { error: `Failed to connect to Ptah Cloud: ${error.message}` };
193
184
  }
194
- const total = results.length;
195
- const entities = results.slice(0, limit).map(entitySummary);
196
- return {
197
- total,
198
- showing: entities.length,
199
- entities,
200
- };
201
185
  }
202
186
  // ─── Tool: get_entity ─────────────────────────────────────────────
203
- export function getEntity(args) {
204
- const projErr = validateProject(args.project);
187
+ export function getEntity(args, loader) {
188
+ const projErr = validateProject(args.project, loader);
205
189
  if (projErr)
206
190
  return { error: projErr };
207
191
  const entity = loader.getEntityById(args.id, args.project) || loader.getEntityByName(args.id, args.project);
@@ -226,12 +210,12 @@ export function getEntity(args) {
226
210
  };
227
211
  }
228
212
  // ─── Tool: get_dependencies ───────────────────────────────────────
229
- export function getDependencies(args) {
230
- const projErr = validateProject(args.project);
213
+ export function getDependencies(args, loader) {
214
+ const projErr = validateProject(args.project, loader);
231
215
  if (projErr)
232
216
  return { error: projErr };
233
217
  const { entity_id, direction = 'both', depth = 2 } = args;
234
- const g = getGraph(args.project);
218
+ const g = getGraph(args.project, loader);
235
219
  const entity = loader.getEntityById(entity_id, args.project) || loader.getEntityByName(entity_id, args.project);
236
220
  if (!entity) {
237
221
  return { error: `Entity not found: ${entity_id}` };
@@ -282,8 +266,8 @@ export function collectTransitive(adjacency, startId, maxDepth) {
282
266
  return [...visited];
283
267
  }
284
268
  // ─── Tool: get_data_flow ──────────────────────────────────────────
285
- export function getDataFlow(args) {
286
- const projErr = validateProject(args.project);
269
+ export function getDataFlow(args, loader) {
270
+ const projErr = validateProject(args.project, loader);
287
271
  if (projErr)
288
272
  return { error: projErr };
289
273
  const entity = loader.getEntityById(args.entity_id, args.project) || loader.getEntityByName(args.entity_id, args.project);
@@ -298,12 +282,12 @@ export function getDataFlow(args) {
298
282
  };
299
283
  }
300
284
  // ─── Tool: find_by_constraint ─────────────────────────────────────
301
- export function findByConstraint(args) {
302
- const projErr = validateProject(args.project);
285
+ export function findByConstraint(args, loader) {
286
+ const projErr = validateProject(args.project, loader);
303
287
  if (projErr)
304
288
  return { error: projErr };
305
289
  const target = args.constraint;
306
- const results = loader.getEntities(args.project).filter(e => constraintMatches(e.constraints, target));
290
+ const results = loader.getEntities(args.project).filter((e) => constraintMatches(e.constraints, target));
307
291
  return {
308
292
  constraint: args.constraint,
309
293
  total: results.length,
@@ -311,11 +295,11 @@ export function findByConstraint(args) {
311
295
  };
312
296
  }
313
297
  // ─── Tool: get_blast_radius ───────────────────────────────────────
314
- export function getBlastRadius(args) {
315
- const projErr = validateProject(args.project);
298
+ export function getBlastRadius(args, loader) {
299
+ const projErr = validateProject(args.project, loader);
316
300
  if (projErr)
317
301
  return { error: projErr };
318
- const g = getGraph(args.project);
302
+ const g = getGraph(args.project, loader);
319
303
  // Resolve names to IDs
320
304
  const resolvedIds = new Set();
321
305
  const notFound = [];
@@ -348,8 +332,8 @@ export function getBlastRadius(args) {
348
332
  };
349
333
  }
350
334
  // ─── Tool: list_modules ───────────────────────────────────────────
351
- export function listModules(args) {
352
- const projErr = validateProject(args.project);
335
+ export function listModules(args, loader) {
336
+ const projErr = validateProject(args.project, loader);
353
337
  if (projErr)
354
338
  return { error: projErr };
355
339
  const { group_by = 'layer' } = args;
@@ -385,8 +369,8 @@ export function listModules(args) {
385
369
  };
386
370
  }
387
371
  // ─── Tool: get_topology ───────────────────────────────────────────
388
- export function getTopology(args) {
389
- const projErr = validateProject(args?.project);
372
+ export function getTopology(args, loader) {
373
+ const projErr = validateProject(args?.project, loader);
390
374
  if (projErr)
391
375
  return { error: projErr };
392
376
  const topology = loader.getTopology(args?.project);
package/dist/types.d.ts CHANGED
@@ -158,3 +158,17 @@ export interface ProjectInfo {
158
158
  extractedAt: string;
159
159
  layers: Record<string, number>;
160
160
  }
161
+ export interface ProjectLoader {
162
+ getEntities(project?: string): JstfEntity[];
163
+ getTopology(project?: string): Topology | null;
164
+ getManifest(project?: string): Manifest | null;
165
+ getEntityById(id: string, project?: string): JstfEntity | undefined;
166
+ getEntityByName(name: string, project?: string): JstfEntity | undefined;
167
+ getProjectNames(): string[];
168
+ getProjectInfo(): ProjectInfo[];
169
+ isMultiProject(): boolean;
170
+ isLoaded(): boolean;
171
+ hasProject(name: string): boolean;
172
+ registerVirtualEntity(entity: JstfEntity, project?: string): void;
173
+ totalEntities(): number;
174
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papyruslabsai/seshat-mcp",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Semantic MCP server — exposes a codebase's structure, dependencies, and constraints as queryable tools",
5
5
  "type": "module",
6
6
  "bin": {