@papyruslabsai/seshat-mcp 0.15.0 → 0.16.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
@@ -6,7 +6,6 @@
6
6
  * proxied to the Ptah Cloud API. Tool descriptions are written as triggers —
7
7
  * they tell the LLM *when* to reach for each tool, not just what it does.
8
8
  */
9
- import fs from 'fs';
10
9
  import path from 'path';
11
10
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
12
11
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -15,21 +14,48 @@ import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextpro
15
14
  // Sent to the LLM at connection time. This is the "first contact" pitch.
16
15
  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.
17
16
 
17
+ GETTING STARTED — If list_projects returns empty, the current project hasn't been synced yet. Use the sync_project tool to import it:
18
+ 1. Detect the git remote: run \`git remote get-url origin\` in the user's terminal
19
+ 2. Call sync_project with that repo URL
20
+ 3. Wait for extraction to complete (typically 5-30 seconds depending on repo size)
21
+ 4. Then call list_projects again — the project will now appear
22
+ 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.
23
+
18
24
  Use Seshat tools instead of grep/Read when you need to understand code structure. Each tool maps to a question you're already asking:
25
+
26
+ Setup & Navigation:
19
27
  - "What projects are loaded?" → list_projects
28
+ - "Sync this repo to Seshat" → sync_project
20
29
  - "How is the codebase organized?" → list_modules
21
30
  - "What's the full API surface?" → get_topology
31
+ - "What tier am I on / what tools are available?" → get_account_status
32
+
33
+ Understanding Code:
22
34
  - "Find functions by name or layer" → query_entities
23
35
  - "Deep-dive a single function" → get_entity
24
36
  - "Who calls this / what does it call?" → get_dependencies
25
- - "What breaks if I change this?" → get_blast_radius
26
37
  - "What data does this read/write/mutate?" → get_data_flow
38
+ - "What should I read before modifying X?" → get_optimal_context
27
39
  - "Which functions touch the DB / require auth / throw?" → find_by_constraint
28
40
  - "What reads or writes the 'users' table?" → find_by_constraint(table="users")
41
+ - "Find functions that can fail, including transitively" → query_traits(trait="fallible")
42
+
43
+ Change Planning:
44
+ - "What breaks if I change this?" → get_blast_radius
45
+ - "Is there dead code I can safely delete?" → find_dead_code
46
+ - "Is there copy-pasted logic I should consolidate?" → find_semantic_clones
47
+
48
+ Security & Quality Audits:
29
49
  - "Which endpoints require auth and which don't?" → get_auth_matrix
30
50
  - "Where is sensitive data exposed without protection?" → find_exposure_leaks
31
- - "What should I read before modifying X?" → get_optimal_context
32
- - "What tier am I on / what tools are available?" → get_account_status
51
+ - "Where are errors thrown but never caught?" → find_error_gaps
52
+ - "Are there architecture violations (e.g. routes calling repos directly)?" → find_layer_violations
53
+ - "Does framework-agnostic code import framework-specific code?" → find_runtime_violations
54
+ - "Are there memory/lifecycle/ownership issues?" → find_ownership_violations
55
+
56
+ Metrics:
57
+ - "How coupled is the codebase? Where are the hotspots?" → get_coupling_metrics
58
+ - "Which functions are tested and which aren't?" → get_test_coverage
33
59
 
34
60
  All tools are read-only and safe to call speculatively — there is no cost to trying them.
35
61
 
@@ -67,10 +93,21 @@ const TOOLS = [
67
93
  // ─── Cartographer (Free Tier) ─────────────────────────────────────
68
94
  {
69
95
  name: 'list_projects',
70
- description: 'Start here. Returns all synced codebases with their size, language, and project name. You need the project name for every other tool.',
96
+ description: 'Start here. Returns all synced codebases with their size, language, and project name. You need the project name for every other tool. If this returns empty, use sync_project to import the current repo.',
71
97
  inputSchema: { type: 'object', properties: {} },
72
98
  annotations: READ_ONLY_OPEN,
73
99
  },
100
+ {
101
+ name: 'sync_project',
102
+ description: 'Import a public GitHub repo into Seshat for structural analysis. Call this when list_projects returns empty or when the user wants to analyze a new repo. Detects the git remote automatically if no URL is provided. Extraction typically takes 5-30 seconds. After syncing, call list_projects to confirm the project is available.',
103
+ inputSchema: {
104
+ type: 'object',
105
+ properties: {
106
+ repo_url: { type: 'string', description: 'Public GitHub repo URL (e.g., https://github.com/org/repo). If omitted, tries to detect from the current git remote.' },
107
+ },
108
+ },
109
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
110
+ },
74
111
  {
75
112
  name: 'query_entities',
76
113
  description: 'Like grep but for code structure. Find functions, classes, and routes by name, architectural layer (route/service/component), or module. Returns matching symbols with their type, file, and layer — use this instead of grep when you need to find code by what it does, not by text content.',
@@ -227,10 +264,14 @@ const TOOLS = [
227
264
  },
228
265
  {
229
266
  name: 'get_auth_matrix',
230
- description: 'Audit authentication coverage. Shows which API routes and controllers require auth and which don\'t, plus inconsistencies like database access without auth checks. Use this for security reviews.',
267
+ description: 'Audit authentication coverage. Shows which API routes and controllers require auth and which don\'t, plus inconsistencies like database access without auth checks. For large codebases, use the module parameter to drill into a specific module/directory.',
231
268
  inputSchema: {
232
269
  type: 'object',
233
- properties: { project: projectParam },
270
+ properties: {
271
+ project: projectParam,
272
+ module: { type: 'string', description: 'Filter to routes/controllers in a specific module or directory path (optional).' },
273
+ layer: { type: 'string', description: 'Filter to a specific layer: "route" or "controller" (optional).' },
274
+ },
234
275
  },
235
276
  annotations: READ_ONLY_OPEN,
236
277
  },
@@ -424,25 +465,10 @@ function resolveProjectName() {
424
465
  if (process.env.SESHAT_PROJECTS && !process.env.SESHAT_PROJECTS.includes(',') && !process.env.SESHAT_PROJECTS.includes('*')) {
425
466
  return path.basename(process.env.SESHAT_PROJECTS);
426
467
  }
427
- // Fallback to reading local manifest if it exists
428
- const manifestPath = path.join(process.cwd(), '.seshat', 'manifest.json');
429
- if (fs.existsSync(manifestPath)) {
430
- try {
431
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
432
- if (manifest.projectName)
433
- return manifest.projectName;
434
- }
435
- catch { }
436
- }
437
- // If we've seen a project from list_projects, use that
468
+ // If we've seen a project from list_projects or sync_project, use that
438
469
  if (_lastKnownProject)
439
470
  return _lastKnownProject;
440
- // Only fall back to CWD basename if there's a .seshat/ dir (i.e., it's actually a Seshat project)
441
- // This prevents agents running from unrelated directories from sending bogus project names
442
- if (fs.existsSync(path.join(process.cwd(), '.seshat'))) {
443
- return path.basename(process.cwd());
444
- }
445
- // No project could be resolved — let the API return a helpful error
471
+ // No project could be resolved caller should use list_projects or sync_project
446
472
  return undefined;
447
473
  }
448
474
  // ─── Cloud API helper ─────────────────────────────────────────────
@@ -456,7 +482,7 @@ function getCloudUrl(path) {
456
482
  async function main() {
457
483
  const server = new Server({
458
484
  name: 'seshat',
459
- version: '0.14.2',
485
+ version: '0.16.0',
460
486
  }, {
461
487
  capabilities: { tools: {} },
462
488
  instructions: SERVER_INSTRUCTIONS,
@@ -473,7 +499,7 @@ async function main() {
473
499
  const apiKey = process.env.SESHAT_API_KEY;
474
500
  if (!apiKey) {
475
501
  return {
476
- content: [{ type: 'text', text: JSON.stringify({ error: 'SESHAT_API_KEY environment variable is required. Get your free key at https://ptah.papyruslabs.ai' }, null, 2) }],
502
+ 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) }],
477
503
  isError: true,
478
504
  };
479
505
  }
@@ -507,7 +533,7 @@ async function main() {
507
533
  status: 'active',
508
534
  tools_available: publicTools.length,
509
535
  your_tools: publicTools,
510
- dashboard: 'https://ptah.papyruslabs.ai/dashboard',
536
+ dashboard: 'https://seshat.papyruslabs.ai/dashboard',
511
537
  };
512
538
  return {
513
539
  content: [{ type: 'text', text: JSON.stringify(response, null, 2) }],
@@ -520,6 +546,133 @@ async function main() {
520
546
  };
521
547
  }
522
548
  }
549
+ // ─── sync_project ──────────────────────────────────────────
550
+ if (name === 'sync_project') {
551
+ let repoUrl = args?.repo_url;
552
+ // If no URL provided, try to detect git remote
553
+ if (!repoUrl) {
554
+ try {
555
+ const { execSync } = await import('child_process');
556
+ const remote = execSync('git remote get-url origin', { timeout: 5000 }).toString().trim();
557
+ // Normalize SSH URLs to HTTPS
558
+ const sshMatch = remote.match(/^git@github\.com:(.+)\.git$/);
559
+ repoUrl = sshMatch ? `https://github.com/${sshMatch[1]}` : remote.replace(/\.git$/, '');
560
+ }
561
+ catch {
562
+ return {
563
+ content: [{ type: 'text', text: JSON.stringify({
564
+ error: 'No repo_url provided and could not detect git remote. Please provide the GitHub repo URL.',
565
+ hint: 'Example: sync_project({ repo_url: "https://github.com/org/repo" })',
566
+ }, null, 2) }],
567
+ isError: true,
568
+ };
569
+ }
570
+ }
571
+ try {
572
+ const res = await fetch(getCloudUrl('/api/extract/create'), {
573
+ method: 'POST',
574
+ headers: {
575
+ 'Content-Type': 'application/json',
576
+ 'x-api-key': apiKey,
577
+ },
578
+ body: JSON.stringify({ repo_url: repoUrl }),
579
+ });
580
+ if (!res.ok) {
581
+ const errorText = await res.text();
582
+ return {
583
+ content: [{ type: 'text', text: JSON.stringify({ error: `Sync failed (${res.status}): ${errorText}` }, null, 2) }],
584
+ isError: true,
585
+ };
586
+ }
587
+ const result = await res.json();
588
+ // If private repo needs GitHub auth
589
+ if (result.status === 'github_auth_required') {
590
+ return {
591
+ content: [{ type: 'text', text: JSON.stringify({
592
+ status: 'github_auth_required',
593
+ message: `This repository is private and requires GitHub authentication.\n\nTo connect your GitHub account, open this URL in your browser:\n\n${result.auth_url}\n\nAfter authorizing, try sync_project again.`,
594
+ auth_url: result.auth_url,
595
+ }, null, 2) }],
596
+ isError: true,
597
+ };
598
+ }
599
+ // If already ready, return immediately
600
+ if (result.status === 'ready') {
601
+ _lastKnownProject = result.repo_name;
602
+ return {
603
+ content: [{ type: 'text', text: JSON.stringify({
604
+ status: 'ready',
605
+ project: result.repo_name,
606
+ total_entities: result.total_entities,
607
+ language: result.language,
608
+ message: 'Project synced and ready. You can now use all Seshat tools with this project.',
609
+ cached: result.cached || false,
610
+ }, null, 2) }],
611
+ };
612
+ }
613
+ // If queued/extracting, poll until complete
614
+ if (result.status === 'queued' || result.status === 'extracting') {
615
+ const pollUrl = getCloudUrl(`/api/extract/status/${result.repo_name}`);
616
+ const maxAttempts = 60; // 5 minutes max
617
+ for (let i = 0; i < maxAttempts; i++) {
618
+ await new Promise(r => setTimeout(r, 5000));
619
+ try {
620
+ const pollRes = await fetch(pollUrl, { headers: { 'x-api-key': apiKey } });
621
+ if (pollRes.ok) {
622
+ const pollResult = await pollRes.json();
623
+ if (pollResult.status === 'ready') {
624
+ _lastKnownProject = pollResult.repo_name;
625
+ return {
626
+ content: [{ type: 'text', text: JSON.stringify({
627
+ status: 'ready',
628
+ project: pollResult.repo_name,
629
+ total_entities: pollResult.total_entities,
630
+ language: pollResult.language,
631
+ message: 'Project synced and ready. You can now use all Seshat tools with this project.',
632
+ }, null, 2) }],
633
+ };
634
+ }
635
+ if (pollResult.status === 'github_auth_required') {
636
+ return {
637
+ content: [{ type: 'text', text: JSON.stringify({
638
+ status: 'github_auth_required',
639
+ message: `This repository is private and requires GitHub authentication.\n\nTo connect your GitHub account, open this URL in your browser:\n\n${pollResult.auth_url || 'https://seshat.papyruslabs.ai/dashboard'}\n\nAfter authorizing, try sync_project again.`,
640
+ auth_url: pollResult.auth_url,
641
+ }, null, 2) }],
642
+ isError: true,
643
+ };
644
+ }
645
+ if (pollResult.status === 'failed') {
646
+ return {
647
+ content: [{ type: 'text', text: JSON.stringify({
648
+ error: `Extraction failed: ${pollResult.error || 'Unknown error'}`,
649
+ hint: 'The repo may be too large, private, or contain unsupported file types.',
650
+ }, null, 2) }],
651
+ isError: true,
652
+ };
653
+ }
654
+ }
655
+ }
656
+ catch { /* continue polling */ }
657
+ }
658
+ return {
659
+ content: [{ type: 'text', text: JSON.stringify({
660
+ status: 'timeout',
661
+ message: 'Extraction is taking longer than expected. Try calling list_projects in a minute to check if it completed.',
662
+ }, null, 2) }],
663
+ };
664
+ }
665
+ return {
666
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
667
+ };
668
+ }
669
+ catch (err) {
670
+ return {
671
+ content: [{ type: 'text', text: JSON.stringify({ error: `Sync failed: ${err.message}` }, null, 2) }],
672
+ isError: true,
673
+ };
674
+ }
675
+ }
523
676
  // Determine the project hash for this workspace
524
677
  // list_projects is unscoped — it returns ALL projects for the user
525
678
  const project_hash = name === 'list_projects'
@@ -532,7 +685,7 @@ async function main() {
532
685
  return {
533
686
  content: [{ type: 'text', text: JSON.stringify({
534
687
  error: 'No project specified. Call list_projects first to see available projects, then pass the project name as the "project" argument.',
535
- hint: 'If list_projects returns empty, the repo may not be synced yet. Use the Ptah demo API (POST /api/demo/create) to import a repo first.',
688
+ hint: 'If list_projects returns empty, use sync_project to import the current repo first.',
536
689
  }, null, 2) }],
537
690
  isError: true,
538
691
  };
@@ -591,7 +744,7 @@ async function main() {
591
744
  });
592
745
  const transport = new StdioServerTransport();
593
746
  await server.connect(transport);
594
- process.stderr.write(`Seshat MCP v0.13.1 connected. Structural code analysis ready.\n`);
747
+ process.stderr.write(`Seshat MCP v0.16.0 connected. Structural intelligence ready.\n`);
595
748
  }
596
749
  main().catch((err) => {
597
750
  process.stderr.write(`Fatal: ${err.message}\n`);
@@ -25,6 +25,8 @@ export declare function getCouplingMetrics(args: {
25
25
  }, loader: ProjectLoader): unknown;
26
26
  export declare function getAuthMatrix(args: {
27
27
  project?: string;
28
+ module?: string;
29
+ layer?: string;
28
30
  }, loader: ProjectLoader): unknown;
29
31
  export declare function findErrorGaps(args: {
30
32
  project?: string;
@@ -296,11 +296,24 @@ export function getAuthMatrix(args, loader) {
296
296
  if (projErr)
297
297
  return { error: projErr };
298
298
  const entities = loader.getEntities(args?.project);
299
- const apiEntities = entities.filter((e) => {
299
+ let apiEntities = entities.filter((e) => {
300
300
  const layer = entityLayer(e);
301
301
  return layer === 'route' || layer === 'controller' ||
302
302
  e.context?.exposure === 'api';
303
303
  });
304
+ // Optional module filter — drill into a specific module/directory
305
+ if (args?.module) {
306
+ const mod = args.module.toLowerCase();
307
+ apiEntities = apiEntities.filter((e) => {
308
+ const eModule = (e.context?.module || e._sourceFile || '').toLowerCase();
309
+ return eModule.includes(mod);
310
+ });
311
+ }
312
+ // Optional layer filter
313
+ if (args?.layer) {
314
+ const targetLayer = args.layer.toLowerCase();
315
+ apiEntities = apiEntities.filter((e) => entityLayer(e) === targetLayer);
316
+ }
304
317
  const withAuth = [];
305
318
  const withoutAuth = [];
306
319
  const inconsistencies = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papyruslabsai/seshat-mcp",
3
- "version": "0.15.0",
3
+ "version": "0.16.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": {