@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 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: Prefix match
94
+ // Strategy 3: Suffix match via index (O(1) lookup)
76
95
  if (!calleeId) {
77
- for (const [id] of entityById) {
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
- // Here we do a basic substring match against source files/modules
101
- for (const [id, entity] of entityById) {
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
- for (const [id] of entityById) {
127
- if (id.endsWith(`.${source}`) || id === source) {
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. Once connected, all future private repo syncs work automatically.
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
- Use Seshat tools instead of grep/Read when you need to understand code structure. Each tool maps to a question you're already asking:
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 / what tools are available?" → get_account_status
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 (e.g. routes calling repos directly)?" → find_layer_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? Where are the hotspots?" → get_coupling_metrics
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
- All tools are read-only and safe to call speculatively there is no cost to trying them.
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
- → shows coupling trend over last 5 syncs
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
- if (result._meta && Object.keys(result._meta).length > 0) {
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
- return {
835
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
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.6 connected. Structural intelligence ready.\n`);
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`);
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papyruslabsai/seshat-mcp",
3
- "version": "0.16.7",
3
+ "version": "0.16.8",
4
4
  "description": "Semantic MCP server — exposes a codebase's structure, dependencies, and constraints as queryable tools",
5
5
  "type": "module",
6
6
  "bin": {