@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 +29 -8
- package/dist/tools/functors.d.ts +26 -0
- package/dist/tools/functors.js +161 -1
- package/package.json +1 -1
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
|
-
|
|
420
|
-
|
|
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.
|
|
655
|
+
version: '0.20.0',
|
|
635
656
|
}, {
|
|
636
657
|
capabilities: { tools: {} },
|
|
637
658
|
instructions: SERVER_INSTRUCTIONS,
|
package/dist/tools/functors.d.ts
CHANGED
|
@@ -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;
|
package/dist/tools/functors.js
CHANGED
|
@@ -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