@papyruslabsai/seshat-mcp 0.19.0 → 0.20.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.js CHANGED
@@ -341,6 +341,31 @@ const TOOLS = [
341
341
  },
342
342
  annotations: READ_ONLY_OPEN,
343
343
  },
344
+ {
345
+ name: 'find_entry_points',
346
+ title: 'Find Entry Points',
347
+ description: 'List the ways into the system: route/controller handlers, test entries, framework plugin registrations, and exported symbols (the public API surface), classified by kind and ranked by reach. Call it first when orienting in an unfamiliar codebase — it answers "where does execution start, and what is the public surface?" Complements list_modules (structure) and find_dead_code (its exact inverse: these are the reachability roots).',
348
+ inputSchema: {
349
+ type: 'object',
350
+ properties: { project: projectParam },
351
+ },
352
+ annotations: READ_ONLY_OPEN,
353
+ },
354
+ {
355
+ name: 'trace_data_path',
356
+ title: 'Trace Data Path',
357
+ description: 'Follow data from one function through the call graph to its sinks. From a start entity it walks callees, records the data each hop consumes/produces/mutates, and reports the chain from the start to every sink the data reaches — database writes, network egress, filesystem writes — plus the tables touched. Call it before changing a function that handles real data: it answers "where does this data end up?", the cross-call composition get_data_flow (one entity) cannot give. Flags untrusted inputs at the source.',
358
+ inputSchema: {
359
+ type: 'object',
360
+ properties: {
361
+ project: projectParam,
362
+ entity_id: { type: 'string', description: 'Entity ID or name to trace data flow from' },
363
+ max_depth: { type: 'number', description: 'How many call hops to follow downstream (default: 5, max: 8)' },
364
+ },
365
+ required: ['entity_id'],
366
+ },
367
+ annotations: READ_ONLY_OPEN,
368
+ },
344
369
  // ─── Analyst Tools (Tier 2) ───────────────────────────────────────
345
370
  {
346
371
  name: 'find_dead_code',
@@ -415,13 +440,9 @@ const TOOLS = [
415
440
  },
416
441
  annotations: READ_ONLY_OPEN,
417
442
  },
418
- {
419
- name: 'find_runtime_violations',
420
- title: 'Find Runtime Violations',
421
- description: 'Find architectural boundary leaks where framework-agnostic code imports framework-specific code. Use this when separating core logic from framework dependencies. Returns 0 for most JS/Python codebases — a non-zero result indicates a serious boundary violation worth investigating.',
422
- inputSchema: { type: 'object', properties: { project: projectParam } },
423
- annotations: READ_ONLY_OPEN,
424
- },
443
+ // find_runtime_violations RETIRED 2026-06-12 (BUILD-LIST B1): built against
444
+ // the v1 runtime-tagging model; the server now answers it with an honest
445
+ // retirement notice for old clients. Discipline-aware rebuild planned.
425
446
  {
426
447
  name: 'find_ownership_violations',
427
448
  title: 'Find Ownership Violations',
@@ -631,7 +652,7 @@ function getCloudUrl(path) {
631
652
  async function main() {
632
653
  const server = new Server({
633
654
  name: 'seshat',
634
- version: '0.16.10',
655
+ version: '0.20.0',
635
656
  }, {
636
657
  capabilities: { tools: {} },
637
658
  instructions: SERVER_INSTRUCTIONS,
@@ -16,6 +16,32 @@ export declare function findDeadCode(args: {
16
16
  include_tests?: boolean;
17
17
  project?: string;
18
18
  }, loader: ProjectLoader): unknown;
19
+ /**
20
+ * The ways into the system — the reachability roots find_dead_code already
21
+ * computes, surfaced as a tool and classified by KIND. An entry point is a
22
+ * route/controller, a test, a framework plugin registration, or an exported
23
+ * symbol (the library's public API surface). entryPointCount here reconciles
24
+ * with find_dead_code's `entryPoints` (same seed definition).
25
+ */
26
+ export declare function findEntryPoints(args: {
27
+ project?: string;
28
+ }, loader: ProjectLoader): unknown;
29
+ /**
30
+ * Follow data from one entity through the call graph to its sinks. Composes
31
+ * the data dimension (inputs/outputs/mutations/db) with call edges: from a
32
+ * start entity, walk callees up to max_depth, record the data ops at each hop,
33
+ * and report the chain from the start to every sink it reaches (db write,
34
+ * network egress, fs write). Answers "I'm touching this function — where does
35
+ * its data end up?" — the cross-edge composition get_data_flow can't give for
36
+ * a single entity. Pairs with trace_boundaries (observability) and
37
+ * query_data_targets (who-touches-a-target); this one is the path itself.
38
+ */
39
+ export declare function traceDataPath(args: {
40
+ entity?: string;
41
+ entity_id?: string;
42
+ project?: string;
43
+ max_depth?: number;
44
+ }, loader: ProjectLoader): unknown;
19
45
  export declare function findLayerViolations(args: {
20
46
  project?: string;
21
47
  }, loader: ProjectLoader): unknown;
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import path from 'path';
9
9
  import { computeBlastRadius } from '../graph.js';
10
- import { getGraph, validateProject, entityName, entityLayer, entitySummary, } from './index.js';
10
+ import { getGraph, validateProject, entityName, entityLayer, entitySummary, normalizeConstraints, } from './index.js';
11
11
  import { isSupabaseConfigured, insertPrediction, updateActualBurn, abandonPrediction, } from '../supabase.js';
12
12
  import { extractDbOps } from './dbops.js';
13
13
  // ─── Layer ordering for violation detection ──────────────────────
@@ -164,6 +164,166 @@ export function findDeadCode(args, loader) {
164
164
  } : {}),
165
165
  };
166
166
  }
167
+ // ─── Tool: find_entry_points (B6) ────────────────────────────────
168
+ /**
169
+ * The ways into the system — the reachability roots find_dead_code already
170
+ * computes, surfaced as a tool and classified by KIND. An entry point is a
171
+ * route/controller, a test, a framework plugin registration, or an exported
172
+ * symbol (the library's public API surface). entryPointCount here reconciles
173
+ * with find_dead_code's `entryPoints` (same seed definition).
174
+ */
175
+ export function findEntryPoints(args, loader) {
176
+ const projErr = validateProject(args.project, loader);
177
+ if (projErr)
178
+ return { error: projErr };
179
+ const g = getGraph(args.project, loader);
180
+ const entities = loader.getEntities(args.project);
181
+ // route/controller first (runtime entries), then plugins, tests, exports.
182
+ const KIND_RANK = { route: 0, plugin: 1, test: 2, export: 3 };
183
+ const entryPoints = [];
184
+ for (const e of entities) {
185
+ if (!e.id)
186
+ continue;
187
+ const layer = entityLayer(e);
188
+ let kind = null;
189
+ if (layer === 'route' || layer === 'controller')
190
+ kind = 'route';
191
+ else if (layer === 'test')
192
+ kind = 'test';
193
+ else if (e.context?.exposure === 'framework')
194
+ kind = 'plugin';
195
+ else if (typeof e.struct !== 'string' && e.struct?.exported)
196
+ kind = 'export';
197
+ if (!kind)
198
+ continue;
199
+ entryPoints.push({ ...entitySummary(e), kind, directCallees: g.callees.get(e.id)?.size || 0 });
200
+ }
201
+ const byKind = {};
202
+ for (const ep of entryPoints)
203
+ byKind[ep.kind] = (byKind[ep.kind] || 0) + 1;
204
+ entryPoints.sort((a, b) => (KIND_RANK[a.kind] - KIND_RANK[b.kind]) ||
205
+ (b.directCallees - a.directCallees));
206
+ return {
207
+ totalEntities: entities.length,
208
+ entryPointCount: entryPoints.length,
209
+ byKind,
210
+ entryPoints: entryPoints.slice(0, 100),
211
+ };
212
+ }
213
+ // ─── Tool: trace_data_path (B6, δ×ε compose) ─────────────────────
214
+ /**
215
+ * Follow data from one entity through the call graph to its sinks. Composes
216
+ * the data dimension (inputs/outputs/mutations/db) with call edges: from a
217
+ * start entity, walk callees up to max_depth, record the data ops at each hop,
218
+ * and report the chain from the start to every sink it reaches (db write,
219
+ * network egress, fs write). Answers "I'm touching this function — where does
220
+ * its data end up?" — the cross-edge composition get_data_flow can't give for
221
+ * a single entity. Pairs with trace_boundaries (observability) and
222
+ * query_data_targets (who-touches-a-target); this one is the path itself.
223
+ */
224
+ export function traceDataPath(args, loader) {
225
+ const projErr = validateProject(args.project, loader);
226
+ if (projErr)
227
+ return { error: projErr };
228
+ const ref = args.entity_id || args.entity;
229
+ if (!ref)
230
+ return { error: 'trace_data_path requires an entity (id or name)' };
231
+ const start = loader.getEntityById(ref, args.project) || loader.getEntityByName(ref, args.project);
232
+ if (!start || !start.id)
233
+ return { error: `Entity not found: ${ref}` };
234
+ const g = getGraph(args.project, loader);
235
+ const maxDepth = Math.min(Math.max(args.max_depth ?? 5, 1), 8);
236
+ const dataOf = (e) => {
237
+ const arr = (v) => (Array.isArray(v) ? v : []);
238
+ const data = (e.data || {});
239
+ const inputs = arr(data.inputs).length ? arr(data.inputs) : arr(data.sources);
240
+ const untrusted = inputs.filter((i) => i && typeof i === 'object' && i.untrusted).length;
241
+ const tags = normalizeConstraints(e.constraints);
242
+ const egress = [];
243
+ if (tags.includes('NETWORK_IO'))
244
+ egress.push('network');
245
+ if (tags.includes('FS_ACCESS'))
246
+ egress.push('fs');
247
+ return {
248
+ in: inputs.length,
249
+ out: (arr(data.outputs).length ? arr(data.outputs) : arr(data.returns)).length,
250
+ mut: arr(data.mutations).length,
251
+ untrusted,
252
+ db: extractDbOps(e).map((o) => ({ table: o.table, op: o.type === 'db_mutate' ? 'write' : 'read' })),
253
+ egress,
254
+ };
255
+ };
256
+ // A node is a sink when data leaves the program through it.
257
+ const sinkKind = (d) => {
258
+ if (d.db.some((o) => o.op === 'write'))
259
+ return 'db-write';
260
+ if (d.egress.includes('network'))
261
+ return 'network';
262
+ if (d.egress.includes('fs'))
263
+ return 'fs';
264
+ return null;
265
+ };
266
+ // BFS downstream over callees; keep one parent per node to reconstruct paths.
267
+ const nodes = new Map();
268
+ const order = [];
269
+ const queue = [[start.id, 0, null]];
270
+ const seen = new Set([start.id]);
271
+ while (queue.length > 0) {
272
+ const [id, depth, parent] = queue.shift();
273
+ const e = g.entityById.get(id);
274
+ if (!e)
275
+ continue;
276
+ const data = dataOf(e);
277
+ nodes.set(id, { entity: e, data, depth, parent, sink: sinkKind(data) });
278
+ order.push(id);
279
+ if (depth >= maxDepth)
280
+ continue;
281
+ for (const callee of g.callees.get(id) || []) {
282
+ if (!seen.has(callee)) {
283
+ seen.add(callee);
284
+ queue.push([callee, depth + 1, id]);
285
+ }
286
+ }
287
+ }
288
+ const pathTo = (id) => {
289
+ const chain = [];
290
+ let cur = id;
291
+ const guard = new Set();
292
+ while (cur != null && !guard.has(cur)) {
293
+ guard.add(cur);
294
+ chain.unshift(cur);
295
+ cur = nodes.get(cur)?.parent ?? null;
296
+ }
297
+ return chain;
298
+ };
299
+ const sinkIds = order.filter((id) => nodes.get(id).sink);
300
+ const chainNode = (id) => {
301
+ const n = nodes.get(id);
302
+ return { ...entitySummary(n.entity), depth: n.depth, data: n.data, sink: n.sink };
303
+ };
304
+ const paths = sinkIds.slice(0, 30).map((id) => ({
305
+ sinkKind: nodes.get(id).sink,
306
+ chain: pathTo(id).map(chainNode),
307
+ }));
308
+ const tableOps = new Map();
309
+ for (const id of order) {
310
+ for (const op of nodes.get(id).data.db) {
311
+ if (!tableOps.has(op.table))
312
+ tableOps.set(op.table, new Set());
313
+ tableOps.get(op.table).add(op.op);
314
+ }
315
+ }
316
+ return {
317
+ entity: { id: start.id, name: entityName(start) },
318
+ maxDepth,
319
+ reached: Math.max(0, order.length - 1),
320
+ sinkCount: sinkIds.length,
321
+ untrustedAtSource: nodes.get(start.id).data.untrusted,
322
+ tables: [...tableOps.entries()].map(([table, ops]) => ({ table, ops: [...ops].sort() })),
323
+ paths,
324
+ pathsTruncated: sinkIds.length > 30 ? sinkIds.length - 30 : 0,
325
+ };
326
+ }
167
327
  // ─── Tool: find_layer_violations ─────────────────────────────────
168
328
  export function findLayerViolations(args, loader) {
169
329
  const projErr = validateProject(args?.project, loader);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papyruslabsai/seshat-mcp",
3
- "version": "0.19.0",
3
+ "version": "0.20.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": {