devsmind-mcp 2.2.2 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +234 -606
- package/dist/cli/analyze.d.ts +13 -0
- package/dist/cli/analyze.js +143 -0
- package/dist/cli/analyze.js.map +1 -0
- package/dist/cli/index.js +62 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +9 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/integrations/memory.js +9 -3
- package/dist/cli/integrations/memory.js.map +1 -1
- package/dist/cli/integrations/prompt.d.ts +2 -0
- package/dist/cli/integrations/prompt.js +21 -7
- package/dist/cli/integrations/prompt.js.map +1 -1
- package/dist/cli/prune.js +4 -3
- package/dist/cli/prune.js.map +1 -1
- package/dist/cli/rule.js +43 -83
- package/dist/cli/rule.js.map +1 -1
- package/dist/cli/sync.d.ts +7 -0
- package/dist/cli/sync.js +40 -7
- package/dist/cli/sync.js.map +1 -1
- package/dist/cli/workflow.d.ts +8 -0
- package/dist/cli/workflow.js +156 -0
- package/dist/cli/workflow.js.map +1 -0
- package/dist/db/analyze.d.ts +67 -0
- package/dist/db/analyze.js +167 -0
- package/dist/db/analyze.js.map +1 -0
- package/dist/db/database.d.ts +195 -2
- package/dist/db/database.js +870 -74
- package/dist/db/database.js.map +1 -1
- package/dist/db/schema.d.ts +28 -1
- package/dist/db/schema.js +34 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/db/staging.d.ts +4 -0
- package/dist/db/staging.js +16 -2
- package/dist/db/staging.js.map +1 -1
- package/dist/db/workflow-import.d.ts +22 -0
- package/dist/db/workflow-import.js +116 -0
- package/dist/db/workflow-import.js.map +1 -0
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +641 -48
- package/dist/mcp/server.js.map +1 -1
- package/dist/utils/ast.d.ts +98 -0
- package/dist/utils/ast.js +262 -10
- package/dist/utils/ast.js.map +1 -1
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.js +11 -0
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/edit.d.ts +41 -0
- package/dist/utils/edit.js +163 -0
- package/dist/utils/edit.js.map +1 -0
- package/dist/utils/git.d.ts +14 -0
- package/dist/utils/git.js +43 -0
- package/dist/utils/git.js.map +1 -0
- package/package.json +1 -1
package/dist/mcp/server.js
CHANGED
|
@@ -52,7 +52,11 @@ 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 ast_1 = require("../utils/ast");
|
|
56
|
+
const edit_1 = require("../utils/edit");
|
|
55
57
|
const staging_1 = require("../db/staging");
|
|
58
|
+
const analyze_1 = require("../db/analyze");
|
|
59
|
+
const workflow_import_1 = require("../db/workflow-import");
|
|
56
60
|
// ─── Port: devs→D(4)E(5)=45 + mind→M(13)=13 → 4513 ─────────────────────────
|
|
57
61
|
exports.DEVSMIND_PORT = 4513;
|
|
58
62
|
/**
|
|
@@ -72,8 +76,11 @@ Non-negotiable workflow:
|
|
|
72
76
|
2. To read one function/class: call get_node_code instead of opening the file. It parses live from disk, so it's cheaper and always current.
|
|
73
77
|
3. Before touching any function's signature: call get_node_graph. Git shows you what changed; it never shows you what depends on it. Find out before you break something, not after.
|
|
74
78
|
4. Before refactoring: call get_node_history. Git blame tells you who and when; it never tells you why. The actual decision context only exists here.
|
|
75
|
-
5.
|
|
76
|
-
6.
|
|
79
|
+
5. Write EVERY file with edit_node — .ts, .vue, .css, .json, .xml, .md, anything — and never your editor's own edit/write tools. It takes file_path + old_string + new_string exactly like an ordinary edit tool and never refuses a file type; to create a file that doesn't exist yet, pass old_string: "" and the whole file as new_string. Because it knows where your text landed, it works out which function/class you changed and records your reasoning against it automatically: no node_id to look up, no code_snapshot to send back, and no stage_change call. It answers with every caller of what you changed. Writes landing outside any function (markup, config, an import) record nothing — normal and expected, not a failure.
|
|
80
|
+
6. stage_change is now only for what no parser can read: a language with no AST support (.py, .go, .java, .cs, .rb, .php, .rs, .swift, .kt, .dart). edit_node still writes those files, and its response tells you when it couldn't trace one — so never guess. One call per node, not per file, never batched for later. On a long task, call commit_changes at natural checkpoints too (not only once at the very end) — waiting until the whole task is "done" is how staged work gets left uncommitted when a session runs long.
|
|
81
|
+
7. Scope: the graph is source code only (functions/classes/logic). stage_change will be REJECTED for stylesheets, markup, JSON/config, docs, images, or any other non-code asset. Do not stage those files — they have no callers/callees to resolve and only bloat the graph.
|
|
82
|
+
8. When you start work that might relate to a multi-session feature, call workflow_list first. If a paused workflow's description looks related to what you're about to do, ask the user whether to resume it (workflow_resume) instead of starting fresh and silently losing its decision history — git blame never shows you a paused feature's prior context, only this does.
|
|
83
|
+
9. If a workflow is active, commit_changes already logs a step for you from what you staged — you do NOT need a separate workflow_add_step call for the normal case. Only call workflow_add_step directly for something a commit doesn't cover (a decision with no code change, or a pending_tasks note).`;
|
|
77
84
|
// Shared node-type taxonomy description, reused by update_history and stage_change.
|
|
78
85
|
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' +
|
|
79
86
|
'UNIVERSAL: function | method | class | abstract_class | interface | type_alias | enum | constant | variable | module | namespace | decorator\n\n' +
|
|
@@ -88,11 +95,11 @@ const NODE_TYPE_DESCRIPTION = 'The type of node. Be highly specific and framewor
|
|
|
88
95
|
'VUE: vue_component | vue_composable | vue_directive | vue_store_module\n\n' +
|
|
89
96
|
'ANGULAR: ng_component | ng_service | ng_directive | ng_pipe | ng_module | ng_guard | ng_interceptor | ng_resolver\n\n' +
|
|
90
97
|
'SVELTE: svelte_component | svelte_store | svelte_action\n\n' +
|
|
91
|
-
'ORM
|
|
92
|
-
'ORM
|
|
93
|
-
'ORM
|
|
94
|
-
'ORM
|
|
95
|
-
'ORM
|
|
98
|
+
'ORM — PRISMA: prisma_model | prisma_query | prisma_migration\n' +
|
|
99
|
+
'ORM — TYPEORM: typeorm_entity | typeorm_repository | typeorm_migration\n' +
|
|
100
|
+
'ORM — MONGOOSE: mongoose_model | mongoose_schema\n' +
|
|
101
|
+
'ORM — SQLALCHEMY: sqlalchemy_model | sqlalchemy_query\n' +
|
|
102
|
+
'ORM — SEQUELIZE: sequelize_model | sequelize_migration\n\n' +
|
|
96
103
|
'REST/API: api_endpoint | rest_controller\n' +
|
|
97
104
|
'GRAPHQL: graphql_resolver | graphql_query | graphql_mutation | graphql_subscription | graphql_schema | graphql_directive\n' +
|
|
98
105
|
'GRPC/PROTO: grpc_service | grpc_method | proto_message\n' +
|
|
@@ -132,12 +139,28 @@ function resolveDevmindPath(rawPath) {
|
|
|
132
139
|
return normalized;
|
|
133
140
|
throw new Error(`devmind_path does not exist: "${resolved}". Make sure you pass the exact DEVMIND_PATH from your workspace rules.`);
|
|
134
141
|
}
|
|
135
|
-
// Not provided
|
|
142
|
+
// Not provided — auto-detect from where devsmind start was run
|
|
136
143
|
const autoDetected = findDevmindDir(process.cwd());
|
|
137
144
|
if (autoDetected)
|
|
138
145
|
return autoDetected;
|
|
139
146
|
throw new Error(`devmind_path was not provided and no .devmind directory was found by walking up from: "${process.cwd()}". Pass devmind_path explicitly.`);
|
|
140
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* A required string argument, or a thrown error naming exactly what's missing.
|
|
150
|
+
*
|
|
151
|
+
* `String(args.x)` alone turns a missing/omitted field into the literal 4-character string
|
|
152
|
+
* "undefined" instead of failing — the call "succeeds" and that garbage gets permanently
|
|
153
|
+
* written wherever the field goes (a workflow's `name`, a step's `summary`, ...). Route every
|
|
154
|
+
* genuinely required string field through this instead; the top-level try/catch in the tool
|
|
155
|
+
* dispatcher turns the throw into a clean `isError` response.
|
|
156
|
+
*/
|
|
157
|
+
function requireStr(args, field, tool) {
|
|
158
|
+
const v = args[field];
|
|
159
|
+
if (v === undefined || v === null || v === '') {
|
|
160
|
+
throw new Error(`${tool} needs '${field}' — it was not provided.`);
|
|
161
|
+
}
|
|
162
|
+
return String(v);
|
|
163
|
+
}
|
|
141
164
|
function getDatabase(devmindPath) {
|
|
142
165
|
const dbFile = path.join(devmindPath, 'brain.db');
|
|
143
166
|
if (!dbCache.has(dbFile)) {
|
|
@@ -158,7 +181,7 @@ function cleanup() {
|
|
|
158
181
|
}
|
|
159
182
|
/**
|
|
160
183
|
* Creates and wires up a DevsMind MCP Server instance.
|
|
161
|
-
* Stateless
|
|
184
|
+
* Stateless — every call receives devmind_path and opens the db from there.
|
|
162
185
|
*/
|
|
163
186
|
function createMcpServer() {
|
|
164
187
|
const server = new index_js_1.Server({ name: 'devsmind-server', version: '1.0.0' }, {
|
|
@@ -215,7 +238,7 @@ function createMcpServer() {
|
|
|
215
238
|
},
|
|
216
239
|
{
|
|
217
240
|
name: 'get_node_code',
|
|
218
|
-
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: reading the raw file instead means the graph never learns you looked at it, so drift between what's recorded and what's actually on disk goes undetected. 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
|
|
241
|
+
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: reading the raw file instead means the graph never learns you looked at it, so drift between what's recorded and what's actually on disk goes undetected. 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. If you're about to edit this node anyway, an edit_node call re-syncs it as a side effect. To force a resync with no real code change, edit_node can't help (it requires old_string to actually differ from new_string) — use stage_change instead, passing the current on-disk code as code_snapshot, then commit_changes. To fetch a whole call flow at once, prefer get_node_graph with include_code instead of calling this repeatedly.",
|
|
219
242
|
inputSchema: {
|
|
220
243
|
type: 'object',
|
|
221
244
|
properties: {
|
|
@@ -290,9 +313,43 @@ function createMcpServer() {
|
|
|
290
313
|
required: ['devmind_path']
|
|
291
314
|
}
|
|
292
315
|
},
|
|
316
|
+
{
|
|
317
|
+
name: 'edit_node',
|
|
318
|
+
description: "Write ANY file in this project. Use this for EVERY edit AND every new file, in place of your editor's own edit/write tools — .ts, .js, .vue, .css, .json, .xml, .md, .py, anything. It never refuses a file for being the wrong type, and it works exactly like an ordinary edit tool: pass `file_path`, the exact `old_string` to find, and the `new_string` to put there. To CREATE a file that doesn't exist yet, pass `old_string: \"\"` and the whole file as `new_string` (parent directories are made for you).\n\n" +
|
|
319
|
+
"What it does that a plain edit tool cannot: it knows WHERE your text landed, so it works out which function/class you actually changed and records your `reasoning` against it automatically — no node_id to look up, no code_snapshot to send back, no follow-up stage_change call. That covers code you just added and files you just created, since the code is on disk by the time it looks. In return it tells you every CALLER of what you changed (i.e. what you may have just broken), what it calls out to, and the reasoning previously recorded against it.\n\n" +
|
|
320
|
+
"Writes that don't land inside any function — markup, config, an import line, a stylesheet — simply record nothing. That is a normal, expected outcome, not a failure: the file is still written and the response says so. So there is never a reason to reach for another edit or write tool.\n\n" +
|
|
321
|
+
"Nothing reaches the graph until commit_changes. For renames use rename_node.",
|
|
322
|
+
inputSchema: {
|
|
323
|
+
type: 'object',
|
|
324
|
+
properties: {
|
|
325
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
326
|
+
file_path: { type: 'string', description: 'The file to write. It does not need to exist yet.' },
|
|
327
|
+
old_string: { type: 'string', description: 'The exact text to replace, matched byte-for-byte including indentation. Must appear exactly once in the file unless replace_all is true. Pass "" to CREATE a file that does not exist yet.' },
|
|
328
|
+
new_string: { type: 'string', description: 'The text to put in its place — or, when creating a file, its entire contents. Pass an empty string to delete the matched text.' },
|
|
329
|
+
replace_all: { type: 'boolean', description: 'Replace every occurrence instead of requiring a unique match (default false). Every occurrence is traced, so an edit hitting three functions records all three.' },
|
|
330
|
+
reasoning: {
|
|
331
|
+
type: 'object',
|
|
332
|
+
description: 'Why you are making this edit. Recorded automatically against whatever function/class the edit turns out to touch — you do not need to know which one. This is the only record of it that will ever exist: the diff shows what changed, never why. Ignored when the edit touches no code (a stylesheet, a config value).',
|
|
333
|
+
properties: {
|
|
334
|
+
what_changed: { type: 'string', description: 'Brief description of the modified code' },
|
|
335
|
+
why: { type: 'string', description: 'The reason this change was made' },
|
|
336
|
+
goal: { type: 'string', description: 'What was being achieved' },
|
|
337
|
+
requirement: { type: 'string', description: 'Ticket / issue / user request ID if applicable' },
|
|
338
|
+
previous_state: { type: 'string', description: 'What the code looked like before and why it was a problem' },
|
|
339
|
+
decision: { type: 'string', description: 'Architectural or implementation decision and why' },
|
|
340
|
+
developer: { type: 'string', description: 'Name of the developer (optional — a configured developer identity from `devsmind init` always overrides this)' },
|
|
341
|
+
model: { type: 'string', description: 'AI model name used' }
|
|
342
|
+
},
|
|
343
|
+
required: ['what_changed', 'why', 'goal']
|
|
344
|
+
},
|
|
345
|
+
session_id: { type: 'string', description: 'Session identifier to associate with this change (optional)' }
|
|
346
|
+
},
|
|
347
|
+
required: ['devmind_path', 'file_path', 'old_string', 'new_string', 'reasoning']
|
|
348
|
+
}
|
|
349
|
+
},
|
|
293
350
|
{
|
|
294
351
|
name: 'stage_change',
|
|
295
|
-
description: `Stage ONE changed code node (function/class/method/etc.) into a buffer
|
|
352
|
+
description: `Stage ONE changed code node (function/class/method/etc.) into a buffer, right after you finish editing it — don't wait until the whole task is done. Call once per NODE, not once per file: a file with 3 changed functions is 3 calls. SCOPE: source code only — ${Array.from(scanner_1.INDEXABLE_EXTENSIONS).sort().join(', ')}. Rejected for stylesheets, markup, JSON/config, docs, or other non-code assets (no callers/callees to resolve). Pass only code + reasoning; connections are resolved automatically by commit_changes, not by you. The \`reasoning\` (why, goal, what was broken, what ticket) exists nowhere else — capture it now, while it's still in context, not after the fact. Staging is buffered on disk and survives a context reset, but is inert until commit_changes runs.`,
|
|
296
353
|
inputSchema: {
|
|
297
354
|
type: 'object',
|
|
298
355
|
properties: {
|
|
@@ -310,7 +367,7 @@ function createMcpServer() {
|
|
|
310
367
|
requirement: { type: 'string', description: 'Ticket / issue / user request ID if applicable' },
|
|
311
368
|
previous_state: { type: 'string', description: 'What the code looked like before and why it was a problem' },
|
|
312
369
|
decision: { type: 'string', description: 'Architectural or implementation decision and why' },
|
|
313
|
-
developer: { type: 'string', description: 'Name of the developer' },
|
|
370
|
+
developer: { type: 'string', description: 'Name of the developer (optional — if this project has a configured developer identity from `devsmind init`, it always overrides whatever is passed here, since the agent has no reliable way to know who the human actually is)' },
|
|
314
371
|
model: { type: 'string', description: 'AI model name used' }
|
|
315
372
|
},
|
|
316
373
|
required: ['what_changed', 'why', 'goal']
|
|
@@ -325,7 +382,7 @@ function createMcpServer() {
|
|
|
325
382
|
},
|
|
326
383
|
{
|
|
327
384
|
name: 'commit_changes',
|
|
328
|
-
description: '
|
|
385
|
+
description: 'Flush all buffered stage_change entries in one atomic pass: creates/updates every staged node, writes every history snapshot, resolves all connections via local AST (auto-creating any referenced-but-missing nodes), then clears the buffer. If a workflow is currently active, this ALSO auto-records a step on its timeline from the staged entries\' reasoning — you do not need a separate workflow_add_step call for the normal case. Call commit_changes at natural checkpoints — after a batch of related nodes, or when switching context — not only once at the very end of a long task; a checkpoint commit can\'t be forgotten the way a single end-of-task one can. Always call it again before ending the turn if anything is still staged: an uncommitted turn leaves the whole team\'s graph stale, not just yours.',
|
|
329
386
|
inputSchema: {
|
|
330
387
|
type: 'object',
|
|
331
388
|
properties: {
|
|
@@ -386,7 +443,7 @@ function createMcpServer() {
|
|
|
386
443
|
},
|
|
387
444
|
{
|
|
388
445
|
name: 'search_nodes',
|
|
389
|
-
description: 'Search for code by name/id/reasoning first; if nothing matches,
|
|
446
|
+
description: 'Search for code by name/id/reasoning first; if nothing matches, falls back to a full code-content search (regex or substring); if that also finds nothing, falls back further to a word-split relevance search that breaks the query into individual words and ranks every node by how many of those words appear in its file_path, name/id, reasoning, or code — so a natural-language, multi-word query (e.g. "product detail page") still finds a match like `pages/product-detail/index.js` even though the phrase never appears verbatim anywhere. This is the ONE search tool to call, in ONE turn, whether the term is an exact identifier, only appears in the code body, or is just a rough natural-language description. Each result is tagged `matched_via: "identifier"`, `"code"`, or `"fuzzy"` so you know how it was found; code matches include line-level `matches`/`match_count`/`match_ratio`, fuzzy matches include `matched_terms`/`score`. If nothing matches at all, the response includes a `hint` suggesting next steps (e.g. list_nodes with a file_path filter). Prefer this over grep/filesystem search — a raw grep finds text, but misses every bit of recorded reasoning behind why that code looks the way it does.',
|
|
390
447
|
inputSchema: {
|
|
391
448
|
type: 'object',
|
|
392
449
|
properties: {
|
|
@@ -499,6 +556,192 @@ function createMcpServer() {
|
|
|
499
556
|
},
|
|
500
557
|
required: ['devmind_path']
|
|
501
558
|
}
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: 'analyze_graph',
|
|
562
|
+
description: 'Run a local, zero-token health check on the graph: god entities (high fan-in/out), circular dependency cycles, orphaned nodes, dangling edges, duplicate/case-collision ids, history missing developer attribution, empty code snapshots, spurious/built-in nodes, missing files, git-detected renames, and git-tracked code files with zero graph nodes. Purely local SQLite/filesystem/git queries — no LLM calls. Call this periodically (or when the graph feels stale/wrong) instead of guessing why context looks off. Set fix:true to auto-apply only the SAFE fixes (soft-deprecate dead nodes, remove dangling edges, migrate detected renames) — everything else is report-only and needs a human or agent decision.',
|
|
563
|
+
inputSchema: {
|
|
564
|
+
type: 'object',
|
|
565
|
+
properties: {
|
|
566
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
567
|
+
fix: { type: 'boolean', description: 'If true, applies safe automatic fixes (default: false — dry run/report only)' },
|
|
568
|
+
god_entity_threshold: { type: 'number', description: 'Connection-degree threshold to flag a god entity (default: 15)' }
|
|
569
|
+
},
|
|
570
|
+
required: ['devmind_path']
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
{
|
|
574
|
+
name: 'workflow_create',
|
|
575
|
+
description: 'Start a new persistent, cross-session workflow for a multi-day feature (e.g. "Wallet Integration"). Becomes the active workflow — call workflow_add_step as you make progress so the timeline survives session/context resets. Auto-pauses whatever workflow was previously active.',
|
|
576
|
+
inputSchema: {
|
|
577
|
+
type: 'object',
|
|
578
|
+
properties: {
|
|
579
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
580
|
+
name: { type: 'string', description: 'Short human-readable name for the feature/workflow' },
|
|
581
|
+
description: { type: 'string', description: 'Brief description of the goal — used by you (the agent) to judge whether a later task relates to this workflow' }
|
|
582
|
+
},
|
|
583
|
+
required: ['devmind_path', 'name', 'description']
|
|
584
|
+
}
|
|
585
|
+
},
|
|
586
|
+
{
|
|
587
|
+
name: 'workflow_add_step',
|
|
588
|
+
description: 'Record a step in the currently active (or specified) workflow\'s timeline — a short note of progress, linked to the history_ids already created via edit_node/stage_change + commit_changes rather than duplicating any code or reasoning. NOTE: commit_changes already auto-records a step from its staged entries whenever a workflow is active — you do NOT need to call this after every commit. Only call it directly for something a commit doesn\'t cover: a decision made without a code change, a note on what\'s still pending (pending_tasks), or a custom summary richer than the auto-generated one.',
|
|
589
|
+
inputSchema: {
|
|
590
|
+
type: 'object',
|
|
591
|
+
properties: {
|
|
592
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
593
|
+
workflow_id: { type: 'string', description: 'Workflow to add this step to (optional — defaults to the currently active workflow)' },
|
|
594
|
+
summary: { type: 'string', description: 'Short summary of what this step accomplished' },
|
|
595
|
+
pending_tasks: { type: 'string', description: 'Optional note on what is still left to do' },
|
|
596
|
+
history_ids: { type: 'array', items: { type: 'string' }, description: 'Optional history row ids (from stage_change/commit_changes results) this step covers' },
|
|
597
|
+
session_id: { type: 'string', description: 'Optional session id grouping this step with related history entries' }
|
|
598
|
+
},
|
|
599
|
+
required: ['devmind_path', 'summary']
|
|
600
|
+
}
|
|
601
|
+
},
|
|
602
|
+
{
|
|
603
|
+
name: 'workflow_pause',
|
|
604
|
+
description: 'Pauses the currently active workflow and clears the active pointer — use when switching to unrelated work, so the next workflow_list call surfaces it as resumable instead of leaving it silently abandoned.',
|
|
605
|
+
inputSchema: {
|
|
606
|
+
type: 'object',
|
|
607
|
+
properties: {
|
|
608
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' }
|
|
609
|
+
},
|
|
610
|
+
required: ['devmind_path']
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
name: 'workflow_resume',
|
|
615
|
+
description: 'Resumes a paused workflow, making it active again (auto-pausing whatever was active before).',
|
|
616
|
+
inputSchema: {
|
|
617
|
+
type: 'object',
|
|
618
|
+
properties: {
|
|
619
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
620
|
+
workflow_id: { type: 'string', description: 'The workflow id to resume' }
|
|
621
|
+
},
|
|
622
|
+
required: ['devmind_path', 'workflow_id']
|
|
623
|
+
}
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
name: 'workflow_list',
|
|
627
|
+
description: 'List workflows (optionally filtered by status). Call this when starting work that MIGHT relate to a paused, multi-session feature — if a description looks related to the current task, ask the user whether to resume it (workflow_resume) instead of silently starting fresh and losing its prior decision history.',
|
|
628
|
+
inputSchema: {
|
|
629
|
+
type: 'object',
|
|
630
|
+
properties: {
|
|
631
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
632
|
+
status: { type: 'string', enum: ['active', 'paused', 'completed'], description: 'Optional status filter (default: all)' }
|
|
633
|
+
},
|
|
634
|
+
required: ['devmind_path']
|
|
635
|
+
}
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
name: 'workflow_get_context',
|
|
639
|
+
description: 'Get a workflow\'s full timeline in one call — every step (in order) plus every reference artifact\'s metadata (and optionally content). Call this right after resuming a workflow to instantly regain the feature\'s full context. For large/long-running workflows, prefer workflow_get_steps (paginated) + workflow_read_artifact (per artifact) instead.',
|
|
640
|
+
inputSchema: {
|
|
641
|
+
type: 'object',
|
|
642
|
+
properties: {
|
|
643
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
644
|
+
workflow_id: { type: 'string', description: 'The workflow id to fetch context for' },
|
|
645
|
+
include_artifact_content: { type: 'boolean', description: 'If true, embeds each artifact\'s file content inline in the response (default: false). Only use for small/short workflows — large workflows should use workflow_read_artifact per artifact instead.' }
|
|
646
|
+
},
|
|
647
|
+
required: ['devmind_path', 'workflow_id']
|
|
648
|
+
}
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
name: 'workflow_add_artifact',
|
|
652
|
+
description: 'Save reference material (a spec excerpt, ticket description, API doc, search-result snippet) to a workflow — written to disk under .devmind/workflows/<id>/ and linked in the DB. Use this for material that informed the work but isn\'t part of the code graph itself.',
|
|
653
|
+
inputSchema: {
|
|
654
|
+
type: 'object',
|
|
655
|
+
properties: {
|
|
656
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
657
|
+
workflow_id: { type: 'string', description: 'The target workflow id' },
|
|
658
|
+
step_id: { type: 'string', description: 'Optional step id this artifact relates to' },
|
|
659
|
+
type: { type: 'string', description: 'Artifact type, e.g. pm_doc, api_spec, web_snippet' },
|
|
660
|
+
source_name: { type: 'string', description: 'Title/filename of the artifact source' },
|
|
661
|
+
content: { type: 'string', description: 'Text or markdown content to save' }
|
|
662
|
+
},
|
|
663
|
+
required: ['devmind_path', 'workflow_id', 'type', 'source_name', 'content']
|
|
664
|
+
}
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
name: 'workflow_sync_retroactive',
|
|
668
|
+
description: 'Backfill a workflow\'s timeline after a whole session went by without using workflow_add_step. You already have the session\'s transcript in your own context — extract the steps yourself and pass them here as structured data. This does NOT accept raw transcript text; DevsMind never runs its own LLM calls, so extraction has to happen on your side, which you can already do for free since you already read it.',
|
|
669
|
+
inputSchema: {
|
|
670
|
+
type: 'object',
|
|
671
|
+
properties: {
|
|
672
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
673
|
+
workflow_id: { type: 'string', description: 'The workflow id to sync into' },
|
|
674
|
+
steps: {
|
|
675
|
+
type: 'array',
|
|
676
|
+
description: 'Steps you extracted from the session, oldest first',
|
|
677
|
+
items: {
|
|
678
|
+
type: 'object',
|
|
679
|
+
properties: {
|
|
680
|
+
summary: { type: 'string' },
|
|
681
|
+
pending_tasks: { type: 'string' },
|
|
682
|
+
history_ids: { type: 'array', items: { type: 'string' } }
|
|
683
|
+
},
|
|
684
|
+
required: ['summary']
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
},
|
|
688
|
+
required: ['devmind_path', 'workflow_id', 'steps']
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
{
|
|
692
|
+
name: 'workflow_import',
|
|
693
|
+
description: 'Import existing flow/architecture docs (markdown files describing a feature — title, summary, implementation details) as paused, resumable workflows, so reference material a team already wrote lives where you already look (workflow_get_context) instead of scattered elsewhere. Re-importing the same file updates its workflow in place rather than duplicating it.',
|
|
694
|
+
inputSchema: {
|
|
695
|
+
type: 'object',
|
|
696
|
+
properties: {
|
|
697
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
698
|
+
folder_path: { type: 'string', description: 'Folder to import every .md file from (one workflow per file)' },
|
|
699
|
+
file_path: { type: 'string', description: 'A single .md file to import instead of a folder' }
|
|
700
|
+
},
|
|
701
|
+
required: ['devmind_path']
|
|
702
|
+
}
|
|
703
|
+
},
|
|
704
|
+
{
|
|
705
|
+
name: 'workflow_search',
|
|
706
|
+
description: 'Search across ALL workflows\' step summaries, pending_tasks notes, and artifact source names for a keyword or phrase. Returns matching steps and artifacts grouped by workflow. Use this when you don\'t know which workflow to look in — one call finds relevant context across the entire project history. Set include_artifact_content:true to also scan inside artifact files (slower but more thorough).',
|
|
707
|
+
inputSchema: {
|
|
708
|
+
type: 'object',
|
|
709
|
+
properties: {
|
|
710
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
711
|
+
query: { type: 'string', description: 'Keyword or phrase to search for' },
|
|
712
|
+
status: { type: 'string', enum: ['active', 'paused', 'completed'], description: 'Optional: only search within workflows of this status' },
|
|
713
|
+
include_artifact_content: { type: 'boolean', description: 'If true, also searches inside artifact file contents and includes a snippet of the matching text (default: false)' }
|
|
714
|
+
},
|
|
715
|
+
required: ['devmind_path', 'query']
|
|
716
|
+
}
|
|
717
|
+
},
|
|
718
|
+
{
|
|
719
|
+
name: 'workflow_read_artifact',
|
|
720
|
+
description: 'Read the full content of a single workflow artifact file. Use this after workflow_get_context or workflow_search returns an artifact you want to read — pass the artifact id. This avoids loading the full context dump just to read one reference doc.',
|
|
721
|
+
inputSchema: {
|
|
722
|
+
type: 'object',
|
|
723
|
+
properties: {
|
|
724
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
725
|
+
workflow_id: { type: 'string', description: 'The workflow id that owns the artifact' },
|
|
726
|
+
artifact_id: { type: 'string', description: 'The artifact id to read (from workflow_get_context or workflow_search results)' }
|
|
727
|
+
},
|
|
728
|
+
required: ['devmind_path', 'workflow_id', 'artifact_id']
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
name: 'workflow_get_steps',
|
|
733
|
+
description: 'Read steps from a workflow with pagination support. Use last_n to get only the most recent N steps (recommended when resuming a long-running workflow — read the tail to catch up, not the full history). Alternatively use limit+offset for forward pagination.',
|
|
734
|
+
inputSchema: {
|
|
735
|
+
type: 'object',
|
|
736
|
+
properties: {
|
|
737
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
738
|
+
workflow_id: { type: 'string', description: 'The workflow id to read steps from' },
|
|
739
|
+
last_n: { type: 'number', description: 'Return only the last N steps (most recent). Recommended for catching up on long workflows.' },
|
|
740
|
+
limit: { type: 'number', description: 'Max steps to return (for forward pagination with offset)' },
|
|
741
|
+
offset: { type: 'number', description: 'Step offset for forward pagination (default: 0)' }
|
|
742
|
+
},
|
|
743
|
+
required: ['devmind_path', 'workflow_id']
|
|
744
|
+
}
|
|
502
745
|
}
|
|
503
746
|
]
|
|
504
747
|
};
|
|
@@ -513,7 +756,7 @@ function createMcpServer() {
|
|
|
513
756
|
switch (name) {
|
|
514
757
|
case 'get_node_summary': {
|
|
515
758
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
516
|
-
const nodeId =
|
|
759
|
+
const nodeId = requireStr(args, 'node_id', 'get_node_summary');
|
|
517
760
|
const db = getDatabase(devmindPath);
|
|
518
761
|
const node = db.getNode(nodeId);
|
|
519
762
|
if (!node) {
|
|
@@ -555,7 +798,7 @@ function createMcpServer() {
|
|
|
555
798
|
}
|
|
556
799
|
case 'get_node_code': {
|
|
557
800
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
558
|
-
const nodeId =
|
|
801
|
+
const nodeId = requireStr(args, 'node_id', 'get_node_code');
|
|
559
802
|
const db = getDatabase(devmindPath);
|
|
560
803
|
const result = db.getLiveCode(nodeId);
|
|
561
804
|
return {
|
|
@@ -564,14 +807,29 @@ function createMcpServer() {
|
|
|
564
807
|
}
|
|
565
808
|
case 'update_history': {
|
|
566
809
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
567
|
-
const
|
|
810
|
+
const rawFilePath = requireStr(args, 'file_path', 'update_history');
|
|
568
811
|
const db = getDatabase(devmindPath);
|
|
812
|
+
const workspaceRoot = path.dirname(devmindPath);
|
|
813
|
+
const filePath = path.isAbsolute(rawFilePath) ? path.resolve(rawFilePath) : path.resolve(workspaceRoot, rawFilePath);
|
|
814
|
+
if (!db.isPathAllowed(filePath)) {
|
|
815
|
+
return {
|
|
816
|
+
isError: true,
|
|
817
|
+
content: [{
|
|
818
|
+
type: 'text',
|
|
819
|
+
text: JSON.stringify({
|
|
820
|
+
success: false,
|
|
821
|
+
error: `file_path resolves outside the project's configured repos — nothing was written.`,
|
|
822
|
+
resolved_path: filePath
|
|
823
|
+
})
|
|
824
|
+
}]
|
|
825
|
+
};
|
|
826
|
+
}
|
|
569
827
|
// Single-shot path: stage one entry and commit it immediately, so a lone edit still
|
|
570
828
|
// gets its node, history, AND outgoing edges resolved via the shared commit logic.
|
|
571
829
|
const entry = {
|
|
572
|
-
node_id:
|
|
830
|
+
node_id: requireStr(args, 'node_id', 'update_history'),
|
|
573
831
|
file_path: filePath,
|
|
574
|
-
code_snapshot:
|
|
832
|
+
code_snapshot: requireStr(args, 'code_snapshot', 'update_history'),
|
|
575
833
|
reasoning: args.reasoning,
|
|
576
834
|
name: args.name ? String(args.name) : undefined,
|
|
577
835
|
type: args.type ? String(args.type) : undefined,
|
|
@@ -713,10 +971,157 @@ function createMcpServer() {
|
|
|
713
971
|
}]
|
|
714
972
|
};
|
|
715
973
|
}
|
|
716
|
-
case '
|
|
974
|
+
case 'edit_node': {
|
|
717
975
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
718
|
-
const
|
|
976
|
+
const editDb = getDatabase(devmindPath);
|
|
977
|
+
const workspaceRoot = path.dirname(devmindPath);
|
|
978
|
+
if (!args.file_path || args.old_string === undefined || args.new_string === undefined) {
|
|
979
|
+
return {
|
|
980
|
+
isError: true,
|
|
981
|
+
content: [{
|
|
982
|
+
type: 'text',
|
|
983
|
+
text: JSON.stringify({
|
|
984
|
+
edited: false,
|
|
985
|
+
error: 'edit_node needs file_path, old_string and new_string (pass an empty new_string to delete).'
|
|
986
|
+
})
|
|
987
|
+
}]
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
if (!args.reasoning) {
|
|
991
|
+
return {
|
|
992
|
+
isError: true,
|
|
993
|
+
content: [{
|
|
994
|
+
type: 'text',
|
|
995
|
+
text: JSON.stringify({
|
|
996
|
+
edited: false,
|
|
997
|
+
error: 'edit_node needs reasoning (what_changed, why, goal). It is recorded against whatever code this edit turns out to touch, and exists nowhere else once this turn ends.'
|
|
998
|
+
})
|
|
999
|
+
}]
|
|
1000
|
+
};
|
|
1001
|
+
}
|
|
1002
|
+
const rawPath = String(args.file_path);
|
|
1003
|
+
const filePath = path.isAbsolute(rawPath) ? path.resolve(rawPath) : path.resolve(workspaceRoot, rawPath);
|
|
1004
|
+
if (!editDb.isPathAllowed(filePath)) {
|
|
1005
|
+
return {
|
|
1006
|
+
isError: true,
|
|
1007
|
+
content: [{
|
|
1008
|
+
type: 'text',
|
|
1009
|
+
text: JSON.stringify({
|
|
1010
|
+
edited: false,
|
|
1011
|
+
error: "file_path resolves outside the project's configured repos — nothing was written.",
|
|
1012
|
+
resolved_path: filePath
|
|
1013
|
+
})
|
|
1014
|
+
}]
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
// An empty old_string means "this file does not exist yet — create it". Anything else
|
|
1018
|
+
// is a replacement. Creating through the same call is what keeps a new file from being
|
|
1019
|
+
// the one case that sends the caller back to a write tool that records nothing.
|
|
1020
|
+
const oldString = String(args.old_string);
|
|
1021
|
+
const fileExists = fs.existsSync(filePath);
|
|
1022
|
+
if (!fileExists && oldString !== '') {
|
|
1023
|
+
return {
|
|
1024
|
+
isError: true,
|
|
1025
|
+
content: [{
|
|
1026
|
+
type: 'text',
|
|
1027
|
+
text: JSON.stringify({
|
|
1028
|
+
edited: false,
|
|
1029
|
+
file_path: filePath,
|
|
1030
|
+
error: `${path.basename(filePath)} does not exist, so there is no old_string to match.`,
|
|
1031
|
+
hint: 'To CREATE this file, call edit_node again with old_string: "" and the full file contents as new_string.'
|
|
1032
|
+
})
|
|
1033
|
+
}]
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
const result = fileExists
|
|
1037
|
+
? (0, edit_1.replaceTextInFile)(filePath, oldString, String(args.new_string), args.replace_all === true)
|
|
1038
|
+
: (0, edit_1.createFileWithContent)(filePath, String(args.new_string));
|
|
1039
|
+
if (!result.ok) {
|
|
1040
|
+
return {
|
|
1041
|
+
isError: true,
|
|
1042
|
+
content: [{ type: 'text', text: JSON.stringify({ edited: false, file_path: filePath, error: result.error }) }]
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
(0, ast_1.invalidateParsedFile)(filePath);
|
|
1046
|
+
// Trace the write back to the code it landed in, and record that automatically.
|
|
1047
|
+
// Anything not traceable (markup, config, a top-level import) is a normal outcome:
|
|
1048
|
+
// the file is still edited, there is simply nothing for the graph to hold.
|
|
1049
|
+
const knownHere = editDb.getNodesByFilePath(filePath).map(n => {
|
|
1050
|
+
const parsed = (0, ast_1.parseNodeId)(n.id);
|
|
1051
|
+
return { id: n.id, symbolName: parsed ? parsed.symbolName : (n.id.split('#').pop() || n.name) };
|
|
1052
|
+
});
|
|
1053
|
+
const touched = (0, ast_1.findTouchedSymbols)(filePath, result.ranges || [], knownHere, result.before);
|
|
1054
|
+
let pending = (0, staging_1.readStaged)(devmindPath).length;
|
|
1055
|
+
const staged = [];
|
|
1056
|
+
for (const t of touched) {
|
|
1057
|
+
const nodeId = t.node_id || `${editDb.toRepoRelativePath(filePath)}#${t.symbolName}`;
|
|
1058
|
+
pending = (0, staging_1.stageEntry)(devmindPath, {
|
|
1059
|
+
node_id: nodeId,
|
|
1060
|
+
file_path: filePath,
|
|
1061
|
+
code_snapshot: t.codeSnapshot,
|
|
1062
|
+
reasoning: args.reasoning,
|
|
1063
|
+
name: t.name,
|
|
1064
|
+
type: t.type,
|
|
1065
|
+
signature: t.signature || undefined,
|
|
1066
|
+
session_id: args.session_id ? String(args.session_id) : undefined
|
|
1067
|
+
});
|
|
1068
|
+
const conns = t.node_id ? editDb.getConnections(t.node_id) : { uses: [], usedBy: [] };
|
|
1069
|
+
const priorHistory = (t.node_id ? editDb.getFullHistory(t.node_id) : [])
|
|
1070
|
+
.flatMap(h => (0, database_1.parseReasoningBlocks)(h.reasoning).map(r => ({ updated_at: h.updated_at, r })))
|
|
1071
|
+
.slice(0, 2)
|
|
1072
|
+
.map(({ updated_at, r }) => ({ updated_at, developer: r.developer, what_changed: r.what_changed, why: r.why }));
|
|
1073
|
+
staged.push({
|
|
1074
|
+
node_id: nodeId,
|
|
1075
|
+
name: t.name,
|
|
1076
|
+
type: t.type,
|
|
1077
|
+
lines: `${t.startLine}-${t.endLine}`,
|
|
1078
|
+
is_new_to_graph: t.isNew,
|
|
1079
|
+
callers: conns.usedBy.slice(0, 10).map(n => ({ id: n.id, name: n.name, file_path: n.file_path })),
|
|
1080
|
+
callers_total: conns.usedBy.length,
|
|
1081
|
+
calls_out: conns.uses.slice(0, 10).map(n => ({ id: n.id, name: n.name })),
|
|
1082
|
+
prior_history: priorHistory
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
719
1085
|
const ext = path.extname(filePath).toLowerCase();
|
|
1086
|
+
const callerCount = staged.reduce((sum, s) => sum + s.callers_total, 0);
|
|
1087
|
+
const what = result.created ? 'Created the file and recorded' : 'Recorded';
|
|
1088
|
+
let reminder;
|
|
1089
|
+
if (staged.length) {
|
|
1090
|
+
reminder = callerCount
|
|
1091
|
+
? `${what} ${staged.length} node(s). ${callerCount} node(s) call what you changed — if you altered a signature or contract, check them before moving on. Nothing reaches the graph until commit_changes.`
|
|
1092
|
+
: `${what} ${staged.length} node(s). Nothing reaches the graph until commit_changes.`;
|
|
1093
|
+
}
|
|
1094
|
+
else if (!scanner_1.INDEXABLE_EXTENSIONS.has(ext)) {
|
|
1095
|
+
reminder = `${ext || 'This file type'} is intentionally out of scope for the graph — there is nothing to record. You are done with this edit.`;
|
|
1096
|
+
}
|
|
1097
|
+
else if ((0, ast_1.isAstParseable)(filePath)) {
|
|
1098
|
+
reminder = result.created
|
|
1099
|
+
? 'The file was created, but it declares no function or class, so there was nothing to record. You are done with this edit.'
|
|
1100
|
+
: 'This edit did not land inside any function or class (an import, a top-level constant, or similar), so there was nothing to record. You are done with this edit.';
|
|
1101
|
+
}
|
|
1102
|
+
else {
|
|
1103
|
+
reminder = `${ext} cannot be parsed for symbols, so this could not be traced automatically. If you wrote a function or class, record it with stage_change yourself.`;
|
|
1104
|
+
}
|
|
1105
|
+
return {
|
|
1106
|
+
content: [{
|
|
1107
|
+
type: 'text',
|
|
1108
|
+
text: JSON.stringify({
|
|
1109
|
+
edited: true,
|
|
1110
|
+
created: !!result.created,
|
|
1111
|
+
file_path: filePath,
|
|
1112
|
+
replacements: result.replacements,
|
|
1113
|
+
recorded: staged.length,
|
|
1114
|
+
pending_count: pending,
|
|
1115
|
+
touched: staged,
|
|
1116
|
+
reminder
|
|
1117
|
+
}, null, 2)
|
|
1118
|
+
}]
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
case 'stage_change': {
|
|
1122
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1123
|
+
const rawFilePath = requireStr(args, 'file_path', 'stage_change');
|
|
1124
|
+
const ext = path.extname(rawFilePath).toLowerCase();
|
|
720
1125
|
if (!scanner_1.INDEXABLE_EXTENSIONS.has(ext)) {
|
|
721
1126
|
return {
|
|
722
1127
|
isError: true,
|
|
@@ -731,10 +1136,33 @@ function createMcpServer() {
|
|
|
731
1136
|
}]
|
|
732
1137
|
};
|
|
733
1138
|
}
|
|
1139
|
+
const workspaceRoot = path.dirname(devmindPath);
|
|
1140
|
+
const filePath = path.isAbsolute(rawFilePath) ? path.resolve(rawFilePath) : path.resolve(workspaceRoot, rawFilePath);
|
|
1141
|
+
const stageDb = getDatabase(devmindPath);
|
|
1142
|
+
if (!stageDb.isPathAllowed(filePath)) {
|
|
1143
|
+
return {
|
|
1144
|
+
isError: true,
|
|
1145
|
+
content: [{
|
|
1146
|
+
type: 'text',
|
|
1147
|
+
text: JSON.stringify({
|
|
1148
|
+
staged: false,
|
|
1149
|
+
error: `file_path resolves outside the project's configured repos — nothing was staged.`,
|
|
1150
|
+
reason: 'stage_change only accepts paths inside a repo this project knows about, to prevent staging/reading files outside the project.',
|
|
1151
|
+
resolved_path: filePath
|
|
1152
|
+
})
|
|
1153
|
+
}]
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
if (!args.reasoning) {
|
|
1157
|
+
return {
|
|
1158
|
+
isError: true,
|
|
1159
|
+
content: [{ type: 'text', text: JSON.stringify({ staged: false, error: "stage_change needs 'reasoning' (what_changed, why, goal) — it is the only record of this change that will ever exist." }) }]
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
734
1162
|
const entry = {
|
|
735
|
-
node_id:
|
|
1163
|
+
node_id: requireStr(args, 'node_id', 'stage_change'),
|
|
736
1164
|
file_path: filePath,
|
|
737
|
-
code_snapshot:
|
|
1165
|
+
code_snapshot: requireStr(args, 'code_snapshot', 'stage_change'),
|
|
738
1166
|
reasoning: args.reasoning,
|
|
739
1167
|
name: args.name ? String(args.name) : undefined,
|
|
740
1168
|
type: args.type ? String(args.type) : undefined,
|
|
@@ -765,14 +1193,28 @@ function createMcpServer() {
|
|
|
765
1193
|
}
|
|
766
1194
|
const summary = (0, staging_1.commitStagedChanges)(db, devmindPath, entries);
|
|
767
1195
|
(0, staging_1.clearStaged)(devmindPath);
|
|
1196
|
+
// If a workflow is active, auto-record this commit as a step — the agent doesn't
|
|
1197
|
+
// need a separate workflow_add_step call for the common case. Call workflow_add_step
|
|
1198
|
+
// directly only when you want to attach pending_tasks or a richer custom summary.
|
|
1199
|
+
let workflowStepId = null;
|
|
1200
|
+
const activeWorkflow = db.getActiveWorkflow();
|
|
1201
|
+
if (activeWorkflow) {
|
|
1202
|
+
const step = db.addWorkflowStep(activeWorkflow.id, {
|
|
1203
|
+
summary: (0, staging_1.summarizeEntriesForWorkflow)(entries),
|
|
1204
|
+
historyIds: summary.history_ids
|
|
1205
|
+
});
|
|
1206
|
+
workflowStepId = step.id;
|
|
1207
|
+
}
|
|
768
1208
|
return {
|
|
769
1209
|
content: [{
|
|
770
1210
|
type: 'text',
|
|
771
1211
|
text: JSON.stringify({
|
|
772
1212
|
committed: true,
|
|
773
1213
|
message: `✅ Committed ${summary.nodes} node(s), ${summary.history_entries} history entr(ies), ${summary.edges_added} connection(s) resolved` +
|
|
774
|
-
(summary.missing_filled > 0 ? `, ${summary.missing_filled} missing node(s) auto-created.` : '.')
|
|
775
|
-
|
|
1214
|
+
(summary.missing_filled > 0 ? `, ${summary.missing_filled} missing node(s) auto-created.` : '.') +
|
|
1215
|
+
(workflowStepId ? ` Logged as a step on active workflow "${activeWorkflow.name}".` : ''),
|
|
1216
|
+
...summary,
|
|
1217
|
+
workflow_step_id: workflowStepId
|
|
776
1218
|
}, null, 2)
|
|
777
1219
|
}]
|
|
778
1220
|
};
|
|
@@ -781,16 +1223,16 @@ function createMcpServer() {
|
|
|
781
1223
|
// stage_change/commit_changes), but retained so any direct/legacy call still works. ──
|
|
782
1224
|
case 'add_node': {
|
|
783
1225
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
784
|
-
const rawNodeId =
|
|
785
|
-
const filePath =
|
|
1226
|
+
const rawNodeId = requireStr(args, 'node_id', 'add_node');
|
|
1227
|
+
const filePath = requireStr(args, 'file_path', 'add_node');
|
|
786
1228
|
const db = getDatabase(devmindPath);
|
|
787
1229
|
const repoRelPath = db.toRepoRelativePath(filePath);
|
|
788
1230
|
const prefix = `${repoRelPath}#`;
|
|
789
1231
|
const nodeId = rawNodeId.includes('#') ? rawNodeId : `${prefix}${rawNodeId}`;
|
|
790
1232
|
db.upsertNode({
|
|
791
1233
|
id: nodeId,
|
|
792
|
-
name:
|
|
793
|
-
type:
|
|
1234
|
+
name: requireStr(args, 'name', 'add_node'),
|
|
1235
|
+
type: requireStr(args, 'type', 'add_node'),
|
|
794
1236
|
file_path: filePath,
|
|
795
1237
|
signature: args.signature ? String(args.signature) : null
|
|
796
1238
|
});
|
|
@@ -801,14 +1243,14 @@ function createMcpServer() {
|
|
|
801
1243
|
case 'add_connection': {
|
|
802
1244
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
803
1245
|
const db = getDatabase(devmindPath);
|
|
804
|
-
db.addConnection(
|
|
1246
|
+
db.addConnection(requireStr(args, 'source_node_id', 'add_connection'), requireStr(args, 'target_node_id', 'add_connection'));
|
|
805
1247
|
return {
|
|
806
1248
|
content: [{ type: 'text', text: JSON.stringify({ added: true, source: args.source_node_id, target: args.target_node_id }) }]
|
|
807
1249
|
};
|
|
808
1250
|
}
|
|
809
1251
|
case 'recheck_graph': {
|
|
810
1252
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
811
|
-
const workspaceRoot =
|
|
1253
|
+
const workspaceRoot = requireStr(args, 'workspace_root', 'recheck_graph');
|
|
812
1254
|
const db = getDatabase(devmindPath);
|
|
813
1255
|
const result = db.pruneSpuriousNodes(workspaceRoot);
|
|
814
1256
|
db.vacuum();
|
|
@@ -826,7 +1268,7 @@ function createMcpServer() {
|
|
|
826
1268
|
}
|
|
827
1269
|
case 'get_node_history': {
|
|
828
1270
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
829
|
-
const nodeId =
|
|
1271
|
+
const nodeId = requireStr(args, 'node_id', 'get_node_history');
|
|
830
1272
|
const db = getDatabase(devmindPath);
|
|
831
1273
|
const history = db.getFullHistory(nodeId);
|
|
832
1274
|
return {
|
|
@@ -835,8 +1277,9 @@ function createMcpServer() {
|
|
|
835
1277
|
}
|
|
836
1278
|
case 'get_node_graph': {
|
|
837
1279
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
838
|
-
const nodeId =
|
|
839
|
-
const
|
|
1280
|
+
const nodeId = requireStr(args, 'node_id', 'get_node_graph');
|
|
1281
|
+
const rawMaxDepth = args.max_depth ? Number(args.max_depth) : 6;
|
|
1282
|
+
const maxDepth = Number.isFinite(rawMaxDepth) ? Math.min(10, Math.max(1, Math.trunc(rawMaxDepth))) : 6;
|
|
840
1283
|
const direction = args.direction === 'out' || args.direction === 'in' || args.direction === 'both'
|
|
841
1284
|
? args.direction
|
|
842
1285
|
: 'both';
|
|
@@ -852,19 +1295,25 @@ function createMcpServer() {
|
|
|
852
1295
|
}
|
|
853
1296
|
case 'search_nodes': {
|
|
854
1297
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
855
|
-
const query =
|
|
1298
|
+
const query = requireStr(args, 'query', 'search_nodes');
|
|
856
1299
|
const isRegex = args.is_regex === true;
|
|
857
1300
|
const caseInsensitive = args.case_insensitive !== false;
|
|
858
1301
|
const db = getDatabase(devmindPath);
|
|
859
1302
|
const results = db.searchNodes(query, { is_regex: isRegex, case_insensitive: caseInsensitive });
|
|
1303
|
+
const payload = results.length > 0
|
|
1304
|
+
? results
|
|
1305
|
+
: {
|
|
1306
|
+
results: [],
|
|
1307
|
+
hint: 'No match in name/reasoning, code, file_path, or partial word matches. Try list_nodes with a file_path filter, or a shorter/more literal query term.'
|
|
1308
|
+
};
|
|
860
1309
|
return {
|
|
861
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
1310
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }]
|
|
862
1311
|
};
|
|
863
1312
|
}
|
|
864
1313
|
case 'rename_node': {
|
|
865
1314
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
866
|
-
const oldNodeId =
|
|
867
|
-
const newNodeId =
|
|
1315
|
+
const oldNodeId = requireStr(args, 'old_node_id', 'rename_node');
|
|
1316
|
+
const newNodeId = requireStr(args, 'new_node_id', 'rename_node');
|
|
868
1317
|
const newName = args.new_name ? String(args.new_name) : undefined;
|
|
869
1318
|
const db = getDatabase(devmindPath);
|
|
870
1319
|
db.renameNode(oldNodeId, newNodeId, newName);
|
|
@@ -874,7 +1323,7 @@ function createMcpServer() {
|
|
|
874
1323
|
}
|
|
875
1324
|
case 'deprecate_node': {
|
|
876
1325
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
877
|
-
const nodeId =
|
|
1326
|
+
const nodeId = requireStr(args, 'node_id', 'deprecate_node');
|
|
878
1327
|
const db = getDatabase(devmindPath);
|
|
879
1328
|
db.deprecateNode(nodeId);
|
|
880
1329
|
return {
|
|
@@ -893,7 +1342,7 @@ function createMcpServer() {
|
|
|
893
1342
|
}
|
|
894
1343
|
case 'get_developer_activity': {
|
|
895
1344
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
896
|
-
const developer =
|
|
1345
|
+
const developer = requireStr(args, 'developer', 'get_developer_activity');
|
|
897
1346
|
const limit = args.limit ? Number(args.limit) : 50;
|
|
898
1347
|
const db = getDatabase(devmindPath);
|
|
899
1348
|
const activity = db.getDeveloperActivity(developer, limit);
|
|
@@ -903,7 +1352,7 @@ function createMcpServer() {
|
|
|
903
1352
|
}
|
|
904
1353
|
case 'get_changes_by_requirement': {
|
|
905
1354
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
906
|
-
const requirementId =
|
|
1355
|
+
const requirementId = requireStr(args, 'requirement_id', 'get_changes_by_requirement');
|
|
907
1356
|
const db = getDatabase(devmindPath);
|
|
908
1357
|
const changes = db.getChangesByRequirement(requirementId);
|
|
909
1358
|
return {
|
|
@@ -912,7 +1361,7 @@ function createMcpServer() {
|
|
|
912
1361
|
}
|
|
913
1362
|
case 'search_decisions': {
|
|
914
1363
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
915
|
-
const query =
|
|
1364
|
+
const query = requireStr(args, 'query', 'search_decisions');
|
|
916
1365
|
const db = getDatabase(devmindPath);
|
|
917
1366
|
const decisions = db.searchDecisions(query);
|
|
918
1367
|
return {
|
|
@@ -921,7 +1370,7 @@ function createMcpServer() {
|
|
|
921
1370
|
}
|
|
922
1371
|
case 'search_code': {
|
|
923
1372
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
924
|
-
const query =
|
|
1373
|
+
const query = requireStr(args, 'query', 'search_code');
|
|
925
1374
|
const isRegex = args.is_regex === true;
|
|
926
1375
|
const caseInsensitive = args.case_insensitive !== false;
|
|
927
1376
|
const db = getDatabase(devmindPath);
|
|
@@ -951,6 +1400,150 @@ function createMcpServer() {
|
|
|
951
1400
|
}]
|
|
952
1401
|
};
|
|
953
1402
|
}
|
|
1403
|
+
case 'analyze_graph': {
|
|
1404
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1405
|
+
const workspaceRoot = path.dirname(devmindPath);
|
|
1406
|
+
const db = getDatabase(devmindPath);
|
|
1407
|
+
const godEntityThreshold = args.god_entity_threshold ? Number(args.god_entity_threshold) : undefined;
|
|
1408
|
+
const report = (0, analyze_1.runAnalysis)(db, workspaceRoot, {
|
|
1409
|
+
fix: args.fix === true,
|
|
1410
|
+
godEntityThreshold: Number.isFinite(godEntityThreshold) ? godEntityThreshold : undefined
|
|
1411
|
+
});
|
|
1412
|
+
return {
|
|
1413
|
+
content: [{ type: 'text', text: JSON.stringify(report, null, 2) }]
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
case 'workflow_create': {
|
|
1417
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1418
|
+
const db = getDatabase(devmindPath);
|
|
1419
|
+
const workflow = db.createWorkflow(requireStr(args, 'name', 'workflow_create'), requireStr(args, 'description', 'workflow_create'));
|
|
1420
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'created', workflow }, null, 2) }] };
|
|
1421
|
+
}
|
|
1422
|
+
case 'workflow_add_step': {
|
|
1423
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1424
|
+
const db = getDatabase(devmindPath);
|
|
1425
|
+
const workflowId = args.workflow_id ? String(args.workflow_id) : db.getActiveWorkflow()?.id;
|
|
1426
|
+
if (!workflowId) {
|
|
1427
|
+
return {
|
|
1428
|
+
isError: true,
|
|
1429
|
+
content: [{ type: 'text', text: JSON.stringify({ error: 'No active workflow and no workflow_id given. Call workflow_create or workflow_resume first, or pass workflow_id explicitly.' }) }]
|
|
1430
|
+
};
|
|
1431
|
+
}
|
|
1432
|
+
const step = db.addWorkflowStep(workflowId, {
|
|
1433
|
+
summary: requireStr(args, 'summary', 'workflow_add_step'),
|
|
1434
|
+
pendingTasks: args.pending_tasks ? String(args.pending_tasks) : undefined,
|
|
1435
|
+
historyIds: Array.isArray(args.history_ids) ? args.history_ids.map(String) : undefined,
|
|
1436
|
+
sessionId: args.session_id ? String(args.session_id) : undefined
|
|
1437
|
+
});
|
|
1438
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'added', step }, null, 2) }] };
|
|
1439
|
+
}
|
|
1440
|
+
case 'workflow_pause': {
|
|
1441
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1442
|
+
const db = getDatabase(devmindPath);
|
|
1443
|
+
const paused = db.pauseWorkflow();
|
|
1444
|
+
return {
|
|
1445
|
+
content: [{ type: 'text', text: JSON.stringify(paused ? { status: 'paused', workflow: paused } : { status: 'no_active_workflow' }, null, 2) }]
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
case 'workflow_resume': {
|
|
1449
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1450
|
+
const db = getDatabase(devmindPath);
|
|
1451
|
+
const workflow = db.resumeWorkflow(requireStr(args, 'workflow_id', 'workflow_resume'));
|
|
1452
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'active', workflow }, null, 2) }] };
|
|
1453
|
+
}
|
|
1454
|
+
case 'workflow_list': {
|
|
1455
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1456
|
+
const db = getDatabase(devmindPath);
|
|
1457
|
+
const status = args.status === 'active' || args.status === 'paused' || args.status === 'completed' ? args.status : undefined;
|
|
1458
|
+
const workflows = db.listWorkflows(status);
|
|
1459
|
+
return { content: [{ type: 'text', text: JSON.stringify({ workflows }, null, 2) }] };
|
|
1460
|
+
}
|
|
1461
|
+
case 'workflow_get_context': {
|
|
1462
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1463
|
+
const db = getDatabase(devmindPath);
|
|
1464
|
+
const context = db.getWorkflowContext(requireStr(args, 'workflow_id', 'workflow_get_context'), {
|
|
1465
|
+
includeArtifactContent: args.include_artifact_content === true
|
|
1466
|
+
});
|
|
1467
|
+
return { content: [{ type: 'text', text: JSON.stringify(context, null, 2) }] };
|
|
1468
|
+
}
|
|
1469
|
+
case 'workflow_add_artifact': {
|
|
1470
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1471
|
+
const db = getDatabase(devmindPath);
|
|
1472
|
+
const artifact = db.addWorkflowArtifact(requireStr(args, 'workflow_id', 'workflow_add_artifact'), {
|
|
1473
|
+
stepId: args.step_id ? String(args.step_id) : undefined,
|
|
1474
|
+
type: requireStr(args, 'type', 'workflow_add_artifact'),
|
|
1475
|
+
sourceName: requireStr(args, 'source_name', 'workflow_add_artifact'),
|
|
1476
|
+
content: requireStr(args, 'content', 'workflow_add_artifact')
|
|
1477
|
+
});
|
|
1478
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'added', artifact }, null, 2) }] };
|
|
1479
|
+
}
|
|
1480
|
+
case 'workflow_sync_retroactive': {
|
|
1481
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1482
|
+
const db = getDatabase(devmindPath);
|
|
1483
|
+
const workflowId = requireStr(args, 'workflow_id', 'workflow_sync_retroactive');
|
|
1484
|
+
const stepsInput = Array.isArray(args.steps) ? args.steps : [];
|
|
1485
|
+
// Unlike workflow_import (idempotent by file name), this has no natural retry key —
|
|
1486
|
+
// an agent re-sending the same backfill after an ambiguous timeout/error would
|
|
1487
|
+
// otherwise double every step. Fingerprint against what's ALREADY on the timeline
|
|
1488
|
+
// (summary + pending_tasks + history_ids) and skip exact repeats, mirroring the
|
|
1489
|
+
// protection workflow_import already has for the same "did this already happen"
|
|
1490
|
+
// problem.
|
|
1491
|
+
const existing = new Set(db.getWorkflowSteps(workflowId).map(s => `${s.summary} ${s.pending_tasks || ''} ${s.history_ids || ''}`));
|
|
1492
|
+
const added = [];
|
|
1493
|
+
let skipped = 0;
|
|
1494
|
+
for (const s of stepsInput) {
|
|
1495
|
+
const summary = requireStr(s, 'summary', 'workflow_sync_retroactive step');
|
|
1496
|
+
const pendingTasks = s.pending_tasks ? String(s.pending_tasks) : undefined;
|
|
1497
|
+
const historyIds = Array.isArray(s.history_ids) ? s.history_ids.map(String) : undefined;
|
|
1498
|
+
const fingerprint = `${summary} ${pendingTasks || ''} ${historyIds && historyIds.length ? JSON.stringify(historyIds) : ''}`;
|
|
1499
|
+
if (existing.has(fingerprint)) {
|
|
1500
|
+
skipped++;
|
|
1501
|
+
continue;
|
|
1502
|
+
}
|
|
1503
|
+
existing.add(fingerprint);
|
|
1504
|
+
added.push(db.addWorkflowStep(workflowId, { summary, pendingTasks, historyIds }));
|
|
1505
|
+
}
|
|
1506
|
+
return {
|
|
1507
|
+
content: [{
|
|
1508
|
+
type: 'text',
|
|
1509
|
+
text: JSON.stringify({
|
|
1510
|
+
status: 'synced', steps_added: added.length, steps_skipped_as_duplicate: skipped, steps: added
|
|
1511
|
+
}, null, 2)
|
|
1512
|
+
}]
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
case 'workflow_import': {
|
|
1516
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1517
|
+
const db = getDatabase(devmindPath);
|
|
1518
|
+
const result = (0, workflow_import_1.importWorkflowDocs)(db, args.folder_path ? String(args.folder_path) : undefined, args.file_path ? String(args.file_path) : undefined);
|
|
1519
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1520
|
+
}
|
|
1521
|
+
case 'workflow_search': {
|
|
1522
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1523
|
+
const db = getDatabase(devmindPath);
|
|
1524
|
+
const status = args.status === 'active' || args.status === 'paused' || args.status === 'completed' ? args.status : undefined;
|
|
1525
|
+
const results = db.searchWorkflows(requireStr(args, 'query', 'workflow_search'), {
|
|
1526
|
+
include_artifact_content: args.include_artifact_content === true,
|
|
1527
|
+
status
|
|
1528
|
+
});
|
|
1529
|
+
return { content: [{ type: 'text', text: JSON.stringify({ results, total_workflows_matched: results.length }, null, 2) }] };
|
|
1530
|
+
}
|
|
1531
|
+
case 'workflow_read_artifact': {
|
|
1532
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1533
|
+
const db = getDatabase(devmindPath);
|
|
1534
|
+
const result = db.readWorkflowArtifact(requireStr(args, 'workflow_id', 'workflow_read_artifact'), requireStr(args, 'artifact_id', 'workflow_read_artifact'));
|
|
1535
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1536
|
+
}
|
|
1537
|
+
case 'workflow_get_steps': {
|
|
1538
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1539
|
+
const db = getDatabase(devmindPath);
|
|
1540
|
+
const steps = db.getWorkflowSteps(requireStr(args, 'workflow_id', 'workflow_get_steps'), {
|
|
1541
|
+
last_n: args.last_n ? Number(args.last_n) : undefined,
|
|
1542
|
+
limit: args.limit ? Number(args.limit) : undefined,
|
|
1543
|
+
offset: args.offset ? Number(args.offset) : undefined
|
|
1544
|
+
});
|
|
1545
|
+
return { content: [{ type: 'text', text: JSON.stringify({ workflow_id: args.workflow_id, steps, count: steps.length }, null, 2) }] };
|
|
1546
|
+
}
|
|
954
1547
|
default:
|
|
955
1548
|
throw new Error(`Tool not found: ${name}`);
|
|
956
1549
|
}
|
|
@@ -979,7 +1572,7 @@ function registerShutdownHandlers(httpServer) {
|
|
|
979
1572
|
process.on('SIGINT', shutdown);
|
|
980
1573
|
process.on('SIGTERM', shutdown);
|
|
981
1574
|
}
|
|
982
|
-
// ── HTTP mode (default)
|
|
1575
|
+
// ── HTTP mode (default) — port 4500 ──────────────────────────────────────────
|
|
983
1576
|
/**
|
|
984
1577
|
* Start DevsMind as an HTTP MCP server on port 4500.
|
|
985
1578
|
* IDEs connect via: http://localhost:4500/mcp
|
|
@@ -1049,7 +1642,7 @@ async function runHttpMcpServer(port = exports.DEVSMIND_PORT) {
|
|
|
1049
1642
|
res.status(500).json({ error: err.message });
|
|
1050
1643
|
}
|
|
1051
1644
|
});
|
|
1052
|
-
// MCP endpoint
|
|
1645
|
+
// MCP endpoint — stateless: each request gets its own server + transport pair
|
|
1053
1646
|
app.all('/mcp', async (req, res) => {
|
|
1054
1647
|
try {
|
|
1055
1648
|
const server = createMcpServer();
|
|
@@ -1076,17 +1669,17 @@ async function runHttpMcpServer(port = exports.DEVSMIND_PORT) {
|
|
|
1076
1669
|
httpServer.listen(port, '127.0.0.1', () => resolve());
|
|
1077
1670
|
httpServer.once('error', reject);
|
|
1078
1671
|
});
|
|
1079
|
-
console.log(
|
|
1672
|
+
console.log(`🧠 DevsMind running → http://localhost:${port}/mcp`);
|
|
1080
1673
|
console.log(` press Ctrl+C to stop`);
|
|
1081
1674
|
registerShutdownHandlers(httpServer);
|
|
1082
1675
|
}
|
|
1083
|
-
// ── Stdio mode
|
|
1676
|
+
// ── Stdio mode — for direct IDE plugin injection ──────────────────────────────
|
|
1084
1677
|
/**
|
|
1085
1678
|
* Start DevsMind as a stdio MCP server.
|
|
1086
1679
|
* Used when an IDE manages the process directly (e.g. Cursor stdio plugin mode).
|
|
1087
1680
|
*/
|
|
1088
1681
|
function runStdioMcpServer() {
|
|
1089
|
-
// NOTE: do NOT write to stdout here
|
|
1682
|
+
// NOTE: do NOT write to stdout here — it is the JSON-RPC pipe.
|
|
1090
1683
|
const server = createMcpServer();
|
|
1091
1684
|
process.on('SIGINT', () => { cleanup(); process.exit(0); });
|
|
1092
1685
|
process.on('SIGTERM', () => { cleanup(); process.exit(0); });
|