@papyruslabsai/seshat-mcp 0.16.7 → 0.16.9

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
@@ -43,7 +43,14 @@ if (process.argv[2] === 'setup') {
43
43
  }
44
44
  config.mcpServers.seshat = mcpEntry;
45
45
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
46
+ // Also persist API key to ~/.seshat/config.json so it's available even when
47
+ // a project-level .mcp.json overrides the user-level config without an env block.
48
+ const seshatDir = path.join(os.homedir(), '.seshat');
49
+ if (!fs.existsSync(seshatDir))
50
+ fs.mkdirSync(seshatDir, { recursive: true });
51
+ fs.writeFileSync(path.join(seshatDir, 'config.json'), JSON.stringify({ apiKey }, null, 2) + '\n', 'utf-8');
46
52
  console.log(`Seshat MCP configured in ${configPath}`);
53
+ console.log(`API key persisted to ${path.join(seshatDir, 'config.json')}`);
47
54
  console.log(` command: ${npxCommand}`);
48
55
  console.log(` api key: ${apiKey.slice(0, 4)}...${apiKey.slice(-4)}`);
49
56
  console.log('');
@@ -54,21 +61,35 @@ if (process.argv[2] === 'setup') {
54
61
  // Sent to the LLM at connection time. This is the "first contact" pitch.
55
62
  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
63
 
64
+ 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.
65
+
66
+ A typical investigation:
67
+ 1. query_entities or list_modules → orient yourself in the codebase
68
+ 2. get_entity → deep-dive the function you care about
69
+ 3. get_blast_radius → discover what's connected and what could break
70
+ 4. get_dependencies or get_data_flow → trace specific edges
71
+ 5. get_optimal_context → decide what source to read before making changes
72
+ 6. Repeat steps 2-5 on newly discovered symbols until you have a complete picture
73
+
74
+ Each call reveals structure that informs the next. The goal is understanding, not minimum calls. 5-15 queries per investigation is normal and expected.
75
+
76
+ All tools are read-only and safe to call at any time.
77
+
57
78
  GETTING STARTED — If list_projects returns empty, the current project hasn't been synced yet. Use the sync_project tool to import it:
58
79
  1. Detect the git remote: run \`git remote get-url origin\` in the user's terminal
59
80
  2. Call sync_project with that repo URL
60
81
  3. Wait for extraction to complete (typically 5-30 seconds depending on repo size)
61
82
  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.
83
+ 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
84
 
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:
85
+ TOOL REFERENCE Each tool maps to a structural question:
65
86
 
66
87
  Setup & Navigation:
67
88
  - "What projects are loaded?" → list_projects
68
89
  - "Sync this repo to Seshat" → sync_project
69
90
  - "How is the codebase organized?" → list_modules
70
91
  - "What's the full API surface?" → get_topology
71
- - "What tier am I on / what tools are available?" → get_account_status
92
+ - "What tier am I on?" → get_account_status
72
93
 
73
94
  Understanding Code:
74
95
  - "Find functions by name or layer" → query_entities
@@ -89,23 +110,17 @@ Security & Quality Audits:
89
110
  - "Which endpoints require auth and which don't?" → get_auth_matrix
90
111
  - "Where is sensitive data exposed without protection?" → find_exposure_leaks
91
112
  - "Where are errors thrown but never caught?" → find_error_gaps
92
- - "Are there architecture violations (e.g. routes calling repos directly)?" → find_layer_violations
113
+ - "Are there architecture violations?" → find_layer_violations
93
114
  - "Does framework-agnostic code import framework-specific code?" → find_runtime_violations
94
115
  - "Are there memory/lifecycle/ownership issues?" → find_ownership_violations
95
116
 
96
117
  Metrics:
97
- - "How coupled is the codebase? Where are the hotspots?" → get_coupling_metrics
118
+ - "How coupled is the codebase?" → get_coupling_metrics
98
119
  - "Which functions are tested and which aren't?" → get_test_coverage
99
120
 
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:
121
+ 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
122
  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.`;
123
+ `;
109
124
  // ─── Private Tools (Ptah IP — never exposed publicly) ────────────
110
125
  // These tools are reserved for the Ptah write layer. They exist in
111
126
  // the codebase but are never listed, never hinted at, never mentioned.
@@ -587,10 +602,22 @@ async function main() {
587
602
  // ─── CallTool handler ──────────────────────────────────────────
588
603
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
589
604
  const { name, arguments: args } = request.params;
590
- const apiKey = process.env.SESHAT_API_KEY;
605
+ let apiKey = process.env.SESHAT_API_KEY;
606
+ if (!apiKey) {
607
+ // Fallback: read from ~/.seshat/config.json (written by `setup` command).
608
+ // This covers the case where a project-level .mcp.json overrides the
609
+ // user-level config without passing the env block.
610
+ try {
611
+ const cfg = JSON.parse(fs.readFileSync(path.join(os.homedir(), '.seshat', 'config.json'), 'utf-8'));
612
+ apiKey = cfg.apiKey;
613
+ }
614
+ catch {
615
+ // file doesn't exist or isn't valid JSON
616
+ }
617
+ }
591
618
  if (!apiKey) {
592
619
  return {
593
- content: [{ type: 'text', text: JSON.stringify({ error: 'SESHAT_API_KEY environment variable is required. Get your free key at https://seshat.papyruslabs.ai' }, null, 2) }],
620
+ content: [{ type: 'text', text: JSON.stringify({ error: 'SESHAT_API_KEY not found. Run: npx @papyruslabsai/seshat-mcp setup YOUR_API_KEY' }, null, 2) }],
594
621
  isError: true,
595
622
  };
596
623
  }
@@ -815,25 +842,26 @@ async function main() {
815
842
  if (name === 'list_projects' && result.projects && result.projects.length > 0) {
816
843
  _lastKnownProject = result.projects[0].name;
817
844
  }
845
+ // Extract pre-formatted text (table-of-contents style) if the API provided one.
846
+ // This is far more token-efficient than JSON for list-heavy responses.
847
+ const textContent = result._text || null;
848
+ if (result._text)
849
+ delete result._text;
818
850
  // Separate _meta into assistant-only content so it doesn't clutter
819
851
  // the user-visible response. The LLM still sees it for context.
820
852
  // 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
853
+ const meta = (result._meta && Object.keys(result._meta).length > 0) ? result._meta : null;
832
854
  if (result._meta)
833
855
  delete result._meta;
834
- return {
835
- content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
836
- };
856
+ // Use text format when available; fall back to compact JSON (no pretty-print)
857
+ const primaryText = textContent || JSON.stringify(result);
858
+ const content = [
859
+ { type: 'text', text: primaryText },
860
+ ];
861
+ if (meta) {
862
+ content.push({ type: 'text', text: JSON.stringify({ _meta: meta }), annotations: { audience: ['assistant'], priority: 0.2 } });
863
+ }
864
+ return { content };
837
865
  }
838
866
  catch (err) {
839
867
  return {
@@ -844,7 +872,7 @@ async function main() {
844
872
  });
845
873
  const transport = new StdioServerTransport();
846
874
  await server.connect(transport);
847
- process.stderr.write(`Seshat MCP v0.16.6 connected. Structural intelligence ready.\n`);
875
+ process.stderr.write(`Seshat MCP v0.16.7 connected. Structural intelligence ready.\n`);
848
876
  }
849
877
  main().catch((err) => {
850
878
  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.9",
4
4
  "description": "Semantic MCP server — exposes a codebase's structure, dependencies, and constraints as queryable tools",
5
5
  "type": "module",
6
6
  "bin": {