@papyruslabsai/seshat-mcp 0.14.2 → 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 +221 -207
- 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,136 +14,63 @@ 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
|
|
|
36
62
|
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.`;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
//
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
get_optimal_context: 'core',
|
|
49
|
-
// Navigation — after agent starts exploring (revealed after ~3 queries)
|
|
50
|
-
query_entities: 'navigate',
|
|
51
|
-
list_modules: 'navigate',
|
|
52
|
-
get_topology: 'navigate',
|
|
53
|
-
get_data_flow: 'navigate',
|
|
54
|
-
find_by_constraint: 'navigate',
|
|
55
|
-
// Security surface — after architecture is understood (~8 queries)
|
|
56
|
-
get_auth_matrix: 'security',
|
|
57
|
-
find_exposure_leaks: 'security',
|
|
58
|
-
// Quality & audit — after deep exploration (~15 queries)
|
|
59
|
-
find_dead_code: 'quality',
|
|
60
|
-
find_layer_violations: 'quality',
|
|
61
|
-
get_coupling_metrics: 'quality',
|
|
62
|
-
find_error_gaps: 'quality',
|
|
63
|
-
get_test_coverage: 'quality',
|
|
64
|
-
find_runtime_violations: 'quality',
|
|
65
|
-
find_ownership_violations: 'quality',
|
|
66
|
-
query_traits: 'quality',
|
|
67
|
-
find_semantic_clones: 'quality',
|
|
68
|
-
// Architect — always gated by tier, shown when quality is visible
|
|
69
|
-
estimate_task_cost: 'full',
|
|
70
|
-
simulate_mutation: 'full',
|
|
71
|
-
create_symbol: 'full',
|
|
72
|
-
diff_bundle: 'full',
|
|
73
|
-
conflict_matrix: 'full',
|
|
74
|
-
query_data_targets: 'full',
|
|
75
|
-
};
|
|
76
|
-
// Query thresholds for each stage transition
|
|
77
|
-
const REVELATION_THRESHOLDS = {
|
|
78
|
-
core: 0, // Immediate
|
|
79
|
-
navigate: 3, // After a few core loop calls
|
|
80
|
-
security: 8, // After architecture is understood
|
|
81
|
-
quality: 15, // After deep exploration
|
|
82
|
-
full: 25, // Power user territory
|
|
83
|
-
};
|
|
84
|
-
// Session state for progressive revelation
|
|
85
|
-
let _sessionQueryCount = 0;
|
|
86
|
-
let _currentStage = 'core';
|
|
87
|
-
let _serverRef = null; // Stored so we can send notifications
|
|
88
|
-
function stageAtLeast(current, required) {
|
|
89
|
-
return REVELATION_ORDER.indexOf(current) >= REVELATION_ORDER.indexOf(required);
|
|
90
|
-
}
|
|
91
|
-
function checkRevelation() {
|
|
92
|
-
for (const stage of REVELATION_ORDER) {
|
|
93
|
-
if (_sessionQueryCount >= REVELATION_THRESHOLDS[stage]) {
|
|
94
|
-
if (REVELATION_ORDER.indexOf(stage) > REVELATION_ORDER.indexOf(_currentStage)) {
|
|
95
|
-
const previousStage = _currentStage;
|
|
96
|
-
_currentStage = stage;
|
|
97
|
-
// Notify client that tool list has changed
|
|
98
|
-
if (_serverRef) {
|
|
99
|
-
_serverRef.notification({ method: 'notifications/tools/list_changed' });
|
|
100
|
-
process.stderr.write(`[Seshat] Revelation: ${previousStage} → ${stage} (${_sessionQueryCount} queries). Notified client.\n`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
const TOOL_TIERS = {
|
|
107
|
-
// Cartographer (free) — explore, navigate, and assess security surface
|
|
108
|
-
list_projects: 'cartographer',
|
|
109
|
-
query_entities: 'cartographer',
|
|
110
|
-
get_entity: 'cartographer',
|
|
111
|
-
get_dependencies: 'cartographer',
|
|
112
|
-
get_data_flow: 'cartographer',
|
|
113
|
-
find_by_constraint: 'cartographer',
|
|
114
|
-
get_blast_radius: 'cartographer',
|
|
115
|
-
list_modules: 'cartographer',
|
|
116
|
-
get_topology: 'cartographer',
|
|
117
|
-
get_optimal_context: 'cartographer',
|
|
118
|
-
get_auth_matrix: 'cartographer',
|
|
119
|
-
find_exposure_leaks: 'cartographer',
|
|
120
|
-
// Analyst (tier 2) — audit and diagnose
|
|
121
|
-
find_dead_code: 'analyst',
|
|
122
|
-
find_layer_violations: 'analyst',
|
|
123
|
-
get_coupling_metrics: 'analyst',
|
|
124
|
-
find_error_gaps: 'analyst',
|
|
125
|
-
get_test_coverage: 'analyst',
|
|
126
|
-
find_runtime_violations: 'analyst',
|
|
127
|
-
find_ownership_violations: 'analyst',
|
|
128
|
-
query_traits: 'analyst',
|
|
129
|
-
find_semantic_clones: 'analyst',
|
|
130
|
-
// Architect (tier 3) — simulate, estimate, and act
|
|
131
|
-
estimate_task_cost: 'architect',
|
|
132
|
-
simulate_mutation: 'architect',
|
|
133
|
-
create_symbol: 'architect',
|
|
134
|
-
diff_bundle: 'architect',
|
|
135
|
-
conflict_matrix: 'architect',
|
|
136
|
-
query_data_targets: 'architect',
|
|
137
|
-
};
|
|
138
|
-
const TIER_LABELS = {
|
|
139
|
-
cartographer: 'Cartographer (Free)',
|
|
140
|
-
pro: 'Seshat Pro',
|
|
141
|
-
analyst: 'Seshat Shield',
|
|
142
|
-
architect: 'Architect',
|
|
143
|
-
founder: 'Founder (All Access)',
|
|
144
|
-
};
|
|
145
|
-
function tierAtLeast(userTier, requiredTier) {
|
|
146
|
-
return TIER_ORDER.indexOf(userTier) >= TIER_ORDER.indexOf(requiredTier);
|
|
147
|
-
}
|
|
63
|
+
// ─── Private Tools (Ptah IP — never exposed publicly) ────────────
|
|
64
|
+
// These tools are reserved for the Ptah write layer. They exist in
|
|
65
|
+
// the codebase but are never listed, never hinted at, never mentioned.
|
|
66
|
+
const PRIVATE_TOOLS = new Set([
|
|
67
|
+
'estimate_task_cost',
|
|
68
|
+
'simulate_mutation',
|
|
69
|
+
'create_symbol',
|
|
70
|
+
'diff_bundle',
|
|
71
|
+
'conflict_matrix',
|
|
72
|
+
'query_data_targets',
|
|
73
|
+
]);
|
|
148
74
|
// ─── Shared Definitions ──────────────────────────────────────────
|
|
149
75
|
const projectParam = {
|
|
150
76
|
type: 'string',
|
|
@@ -167,10 +93,21 @@ const TOOLS = [
|
|
|
167
93
|
// ─── Cartographer (Free Tier) ─────────────────────────────────────
|
|
168
94
|
{
|
|
169
95
|
name: 'list_projects',
|
|
170
|
-
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.',
|
|
171
97
|
inputSchema: { type: 'object', properties: {} },
|
|
172
98
|
annotations: READ_ONLY_OPEN,
|
|
173
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
|
+
},
|
|
174
111
|
{
|
|
175
112
|
name: 'query_entities',
|
|
176
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.',
|
|
@@ -327,10 +264,14 @@ const TOOLS = [
|
|
|
327
264
|
},
|
|
328
265
|
{
|
|
329
266
|
name: 'get_auth_matrix',
|
|
330
|
-
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.',
|
|
331
268
|
inputSchema: {
|
|
332
269
|
type: 'object',
|
|
333
|
-
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
|
+
},
|
|
334
275
|
},
|
|
335
276
|
annotations: READ_ONLY_OPEN,
|
|
336
277
|
},
|
|
@@ -511,6 +452,11 @@ const TOOLS = [
|
|
|
511
452
|
annotations: READ_ONLY_OPEN,
|
|
512
453
|
},
|
|
513
454
|
];
|
|
455
|
+
// ─── _meta Steering Hints ────────────────────────────────────────
|
|
456
|
+
// After each tool call, suggest the next logical tool based on what
|
|
457
|
+
// the agent has been doing. This replaces progressive revelation —
|
|
458
|
+
// all tools are visible, but hints guide the agent toward the right
|
|
459
|
+
// tool at the right time.
|
|
514
460
|
// ─── Project Resolution (Fallback) ────────────────────────────────
|
|
515
461
|
// Cache for the last project used — so tools auto-scope after list_projects
|
|
516
462
|
let _lastKnownProject;
|
|
@@ -519,25 +465,10 @@ function resolveProjectName() {
|
|
|
519
465
|
if (process.env.SESHAT_PROJECTS && !process.env.SESHAT_PROJECTS.includes(',') && !process.env.SESHAT_PROJECTS.includes('*')) {
|
|
520
466
|
return path.basename(process.env.SESHAT_PROJECTS);
|
|
521
467
|
}
|
|
522
|
-
//
|
|
523
|
-
const manifestPath = path.join(process.cwd(), '.seshat', 'manifest.json');
|
|
524
|
-
if (fs.existsSync(manifestPath)) {
|
|
525
|
-
try {
|
|
526
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
527
|
-
if (manifest.projectName)
|
|
528
|
-
return manifest.projectName;
|
|
529
|
-
}
|
|
530
|
-
catch { }
|
|
531
|
-
}
|
|
532
|
-
// 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
|
|
533
469
|
if (_lastKnownProject)
|
|
534
470
|
return _lastKnownProject;
|
|
535
|
-
//
|
|
536
|
-
// This prevents agents running from unrelated directories from sending bogus project names
|
|
537
|
-
if (fs.existsSync(path.join(process.cwd(), '.seshat'))) {
|
|
538
|
-
return path.basename(process.cwd());
|
|
539
|
-
}
|
|
540
|
-
// 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
|
|
541
472
|
return undefined;
|
|
542
473
|
}
|
|
543
474
|
// ─── Cloud API helper ─────────────────────────────────────────────
|
|
@@ -551,46 +482,15 @@ function getCloudUrl(path) {
|
|
|
551
482
|
async function main() {
|
|
552
483
|
const server = new Server({
|
|
553
484
|
name: 'seshat',
|
|
554
|
-
version: '0.
|
|
485
|
+
version: '0.16.0',
|
|
555
486
|
}, {
|
|
556
|
-
capabilities: { tools: {
|
|
487
|
+
capabilities: { tools: {} },
|
|
557
488
|
instructions: SERVER_INSTRUCTIONS,
|
|
558
489
|
});
|
|
559
|
-
//
|
|
560
|
-
_serverRef = server;
|
|
561
|
-
// ─── Dynamic ListTools — only expose tools the user can access ──
|
|
490
|
+
// ─── ListTools — all public tools, Architect tools hidden (Ptah IP) ──
|
|
562
491
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
// Fetch account status to determine which tools to expose
|
|
566
|
-
if (apiKey) {
|
|
567
|
-
try {
|
|
568
|
-
const res = await fetch(getCloudUrl('/api/mcp/account'), {
|
|
569
|
-
method: 'GET',
|
|
570
|
-
headers: { 'x-api-key': apiKey },
|
|
571
|
-
});
|
|
572
|
-
if (res.ok) {
|
|
573
|
-
const account = await res.json();
|
|
574
|
-
userTier = account.tier || 'cartographer';
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
catch {
|
|
578
|
-
// If unavailable, default to cartographer (free tier tools only)
|
|
579
|
-
}
|
|
580
|
-
}
|
|
581
|
-
// Filter tools by BOTH tier access AND revelation stage
|
|
582
|
-
const visibleTools = TOOLS.filter((tool) => {
|
|
583
|
-
// Tier gate: user must have access
|
|
584
|
-
const requiredTier = TOOL_TIERS[tool.name];
|
|
585
|
-
if (requiredTier && !tierAtLeast(userTier, requiredTier))
|
|
586
|
-
return false;
|
|
587
|
-
// Revelation gate: tool must be revealed at current stage
|
|
588
|
-
const requiredStage = TOOL_REVELATION[tool.name];
|
|
589
|
-
if (requiredStage && !stageAtLeast(_currentStage, requiredStage))
|
|
590
|
-
return false;
|
|
591
|
-
return true;
|
|
592
|
-
});
|
|
593
|
-
process.stderr.write(`[Seshat] ListTools: stage=${_currentStage}, queries=${_sessionQueryCount}, tools=${visibleTools.length}/${TOOLS.length}\n`);
|
|
492
|
+
const visibleTools = TOOLS.filter((tool) => !PRIVATE_TOOLS.has(tool.name));
|
|
493
|
+
process.stderr.write(`[Seshat] ListTools: ${visibleTools.length} tools (${PRIVATE_TOOLS.size} private)\n`);
|
|
594
494
|
return { tools: visibleTools };
|
|
595
495
|
});
|
|
596
496
|
// ─── CallTool handler ──────────────────────────────────────────
|
|
@@ -599,7 +499,14 @@ async function main() {
|
|
|
599
499
|
const apiKey = process.env.SESHAT_API_KEY;
|
|
600
500
|
if (!apiKey) {
|
|
601
501
|
return {
|
|
602
|
-
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) }],
|
|
503
|
+
isError: true,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
// Block private Ptah tools from being called
|
|
507
|
+
if (PRIVATE_TOOLS.has(name)) {
|
|
508
|
+
return {
|
|
509
|
+
content: [{ type: 'text', text: JSON.stringify({ error: `Unknown tool: ${name}` }, null, 2) }],
|
|
603
510
|
isError: true,
|
|
604
511
|
};
|
|
605
512
|
}
|
|
@@ -618,37 +525,16 @@ async function main() {
|
|
|
618
525
|
};
|
|
619
526
|
}
|
|
620
527
|
const account = await res.json();
|
|
621
|
-
|
|
622
|
-
const
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
const upgradeTeaser = {};
|
|
626
|
-
for (const [toolName, requiredTier] of Object.entries(TOOL_TIERS)) {
|
|
627
|
-
if (tierAtLeast(userTier, requiredTier)) {
|
|
628
|
-
availableTools.push(toolName);
|
|
629
|
-
}
|
|
630
|
-
else {
|
|
631
|
-
if (!upgradeTeaser[TIER_LABELS[requiredTier]]) {
|
|
632
|
-
upgradeTeaser[TIER_LABELS[requiredTier]] = [];
|
|
633
|
-
}
|
|
634
|
-
upgradeTeaser[TIER_LABELS[requiredTier]].push(toolName);
|
|
635
|
-
}
|
|
636
|
-
}
|
|
528
|
+
// List all public tools
|
|
529
|
+
const publicTools = TOOLS
|
|
530
|
+
.filter(t => !PRIVATE_TOOLS.has(t.name))
|
|
531
|
+
.map(t => t.name);
|
|
637
532
|
const response = {
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
tool_count: `${availableTools.length} tools available`,
|
|
533
|
+
status: 'active',
|
|
534
|
+
tools_available: publicTools.length,
|
|
535
|
+
your_tools: publicTools,
|
|
536
|
+
dashboard: 'https://seshat.papyruslabs.ai/dashboard',
|
|
643
537
|
};
|
|
644
|
-
// Only mention upgrades if there are locked tools, and frame positively
|
|
645
|
-
if (Object.keys(upgradeTeaser).length > 0) {
|
|
646
|
-
const totalLocked = Object.values(upgradeTeaser).reduce((sum, t) => sum + t.length, 0);
|
|
647
|
-
response.upgrades_available = {
|
|
648
|
-
summary: `${totalLocked} additional diagnostic and simulation tools available with a tier upgrade — find dead code, coupling hotspots, test gaps, layer violations, and simulate changes before making them.`,
|
|
649
|
-
url: 'https://ptah.papyruslabs.ai/settings/billing',
|
|
650
|
-
};
|
|
651
|
-
}
|
|
652
538
|
return {
|
|
653
539
|
content: [{ type: 'text', text: JSON.stringify(response, null, 2) }],
|
|
654
540
|
};
|
|
@@ -660,6 +546,133 @@ async function main() {
|
|
|
660
546
|
};
|
|
661
547
|
}
|
|
662
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
|
+
}
|
|
663
676
|
// Determine the project hash for this workspace
|
|
664
677
|
// list_projects is unscoped — it returns ALL projects for the user
|
|
665
678
|
const project_hash = name === 'list_projects'
|
|
@@ -672,7 +685,7 @@ async function main() {
|
|
|
672
685
|
return {
|
|
673
686
|
content: [{ type: 'text', text: JSON.stringify({
|
|
674
687
|
error: 'No project specified. Call list_projects first to see available projects, then pass the project name as the "project" argument.',
|
|
675
|
-
hint: 'If list_projects returns empty,
|
|
688
|
+
hint: 'If list_projects returns empty, use sync_project to import the current repo first.',
|
|
676
689
|
}, null, 2) }],
|
|
677
690
|
isError: true,
|
|
678
691
|
};
|
|
@@ -702,12 +715,10 @@ async function main() {
|
|
|
702
715
|
if (name === 'list_projects' && result.projects && result.projects.length > 0) {
|
|
703
716
|
_lastKnownProject = result.projects[0].name;
|
|
704
717
|
}
|
|
705
|
-
// Progressive revelation: count queries and check for stage transitions
|
|
706
|
-
_sessionQueryCount++;
|
|
707
|
-
checkRevelation();
|
|
708
718
|
// Separate _meta into assistant-only content so it doesn't clutter
|
|
709
719
|
// the user-visible response. The LLM still sees it for context.
|
|
710
|
-
|
|
720
|
+
// Server-side _meta now includes cross-tool recommendations.
|
|
721
|
+
if (result._meta && Object.keys(result._meta).length > 0) {
|
|
711
722
|
const meta = result._meta;
|
|
712
723
|
delete result._meta;
|
|
713
724
|
return {
|
|
@@ -717,6 +728,9 @@ async function main() {
|
|
|
717
728
|
],
|
|
718
729
|
};
|
|
719
730
|
}
|
|
731
|
+
// Strip empty _meta
|
|
732
|
+
if (result._meta)
|
|
733
|
+
delete result._meta;
|
|
720
734
|
return {
|
|
721
735
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
722
736
|
};
|
|
@@ -730,7 +744,7 @@ async function main() {
|
|
|
730
744
|
});
|
|
731
745
|
const transport = new StdioServerTransport();
|
|
732
746
|
await server.connect(transport);
|
|
733
|
-
process.stderr.write(`Seshat MCP v0.
|
|
747
|
+
process.stderr.write(`Seshat MCP v0.16.0 connected. Structural intelligence ready.\n`);
|
|
734
748
|
}
|
|
735
749
|
main().catch((err) => {
|
|
736
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