devsmind-mcp 1.0.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,1104 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.DEVSMIND_PORT = void 0;
40
+ exports.runHttpMcpServer = runHttpMcpServer;
41
+ exports.runStdioMcpServer = runStdioMcpServer;
42
+ exports.runMcpServer = runMcpServer;
43
+ const index_js_1 = require("@modelcontextprotocol/sdk/server/index.js");
44
+ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
45
+ const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
46
+ const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
47
+ const path = __importStar(require("path"));
48
+ const http = __importStar(require("http"));
49
+ const fs = __importStar(require("fs"));
50
+ const express_1 = __importDefault(require("express"));
51
+ const database_1 = require("../db/database");
52
+ const visualizer_1 = require("./visualizer");
53
+ const indexer_1 = require("../db/indexer");
54
+ const scanner_1 = require("../utils/scanner");
55
+ // ─── Port: devs→D(4)E(5)=45 + mind→M(13)=13 → 4513 ─────────────────────────
56
+ exports.DEVSMIND_PORT = 4513;
57
+ // Cache database connections by their resolved path to avoid re-opening constantly
58
+ const dbCache = new Map();
59
+ // Walk up from a start directory to find a .devmind folder containing config.json
60
+ function findDevmindDir(startDir) {
61
+ let current = path.resolve(startDir);
62
+ while (true) {
63
+ const candidate = path.join(current, '.devmind');
64
+ if (fs.existsSync(path.join(candidate, 'config.json')))
65
+ return candidate;
66
+ const parent = path.dirname(current);
67
+ if (parent === current)
68
+ return null;
69
+ current = parent;
70
+ }
71
+ }
72
+ // Resolve devmind_path from args, falling back to auto-detect from cwd
73
+ function resolveDevmindPath(rawPath) {
74
+ const given = rawPath != null && String(rawPath) !== 'undefined' ? String(rawPath).trim() : '';
75
+ if (given) {
76
+ const resolved = path.resolve(given);
77
+ if (fs.existsSync(resolved))
78
+ return resolved;
79
+ // Try forward-slash variant (AI sometimes sends forward slashes on Windows)
80
+ const normalized = path.resolve(given.replace(/\//g, path.sep));
81
+ if (fs.existsSync(normalized))
82
+ return normalized;
83
+ throw new Error(`devmind_path does not exist: "${resolved}". Make sure you pass the exact DEVMIND_PATH from your workspace rules.`);
84
+ }
85
+ // Not provided — auto-detect from where devsmind start was run
86
+ const autoDetected = findDevmindDir(process.cwd());
87
+ if (autoDetected)
88
+ return autoDetected;
89
+ throw new Error(`devmind_path was not provided and no .devmind directory was found by walking up from: "${process.cwd()}". Pass devmind_path explicitly.`);
90
+ }
91
+ function getDatabase(devmindPath) {
92
+ const dbFile = path.join(devmindPath, 'brain.db');
93
+ if (!dbCache.has(dbFile)) {
94
+ dbCache.set(dbFile, new database_1.DevMindDatabase(dbFile));
95
+ }
96
+ return dbCache.get(dbFile);
97
+ }
98
+ function cleanup() {
99
+ for (const [dbPath, db] of dbCache.entries()) {
100
+ try {
101
+ db.close();
102
+ }
103
+ catch (err) {
104
+ // best-effort close
105
+ }
106
+ }
107
+ dbCache.clear();
108
+ }
109
+ /**
110
+ * Creates and wires up a DevsMind MCP Server instance.
111
+ * Stateless — every call receives devmind_path and opens the db from there.
112
+ */
113
+ function createMcpServer() {
114
+ const server = new index_js_1.Server({ name: 'devsmind-server', version: '1.0.0' }, { capabilities: { tools: {} } });
115
+ // ── Tool Definitions ────────────────────────────────────────────────────────
116
+ server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
117
+ return {
118
+ tools: [
119
+ {
120
+ name: 'get_node_summary',
121
+ description: 'Get a quick summary of a specific code node (existence, file location, connections count, history count, and last update timestamp).',
122
+ inputSchema: {
123
+ type: 'object',
124
+ properties: {
125
+ devmind_path: {
126
+ type: 'string',
127
+ description: 'Absolute path to the .devmind directory'
128
+ },
129
+ node_id: {
130
+ type: 'string',
131
+ description: 'Unique identifier for the node (e.g. function or class name)'
132
+ }
133
+ },
134
+ required: ['devmind_path', 'node_id']
135
+ }
136
+ },
137
+ {
138
+ name: 'list_nodes',
139
+ description: 'List all nodes matching optional type and file path filters. Useful to discover all entities in a component, package, or directory.',
140
+ inputSchema: {
141
+ type: 'object',
142
+ properties: {
143
+ devmind_path: {
144
+ type: 'string',
145
+ description: 'Absolute path to the .devmind directory'
146
+ },
147
+ type: {
148
+ type: 'string',
149
+ description: 'Optional filter by exact node type (e.g. nest_controller, react_component, function)'
150
+ },
151
+ file_path: {
152
+ type: 'string',
153
+ description: 'Optional filter by file path substring (e.g. "src/components" or specific file name)'
154
+ },
155
+ include_deprecated: {
156
+ type: 'boolean',
157
+ description: 'Optional flag to include deprecated nodes (default: false)'
158
+ }
159
+ },
160
+ required: ['devmind_path']
161
+ }
162
+ },
163
+ {
164
+ name: 'get_node_code',
165
+ description: 'Returns only the latest code snapshot stored for a node. Token-efficient alternative to get_node_history when you only need the current code. Returns null if no snapshot exists — in that case you MUST read the file, then call update_history to store the code so future agents benefit from the cache.',
166
+ inputSchema: {
167
+ type: 'object',
168
+ properties: {
169
+ devmind_path: {
170
+ type: 'string',
171
+ description: 'Absolute path to the .devmind directory'
172
+ },
173
+ node_id: {
174
+ type: 'string',
175
+ description: 'Unique identifier for the node'
176
+ }
177
+ },
178
+ required: ['devmind_path', 'node_id']
179
+ }
180
+ },
181
+ {
182
+ name: 'update_history',
183
+ description: 'Update the version history of a code node (applies the 1-hour session boundary rule). Also creates the node if it does not exist.',
184
+ inputSchema: {
185
+ type: 'object',
186
+ properties: {
187
+ devmind_path: {
188
+ type: 'string',
189
+ description: 'Absolute path to the .devmind directory'
190
+ },
191
+ node_id: {
192
+ type: 'string',
193
+ description: 'Unique identifier for the node (e.g. function/method identifier)'
194
+ },
195
+ file_path: {
196
+ type: 'string',
197
+ description: 'Source file path where the node is located'
198
+ },
199
+ code_snapshot: {
200
+ type: 'string',
201
+ description: 'Full source code content of the node at this moment'
202
+ },
203
+ reasoning: {
204
+ type: 'object',
205
+ description: 'Structured details about this change',
206
+ properties: {
207
+ what_changed: {
208
+ type: 'string',
209
+ description: 'Brief description of the modified code'
210
+ },
211
+ why: { type: 'string', description: 'The reason this change was made' },
212
+ goal: { type: 'string', description: 'What was being achieved' },
213
+ requirement: {
214
+ type: 'string',
215
+ description: 'Ticket / issue / user request ID if applicable'
216
+ },
217
+ previous_state: {
218
+ type: 'string',
219
+ description: 'What the code looked like before and why it was a problem'
220
+ },
221
+ decision: {
222
+ type: 'string',
223
+ description: 'Architectural or implementation decision and why'
224
+ },
225
+ developer: { type: 'string', description: 'Name of the developer' },
226
+ model: { type: 'string', description: 'AI model name used' }
227
+ },
228
+ required: ['what_changed', 'why', 'goal']
229
+ },
230
+ name: {
231
+ type: 'string',
232
+ description: 'Display name of the node (optional, will be inferred if omitted)'
233
+ },
234
+ type: {
235
+ type: 'string',
236
+ description: '(optional, defaults to function) The type of node. Be highly specific and framework-aware. Choose from the taxonomy below (or use a custom value if nothing fits).\n\n' +
237
+ 'UNIVERSAL: function | method | class | abstract_class | interface | type_alias | enum | constant | variable | module | namespace | decorator\n\n' +
238
+ 'NESTJS: nest_module | nest_controller | nest_service | nest_provider | nest_guard | nest_interceptor | nest_pipe | nest_filter | nest_decorator | nest_middleware | nest_gateway | nest_resolver | nest_schema | nest_dto\n\n' +
239
+ 'EXPRESS/FASTIFY/KOA/HONO: route_handler | middleware | router\n\n' +
240
+ 'SPRING (Java): spring_controller | spring_service | spring_repository | spring_component | spring_bean | spring_config | spring_entity\n\n' +
241
+ 'DJANGO/FASTAPI (Python): django_view | django_model | django_serializer | django_form | django_signal | fastapi_router | fastapi_dependency\n\n' +
242
+ 'GO: go_handler | go_middleware | go_struct | go_interface | go_func\n\n' +
243
+ 'RUST: rust_struct | rust_impl | rust_trait | rust_enum | rust_fn | rust_macro\n\n' +
244
+ 'REACT: react_component | react_hook | react_context | react_hoc | react_page\n\n' +
245
+ 'NEXT.JS: next_page | next_layout | next_api_route | next_server_action | next_middleware\n\n' +
246
+ 'VUE: vue_component | vue_composable | vue_directive | vue_store_module\n\n' +
247
+ 'ANGULAR: ng_component | ng_service | ng_directive | ng_pipe | ng_module | ng_guard | ng_interceptor | ng_resolver\n\n' +
248
+ 'SVELTE: svelte_component | svelte_store | svelte_action\n\n' +
249
+ 'ORM — PRISMA: prisma_model | prisma_query | prisma_migration\n' +
250
+ 'ORM — TYPEORM: typeorm_entity | typeorm_repository | typeorm_migration\n' +
251
+ 'ORM — MONGOOSE: mongoose_model | mongoose_schema\n' +
252
+ 'ORM — SQLALCHEMY: sqlalchemy_model | sqlalchemy_query\n' +
253
+ 'ORM — SEQUELIZE: sequelize_model | sequelize_migration\n\n' +
254
+ 'REST/API: api_endpoint | rest_controller\n' +
255
+ 'GRAPHQL: graphql_resolver | graphql_query | graphql_mutation | graphql_subscription | graphql_schema | graphql_directive\n' +
256
+ 'GRPC/PROTO: grpc_service | grpc_method | proto_message\n' +
257
+ 'WEBSOCKET: ws_gateway | ws_handler\n' +
258
+ 'MESSAGE QUEUE: mq_producer | mq_consumer | mq_handler\n\n' +
259
+ 'CONFIG/AUTH: config_loader | env_config | feature_flag | auth_guard | auth_strategy | jwt_util | permission_policy\n' +
260
+ 'OBSERVABILITY: logger | metric | trace_span\n' +
261
+ 'CLI: cli_command | cli_option\n' +
262
+ 'SCRIPTS: build_script | migration_script | seed_script\n' +
263
+ 'TESTS: test_suite | test_case | test_helper | mock | fixture\n' +
264
+ 'UTILITY: util_function | helper | transformer | validator | formatter'
265
+ },
266
+ signature: {
267
+ type: 'string',
268
+ description: 'Function parameter types and return type signature (optional)'
269
+ },
270
+ session_id: {
271
+ type: 'string',
272
+ description: 'Session identifier to associate with this history update (optional)'
273
+ }
274
+ },
275
+ required: ['devmind_path', 'node_id', 'file_path', 'code_snapshot', 'reasoning']
276
+ }
277
+ },
278
+ // ── Indexing tools ─────────────────────────────────────────────
279
+ {
280
+ name: 'index_start',
281
+ description: 'Initialize an indexing session. Scans all configured repos, counts files, creates a scratchpad to track progress. Returns the full file list per repo so the AI can begin reading and indexing files.',
282
+ inputSchema: {
283
+ type: 'object',
284
+ properties: {
285
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' }
286
+ },
287
+ required: ['devmind_path']
288
+ }
289
+ },
290
+ {
291
+ name: 'index_checkpoint',
292
+ description: 'Save current indexing progress to the scratchpad. Call this every ~10 files so progress survives a context reset.',
293
+ inputSchema: {
294
+ type: 'object',
295
+ properties: {
296
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
297
+ last_file_indexed: { type: 'string', description: 'Absolute path to the last file that was fully indexed' },
298
+ files_done: { type: 'number', description: 'Total files indexed so far' },
299
+ nodes_created: { type: 'number', description: 'Total nodes created so far' },
300
+ connections_created: { type: 'number', description: 'Total connections created so far' },
301
+ current_repo: { type: 'string', description: 'Name of the repo currently being indexed' },
302
+ repos_done: {
303
+ type: 'array',
304
+ items: { type: 'string' },
305
+ description: 'Names of repos fully indexed so far'
306
+ }
307
+ },
308
+ required: ['devmind_path', 'files_done', 'nodes_created']
309
+ }
310
+ },
311
+ {
312
+ name: 'index_continue',
313
+ description: 'Read the scratchpad and return exactly where indexing left off. Use this to resume after a context reset.',
314
+ inputSchema: {
315
+ type: 'object',
316
+ properties: {
317
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' }
318
+ },
319
+ required: ['devmind_path']
320
+ }
321
+ },
322
+ {
323
+ name: 'index_complete',
324
+ description: 'Mark the indexing session as complete. Call this when all files in all repos have been indexed.',
325
+ inputSchema: {
326
+ type: 'object',
327
+ properties: {
328
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' }
329
+ },
330
+ required: ['devmind_path']
331
+ }
332
+ },
333
+ {
334
+ name: 'add_node',
335
+ description: 'Add or update a code node in the graph (function, method, class, service, controller, type, interface, schema, enum, variable, etc.). Called during indexing for every entity found.',
336
+ inputSchema: {
337
+ type: 'object',
338
+ properties: {
339
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
340
+ node_id: { type: 'string', description: 'Unique identifier e.g. "CartService.applyPromoCode" or "calculateDiscount"' },
341
+ name: { type: 'string', description: 'Display name of the node' },
342
+ type: {
343
+ type: 'string',
344
+ description: 'The type of node. Be highly specific and framework-aware. Choose from the taxonomy below (or use a custom value if nothing fits).\n\n' +
345
+ 'UNIVERSAL: function | method | class | abstract_class | interface | type_alias | enum | constant | variable | module | namespace | decorator\n\n' +
346
+ 'NESTJS: nest_module | nest_controller | nest_service | nest_provider | nest_guard | nest_interceptor | nest_pipe | nest_filter | nest_decorator | nest_middleware | nest_gateway | nest_resolver | nest_schema | nest_dto\n\n' +
347
+ 'EXPRESS/FASTIFY/KOA/HONO: route_handler | middleware | router\n\n' +
348
+ 'SPRING (Java): spring_controller | spring_service | spring_repository | spring_component | spring_bean | spring_config | spring_entity\n\n' +
349
+ 'DJANGO/FASTAPI (Python): django_view | django_model | django_serializer | django_form | django_signal | fastapi_router | fastapi_dependency\n\n' +
350
+ 'GO: go_handler | go_middleware | go_struct | go_interface | go_func\n\n' +
351
+ 'RUST: rust_struct | rust_impl | rust_trait | rust_enum | rust_fn | rust_macro\n\n' +
352
+ 'REACT: react_component | react_hook | react_context | react_hoc | react_page\n\n' +
353
+ 'NEXT.JS: next_page | next_layout | next_api_route | next_server_action | next_middleware\n\n' +
354
+ 'VUE: vue_component | vue_composable | vue_directive | vue_store_module\n\n' +
355
+ 'ANGULAR: ng_component | ng_service | ng_directive | ng_pipe | ng_module | ng_guard | ng_interceptor | ng_resolver\n\n' +
356
+ 'SVELTE: svelte_component | svelte_store | svelte_action\n\n' +
357
+ 'ORM — PRISMA: prisma_model | prisma_query | prisma_migration\n' +
358
+ 'ORM — TYPEORM: typeorm_entity | typeorm_repository | typeorm_migration\n' +
359
+ 'ORM — MONGOOSE: mongoose_model | mongoose_schema\n' +
360
+ 'ORM — SQLALCHEMY: sqlalchemy_model | sqlalchemy_query\n' +
361
+ 'ORM — SEQUELIZE: sequelize_model | sequelize_migration\n\n' +
362
+ 'REST/API: api_endpoint | rest_controller\n' +
363
+ 'GRAPHQL: graphql_resolver | graphql_query | graphql_mutation | graphql_subscription | graphql_schema | graphql_directive\n' +
364
+ 'GRPC/PROTO: grpc_service | grpc_method | proto_message\n' +
365
+ 'WEBSOCKET: ws_gateway | ws_handler\n' +
366
+ 'MESSAGE QUEUE: mq_producer | mq_consumer | mq_handler\n\n' +
367
+ 'CONFIG/AUTH: config_loader | env_config | feature_flag | auth_guard | auth_strategy | jwt_util | permission_policy\n' +
368
+ 'OBSERVABILITY: logger | metric | trace_span\n' +
369
+ 'CLI: cli_command | cli_option\n' +
370
+ 'SCRIPTS: build_script | migration_script | seed_script\n' +
371
+ 'TESTS: test_suite | test_case | test_helper | mock | fixture\n' +
372
+ 'UTILITY: util_function | helper | transformer | validator | formatter'
373
+ },
374
+ file_path: { type: 'string', description: 'Absolute path to the source file' },
375
+ signature: { type: 'string', description: 'Parameter types + return type (optional)' }
376
+ },
377
+ required: ['devmind_path', 'node_id', 'name', 'type', 'file_path']
378
+ }
379
+ },
380
+ {
381
+ name: 'add_connection',
382
+ description: 'Add a directional relationship between two nodes: source USES/CALLS target. Called during indexing to map the dependency graph.',
383
+ inputSchema: {
384
+ type: 'object',
385
+ properties: {
386
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
387
+ source_node_id: { type: 'string', description: 'The node that calls or uses the target' },
388
+ target_node_id: { type: 'string', description: 'The node being called or used' }
389
+ },
390
+ required: ['devmind_path', 'source_node_id', 'target_node_id']
391
+ }
392
+ },
393
+ {
394
+ name: 'recheck_graph',
395
+ description: 'Recheck and prune spurious nodes/connections from the code graph. Removes primitives, language globals/built-ins, and nodes pointing to deleted/missing files, provided they have zero history entries (preserving change logs).',
396
+ inputSchema: {
397
+ type: 'object',
398
+ properties: {
399
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
400
+ workspace_root: { type: 'string', description: 'Absolute path to the workspace root directory to resolve relative paths and verify files exist' }
401
+ },
402
+ required: ['devmind_path', 'workspace_root']
403
+ }
404
+ },
405
+ {
406
+ name: 'get_node_history',
407
+ description: 'Get the full version history of a code node, including all past code snapshots and change reasoning.',
408
+ inputSchema: {
409
+ type: 'object',
410
+ properties: {
411
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
412
+ node_id: { type: 'string', description: 'Unique identifier for the node (e.g. function or class name)' }
413
+ },
414
+ required: ['devmind_path', 'node_id']
415
+ }
416
+ },
417
+ {
418
+ name: 'get_node_graph',
419
+ description: 'Get a localized node dependency graph up to a specified depth (default 6). Returns connected nodes and relationships.',
420
+ inputSchema: {
421
+ type: 'object',
422
+ properties: {
423
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
424
+ node_id: { type: 'string', description: 'Unique identifier for the starting node (e.g. function or class name)' },
425
+ max_depth: { type: 'number', description: 'Maximum depth to traverse (optional, default 6)' }
426
+ },
427
+ required: ['devmind_path', 'node_id']
428
+ }
429
+ },
430
+ {
431
+ name: 'search_nodes',
432
+ description: 'Search node names, identifiers, or reasoning logs matching a query.',
433
+ inputSchema: {
434
+ type: 'object',
435
+ properties: {
436
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
437
+ query: { type: 'string', description: 'Search term or query string' }
438
+ },
439
+ required: ['devmind_path', 'query']
440
+ }
441
+ },
442
+ {
443
+ name: 'rename_node',
444
+ description: 'Rename a code node ID (and optionally its display name), updating all its associations (connections and history).',
445
+ inputSchema: {
446
+ type: 'object',
447
+ properties: {
448
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
449
+ old_node_id: { type: 'string', description: 'Current unique identifier for the node' },
450
+ new_node_id: { type: 'string', description: 'New unique identifier for the node' },
451
+ new_name: { type: 'string', description: 'Optional new display name for the node' }
452
+ },
453
+ required: ['devmind_path', 'old_node_id', 'new_node_id']
454
+ }
455
+ },
456
+ {
457
+ name: 'deprecate_node',
458
+ description: 'Mark a code node as deprecated, removing all its connection mappings while retaining its entry and evolution history in the database. Use this if a function/class is deleted/removed from the codebase.',
459
+ inputSchema: {
460
+ type: 'object',
461
+ properties: {
462
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
463
+ node_id: { type: 'string', description: 'Unique identifier for the node to deprecate' }
464
+ },
465
+ required: ['devmind_path', 'node_id']
466
+ }
467
+ },
468
+ {
469
+ name: 'get_recent_changes',
470
+ description: 'Get team modifications and history updates over the last N hours.',
471
+ inputSchema: {
472
+ type: 'object',
473
+ properties: {
474
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
475
+ hours: { type: 'number', description: 'Lookback window in hours (optional, default 24)' }
476
+ },
477
+ required: ['devmind_path']
478
+ }
479
+ },
480
+ {
481
+ name: 'get_developer_activity',
482
+ description: 'List recent history logs and changes made by a specific developer.',
483
+ inputSchema: {
484
+ type: 'object',
485
+ properties: {
486
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
487
+ developer: { type: 'string', description: 'Name or email of the developer' },
488
+ limit: { type: 'number', description: 'Maximum logs to return (optional, default 50)' }
489
+ },
490
+ required: ['devmind_path', 'developer']
491
+ }
492
+ },
493
+ {
494
+ name: 'get_changes_by_requirement',
495
+ description: 'List all modifications linked to a specific requirement, ticket, or issue ID.',
496
+ inputSchema: {
497
+ type: 'object',
498
+ properties: {
499
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
500
+ requirement_id: { type: 'string', description: 'The requirement, ticket, or issue ID' }
501
+ },
502
+ required: ['devmind_path', 'requirement_id']
503
+ }
504
+ },
505
+ {
506
+ name: 'search_decisions',
507
+ description: 'Search reasoning logs for specific architectural or implementation decisions.',
508
+ inputSchema: {
509
+ type: 'object',
510
+ properties: {
511
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
512
+ query: { type: 'string', description: 'Term or keyword to search' }
513
+ },
514
+ required: ['devmind_path', 'query']
515
+ }
516
+ },
517
+ {
518
+ name: 'get_orphaned_nodes',
519
+ description: 'Find disconnected code nodes in the graph that have no incoming or outgoing connections.',
520
+ inputSchema: {
521
+ type: 'object',
522
+ properties: {
523
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' }
524
+ },
525
+ required: ['devmind_path']
526
+ }
527
+ },
528
+ {
529
+ name: 'get_visualizer_url',
530
+ description: 'Get local URLs to open the interactive 2D and 3D code graph visualizer pages.',
531
+ inputSchema: {
532
+ type: 'object',
533
+ properties: {
534
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' }
535
+ },
536
+ required: ['devmind_path']
537
+ }
538
+ }
539
+ ]
540
+ };
541
+ });
542
+ // ── Tool Execution ───────────────────────────────────────────────────────────
543
+ server.setRequestHandler(types_js_1.CallToolRequestSchema, async (request) => {
544
+ const { name, arguments: args } = request.params;
545
+ if (!args) {
546
+ throw new Error('Arguments are required');
547
+ }
548
+ try {
549
+ switch (name) {
550
+ case 'get_node_summary': {
551
+ const devmindPath = resolveDevmindPath(args.devmind_path);
552
+ const nodeId = String(args.node_id);
553
+ const db = getDatabase(devmindPath);
554
+ const node = db.getNode(nodeId);
555
+ if (!node) {
556
+ return {
557
+ content: [
558
+ { type: 'text', text: JSON.stringify({ exists: false, node_id: nodeId }) }
559
+ ]
560
+ };
561
+ }
562
+ const connections = db.getConnections(nodeId);
563
+ const connectionCount = connections.uses.length + connections.usedBy.length;
564
+ const historyList = db.listHistory(nodeId);
565
+ const latestHistory = db.getLatestHistory(nodeId);
566
+ const summary = {
567
+ exists: true,
568
+ node_id: node.id,
569
+ name: node.name,
570
+ type: node.type,
571
+ file_path: node.file_path,
572
+ signature: node.signature,
573
+ connection_count: connectionCount,
574
+ history_count: historyList.length,
575
+ last_updated: latestHistory ? latestHistory.updated_at : node.created_at
576
+ };
577
+ return {
578
+ content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }]
579
+ };
580
+ }
581
+ case 'list_nodes': {
582
+ const devmindPath = resolveDevmindPath(args.devmind_path);
583
+ const type = args.type ? String(args.type) : undefined;
584
+ const filePath = args.file_path ? String(args.file_path) : undefined;
585
+ const includeDeprecated = args.include_deprecated === true;
586
+ const db = getDatabase(devmindPath);
587
+ const nodes = db.listNodes({ type, file_path: filePath, include_deprecated: includeDeprecated });
588
+ return {
589
+ content: [{ type: 'text', text: JSON.stringify(nodes, null, 2) }]
590
+ };
591
+ }
592
+ case 'get_node_code': {
593
+ const devmindPath = resolveDevmindPath(args.devmind_path);
594
+ const nodeId = String(args.node_id);
595
+ const db = getDatabase(devmindPath);
596
+ const result = db.getLatestCode(nodeId);
597
+ if (!result) {
598
+ return {
599
+ content: [{ type: 'text', text: JSON.stringify({
600
+ exists: false,
601
+ node_id: nodeId,
602
+ message: 'No code snapshot found. Read the source file, then call update_history to cache the code so future agents skip the file read entirely.'
603
+ }) }]
604
+ };
605
+ }
606
+ return {
607
+ content: [{ type: 'text', text: JSON.stringify({
608
+ exists: true,
609
+ node_id: nodeId,
610
+ updated_at: result.updated_at,
611
+ code_snapshot: result.code_snapshot
612
+ }) }]
613
+ };
614
+ }
615
+ case 'update_history': {
616
+ const devmindPath = resolveDevmindPath(args.devmind_path);
617
+ const nodeId = String(args.node_id);
618
+ const filePath = String(args.file_path);
619
+ const codeSnapshot = String(args.code_snapshot);
620
+ const reasoning = args.reasoning;
621
+ let nodeName = args.name ? String(args.name) : undefined;
622
+ let nodeType = args.type ? String(args.type) : undefined;
623
+ const signature = args.signature ? String(args.signature) : undefined;
624
+ const sessionId = args.session_id ? String(args.session_id) : undefined;
625
+ // Infer name and type if not provided
626
+ if (!nodeName) {
627
+ nodeName = nodeId.includes('.') ? nodeId.split('.').pop() : nodeId;
628
+ }
629
+ if (!nodeType) {
630
+ nodeType = nodeId.includes('.') ? 'method' : 'function';
631
+ }
632
+ const db = getDatabase(devmindPath);
633
+ // 1. Ensure node exists (Upsert)
634
+ db.upsertNode({
635
+ id: nodeId,
636
+ name: nodeName,
637
+ type: nodeType,
638
+ file_path: filePath,
639
+ signature: signature || null
640
+ });
641
+ // 2. Update history with 1-hour session boundary rule
642
+ const historyEntry = db.updateHistory({
643
+ node_id: nodeId,
644
+ code_snapshot: codeSnapshot,
645
+ reasoning,
646
+ session_id: sessionId
647
+ });
648
+ return {
649
+ content: [
650
+ {
651
+ type: 'text',
652
+ text: JSON.stringify({
653
+ success: true,
654
+ message: 'History updated successfully',
655
+ node: { id: nodeId, name: nodeName, type: nodeType },
656
+ history_entry: {
657
+ id: historyEntry.id,
658
+ session_id: historyEntry.session_id,
659
+ created_at: historyEntry.created_at,
660
+ updated_at: historyEntry.updated_at
661
+ }
662
+ }, null, 2)
663
+ }
664
+ ]
665
+ };
666
+ }
667
+ // ── Indexing tool handlers ─────────────────────────────────────
668
+ case 'index_start': {
669
+ const devmindPath = resolveDevmindPath(args.devmind_path);
670
+ const { repos, total_files } = (0, scanner_1.scanRepoFiles)(devmindPath);
671
+ const pad = (0, indexer_1.createScratchpad)(devmindPath, total_files);
672
+ const repoSummaries = repos.map(r => ({
673
+ repo_name: r.repo_name,
674
+ repo_path: r.repo_path,
675
+ file_count: r.file_count,
676
+ files: r.files // full list so AI can iterate
677
+ }));
678
+ return {
679
+ content: [{
680
+ type: 'text',
681
+ text: JSON.stringify({
682
+ message: 'Indexing session started. Begin reading files and calling add_node + add_connection for every entity found. Call index_checkpoint every 10 files.',
683
+ scratchpad: pad,
684
+ repos: repoSummaries,
685
+ total_files,
686
+ instructions: [
687
+ '⚠️⚠️⚠️ CRITICAL INSTRUCTION FOR THE INDEXING AGENT — MUST READ ⚠️⚠️⚠️',
688
+ 'YOU MUST EXPLICITLY CALL THE "add_node" AND "add_connection" MCP TOOLS FOR EVERY NODE/CONNECTION YOU EXTRACT.',
689
+ 'DO NOT JUST PRINT THE RESULTS AS TEXT IN THE CHAT WINDOW. PRINTING RESULTS WITHOUT CALLING THE MCP TOOLS DOES NOT WRITE THEM TO THE DATABASE AND MAKES THE ENTIRE INDEXING RUN A WASTE OF TIME AND TOKENS.',
690
+ 'IF YOU ENCOUNTER CONTEXT RESETS, RESUME WORK BY CALLING "index_continue" AND CONTINUOUSLY COMMIT PROGRESS BY CALLING "index_checkpoint" EVERY 10 FILES.',
691
+ '',
692
+ '📋 CODE EXCLUSION & PRECISION RULES:',
693
+ '1. EXCLUDE Language Globals / Built-ins: Do NOT call add_node/add_connection for Promise, Map, Set, JSON, console, Error, Object, Array, RegExp, Date, Math, etc.',
694
+ '2. EXCLUDE Primitive/Native Types: Do NOT call add_node/add_connection for string, number, boolean, any, void, unknown, never, null, undefined, dict, list, etc.',
695
+ '3. EXCLUDE External / Third-party Modules: Do NOT call add_node/add_connection for lodash, express, react, @nestjs/common, etc.',
696
+ '4. INTERNAL ENTITIES ONLY: Only create nodes and connections for constructs defined inside this codebase.',
697
+ '',
698
+ '📋 GENERAL STEPS:',
699
+ '1. For each file in each repo: read it, extract ALL nodes — functions, methods, classes, interfaces, types, enums, constants, exported variables, decorators, DTO classes, schema definitions, route handlers, components, hooks, guards, resolvers, etc.',
700
+ '2. Call add_node for every entity found. Choose the most specific type from the taxonomy (e.g. nest_guard, react_hook, typeorm_entity, graphql_resolver). Fall back to universal types (function, class, interface, enum) only when no framework-specific type applies.',
701
+ ' TAXONOMY QUICK REFERENCE:',
702
+ ' • Universal: function | method | class | abstract_class | interface | type_alias | enum | constant | variable | module | namespace | decorator',
703
+ ' • NestJS: nest_module | nest_controller | nest_service | nest_provider | nest_guard | nest_interceptor | nest_pipe | nest_filter | nest_decorator | nest_middleware | nest_gateway | nest_resolver | nest_schema | nest_dto',
704
+ ' • Express/Koa: route_handler | middleware | router',
705
+ ' • Spring(Java): spring_controller | spring_service | spring_repository | spring_component | spring_bean | spring_config | spring_entity',
706
+ ' • Django/FastAPI: django_view | django_model | django_serializer | django_form | django_signal | fastapi_router | fastapi_dependency',
707
+ ' • Go: go_handler | go_middleware | go_struct | go_interface | go_func',
708
+ ' • Rust: rust_struct | rust_impl | rust_trait | rust_enum | rust_fn | rust_macro',
709
+ ' • React: react_component | react_hook | react_context | react_hoc | react_page',
710
+ ' • Next.js: next_page | next_layout | next_api_route | next_server_action | next_middleware',
711
+ ' • Vue: vue_component | vue_composable | vue_directive | vue_store_module',
712
+ ' • Angular: ng_component | ng_service | ng_directive | ng_pipe | ng_module | ng_guard | ng_interceptor | ng_resolver',
713
+ ' • Svelte: svelte_component | svelte_store | svelte_action',
714
+ ' • ORM/Prisma: prisma_model | prisma_query | prisma_migration',
715
+ ' • ORM/TypeORM: typeorm_entity | typeorm_repository | typeorm_migration',
716
+ ' • ORM/Mongoose: mongoose_model | mongoose_schema',
717
+ ' • GraphQL: graphql_resolver | graphql_query | graphql_mutation | graphql_subscription | graphql_schema | graphql_directive',
718
+ ' • gRPC: grpc_service | grpc_method | proto_message',
719
+ ' • WebSocket: ws_gateway | ws_handler',
720
+ ' • MQ: mq_producer | mq_consumer | mq_handler',
721
+ ' • Auth/Config: config_loader | env_config | feature_flag | auth_guard | auth_strategy | jwt_util | permission_policy',
722
+ ' • Observability: logger | metric | trace_span',
723
+ ' • CLI: cli_command | cli_option',
724
+ ' • Scripts: build_script | migration_script | seed_script',
725
+ ' • Tests: test_suite | test_case | test_helper | mock | fixture',
726
+ ' • Utility: util_function | helper | transformer | validator | formatter',
727
+ '3. Call add_connection for every call/use relationship (source USES target). Only map to internal nodes.',
728
+ '4. Call index_checkpoint every 10 files with updated progress.',
729
+ '5. When all files are indexed, call index_complete.',
730
+ '6. AFTER index_complete, CALL "recheck_graph" to automatically prune any spurious, built-in, or orphaned nodes and ensure high graph precision.'
731
+ ]
732
+ }, null, 2)
733
+ }]
734
+ };
735
+ }
736
+ case 'index_checkpoint': {
737
+ const devmindPath = resolveDevmindPath(args.devmind_path);
738
+ const pad = (0, indexer_1.updateScratchpad)(devmindPath, {
739
+ last_file_indexed: args.last_file_indexed ? String(args.last_file_indexed) : undefined,
740
+ files_done: typeof args.files_done === 'number' ? args.files_done : 0,
741
+ nodes_created: typeof args.nodes_created === 'number' ? args.nodes_created : 0,
742
+ connections_created: typeof args.connections_created === 'number' ? args.connections_created : 0,
743
+ current_repo: args.current_repo ? String(args.current_repo) : undefined,
744
+ repos_done: Array.isArray(args.repos_done) ? args.repos_done : undefined
745
+ });
746
+ const pct = pad.files_total > 0
747
+ ? Math.round((pad.files_done / pad.files_total) * 100)
748
+ : 0;
749
+ return {
750
+ content: [{
751
+ type: 'text',
752
+ text: JSON.stringify({ saved: true, progress: `${pad.files_done}/${pad.files_total} files (${pct}%)`, scratchpad: pad }, null, 2)
753
+ }]
754
+ };
755
+ }
756
+ case 'index_continue': {
757
+ const devmindPath = resolveDevmindPath(args.devmind_path);
758
+ const pad = (0, indexer_1.readScratchpad)(devmindPath);
759
+ if (!pad) {
760
+ return {
761
+ content: [{ type: 'text', text: JSON.stringify({ error: 'No indexing session found. Call index_start first.' }) }]
762
+ };
763
+ }
764
+ if (pad.status === 'complete') {
765
+ return {
766
+ content: [{ type: 'text', text: JSON.stringify({ status: 'complete', message: 'Indexing already completed.', scratchpad: pad }, null, 2) }]
767
+ };
768
+ }
769
+ // Re-scan to get file lists so AI knows which files are left
770
+ const { repos } = (0, scanner_1.scanRepoFiles)(devmindPath);
771
+ const reposDone = new Set(pad.repos_done);
772
+ const remaining = repos
773
+ .filter(r => !reposDone.has(r.repo_name))
774
+ .map(r => ({ repo_name: r.repo_name, repo_path: r.repo_path, files: r.files, file_count: r.file_count }));
775
+ return {
776
+ content: [{
777
+ type: 'text',
778
+ text: JSON.stringify({
779
+ message: 'Resume indexing from where you left off.',
780
+ scratchpad: pad,
781
+ last_file_indexed: pad.last_file_indexed,
782
+ repos_done: pad.repos_done,
783
+ remaining_repos: remaining
784
+ }, null, 2)
785
+ }]
786
+ };
787
+ }
788
+ case 'index_complete': {
789
+ const devmindPath = resolveDevmindPath(args.devmind_path);
790
+ const pad = (0, indexer_1.completeScratchpad)(devmindPath);
791
+ return {
792
+ content: [{
793
+ type: 'text',
794
+ text: JSON.stringify({
795
+ message: '✅ Indexing complete! Full graph is now available.',
796
+ summary: {
797
+ files_indexed: pad.files_done,
798
+ nodes_created: pad.nodes_created,
799
+ connections_created: pad.connections_created,
800
+ started_at: pad.started_at,
801
+ completed_at: pad.updated_at
802
+ }
803
+ }, null, 2)
804
+ }]
805
+ };
806
+ }
807
+ case 'add_node': {
808
+ const devmindPath = resolveDevmindPath(args.devmind_path);
809
+ const db = getDatabase(devmindPath);
810
+ db.upsertNode({
811
+ id: String(args.node_id),
812
+ name: String(args.name),
813
+ type: String(args.type),
814
+ file_path: String(args.file_path),
815
+ signature: args.signature ? String(args.signature) : null
816
+ });
817
+ return {
818
+ content: [{ type: 'text', text: JSON.stringify({ added: true, node_id: args.node_id }) }]
819
+ };
820
+ }
821
+ case 'add_connection': {
822
+ const devmindPath = resolveDevmindPath(args.devmind_path);
823
+ const db = getDatabase(devmindPath);
824
+ db.addConnection(String(args.source_node_id), String(args.target_node_id));
825
+ return {
826
+ content: [{ type: 'text', text: JSON.stringify({ added: true, source: args.source_node_id, target: args.target_node_id }) }]
827
+ };
828
+ }
829
+ case 'recheck_graph': {
830
+ const devmindPath = resolveDevmindPath(args.devmind_path);
831
+ const workspaceRoot = String(args.workspace_root);
832
+ const db = getDatabase(devmindPath);
833
+ const result = db.pruneSpuriousNodes(workspaceRoot);
834
+ return {
835
+ content: [{
836
+ type: 'text',
837
+ text: JSON.stringify({
838
+ success: true,
839
+ message: `✅ Graph recheck completed. Pruned ${result.prunedCount} spurious node(s) and their connections.`,
840
+ pruned_count: result.prunedCount,
841
+ pruned_nodes: result.prunedNodes
842
+ }, null, 2)
843
+ }]
844
+ };
845
+ }
846
+ case 'get_node_history': {
847
+ const devmindPath = resolveDevmindPath(args.devmind_path);
848
+ const nodeId = String(args.node_id);
849
+ const db = getDatabase(devmindPath);
850
+ const history = db.getFullHistory(nodeId);
851
+ return {
852
+ content: [{ type: 'text', text: JSON.stringify(history, null, 2) }]
853
+ };
854
+ }
855
+ case 'get_node_graph': {
856
+ const devmindPath = resolveDevmindPath(args.devmind_path);
857
+ const nodeId = String(args.node_id);
858
+ const maxDepth = args.max_depth ? Number(args.max_depth) : 6;
859
+ const db = getDatabase(devmindPath);
860
+ const graph = db.getGraph(nodeId, maxDepth);
861
+ return {
862
+ content: [{ type: 'text', text: JSON.stringify(graph, null, 2) }]
863
+ };
864
+ }
865
+ case 'search_nodes': {
866
+ const devmindPath = resolveDevmindPath(args.devmind_path);
867
+ const query = String(args.query);
868
+ const db = getDatabase(devmindPath);
869
+ const results = db.searchNodes(query);
870
+ return {
871
+ content: [{ type: 'text', text: JSON.stringify(results, null, 2) }]
872
+ };
873
+ }
874
+ case 'rename_node': {
875
+ const devmindPath = resolveDevmindPath(args.devmind_path);
876
+ const oldNodeId = String(args.old_node_id);
877
+ const newNodeId = String(args.new_node_id);
878
+ const newName = args.new_name ? String(args.new_name) : undefined;
879
+ const db = getDatabase(devmindPath);
880
+ db.renameNode(oldNodeId, newNodeId, newName);
881
+ return {
882
+ content: [{ type: 'text', text: JSON.stringify({ success: true, old_node_id: oldNodeId, new_node_id: newNodeId }) }]
883
+ };
884
+ }
885
+ case 'deprecate_node': {
886
+ const devmindPath = resolveDevmindPath(args.devmind_path);
887
+ const nodeId = String(args.node_id);
888
+ const db = getDatabase(devmindPath);
889
+ db.deprecateNode(nodeId);
890
+ return {
891
+ content: [{ type: 'text', text: JSON.stringify({ success: true, deprecated: nodeId }) }]
892
+ };
893
+ }
894
+ case 'get_recent_changes': {
895
+ const devmindPath = resolveDevmindPath(args.devmind_path);
896
+ const hours = args.hours ? Number(args.hours) : 24;
897
+ const db = getDatabase(devmindPath);
898
+ const changes = db.getRecentChanges(hours);
899
+ return {
900
+ content: [{ type: 'text', text: JSON.stringify(changes, null, 2) }]
901
+ };
902
+ }
903
+ case 'get_developer_activity': {
904
+ const devmindPath = resolveDevmindPath(args.devmind_path);
905
+ const developer = String(args.developer);
906
+ const limit = args.limit ? Number(args.limit) : 50;
907
+ const db = getDatabase(devmindPath);
908
+ const activity = db.getDeveloperActivity(developer, limit);
909
+ return {
910
+ content: [{ type: 'text', text: JSON.stringify(activity, null, 2) }]
911
+ };
912
+ }
913
+ case 'get_changes_by_requirement': {
914
+ const devmindPath = resolveDevmindPath(args.devmind_path);
915
+ const requirementId = String(args.requirement_id);
916
+ const db = getDatabase(devmindPath);
917
+ const changes = db.getChangesByRequirement(requirementId);
918
+ return {
919
+ content: [{ type: 'text', text: JSON.stringify(changes, null, 2) }]
920
+ };
921
+ }
922
+ case 'search_decisions': {
923
+ const devmindPath = resolveDevmindPath(args.devmind_path);
924
+ const query = String(args.query);
925
+ const db = getDatabase(devmindPath);
926
+ const decisions = db.searchDecisions(query);
927
+ return {
928
+ content: [{ type: 'text', text: JSON.stringify(decisions, null, 2) }]
929
+ };
930
+ }
931
+ case 'get_orphaned_nodes': {
932
+ const devmindPath = resolveDevmindPath(args.devmind_path);
933
+ const db = getDatabase(devmindPath);
934
+ const nodes = db.getOrphanedNodes();
935
+ return {
936
+ content: [{ type: 'text', text: JSON.stringify(nodes, null, 2) }]
937
+ };
938
+ }
939
+ case 'get_visualizer_url': {
940
+ const devmindPath = path.resolve(resolveDevmindPath(args.devmind_path));
941
+ const devmindPathEscaped = encodeURIComponent(devmindPath);
942
+ return {
943
+ content: [{
944
+ type: 'text',
945
+ text: JSON.stringify({
946
+ visualizer_2d: `http://localhost:${exports.DEVSMIND_PORT}/?path=${devmindPathEscaped}`,
947
+ visualizer_3d: `http://localhost:${exports.DEVSMIND_PORT}/3d?path=${devmindPathEscaped}`
948
+ }, null, 2)
949
+ }]
950
+ };
951
+ }
952
+ default:
953
+ throw new Error(`Tool not found: ${name}`);
954
+ }
955
+ }
956
+ catch (err) {
957
+ console.error(`[DevsMind Error] Tool execution failed: ${err.message}`);
958
+ return {
959
+ isError: true,
960
+ content: [{ type: 'text', text: `Error: ${err.message}` }]
961
+ };
962
+ }
963
+ });
964
+ return server;
965
+ }
966
+ // ── Graceful shutdown helpers ─────────────────────────────────────────────────
967
+ function registerShutdownHandlers(httpServer) {
968
+ const shutdown = () => {
969
+ cleanup();
970
+ if (httpServer) {
971
+ httpServer.close(() => process.exit(0));
972
+ }
973
+ else {
974
+ process.exit(0);
975
+ }
976
+ };
977
+ process.on('SIGINT', shutdown);
978
+ process.on('SIGTERM', shutdown);
979
+ }
980
+ // ── HTTP mode (default) — port 4500 ──────────────────────────────────────────
981
+ /**
982
+ * Start DevsMind as an HTTP MCP server on port 4500.
983
+ * IDEs connect via: http://localhost:4500/mcp
984
+ *
985
+ * Port mnemonic: devs→45 (D=4,E=5) + mind→13 (M=13) = 4513
986
+ */
987
+ async function runHttpMcpServer(port = exports.DEVSMIND_PORT) {
988
+ const app = (0, express_1.default)();
989
+ app.use(express_1.default.json());
990
+ // Health-check endpoint
991
+ app.get('/health', (_req, res) => {
992
+ res.json({
993
+ status: 'ok',
994
+ name: 'devsmind-mcp-server',
995
+ version: '1.0.0',
996
+ port,
997
+ transport: 'http+streamable',
998
+ endpoint: `http://localhost:${port}/mcp`
999
+ });
1000
+ });
1001
+ // Visualizer Page endpoint (2D)
1002
+ app.get('/', (_req, res) => {
1003
+ res.setHeader('Content-Type', 'text/html');
1004
+ res.send(visualizer_1.VISUALIZER_2D_HTML);
1005
+ });
1006
+ // Visualizer Page endpoint (3D)
1007
+ app.get('/3d', (_req, res) => {
1008
+ res.setHeader('Content-Type', 'text/html');
1009
+ res.send(visualizer_1.VISUALIZER_3D_HTML);
1010
+ });
1011
+ // Temporary UMD Test endpoint
1012
+ app.get('/test-umd', (_req, res) => {
1013
+ res.setHeader('Content-Type', 'text/html');
1014
+ res.send(`<!DOCTYPE html>
1015
+ <html>
1016
+ <head>
1017
+ <script src="https://unpkg.com/three@0.128.0/build/three.min.js"></script>
1018
+ <script src="https://unpkg.com/3d-force-graph@1.72.0/dist/3d-force-graph.min.js"></script>
1019
+ </head>
1020
+ <body>
1021
+ <div id="3d-graph"></div>
1022
+ <script>
1023
+ const graph = ForceGraph3D()(document.getElementById('3d-graph'));
1024
+ console.log("ForceGraph3D keys:", Object.keys(ForceGraph3D));
1025
+ console.log("Graph instance keys:", Object.keys(graph));
1026
+ console.log("Graph scene constructor:", graph.scene().constructor.name);
1027
+ console.log("window.THREE exists:", typeof window.THREE);
1028
+ </script>
1029
+ </body>
1030
+ </html>`);
1031
+ });
1032
+ // Graph Data API endpoint
1033
+ app.get('/api/graph-data', (req, res) => {
1034
+ try {
1035
+ const devmindPath = req.query.path ? String(req.query.path) : path.join(process.cwd(), '.devmind');
1036
+ if (!fs.existsSync(devmindPath)) {
1037
+ return res.status(400).json({ error: `Brain directory not found at: ${devmindPath}` });
1038
+ }
1039
+ const db = getDatabase(devmindPath);
1040
+ const nodes = db.getAllNodes();
1041
+ const connections = db.getAllConnections();
1042
+ const history = db.getAllHistory();
1043
+ res.json({ nodes, connections, history });
1044
+ }
1045
+ catch (err) {
1046
+ console.error('[DevsMind API Error]:', err);
1047
+ res.status(500).json({ error: err.message });
1048
+ }
1049
+ });
1050
+ // MCP endpoint — stateless: each request gets its own server + transport pair
1051
+ app.all('/mcp', async (req, res) => {
1052
+ try {
1053
+ const server = createMcpServer();
1054
+ const transport = new streamableHttp_js_1.StreamableHTTPServerTransport({
1055
+ sessionIdGenerator: undefined // stateless mode
1056
+ });
1057
+ // Clean up this transport's server on close
1058
+ res.on('close', () => {
1059
+ transport.close().catch(() => { });
1060
+ server.close().catch(() => { });
1061
+ });
1062
+ await server.connect(transport);
1063
+ await transport.handleRequest(req, res, req.body);
1064
+ }
1065
+ catch (err) {
1066
+ console.error('[DevsMind] HTTP request error:', err);
1067
+ if (!res.headersSent) {
1068
+ res.status(500).json({ error: 'Internal server error' });
1069
+ }
1070
+ }
1071
+ });
1072
+ const httpServer = http.createServer(app);
1073
+ await new Promise((resolve, reject) => {
1074
+ httpServer.listen(port, '127.0.0.1', () => resolve());
1075
+ httpServer.once('error', reject);
1076
+ });
1077
+ console.log(`🧠 DevsMind running → http://localhost:${port}/mcp`);
1078
+ console.log(` press Ctrl+C to stop`);
1079
+ registerShutdownHandlers(httpServer);
1080
+ }
1081
+ // ── Stdio mode — for direct IDE plugin injection ──────────────────────────────
1082
+ /**
1083
+ * Start DevsMind as a stdio MCP server.
1084
+ * Used when an IDE manages the process directly (e.g. Cursor stdio plugin mode).
1085
+ */
1086
+ function runStdioMcpServer() {
1087
+ // NOTE: do NOT write to stdout here — it is the JSON-RPC pipe.
1088
+ const server = createMcpServer();
1089
+ process.on('SIGINT', () => { cleanup(); process.exit(0); });
1090
+ process.on('SIGTERM', () => { cleanup(); process.exit(0); });
1091
+ const transport = new stdio_js_1.StdioServerTransport();
1092
+ server.connect(transport).then(() => {
1093
+ // connected
1094
+ }).catch((err) => {
1095
+ console.error(`❌ Stdio connection failed: ${err.message}`);
1096
+ process.exit(1);
1097
+ });
1098
+ }
1099
+ // ── Backward-compat alias (used by existing CLI index.ts) ────────────────────
1100
+ /** @deprecated Use runHttpMcpServer() or runStdioMcpServer() directly */
1101
+ function runMcpServer() {
1102
+ runStdioMcpServer();
1103
+ }
1104
+ //# sourceMappingURL=server.js.map