devsmind-mcp 2.0.4 → 2.1.1

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.
@@ -52,8 +52,39 @@ const database_1 = require("../db/database");
52
52
  const visualizer_1 = require("./visualizer");
53
53
  const indexer_1 = require("../db/indexer");
54
54
  const scanner_1 = require("../utils/scanner");
55
+ const staging_1 = require("../db/staging");
55
56
  // ─── Port: devs→D(4)E(5)=45 + mind→M(13)=13 → 4513 ─────────────────────────
56
57
  exports.DEVSMIND_PORT = 4513;
58
+ // Shared node-type taxonomy description, reused by update_history and stage_change.
59
+ const NODE_TYPE_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' +
60
+ 'UNIVERSAL: function | method | class | abstract_class | interface | type_alias | enum | constant | variable | module | namespace | decorator\n\n' +
61
+ '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' +
62
+ 'EXPRESS/FASTIFY/KOA/HONO: route_handler | middleware | router\n\n' +
63
+ 'SPRING (Java): spring_controller | spring_service | spring_repository | spring_component | spring_bean | spring_config | spring_entity\n\n' +
64
+ 'DJANGO/FASTAPI (Python): django_view | django_model | django_serializer | django_form | django_signal | fastapi_router | fastapi_dependency\n\n' +
65
+ 'GO: go_handler | go_middleware | go_struct | go_interface | go_func\n\n' +
66
+ 'RUST: rust_struct | rust_impl | rust_trait | rust_enum | rust_fn | rust_macro\n\n' +
67
+ 'REACT: react_component | react_hook | react_context | react_hoc | react_page\n\n' +
68
+ 'NEXT.JS: next_page | next_layout | next_api_route | next_server_action | next_middleware\n\n' +
69
+ 'VUE: vue_component | vue_composable | vue_directive | vue_store_module\n\n' +
70
+ 'ANGULAR: ng_component | ng_service | ng_directive | ng_pipe | ng_module | ng_guard | ng_interceptor | ng_resolver\n\n' +
71
+ 'SVELTE: svelte_component | svelte_store | svelte_action\n\n' +
72
+ 'ORM — PRISMA: prisma_model | prisma_query | prisma_migration\n' +
73
+ 'ORM — TYPEORM: typeorm_entity | typeorm_repository | typeorm_migration\n' +
74
+ 'ORM — MONGOOSE: mongoose_model | mongoose_schema\n' +
75
+ 'ORM — SQLALCHEMY: sqlalchemy_model | sqlalchemy_query\n' +
76
+ 'ORM — SEQUELIZE: sequelize_model | sequelize_migration\n\n' +
77
+ 'REST/API: api_endpoint | rest_controller\n' +
78
+ 'GRAPHQL: graphql_resolver | graphql_query | graphql_mutation | graphql_subscription | graphql_schema | graphql_directive\n' +
79
+ 'GRPC/PROTO: grpc_service | grpc_method | proto_message\n' +
80
+ 'WEBSOCKET: ws_gateway | ws_handler\n' +
81
+ 'MESSAGE QUEUE: mq_producer | mq_consumer | mq_handler\n\n' +
82
+ 'CONFIG/AUTH: config_loader | env_config | feature_flag | auth_guard | auth_strategy | jwt_util | permission_policy\n' +
83
+ 'OBSERVABILITY: logger | metric | trace_span\n' +
84
+ 'CLI: cli_command | cli_option\n' +
85
+ 'SCRIPTS: build_script | migration_script | seed_script\n' +
86
+ 'TESTS: test_suite | test_case | test_helper | mock | fixture\n' +
87
+ 'UTILITY: util_function | helper | transformer | validator | formatter';
57
88
  // Cache database connections by their resolved path to avoid re-opening constantly
58
89
  const dbCache = new Map();
59
90
  // Walk up from a start directory to find a .devmind folder containing config.json
@@ -162,7 +193,7 @@ function createMcpServer() {
162
193
  },
163
194
  {
164
195
  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.',
196
+ description: "Get a single node's CURRENT source code, parsed live from its file on disk — token-efficient, since it returns only that function/class/route rather than the whole file. Call this instead of reading a file whenever you need one specific entity. Response fields: `source: \"live\"` means the code was read from disk and is current. `source: \"cached\"` means the symbol could not be located in its file (not a TS/JS file, or it was renamed/moved/deleted) so a possibly-stale cached snapshot was returned — verify it against the file before relying on it. `snapshot_outdated: true` means the stored graph has drifted from disk; re-stage the node with stage_change + commit_changes to bring the brain back in sync. To fetch a whole call flow at once, prefer get_node_graph with include_code instead of calling this repeatedly.",
166
197
  inputSchema: {
167
198
  type: 'object',
168
199
  properties: {
@@ -178,103 +209,10 @@ function createMcpServer() {
178
209
  required: ['devmind_path', 'node_id']
179
210
  }
180
211
  },
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
- },
212
+ // NOTE: `update_history`, `add_node`, and `add_connection` are intentionally NOT listed
213
+ // here. They are deprecated in favour of `stage_change` + `commit_changes` (to avoid
214
+ // confusing the AI with overlapping write tools), but their handlers are retained below
215
+ // so any direct/legacy call still works.
278
216
  // ────────────────── Indexing tools ─────────────────────────────────────────
279
217
  {
280
218
  name: 'index_start',
@@ -331,63 +269,47 @@ function createMcpServer() {
331
269
  }
332
270
  },
333
271
  {
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.',
272
+ name: 'stage_change',
273
+ description: 'Stage ONE changed code node (function/class/method/etc.) into a buffer without writing to the graph yet. Call this once for EVERY file/entity you touched during a task — passing only the code and reasoning; you do NOT reason about connections here. When you are done with all the files, call commit_changes ONCE — it creates every node, writes every history entry, and resolves all connections between them via local AST in a single pass (so a call from one changed file into another resolves correctly no matter which order you staged them). Staging is buffered on disk, so it survives a context reset. ⚠️ YOU MUST CALL commit_changes at the end, or nothing is written to the graph.',
336
274
  inputSchema: {
337
275
  type: 'object',
338
276
  properties: {
339
277
  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'
278
+ node_id: { type: 'string', description: 'Unique identifier for the node (e.g. "CartService.applyPromoCode" or "calculateDiscount")' },
279
+ file_path: { type: 'string', description: 'Source file path where the node is located' },
280
+ code_snapshot: { type: 'string', description: 'Full source code content of the node at this moment' },
281
+ reasoning: {
282
+ type: 'object',
283
+ description: 'Structured details about this change',
284
+ properties: {
285
+ what_changed: { type: 'string', description: 'Brief description of the modified code' },
286
+ why: { type: 'string', description: 'The reason this change was made' },
287
+ goal: { type: 'string', description: 'What was being achieved' },
288
+ requirement: { type: 'string', description: 'Ticket / issue / user request ID if applicable' },
289
+ previous_state: { type: 'string', description: 'What the code looked like before and why it was a problem' },
290
+ decision: { type: 'string', description: 'Architectural or implementation decision and why' },
291
+ developer: { type: 'string', description: 'Name of the developer' },
292
+ model: { type: 'string', description: 'AI model name used' }
293
+ },
294
+ required: ['what_changed', 'why', 'goal']
373
295
  },
374
- file_path: { type: 'string', description: 'Absolute path to the source file' },
375
- signature: { type: 'string', description: 'Parameter types + return type (optional)' }
296
+ name: { type: 'string', description: 'Display name of the node (optional, inferred if omitted)' },
297
+ type: { type: 'string', description: '(optional, defaults to function) ' + NODE_TYPE_DESCRIPTION },
298
+ signature: { type: 'string', description: 'Parameter types + return type signature (optional)' },
299
+ session_id: { type: 'string', description: 'Session identifier to associate with this change (optional)' }
376
300
  },
377
- required: ['devmind_path', 'node_id', 'name', 'type', 'file_path']
301
+ required: ['devmind_path', 'node_id', 'file_path', 'code_snapshot', 'reasoning']
378
302
  }
379
303
  },
380
304
  {
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.',
305
+ name: 'commit_changes',
306
+ description: 'Commit all buffered stage_change entries in one atomic pass: creates/updates every staged node, writes every history snapshot, then resolves all connections between the staged nodes (and into the existing graph) via local AST — auto-creating any referenced-but-missing target nodes. Clears the buffer on success. Call this exactly once after you have finished staging every file you touched.',
383
307
  inputSchema: {
384
308
  type: 'object',
385
309
  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' }
310
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' }
389
311
  },
390
- required: ['devmind_path', 'source_node_id', 'target_node_id']
312
+ required: ['devmind_path']
391
313
  }
392
314
  },
393
315
  {
@@ -416,13 +338,26 @@ function createMcpServer() {
416
338
  },
417
339
  {
418
340
  name: 'get_node_graph',
419
- description: 'Get a localized node dependency graph up to a specified depth (default 6). Returns connected nodes and relationships.',
341
+ description: 'Get a node\'s dependency graph connected nodes and the relationships between them. Set direction:"out" AND include_code:true to pull an ENTIRE CALL FLOW in a single call: the starting node plus everything it transitively calls, each with its current source code read from disk. Use that combination whenever you are tracing how a request, endpoint, or feature flows through the codebase — it replaces a long chain of get_node_code calls with one round trip. Use direction:"in" to find every caller of a node (impact analysis before a change). If `code_truncated` is true in the response, the character budget ran out and `nodes_without_code` nodes came back with metadata but no code — fetch those individually with get_node_code, or raise code_char_budget.',
420
342
  inputSchema: {
421
343
  type: 'object',
422
344
  properties: {
423
345
  devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
424
346
  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)' }
347
+ max_depth: { type: 'number', description: 'Maximum depth to traverse (optional, default 6). For a call flow with include_code, 2-3 is usually right.' },
348
+ direction: {
349
+ type: 'string',
350
+ enum: ['out', 'in', 'both'],
351
+ description: '"out" = only what this node calls, transitively (a call flow — use this for tracing). "in" = only what calls this node (impact analysis). "both" = the surrounding neighborhood in both directions (default).'
352
+ },
353
+ include_code: {
354
+ type: 'boolean',
355
+ description: 'Attach each node\'s current source code, read live from disk (default: false). Combine with direction:"out" to retrieve a whole call flow in one call.'
356
+ },
357
+ code_char_budget: {
358
+ type: 'number',
359
+ description: 'Max total characters of code to return, spent on the nodes nearest the starting node first (default: 60000). Only applies when include_code is true.'
360
+ }
426
361
  },
427
362
  required: ['devmind_path', 'node_id']
428
363
  }
@@ -515,6 +450,20 @@ function createMcpServer() {
515
450
  required: ['devmind_path', 'query']
516
451
  }
517
452
  },
453
+ {
454
+ name: 'search_code',
455
+ description: 'Regex or string search over cached codebase code snapshots. Returns matches grouped by DevsMind Node ID, file path, and matching lines, along with matching statistics (ratio, count). Prefer this over direct grep search.',
456
+ inputSchema: {
457
+ type: 'object',
458
+ properties: {
459
+ devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
460
+ query: { type: 'string', description: 'Regex or substring pattern to search for in code' },
461
+ is_regex: { type: 'boolean', description: 'Whether the query is a regex pattern (default: false)' },
462
+ case_insensitive: { type: 'boolean', description: 'Perform case-insensitive search (default: true)' }
463
+ },
464
+ required: ['devmind_path', 'query']
465
+ }
466
+ },
518
467
  {
519
468
  name: 'get_orphaned_nodes',
520
469
  description: 'Find disconnected code nodes in the graph that have no incoming or outgoing connections.',
@@ -594,75 +543,39 @@ function createMcpServer() {
594
543
  const devmindPath = resolveDevmindPath(args.devmind_path);
595
544
  const nodeId = String(args.node_id);
596
545
  const db = getDatabase(devmindPath);
597
- const result = db.getLatestCode(nodeId);
598
- if (!result) {
599
- return {
600
- content: [{ type: 'text', text: JSON.stringify({
601
- exists: false,
602
- node_id: nodeId,
603
- 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.'
604
- }) }]
605
- };
606
- }
546
+ const result = db.getLiveCode(nodeId);
607
547
  return {
608
- content: [{ type: 'text', text: JSON.stringify({
609
- exists: true,
610
- node_id: nodeId,
611
- updated_at: result.updated_at,
612
- code_snapshot: result.code_snapshot
613
- }) }]
548
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }]
614
549
  };
615
550
  }
616
551
  case 'update_history': {
617
552
  const devmindPath = resolveDevmindPath(args.devmind_path);
618
- const rawNodeId = String(args.node_id);
619
553
  const filePath = String(args.file_path);
620
- const codeSnapshot = String(args.code_snapshot);
621
- const reasoning = args.reasoning;
622
554
  const db = getDatabase(devmindPath);
623
- const repoRelPath = db.toRepoRelativePath(filePath);
624
- const prefix = `${repoRelPath}#`;
625
- const nodeId = rawNodeId.includes('#') ? rawNodeId : `${prefix}${rawNodeId}`;
626
- let nodeName = args.name ? String(args.name) : undefined;
627
- let nodeType = args.type ? String(args.type) : undefined;
628
- const signature = args.signature ? String(args.signature) : undefined;
629
- const sessionId = args.session_id ? String(args.session_id) : undefined;
630
- // Infer name and type if not provided
631
- if (!nodeName) {
632
- nodeName = rawNodeId.includes('.') ? rawNodeId.split('.').pop() : rawNodeId;
633
- }
634
- if (!nodeType) {
635
- nodeType = rawNodeId.includes('.') ? 'method' : 'function';
636
- }
637
- // 1. Ensure node exists (Upsert)
638
- db.upsertNode({
639
- id: nodeId,
640
- name: nodeName,
641
- type: nodeType,
555
+ // Single-shot path: stage one entry and commit it immediately, so a lone edit still
556
+ // gets its node, history, AND outgoing edges resolved via the shared commit logic.
557
+ const entry = {
558
+ node_id: String(args.node_id),
642
559
  file_path: filePath,
643
- signature: signature || null
644
- });
645
- // 2. Update history with 1-hour session boundary rule
646
- const historyEntry = db.updateHistory({
647
- node_id: nodeId,
648
- code_snapshot: codeSnapshot,
649
- reasoning,
650
- session_id: sessionId
651
- });
560
+ code_snapshot: String(args.code_snapshot),
561
+ reasoning: args.reasoning,
562
+ name: args.name ? String(args.name) : undefined,
563
+ type: args.type ? String(args.type) : undefined,
564
+ signature: args.signature ? String(args.signature) : undefined,
565
+ session_id: args.session_id ? String(args.session_id) : undefined
566
+ };
567
+ const summary = (0, staging_1.commitStagedChanges)(db, devmindPath, [entry]);
568
+ const nodeId = entry.node_id.includes('#') ? entry.node_id : `${db.toRepoRelativePath(filePath)}#${entry.node_id}`;
652
569
  return {
653
570
  content: [
654
571
  {
655
572
  type: 'text',
656
573
  text: JSON.stringify({
657
574
  success: true,
658
- message: 'History updated successfully',
659
- node: { id: nodeId, name: nodeName, type: nodeType },
660
- history_entry: {
661
- id: historyEntry.id,
662
- session_id: historyEntry.session_id,
663
- created_at: historyEntry.created_at,
664
- updated_at: historyEntry.updated_at
665
- }
575
+ message: 'History updated and connections resolved.',
576
+ node: { id: nodeId },
577
+ edges_added: summary.edges_added,
578
+ missing_nodes_filled: summary.missing_filled
666
579
  }, null, 2)
667
580
  }
668
581
  ]
@@ -683,37 +596,30 @@ function createMcpServer() {
683
596
  content: [{
684
597
  type: 'text',
685
598
  text: JSON.stringify({
686
- message: 'Indexing session started. Perform indexing in two passes: Phase 1 (call add_node + update_history for all entities) and Phase 2 (call add_connection to link entities once all nodes exist). Call index_checkpoint every 10 files/nodes.',
599
+ message: 'Indexing session started. Extract nodes with stage_change (one call per entity), then call commit_changes to write them all and resolve connections automatically via AST. Call index_checkpoint every 10 files.',
687
600
  scratchpad: pad,
688
601
  repos: repoSummaries,
689
602
  total_files,
690
603
  instructions: [
691
604
  '⚠️⚠️⚠️ CRITICAL INSTRUCTION FOR THE INDEXING AGENT — MUST READ ⚠️⚠️⚠️ ',
692
- 'YOU MUST EXPLICITLY CALL THE "add_node", "update_history" AND "add_connection" MCP TOOLS FOR EVERY NODE/CONNECTION YOU EXTRACT.',
605
+ 'YOU MUST EXPLICITLY CALL THE "stage_change" MCP TOOL FOR EVERY ENTITY YOU EXTRACT, THEN "commit_changes" TO WRITE THEM.',
693
606
  '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.',
694
607
  'NEVER WRITE OR EXECUTE EXTERNAL SCRIPTS (like Python, Node.js, Bash, etc.) to automate or lazy load indexing. You must read files and call the MCP tools step-by-step natively in the chat. This ensures progress is tracked in the SQLite scratchpad database and can be resumed/continued in subsequent chats if context limits are hit.',
695
608
  'ONCE YOU START INDEXING, DO NOT STOP or pause to ask for confirmation between checkpoints. Keep executing and indexing files continuously until the codebase is fully indexed or your context token limit is reached.',
696
609
  'IF YOU ENCOUNTER CONTEXT RESETS, RESUME WORK BY CALLING "index_continue" AND CONTINUOUSLY COMMIT PROGRESS BY CALLING "index_checkpoint" EVERY 10 FILES.',
697
610
  '',
698
611
  '📋 CODE EXCLUSION & PRECISION RULES:',
699
- '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.',
700
- '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.',
701
- '3. EXCLUDE External / Third-party Modules: Do NOT call add_node/add_connection for lodash, express, react, @nestjs/common, etc.',
702
- '4. INTERNAL ENTITIES ONLY: Only create nodes and connections for constructs defined inside this codebase.',
612
+ '1. EXCLUDE Language Globals / Built-ins: Do NOT stage nodes for Promise, Map, Set, JSON, console, Error, Object, Array, RegExp, Date, Math, etc.',
613
+ '2. EXCLUDE Primitive/Native Types: Do NOT stage nodes for string, number, boolean, any, void, unknown, never, null, undefined, dict, list, etc.',
614
+ '3. EXCLUDE External / Third-party Modules: Do NOT stage nodes for lodash, express, react, @nestjs/common, etc.',
615
+ '4. INTERNAL ENTITIES ONLY: Only stage nodes for constructs defined inside this codebase.',
703
616
  '',
704
- '📋 TWO-PHASE INDEXING PROTOCOL:',
705
- 'PHASE 1: NODE & CODE EXTRACTION',
617
+ '📋 STAGE → COMMIT INDEXING PROTOCOL:',
706
618
  '1. For each file in each repo: read it, extract ALL defined nodes — functions, methods, classes, interfaces, types, DTOs, routing handlers, schemas, resolvers, etc.',
707
- '2. Call add_node for every entity found, selecting the most specific taxonomy type.',
708
- '3. Call update_history for every entity found to save its raw source code snapshot.',
709
- '4. Call index_checkpoint every 10 files to save progress.',
710
- '',
711
- 'PHASE 2: CONNECTION RESOLUTION (LINKING)',
712
- '1. Once all files have been indexed in Phase 1, get the list of all nodes using list_nodes.',
713
- '2. Iterate through all nodes. For each node, review its code snapshot and identify which other active codebase nodes it calls/references.',
714
- '3. Call add_connection (source_node_id, target_node_id) to link them.',
715
- '4. Call index_checkpoint every 10 nodes to save progress.',
716
- '5. When all connections have been linked, call index_complete.',
619
+ '2. Call stage_change for EVERY entity found — pass its node_id, file_path, code_snapshot, reasoning, and the most specific taxonomy type. You do NOT need to figure out connections; commit_changes resolves them from the code via AST.',
620
+ '3. Call index_checkpoint every 10 files to save progress.',
621
+ '4. Every ~50 entities (or at the end of a repo), call commit_changes to flush the staged buffer — it creates all nodes, writes all history, and resolves all connections (including into already-committed nodes) in one pass. Committing in batches keeps the buffer small.',
622
+ '5. When the whole codebase is staged and committed, call index_complete.',
717
623
  '6. AFTER index_complete, CALL "recheck_graph" to automatically prune any spurious, built-in, or orphaned nodes and ensure high graph precision.'
718
624
  ]
719
625
  }, null, 2)
@@ -793,6 +699,56 @@ function createMcpServer() {
793
699
  }]
794
700
  };
795
701
  }
702
+ case 'stage_change': {
703
+ const devmindPath = resolveDevmindPath(args.devmind_path);
704
+ const entry = {
705
+ node_id: String(args.node_id),
706
+ file_path: String(args.file_path),
707
+ code_snapshot: String(args.code_snapshot),
708
+ reasoning: args.reasoning,
709
+ name: args.name ? String(args.name) : undefined,
710
+ type: args.type ? String(args.type) : undefined,
711
+ signature: args.signature ? String(args.signature) : undefined,
712
+ session_id: args.session_id ? String(args.session_id) : undefined
713
+ };
714
+ const pendingCount = (0, staging_1.stageEntry)(devmindPath, entry);
715
+ return {
716
+ content: [{
717
+ type: 'text',
718
+ text: JSON.stringify({
719
+ staged: true,
720
+ node_id: entry.node_id,
721
+ pending_count: pendingCount,
722
+ reminder: 'Call commit_changes once you have staged every touched file, or nothing is written to the graph.'
723
+ })
724
+ }]
725
+ };
726
+ }
727
+ case 'commit_changes': {
728
+ const devmindPath = resolveDevmindPath(args.devmind_path);
729
+ const db = getDatabase(devmindPath);
730
+ const entries = (0, staging_1.readStaged)(devmindPath);
731
+ if (entries.length === 0) {
732
+ return {
733
+ content: [{ type: 'text', text: JSON.stringify({ committed: false, message: 'Nothing staged. Call stage_change first.' }) }]
734
+ };
735
+ }
736
+ const summary = (0, staging_1.commitStagedChanges)(db, devmindPath, entries);
737
+ (0, staging_1.clearStaged)(devmindPath);
738
+ return {
739
+ content: [{
740
+ type: 'text',
741
+ text: JSON.stringify({
742
+ committed: true,
743
+ message: `✅ Committed ${summary.nodes} node(s), ${summary.history_entries} history entr(ies), ${summary.edges_added} connection(s) resolved` +
744
+ (summary.missing_filled > 0 ? `, ${summary.missing_filled} missing node(s) auto-created.` : '.'),
745
+ ...summary
746
+ }, null, 2)
747
+ }]
748
+ };
749
+ }
750
+ // ── Deprecated write handlers: NOT advertised in ListTools (superseded by
751
+ // stage_change/commit_changes), but retained so any direct/legacy call still works. ──
796
752
  case 'add_node': {
797
753
  const devmindPath = resolveDevmindPath(args.devmind_path);
798
754
  const rawNodeId = String(args.node_id);
@@ -851,8 +807,15 @@ function createMcpServer() {
851
807
  const devmindPath = resolveDevmindPath(args.devmind_path);
852
808
  const nodeId = String(args.node_id);
853
809
  const maxDepth = args.max_depth ? Number(args.max_depth) : 6;
810
+ const direction = args.direction === 'out' || args.direction === 'in' || args.direction === 'both'
811
+ ? args.direction
812
+ : 'both';
854
813
  const db = getDatabase(devmindPath);
855
- const graph = db.getGraph(nodeId, maxDepth);
814
+ const graph = db.getGraph(nodeId, maxDepth, {
815
+ direction,
816
+ includeCode: args.include_code === true,
817
+ codeCharBudget: args.code_char_budget ? Number(args.code_char_budget) : undefined
818
+ });
856
819
  return {
857
820
  content: [{ type: 'text', text: JSON.stringify(graph, null, 2) }]
858
821
  };
@@ -924,6 +887,17 @@ function createMcpServer() {
924
887
  content: [{ type: 'text', text: JSON.stringify(decisions, null, 2) }]
925
888
  };
926
889
  }
890
+ case 'search_code': {
891
+ const devmindPath = resolveDevmindPath(args.devmind_path);
892
+ const query = String(args.query);
893
+ const isRegex = args.is_regex === true;
894
+ const caseInsensitive = args.case_insensitive !== false;
895
+ const db = getDatabase(devmindPath);
896
+ const results = db.searchCode({ query, is_regex: isRegex, case_insensitive: caseInsensitive });
897
+ return {
898
+ content: [{ type: 'text', text: JSON.stringify(results, null, 2) }]
899
+ };
900
+ }
927
901
  case 'get_orphaned_nodes': {
928
902
  const devmindPath = resolveDevmindPath(args.devmind_path);
929
903
  const db = getDatabase(devmindPath);