@papyruslabsai/seshat-mcp 0.16.7 → 0.16.8
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 +26 -22
- package/dist/index.js +37 -28
- package/dist/tools/index.js +6 -0
- package/package.json +1 -1
package/dist/graph.js
CHANGED
|
@@ -29,6 +29,25 @@ export function buildCallGraph(entities) {
|
|
|
29
29
|
entityByModule.get(mod).push(entity);
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
|
+
// Suffix index: map last dotted segment → first matching entity ID (for O(1) suffix lookups)
|
|
33
|
+
const bySuffix = new Map();
|
|
34
|
+
for (const [id] of entityById) {
|
|
35
|
+
const suffix = id.split('.').pop();
|
|
36
|
+
if (suffix && !bySuffix.has(suffix))
|
|
37
|
+
bySuffix.set(suffix, id);
|
|
38
|
+
}
|
|
39
|
+
// Filepath index: map source file paths and basenames → entity ID (for O(1) import resolution)
|
|
40
|
+
const byFilePath = new Map();
|
|
41
|
+
for (const [id, entity] of entityById) {
|
|
42
|
+
const src = entity._sourceFile || entity.context?.module || '';
|
|
43
|
+
if (src) {
|
|
44
|
+
const basename = src.split('/').pop()?.replace(/\.[^.]+$/, '') || '';
|
|
45
|
+
if (basename && !byFilePath.has(basename))
|
|
46
|
+
byFilePath.set(basename, id);
|
|
47
|
+
if (!byFilePath.has(src))
|
|
48
|
+
byFilePath.set(src, id);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
32
51
|
// Build call edges from dependency data
|
|
33
52
|
for (const caller of entities) {
|
|
34
53
|
if (!caller.id || !caller.edges)
|
|
@@ -72,14 +91,9 @@ export function buildCallGraph(entities) {
|
|
|
72
91
|
calleeId = `${modulePart}.${methodPart}`;
|
|
73
92
|
}
|
|
74
93
|
}
|
|
75
|
-
// Strategy 3:
|
|
94
|
+
// Strategy 3: Suffix match via index (O(1) lookup)
|
|
76
95
|
if (!calleeId) {
|
|
77
|
-
|
|
78
|
-
if (id.endsWith(`.${target}`) || id === target) {
|
|
79
|
-
calleeId = id;
|
|
80
|
-
break;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
96
|
+
calleeId = bySuffix.get(target) || null;
|
|
83
97
|
}
|
|
84
98
|
if (calleeId && calleeId !== caller.id) {
|
|
85
99
|
callees.get(caller.id)?.add(calleeId);
|
|
@@ -96,15 +110,9 @@ export function buildCallGraph(entities) {
|
|
|
96
110
|
const source = imp.source || imp.module;
|
|
97
111
|
if (!source)
|
|
98
112
|
continue;
|
|
99
|
-
// Attempt to find the imported module or entity
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const eSource = entity._sourceFile || entity.context?.module || '';
|
|
103
|
-
if (eSource && (eSource.includes(source) || source.includes(eSource))) {
|
|
104
|
-
calleeId = id;
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
113
|
+
// Attempt to find the imported module or entity via filepath index (O(1) lookup)
|
|
114
|
+
const sourceBasename = source.split('/').pop()?.replace(/\.[^.]+$/, '') || '';
|
|
115
|
+
calleeId = byFilePath.get(source) || (sourceBasename ? byFilePath.get(sourceBasename) : null) || null;
|
|
108
116
|
if (calleeId && calleeId !== caller.id) {
|
|
109
117
|
callees.get(caller.id)?.add(calleeId);
|
|
110
118
|
callers.get(calleeId)?.add(caller.id);
|
|
@@ -123,12 +131,8 @@ export function buildCallGraph(entities) {
|
|
|
123
131
|
callerId = source;
|
|
124
132
|
}
|
|
125
133
|
else {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
callerId = id;
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
134
|
+
// Suffix match via index (O(1) lookup)
|
|
135
|
+
callerId = bySuffix.get(source) || null;
|
|
132
136
|
}
|
|
133
137
|
// In this relationship, the current entity (`caller` in this loop context, despite the name)
|
|
134
138
|
// is actually the CALLEE, and the `source` is the true CALLER.
|
package/dist/index.js
CHANGED
|
@@ -54,21 +54,35 @@ if (process.argv[2] === 'setup') {
|
|
|
54
54
|
// Sent to the LLM at connection time. This is the "first contact" pitch.
|
|
55
55
|
const SERVER_INSTRUCTIONS = `Seshat provides structural code analysis backed by a compiled intermediate representation — not heuristic guesses or text search. Every function, class, and route in the synced codebase has been extracted into a typed symbol graph with dependency edges, data flow, constraints, and architectural layer tags. Results are precise and complete — if Seshat says a function has 3 callers, it has exactly 3 callers.
|
|
56
56
|
|
|
57
|
+
HOW TO USE SESHAT — Seshat tools are designed for iterative exploration, not one-shot lookups. A single tool call answers a single structural question. Understanding a system requires several calls that build on each other — the same way a senior developer investigates code before changing it.
|
|
58
|
+
|
|
59
|
+
A typical investigation:
|
|
60
|
+
1. query_entities or list_modules → orient yourself in the codebase
|
|
61
|
+
2. get_entity → deep-dive the function you care about
|
|
62
|
+
3. get_blast_radius → discover what's connected and what could break
|
|
63
|
+
4. get_dependencies or get_data_flow → trace specific edges
|
|
64
|
+
5. get_optimal_context → decide what source to read before making changes
|
|
65
|
+
6. Repeat steps 2-5 on newly discovered symbols until you have a complete picture
|
|
66
|
+
|
|
67
|
+
Each call reveals structure that informs the next. The goal is understanding, not minimum calls. 5-15 queries per investigation is normal and expected.
|
|
68
|
+
|
|
69
|
+
All tools are read-only and safe to call at any time.
|
|
70
|
+
|
|
57
71
|
GETTING STARTED — If list_projects returns empty, the current project hasn't been synced yet. Use the sync_project tool to import it:
|
|
58
72
|
1. Detect the git remote: run \`git remote get-url origin\` in the user's terminal
|
|
59
73
|
2. Call sync_project with that repo URL
|
|
60
74
|
3. Wait for extraction to complete (typically 5-30 seconds depending on repo size)
|
|
61
75
|
4. Then call list_projects again — the project will now appear
|
|
62
|
-
Note: For private repos, sync_project will return a GitHub authorization URL. Direct the user to open it in their browser to connect their GitHub account, then retry sync_project.
|
|
76
|
+
Note: For private repos, sync_project will return a GitHub authorization URL. Direct the user to open it in their browser to connect their GitHub account, then retry sync_project.
|
|
63
77
|
|
|
64
|
-
|
|
78
|
+
TOOL REFERENCE — Each tool maps to a structural question:
|
|
65
79
|
|
|
66
80
|
Setup & Navigation:
|
|
67
81
|
- "What projects are loaded?" → list_projects
|
|
68
82
|
- "Sync this repo to Seshat" → sync_project
|
|
69
83
|
- "How is the codebase organized?" → list_modules
|
|
70
84
|
- "What's the full API surface?" → get_topology
|
|
71
|
-
- "What tier am I on
|
|
85
|
+
- "What tier am I on?" → get_account_status
|
|
72
86
|
|
|
73
87
|
Understanding Code:
|
|
74
88
|
- "Find functions by name or layer" → query_entities
|
|
@@ -89,23 +103,17 @@ Security & Quality Audits:
|
|
|
89
103
|
- "Which endpoints require auth and which don't?" → get_auth_matrix
|
|
90
104
|
- "Where is sensitive data exposed without protection?" → find_exposure_leaks
|
|
91
105
|
- "Where are errors thrown but never caught?" → find_error_gaps
|
|
92
|
-
- "Are there architecture violations
|
|
106
|
+
- "Are there architecture violations?" → find_layer_violations
|
|
93
107
|
- "Does framework-agnostic code import framework-specific code?" → find_runtime_violations
|
|
94
108
|
- "Are there memory/lifecycle/ownership issues?" → find_ownership_violations
|
|
95
109
|
|
|
96
110
|
Metrics:
|
|
97
|
-
- "How coupled is the codebase?
|
|
111
|
+
- "How coupled is the codebase?" → get_coupling_metrics
|
|
98
112
|
- "Which functions are tested and which aren't?" → get_test_coverage
|
|
99
113
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
TEMPORAL ANALYSIS — Any tool accepts an optional temporal parameter: { temporal: { last_n: N } }. This runs the tool across the N most recent snapshots and returns a trend — timeseries of scalar metrics plus entity-level diffs (what appeared/disappeared). Requires at least 2 snapshots (call sync_project after commits to build history). Example:
|
|
114
|
+
TEMPORAL ANALYSIS — Any tool accepts an optional temporal parameter: { temporal: { last_n: N } }. This runs the tool across recent snapshots and returns a trend. Requires at least 2 snapshots. Example:
|
|
103
115
|
get_coupling_metrics({ project: "myapp", temporal: { last_n: 5 } })
|
|
104
|
-
|
|
105
|
-
find_dead_code({ project: "myapp", temporal: { last_n: 3 } })
|
|
106
|
-
→ shows whether dead code is accumulating or being cleaned up
|
|
107
|
-
|
|
108
|
-
get_blast_radius and get_optimal_context are designed to be called iteratively. Start with any entity, then feed discovered entities back in to expand your understanding. Each round reveals new structure that informs where to look next. When answering "what does this system do?" questions, a few rounds of blast_radius → get_entity → blast_radius on the newly discovered symbols will build a complete picture faster than reading files.`;
|
|
116
|
+
`;
|
|
109
117
|
// ─── Private Tools (Ptah IP — never exposed publicly) ────────────
|
|
110
118
|
// These tools are reserved for the Ptah write layer. They exist in
|
|
111
119
|
// the codebase but are never listed, never hinted at, never mentioned.
|
|
@@ -815,25 +823,26 @@ async function main() {
|
|
|
815
823
|
if (name === 'list_projects' && result.projects && result.projects.length > 0) {
|
|
816
824
|
_lastKnownProject = result.projects[0].name;
|
|
817
825
|
}
|
|
826
|
+
// Extract pre-formatted text (table-of-contents style) if the API provided one.
|
|
827
|
+
// This is far more token-efficient than JSON for list-heavy responses.
|
|
828
|
+
const textContent = result._text || null;
|
|
829
|
+
if (result._text)
|
|
830
|
+
delete result._text;
|
|
818
831
|
// Separate _meta into assistant-only content so it doesn't clutter
|
|
819
832
|
// the user-visible response. The LLM still sees it for context.
|
|
820
833
|
// Server-side _meta now includes cross-tool recommendations.
|
|
821
|
-
|
|
822
|
-
const meta = result._meta;
|
|
823
|
-
delete result._meta;
|
|
824
|
-
return {
|
|
825
|
-
content: [
|
|
826
|
-
{ type: 'text', text: JSON.stringify(result, null, 2) },
|
|
827
|
-
{ type: 'text', text: JSON.stringify({ _meta: meta }), annotations: { audience: ['assistant'], priority: 0.2 } },
|
|
828
|
-
],
|
|
829
|
-
};
|
|
830
|
-
}
|
|
831
|
-
// Strip empty _meta
|
|
834
|
+
const meta = (result._meta && Object.keys(result._meta).length > 0) ? result._meta : null;
|
|
832
835
|
if (result._meta)
|
|
833
836
|
delete result._meta;
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
+
// Use text format when available; fall back to compact JSON (no pretty-print)
|
|
838
|
+
const primaryText = textContent || JSON.stringify(result);
|
|
839
|
+
const content = [
|
|
840
|
+
{ type: 'text', text: primaryText },
|
|
841
|
+
];
|
|
842
|
+
if (meta) {
|
|
843
|
+
content.push({ type: 'text', text: JSON.stringify({ _meta: meta }), annotations: { audience: ['assistant'], priority: 0.2 } });
|
|
844
|
+
}
|
|
845
|
+
return { content };
|
|
837
846
|
}
|
|
838
847
|
catch (err) {
|
|
839
848
|
return {
|
|
@@ -844,7 +853,7 @@ async function main() {
|
|
|
844
853
|
});
|
|
845
854
|
const transport = new StdioServerTransport();
|
|
846
855
|
await server.connect(transport);
|
|
847
|
-
process.stderr.write(`Seshat MCP v0.16.
|
|
856
|
+
process.stderr.write(`Seshat MCP v0.16.7 connected. Structural intelligence ready.\n`);
|
|
848
857
|
}
|
|
849
858
|
main().catch((err) => {
|
|
850
859
|
process.stderr.write(`Fatal: ${err.message}\n`);
|
package/dist/tools/index.js
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { buildCallGraph, computeBlastRadius } from '../graph.js';
|
|
11
11
|
export function getGraph(project, loader) {
|
|
12
|
+
// Fast path: use pre-computed call graph edges cached on the snapshot row
|
|
13
|
+
// (avoids 3-10s CPU rebuild on first request for large codebases)
|
|
14
|
+
const precomputed = loader.getPrecomputedGraph?.();
|
|
15
|
+
if (precomputed)
|
|
16
|
+
return precomputed;
|
|
17
|
+
// Fallback for old snapshots (NULL call_graph_edges) or local file loaders
|
|
12
18
|
return buildCallGraph(loader.getEntities(project));
|
|
13
19
|
}
|
|
14
20
|
/**
|
package/package.json
CHANGED