@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 +182 -29
- package/dist/tools/functors.d.ts +2 -0
- package/dist/tools/functors.js +14 -1
- package/package.json +1 -1
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
|
-
- "
|
|
32
|
-
- "
|
|
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.
|
|
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: {
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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.
|
|
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://
|
|
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://
|
|
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,
|
|
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.
|
|
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`);
|
package/dist/tools/functors.d.ts
CHANGED
|
@@ -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;
|
package/dist/tools/functors.js
CHANGED
|
@@ -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
|
-
|
|
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