@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.
@@ -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
- const loader = new MultiLoader(projectDirs);
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.3.3',
457
+ version: '0.4.0',
432
458
  }, {
433
459
  capabilities: {
434
460
  tools: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papyruslabsai/seshat-mcp",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Semantic MCP server — exposes a codebase's 9D JSTF-T coordinate space as queryable tools",
5
5
  "type": "module",
6
6
  "bin": {