@papyruslabsai/seshat-mcp 0.3.3 → 0.4.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/bootstrap.d.ts +29 -0
- package/dist/bootstrap.js +187 -0
- package/dist/index.js +28 -2
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-Bootstrap — extract a .seshat/ bundle on first run.
|
|
3
|
+
*
|
|
4
|
+
* When the MCP server starts and finds no .seshat/_bundle.json, this module
|
|
5
|
+
* spawns `npx @papyruslabsai/seshat-extract` to generate one on the fly.
|
|
6
|
+
*
|
|
7
|
+
* CI continues to regenerate the bundle on every merge to main, overwriting
|
|
8
|
+
* the bootstrap result. The bootstrap only runs when .seshat/ doesn't exist.
|
|
9
|
+
*
|
|
10
|
+
* Env vars:
|
|
11
|
+
* SESHAT_BOOTSTRAP_TIMEOUT — spawn timeout in ms (default: 120000)
|
|
12
|
+
*/
|
|
13
|
+
export interface BootstrapResult {
|
|
14
|
+
success: boolean;
|
|
15
|
+
entityCount: number;
|
|
16
|
+
languages: string[];
|
|
17
|
+
durationMs: number;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Auto-bootstrap: extract a .seshat/ bundle for the given project directory.
|
|
22
|
+
*
|
|
23
|
+
* Spawns `npx @papyruslabsai/seshat-extract <dir> <dir>/.seshat <name>`
|
|
24
|
+
* and waits for it to complete.
|
|
25
|
+
*
|
|
26
|
+
* @param projectDir - Absolute path to the project root
|
|
27
|
+
* @returns Bootstrap result with entity count, languages, duration, and any error
|
|
28
|
+
*/
|
|
29
|
+
export declare function bootstrap(projectDir: string): Promise<BootstrapResult>;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-Bootstrap — extract a .seshat/ bundle on first run.
|
|
3
|
+
*
|
|
4
|
+
* When the MCP server starts and finds no .seshat/_bundle.json, this module
|
|
5
|
+
* spawns `npx @papyruslabsai/seshat-extract` to generate one on the fly.
|
|
6
|
+
*
|
|
7
|
+
* CI continues to regenerate the bundle on every merge to main, overwriting
|
|
8
|
+
* the bootstrap result. The bootstrap only runs when .seshat/ doesn't exist.
|
|
9
|
+
*
|
|
10
|
+
* Env vars:
|
|
11
|
+
* SESHAT_BOOTSTRAP_TIMEOUT — spawn timeout in ms (default: 120000)
|
|
12
|
+
*/
|
|
13
|
+
import { spawn } from 'child_process';
|
|
14
|
+
import fs from 'fs';
|
|
15
|
+
import path from 'path';
|
|
16
|
+
import { execSync } from 'child_process';
|
|
17
|
+
// Project marker files — presence of any means "this is a code project"
|
|
18
|
+
const PROJECT_MARKERS = [
|
|
19
|
+
'.git',
|
|
20
|
+
'package.json',
|
|
21
|
+
'go.mod',
|
|
22
|
+
'Cargo.toml',
|
|
23
|
+
'pyproject.toml',
|
|
24
|
+
'pom.xml',
|
|
25
|
+
'build.gradle',
|
|
26
|
+
'Makefile',
|
|
27
|
+
'CMakeLists.txt',
|
|
28
|
+
];
|
|
29
|
+
/**
|
|
30
|
+
* Check if a directory looks like a code project.
|
|
31
|
+
*/
|
|
32
|
+
function isCodeProject(dir) {
|
|
33
|
+
for (const marker of PROJECT_MARKERS) {
|
|
34
|
+
const markerPath = path.join(dir, marker);
|
|
35
|
+
if (fs.existsSync(markerPath))
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
// Also check for *.sln files (C# solutions)
|
|
39
|
+
try {
|
|
40
|
+
const entries = fs.readdirSync(dir);
|
|
41
|
+
if (entries.some(e => e.endsWith('.sln')))
|
|
42
|
+
return true;
|
|
43
|
+
}
|
|
44
|
+
catch { }
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Infer a project name from the directory.
|
|
49
|
+
* Priority: package.json name → git remote → directory basename
|
|
50
|
+
*/
|
|
51
|
+
function inferProjectName(dir) {
|
|
52
|
+
// 1. Try package.json
|
|
53
|
+
try {
|
|
54
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
55
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
56
|
+
if (pkg.name) {
|
|
57
|
+
return pkg.name.replace(/^@[^/]+\//, '');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch { }
|
|
61
|
+
// 2. Try git remote
|
|
62
|
+
try {
|
|
63
|
+
const remote = execSync('git remote get-url origin', {
|
|
64
|
+
cwd: dir, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe']
|
|
65
|
+
}).trim();
|
|
66
|
+
const match = remote.match(/\/([^/]+?)(?:\.git)?$/);
|
|
67
|
+
if (match)
|
|
68
|
+
return match[1];
|
|
69
|
+
}
|
|
70
|
+
catch { }
|
|
71
|
+
// 3. Fallback: directory basename
|
|
72
|
+
return path.basename(dir);
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Auto-bootstrap: extract a .seshat/ bundle for the given project directory.
|
|
76
|
+
*
|
|
77
|
+
* Spawns `npx @papyruslabsai/seshat-extract <dir> <dir>/.seshat <name>`
|
|
78
|
+
* and waits for it to complete.
|
|
79
|
+
*
|
|
80
|
+
* @param projectDir - Absolute path to the project root
|
|
81
|
+
* @returns Bootstrap result with entity count, languages, duration, and any error
|
|
82
|
+
*/
|
|
83
|
+
export async function bootstrap(projectDir) {
|
|
84
|
+
const startTime = Date.now();
|
|
85
|
+
const timeoutMs = parseInt(process.env.SESHAT_BOOTSTRAP_TIMEOUT || '120000', 10);
|
|
86
|
+
// Sanity checks
|
|
87
|
+
if (!isCodeProject(projectDir)) {
|
|
88
|
+
return {
|
|
89
|
+
success: false,
|
|
90
|
+
entityCount: 0,
|
|
91
|
+
languages: [],
|
|
92
|
+
durationMs: Date.now() - startTime,
|
|
93
|
+
error: `${projectDir} does not appear to be a code project (no package.json, .git, go.mod, etc.)`,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
const seshatDir = path.join(projectDir, '.seshat');
|
|
97
|
+
const projectName = inferProjectName(projectDir);
|
|
98
|
+
process.stderr.write(`[seshat-mcp] Auto-bootstrap: extracting ${projectName} from ${projectDir}\n`);
|
|
99
|
+
return new Promise((resolve) => {
|
|
100
|
+
// On Windows, npx is npx.cmd
|
|
101
|
+
const isWindows = process.platform === 'win32';
|
|
102
|
+
const npxCmd = isWindows ? 'npx.cmd' : 'npx';
|
|
103
|
+
const child = spawn(npxCmd, ['-y', '@papyruslabsai/seshat-extract', projectDir, seshatDir, projectName], {
|
|
104
|
+
cwd: projectDir,
|
|
105
|
+
shell: isWindows, // Windows needs shell: true for .cmd files
|
|
106
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
107
|
+
timeout: timeoutMs,
|
|
108
|
+
});
|
|
109
|
+
let stdout = '';
|
|
110
|
+
let stderr = '';
|
|
111
|
+
child.stdout?.on('data', (data) => {
|
|
112
|
+
stdout += data.toString();
|
|
113
|
+
});
|
|
114
|
+
child.stderr?.on('data', (data) => {
|
|
115
|
+
const text = data.toString();
|
|
116
|
+
stderr += text;
|
|
117
|
+
// Forward progress to parent stderr
|
|
118
|
+
process.stderr.write(`[seshat-extract] ${text}`);
|
|
119
|
+
});
|
|
120
|
+
child.on('error', (err) => {
|
|
121
|
+
resolve({
|
|
122
|
+
success: false,
|
|
123
|
+
entityCount: 0,
|
|
124
|
+
languages: [],
|
|
125
|
+
durationMs: Date.now() - startTime,
|
|
126
|
+
error: `Failed to spawn seshat-extract: ${err.message}`,
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
child.on('close', (code) => {
|
|
130
|
+
const durationMs = Date.now() - startTime;
|
|
131
|
+
if (code !== 0) {
|
|
132
|
+
resolve({
|
|
133
|
+
success: false,
|
|
134
|
+
entityCount: 0,
|
|
135
|
+
languages: [],
|
|
136
|
+
durationMs,
|
|
137
|
+
error: `seshat-extract exited with code ${code}. stderr: ${stderr.slice(-500)}`,
|
|
138
|
+
});
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
// Parse the JSON result from stdout
|
|
142
|
+
try {
|
|
143
|
+
const result = JSON.parse(stdout.trim());
|
|
144
|
+
if (result.ok) {
|
|
145
|
+
process.stderr.write(`[seshat-mcp] Bootstrap complete: ${result.entities} entities, ${result.languages?.join(', ')} in ${(durationMs / 1000).toFixed(1)}s\n`);
|
|
146
|
+
resolve({
|
|
147
|
+
success: true,
|
|
148
|
+
entityCount: result.entities || 0,
|
|
149
|
+
languages: result.languages || [],
|
|
150
|
+
durationMs,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
resolve({
|
|
155
|
+
success: false,
|
|
156
|
+
entityCount: 0,
|
|
157
|
+
languages: [],
|
|
158
|
+
durationMs,
|
|
159
|
+
error: result.error || 'Unknown extraction error',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
// JSON parse failed — check if bundle was written anyway
|
|
165
|
+
const bundlePath = path.join(seshatDir, '_bundle.json');
|
|
166
|
+
if (fs.existsSync(bundlePath)) {
|
|
167
|
+
process.stderr.write(`[seshat-mcp] Bootstrap produced bundle but JSON result was malformed. Proceeding.\n`);
|
|
168
|
+
resolve({
|
|
169
|
+
success: true,
|
|
170
|
+
entityCount: 0,
|
|
171
|
+
languages: [],
|
|
172
|
+
durationMs,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
resolve({
|
|
177
|
+
success: false,
|
|
178
|
+
entityCount: 0,
|
|
179
|
+
languages: [],
|
|
180
|
+
durationMs,
|
|
181
|
+
error: `seshat-extract succeeded but produced no parseable result. stdout: ${stdout.slice(-200)}`,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,7 @@ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
|
44
44
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
45
45
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
46
46
|
import { MultiLoader } from './loader.js';
|
|
47
|
+
import { bootstrap } from './bootstrap.js';
|
|
47
48
|
import { initTools, queryEntities, getEntity, getDependencies, getDataFlow, findByConstraint, getBlastRadius, listModules, getTopology, } from './tools/index.js';
|
|
48
49
|
import { findDeadCode, findLayerViolations, getCouplingMetrics, getAuthMatrix, findErrorGaps, getTestCoverage, getOptimalContext, estimateTaskCost, reportActualBurn, } from './tools/functors.js';
|
|
49
50
|
// ─── Project Discovery ───────────────────────────────────────────
|
|
@@ -403,13 +404,38 @@ const TOOLS = [
|
|
|
403
404
|
async function main() {
|
|
404
405
|
// Discover and load projects
|
|
405
406
|
const projectDirs = discoverProjects();
|
|
406
|
-
|
|
407
|
+
let loader = new MultiLoader(projectDirs);
|
|
407
408
|
try {
|
|
408
409
|
loader.load();
|
|
409
410
|
}
|
|
410
411
|
catch (err) {
|
|
411
412
|
process.stderr.write(`Warning: ${err.message}\n`);
|
|
412
413
|
}
|
|
414
|
+
// Auto-bootstrap: if no projects loaded, try to extract from CWD
|
|
415
|
+
if (!loader.isLoaded() && !process.env.SESHAT_PROJECTS) {
|
|
416
|
+
const cwd = process.cwd();
|
|
417
|
+
const seshatDir = path.join(cwd, '.seshat');
|
|
418
|
+
// Only bootstrap when .seshat/ doesn't exist at all (not corruption)
|
|
419
|
+
if (!fs.existsSync(seshatDir)) {
|
|
420
|
+
process.stderr.write(`[seshat-mcp] No .seshat/ found — attempting auto-bootstrap...\n`);
|
|
421
|
+
const result = await bootstrap(cwd);
|
|
422
|
+
if (result.success) {
|
|
423
|
+
// Re-create loader and retry
|
|
424
|
+
loader = new MultiLoader([cwd]);
|
|
425
|
+
try {
|
|
426
|
+
loader.load();
|
|
427
|
+
process.stderr.write(`[seshat-mcp] Auto-bootstrap succeeded: ${loader.totalEntities()} entities loaded\n`);
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
process.stderr.write(`[seshat-mcp] Auto-bootstrap produced files but load failed: ${err.message}\n`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
process.stderr.write(`[seshat-mcp] Auto-bootstrap failed: ${result.error || 'unknown error'}\n`);
|
|
435
|
+
process.stderr.write(`[seshat-mcp] Starting with 0 entities. Run extraction manually or push to trigger CI.\n`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
413
439
|
initTools(loader);
|
|
414
440
|
// Build server name
|
|
415
441
|
const projectNames = loader.getProjectNames();
|
|
@@ -428,7 +454,7 @@ async function main() {
|
|
|
428
454
|
}
|
|
429
455
|
const server = new Server({
|
|
430
456
|
name: serverLabel,
|
|
431
|
-
version: '0.
|
|
457
|
+
version: '0.4.0',
|
|
432
458
|
}, {
|
|
433
459
|
capabilities: {
|
|
434
460
|
tools: {},
|