devsmind-mcp 2.3.0 → 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 +2 -2
- package/dist/cli/init.js +9 -0
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/integrations/memory.js +2 -1
- package/dist/cli/integrations/memory.js.map +1 -1
- package/dist/cli/prune.js +4 -3
- package/dist/cli/prune.js.map +1 -1
- package/dist/cli/rule.js +14 -35
- package/dist/cli/rule.js.map +1 -1
- package/dist/db/analyze.js +7 -3
- package/dist/db/analyze.js.map +1 -1
- package/dist/db/database.d.ts +44 -0
- package/dist/db/database.js +197 -21
- package/dist/db/database.js.map +1 -1
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +297 -62
- 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/edit.d.ts +41 -0
- package/dist/utils/edit.js +163 -0
- package/dist/utils/edit.js.map +1 -0
- package/package.json +1 -1
package/dist/mcp/server.js
CHANGED
|
@@ -52,6 +52,8 @@ 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");
|
|
56
58
|
const analyze_1 = require("../db/analyze");
|
|
57
59
|
const workflow_import_1 = require("../db/workflow-import");
|
|
@@ -74,10 +76,11 @@ Non-negotiable workflow:
|
|
|
74
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.
|
|
75
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.
|
|
76
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.
|
|
77
|
-
5.
|
|
78
|
-
6.
|
|
79
|
-
7.
|
|
80
|
-
8.
|
|
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).`;
|
|
81
84
|
// Shared node-type taxonomy description, reused by update_history and stage_change.
|
|
82
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' +
|
|
83
86
|
'UNIVERSAL: function | method | class | abstract_class | interface | type_alias | enum | constant | variable | module | namespace | decorator\n\n' +
|
|
@@ -92,11 +95,11 @@ const NODE_TYPE_DESCRIPTION = 'The type of node. Be highly specific and framewor
|
|
|
92
95
|
'VUE: vue_component | vue_composable | vue_directive | vue_store_module\n\n' +
|
|
93
96
|
'ANGULAR: ng_component | ng_service | ng_directive | ng_pipe | ng_module | ng_guard | ng_interceptor | ng_resolver\n\n' +
|
|
94
97
|
'SVELTE: svelte_component | svelte_store | svelte_action\n\n' +
|
|
95
|
-
'ORM
|
|
96
|
-
'ORM
|
|
97
|
-
'ORM
|
|
98
|
-
'ORM
|
|
99
|
-
'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' +
|
|
100
103
|
'REST/API: api_endpoint | rest_controller\n' +
|
|
101
104
|
'GRAPHQL: graphql_resolver | graphql_query | graphql_mutation | graphql_subscription | graphql_schema | graphql_directive\n' +
|
|
102
105
|
'GRPC/PROTO: grpc_service | grpc_method | proto_message\n' +
|
|
@@ -136,12 +139,28 @@ function resolveDevmindPath(rawPath) {
|
|
|
136
139
|
return normalized;
|
|
137
140
|
throw new Error(`devmind_path does not exist: "${resolved}". Make sure you pass the exact DEVMIND_PATH from your workspace rules.`);
|
|
138
141
|
}
|
|
139
|
-
// Not provided
|
|
142
|
+
// Not provided — auto-detect from where devsmind start was run
|
|
140
143
|
const autoDetected = findDevmindDir(process.cwd());
|
|
141
144
|
if (autoDetected)
|
|
142
145
|
return autoDetected;
|
|
143
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.`);
|
|
144
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
|
+
}
|
|
145
164
|
function getDatabase(devmindPath) {
|
|
146
165
|
const dbFile = path.join(devmindPath, 'brain.db');
|
|
147
166
|
if (!dbCache.has(dbFile)) {
|
|
@@ -162,7 +181,7 @@ function cleanup() {
|
|
|
162
181
|
}
|
|
163
182
|
/**
|
|
164
183
|
* Creates and wires up a DevsMind MCP Server instance.
|
|
165
|
-
* Stateless
|
|
184
|
+
* Stateless — every call receives devmind_path and opens the db from there.
|
|
166
185
|
*/
|
|
167
186
|
function createMcpServer() {
|
|
168
187
|
const server = new index_js_1.Server({ name: 'devsmind-server', version: '1.0.0' }, {
|
|
@@ -219,7 +238,7 @@ function createMcpServer() {
|
|
|
219
238
|
},
|
|
220
239
|
{
|
|
221
240
|
name: 'get_node_code',
|
|
222
|
-
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.",
|
|
223
242
|
inputSchema: {
|
|
224
243
|
type: 'object',
|
|
225
244
|
properties: {
|
|
@@ -294,6 +313,40 @@ function createMcpServer() {
|
|
|
294
313
|
required: ['devmind_path']
|
|
295
314
|
}
|
|
296
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
|
+
},
|
|
297
350
|
{
|
|
298
351
|
name: 'stage_change',
|
|
299
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.`,
|
|
@@ -390,7 +443,7 @@ function createMcpServer() {
|
|
|
390
443
|
},
|
|
391
444
|
{
|
|
392
445
|
name: 'search_nodes',
|
|
393
|
-
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.',
|
|
394
447
|
inputSchema: {
|
|
395
448
|
type: 'object',
|
|
396
449
|
properties: {
|
|
@@ -532,7 +585,7 @@ function createMcpServer() {
|
|
|
532
585
|
},
|
|
533
586
|
{
|
|
534
587
|
name: 'workflow_add_step',
|
|
535
|
-
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 stage_change
|
|
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.',
|
|
536
589
|
inputSchema: {
|
|
537
590
|
type: 'object',
|
|
538
591
|
properties: {
|
|
@@ -703,7 +756,7 @@ function createMcpServer() {
|
|
|
703
756
|
switch (name) {
|
|
704
757
|
case 'get_node_summary': {
|
|
705
758
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
706
|
-
const nodeId =
|
|
759
|
+
const nodeId = requireStr(args, 'node_id', 'get_node_summary');
|
|
707
760
|
const db = getDatabase(devmindPath);
|
|
708
761
|
const node = db.getNode(nodeId);
|
|
709
762
|
if (!node) {
|
|
@@ -745,7 +798,7 @@ function createMcpServer() {
|
|
|
745
798
|
}
|
|
746
799
|
case 'get_node_code': {
|
|
747
800
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
748
|
-
const nodeId =
|
|
801
|
+
const nodeId = requireStr(args, 'node_id', 'get_node_code');
|
|
749
802
|
const db = getDatabase(devmindPath);
|
|
750
803
|
const result = db.getLiveCode(nodeId);
|
|
751
804
|
return {
|
|
@@ -754,7 +807,7 @@ function createMcpServer() {
|
|
|
754
807
|
}
|
|
755
808
|
case 'update_history': {
|
|
756
809
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
757
|
-
const rawFilePath =
|
|
810
|
+
const rawFilePath = requireStr(args, 'file_path', 'update_history');
|
|
758
811
|
const db = getDatabase(devmindPath);
|
|
759
812
|
const workspaceRoot = path.dirname(devmindPath);
|
|
760
813
|
const filePath = path.isAbsolute(rawFilePath) ? path.resolve(rawFilePath) : path.resolve(workspaceRoot, rawFilePath);
|
|
@@ -774,9 +827,9 @@ function createMcpServer() {
|
|
|
774
827
|
// Single-shot path: stage one entry and commit it immediately, so a lone edit still
|
|
775
828
|
// gets its node, history, AND outgoing edges resolved via the shared commit logic.
|
|
776
829
|
const entry = {
|
|
777
|
-
node_id:
|
|
830
|
+
node_id: requireStr(args, 'node_id', 'update_history'),
|
|
778
831
|
file_path: filePath,
|
|
779
|
-
code_snapshot:
|
|
832
|
+
code_snapshot: requireStr(args, 'code_snapshot', 'update_history'),
|
|
780
833
|
reasoning: args.reasoning,
|
|
781
834
|
name: args.name ? String(args.name) : undefined,
|
|
782
835
|
type: args.type ? String(args.type) : undefined,
|
|
@@ -918,9 +971,156 @@ function createMcpServer() {
|
|
|
918
971
|
}]
|
|
919
972
|
};
|
|
920
973
|
}
|
|
974
|
+
case 'edit_node': {
|
|
975
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
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
|
+
}
|
|
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
|
+
}
|
|
921
1121
|
case 'stage_change': {
|
|
922
1122
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
923
|
-
const rawFilePath =
|
|
1123
|
+
const rawFilePath = requireStr(args, 'file_path', 'stage_change');
|
|
924
1124
|
const ext = path.extname(rawFilePath).toLowerCase();
|
|
925
1125
|
if (!scanner_1.INDEXABLE_EXTENSIONS.has(ext)) {
|
|
926
1126
|
return {
|
|
@@ -953,10 +1153,16 @@ function createMcpServer() {
|
|
|
953
1153
|
}]
|
|
954
1154
|
};
|
|
955
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
|
+
}
|
|
956
1162
|
const entry = {
|
|
957
|
-
node_id:
|
|
1163
|
+
node_id: requireStr(args, 'node_id', 'stage_change'),
|
|
958
1164
|
file_path: filePath,
|
|
959
|
-
code_snapshot:
|
|
1165
|
+
code_snapshot: requireStr(args, 'code_snapshot', 'stage_change'),
|
|
960
1166
|
reasoning: args.reasoning,
|
|
961
1167
|
name: args.name ? String(args.name) : undefined,
|
|
962
1168
|
type: args.type ? String(args.type) : undefined,
|
|
@@ -1017,16 +1223,16 @@ function createMcpServer() {
|
|
|
1017
1223
|
// stage_change/commit_changes), but retained so any direct/legacy call still works. ──
|
|
1018
1224
|
case 'add_node': {
|
|
1019
1225
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1020
|
-
const rawNodeId =
|
|
1021
|
-
const filePath =
|
|
1226
|
+
const rawNodeId = requireStr(args, 'node_id', 'add_node');
|
|
1227
|
+
const filePath = requireStr(args, 'file_path', 'add_node');
|
|
1022
1228
|
const db = getDatabase(devmindPath);
|
|
1023
1229
|
const repoRelPath = db.toRepoRelativePath(filePath);
|
|
1024
1230
|
const prefix = `${repoRelPath}#`;
|
|
1025
1231
|
const nodeId = rawNodeId.includes('#') ? rawNodeId : `${prefix}${rawNodeId}`;
|
|
1026
1232
|
db.upsertNode({
|
|
1027
1233
|
id: nodeId,
|
|
1028
|
-
name:
|
|
1029
|
-
type:
|
|
1234
|
+
name: requireStr(args, 'name', 'add_node'),
|
|
1235
|
+
type: requireStr(args, 'type', 'add_node'),
|
|
1030
1236
|
file_path: filePath,
|
|
1031
1237
|
signature: args.signature ? String(args.signature) : null
|
|
1032
1238
|
});
|
|
@@ -1037,14 +1243,14 @@ function createMcpServer() {
|
|
|
1037
1243
|
case 'add_connection': {
|
|
1038
1244
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1039
1245
|
const db = getDatabase(devmindPath);
|
|
1040
|
-
db.addConnection(
|
|
1246
|
+
db.addConnection(requireStr(args, 'source_node_id', 'add_connection'), requireStr(args, 'target_node_id', 'add_connection'));
|
|
1041
1247
|
return {
|
|
1042
1248
|
content: [{ type: 'text', text: JSON.stringify({ added: true, source: args.source_node_id, target: args.target_node_id }) }]
|
|
1043
1249
|
};
|
|
1044
1250
|
}
|
|
1045
1251
|
case 'recheck_graph': {
|
|
1046
1252
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1047
|
-
const workspaceRoot =
|
|
1253
|
+
const workspaceRoot = requireStr(args, 'workspace_root', 'recheck_graph');
|
|
1048
1254
|
const db = getDatabase(devmindPath);
|
|
1049
1255
|
const result = db.pruneSpuriousNodes(workspaceRoot);
|
|
1050
1256
|
db.vacuum();
|
|
@@ -1062,7 +1268,7 @@ function createMcpServer() {
|
|
|
1062
1268
|
}
|
|
1063
1269
|
case 'get_node_history': {
|
|
1064
1270
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1065
|
-
const nodeId =
|
|
1271
|
+
const nodeId = requireStr(args, 'node_id', 'get_node_history');
|
|
1066
1272
|
const db = getDatabase(devmindPath);
|
|
1067
1273
|
const history = db.getFullHistory(nodeId);
|
|
1068
1274
|
return {
|
|
@@ -1071,7 +1277,7 @@ function createMcpServer() {
|
|
|
1071
1277
|
}
|
|
1072
1278
|
case 'get_node_graph': {
|
|
1073
1279
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1074
|
-
const nodeId =
|
|
1280
|
+
const nodeId = requireStr(args, 'node_id', 'get_node_graph');
|
|
1075
1281
|
const rawMaxDepth = args.max_depth ? Number(args.max_depth) : 6;
|
|
1076
1282
|
const maxDepth = Number.isFinite(rawMaxDepth) ? Math.min(10, Math.max(1, Math.trunc(rawMaxDepth))) : 6;
|
|
1077
1283
|
const direction = args.direction === 'out' || args.direction === 'in' || args.direction === 'both'
|
|
@@ -1089,19 +1295,25 @@ function createMcpServer() {
|
|
|
1089
1295
|
}
|
|
1090
1296
|
case 'search_nodes': {
|
|
1091
1297
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1092
|
-
const query =
|
|
1298
|
+
const query = requireStr(args, 'query', 'search_nodes');
|
|
1093
1299
|
const isRegex = args.is_regex === true;
|
|
1094
1300
|
const caseInsensitive = args.case_insensitive !== false;
|
|
1095
1301
|
const db = getDatabase(devmindPath);
|
|
1096
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
|
+
};
|
|
1097
1309
|
return {
|
|
1098
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
1310
|
+
content: [{ type: 'text', text: JSON.stringify(payload, null, 2) }]
|
|
1099
1311
|
};
|
|
1100
1312
|
}
|
|
1101
1313
|
case 'rename_node': {
|
|
1102
1314
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1103
|
-
const oldNodeId =
|
|
1104
|
-
const newNodeId =
|
|
1315
|
+
const oldNodeId = requireStr(args, 'old_node_id', 'rename_node');
|
|
1316
|
+
const newNodeId = requireStr(args, 'new_node_id', 'rename_node');
|
|
1105
1317
|
const newName = args.new_name ? String(args.new_name) : undefined;
|
|
1106
1318
|
const db = getDatabase(devmindPath);
|
|
1107
1319
|
db.renameNode(oldNodeId, newNodeId, newName);
|
|
@@ -1111,7 +1323,7 @@ function createMcpServer() {
|
|
|
1111
1323
|
}
|
|
1112
1324
|
case 'deprecate_node': {
|
|
1113
1325
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1114
|
-
const nodeId =
|
|
1326
|
+
const nodeId = requireStr(args, 'node_id', 'deprecate_node');
|
|
1115
1327
|
const db = getDatabase(devmindPath);
|
|
1116
1328
|
db.deprecateNode(nodeId);
|
|
1117
1329
|
return {
|
|
@@ -1130,7 +1342,7 @@ function createMcpServer() {
|
|
|
1130
1342
|
}
|
|
1131
1343
|
case 'get_developer_activity': {
|
|
1132
1344
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1133
|
-
const developer =
|
|
1345
|
+
const developer = requireStr(args, 'developer', 'get_developer_activity');
|
|
1134
1346
|
const limit = args.limit ? Number(args.limit) : 50;
|
|
1135
1347
|
const db = getDatabase(devmindPath);
|
|
1136
1348
|
const activity = db.getDeveloperActivity(developer, limit);
|
|
@@ -1140,7 +1352,7 @@ function createMcpServer() {
|
|
|
1140
1352
|
}
|
|
1141
1353
|
case 'get_changes_by_requirement': {
|
|
1142
1354
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1143
|
-
const requirementId =
|
|
1355
|
+
const requirementId = requireStr(args, 'requirement_id', 'get_changes_by_requirement');
|
|
1144
1356
|
const db = getDatabase(devmindPath);
|
|
1145
1357
|
const changes = db.getChangesByRequirement(requirementId);
|
|
1146
1358
|
return {
|
|
@@ -1149,7 +1361,7 @@ function createMcpServer() {
|
|
|
1149
1361
|
}
|
|
1150
1362
|
case 'search_decisions': {
|
|
1151
1363
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1152
|
-
const query =
|
|
1364
|
+
const query = requireStr(args, 'query', 'search_decisions');
|
|
1153
1365
|
const db = getDatabase(devmindPath);
|
|
1154
1366
|
const decisions = db.searchDecisions(query);
|
|
1155
1367
|
return {
|
|
@@ -1158,7 +1370,7 @@ function createMcpServer() {
|
|
|
1158
1370
|
}
|
|
1159
1371
|
case 'search_code': {
|
|
1160
1372
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1161
|
-
const query =
|
|
1373
|
+
const query = requireStr(args, 'query', 'search_code');
|
|
1162
1374
|
const isRegex = args.is_regex === true;
|
|
1163
1375
|
const caseInsensitive = args.case_insensitive !== false;
|
|
1164
1376
|
const db = getDatabase(devmindPath);
|
|
@@ -1204,7 +1416,7 @@ function createMcpServer() {
|
|
|
1204
1416
|
case 'workflow_create': {
|
|
1205
1417
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1206
1418
|
const db = getDatabase(devmindPath);
|
|
1207
|
-
const workflow = db.createWorkflow(
|
|
1419
|
+
const workflow = db.createWorkflow(requireStr(args, 'name', 'workflow_create'), requireStr(args, 'description', 'workflow_create'));
|
|
1208
1420
|
return { content: [{ type: 'text', text: JSON.stringify({ status: 'created', workflow }, null, 2) }] };
|
|
1209
1421
|
}
|
|
1210
1422
|
case 'workflow_add_step': {
|
|
@@ -1218,7 +1430,7 @@ function createMcpServer() {
|
|
|
1218
1430
|
};
|
|
1219
1431
|
}
|
|
1220
1432
|
const step = db.addWorkflowStep(workflowId, {
|
|
1221
|
-
summary:
|
|
1433
|
+
summary: requireStr(args, 'summary', 'workflow_add_step'),
|
|
1222
1434
|
pendingTasks: args.pending_tasks ? String(args.pending_tasks) : undefined,
|
|
1223
1435
|
historyIds: Array.isArray(args.history_ids) ? args.history_ids.map(String) : undefined,
|
|
1224
1436
|
sessionId: args.session_id ? String(args.session_id) : undefined
|
|
@@ -1236,7 +1448,7 @@ function createMcpServer() {
|
|
|
1236
1448
|
case 'workflow_resume': {
|
|
1237
1449
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1238
1450
|
const db = getDatabase(devmindPath);
|
|
1239
|
-
const workflow = db.resumeWorkflow(
|
|
1451
|
+
const workflow = db.resumeWorkflow(requireStr(args, 'workflow_id', 'workflow_resume'));
|
|
1240
1452
|
return { content: [{ type: 'text', text: JSON.stringify({ status: 'active', workflow }, null, 2) }] };
|
|
1241
1453
|
}
|
|
1242
1454
|
case 'workflow_list': {
|
|
@@ -1249,7 +1461,7 @@ function createMcpServer() {
|
|
|
1249
1461
|
case 'workflow_get_context': {
|
|
1250
1462
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1251
1463
|
const db = getDatabase(devmindPath);
|
|
1252
|
-
const context = db.getWorkflowContext(
|
|
1464
|
+
const context = db.getWorkflowContext(requireStr(args, 'workflow_id', 'workflow_get_context'), {
|
|
1253
1465
|
includeArtifactContent: args.include_artifact_content === true
|
|
1254
1466
|
});
|
|
1255
1467
|
return { content: [{ type: 'text', text: JSON.stringify(context, null, 2) }] };
|
|
@@ -1257,25 +1469,48 @@ function createMcpServer() {
|
|
|
1257
1469
|
case 'workflow_add_artifact': {
|
|
1258
1470
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1259
1471
|
const db = getDatabase(devmindPath);
|
|
1260
|
-
const artifact = db.addWorkflowArtifact(
|
|
1472
|
+
const artifact = db.addWorkflowArtifact(requireStr(args, 'workflow_id', 'workflow_add_artifact'), {
|
|
1261
1473
|
stepId: args.step_id ? String(args.step_id) : undefined,
|
|
1262
|
-
type:
|
|
1263
|
-
sourceName:
|
|
1264
|
-
content:
|
|
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')
|
|
1265
1477
|
});
|
|
1266
1478
|
return { content: [{ type: 'text', text: JSON.stringify({ status: 'added', artifact }, null, 2) }] };
|
|
1267
1479
|
}
|
|
1268
1480
|
case 'workflow_sync_retroactive': {
|
|
1269
1481
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1270
1482
|
const db = getDatabase(devmindPath);
|
|
1271
|
-
const workflowId =
|
|
1483
|
+
const workflowId = requireStr(args, 'workflow_id', 'workflow_sync_retroactive');
|
|
1272
1484
|
const stepsInput = Array.isArray(args.steps) ? args.steps : [];
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
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
|
+
};
|
|
1279
1514
|
}
|
|
1280
1515
|
case 'workflow_import': {
|
|
1281
1516
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
@@ -1287,7 +1522,7 @@ function createMcpServer() {
|
|
|
1287
1522
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1288
1523
|
const db = getDatabase(devmindPath);
|
|
1289
1524
|
const status = args.status === 'active' || args.status === 'paused' || args.status === 'completed' ? args.status : undefined;
|
|
1290
|
-
const results = db.searchWorkflows(
|
|
1525
|
+
const results = db.searchWorkflows(requireStr(args, 'query', 'workflow_search'), {
|
|
1291
1526
|
include_artifact_content: args.include_artifact_content === true,
|
|
1292
1527
|
status
|
|
1293
1528
|
});
|
|
@@ -1296,13 +1531,13 @@ function createMcpServer() {
|
|
|
1296
1531
|
case 'workflow_read_artifact': {
|
|
1297
1532
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1298
1533
|
const db = getDatabase(devmindPath);
|
|
1299
|
-
const result = db.readWorkflowArtifact(
|
|
1534
|
+
const result = db.readWorkflowArtifact(requireStr(args, 'workflow_id', 'workflow_read_artifact'), requireStr(args, 'artifact_id', 'workflow_read_artifact'));
|
|
1300
1535
|
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1301
1536
|
}
|
|
1302
1537
|
case 'workflow_get_steps': {
|
|
1303
1538
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1304
1539
|
const db = getDatabase(devmindPath);
|
|
1305
|
-
const steps = db.getWorkflowSteps(
|
|
1540
|
+
const steps = db.getWorkflowSteps(requireStr(args, 'workflow_id', 'workflow_get_steps'), {
|
|
1306
1541
|
last_n: args.last_n ? Number(args.last_n) : undefined,
|
|
1307
1542
|
limit: args.limit ? Number(args.limit) : undefined,
|
|
1308
1543
|
offset: args.offset ? Number(args.offset) : undefined
|
|
@@ -1337,7 +1572,7 @@ function registerShutdownHandlers(httpServer) {
|
|
|
1337
1572
|
process.on('SIGINT', shutdown);
|
|
1338
1573
|
process.on('SIGTERM', shutdown);
|
|
1339
1574
|
}
|
|
1340
|
-
// ── HTTP mode (default)
|
|
1575
|
+
// ── HTTP mode (default) — port 4500 ──────────────────────────────────────────
|
|
1341
1576
|
/**
|
|
1342
1577
|
* Start DevsMind as an HTTP MCP server on port 4500.
|
|
1343
1578
|
* IDEs connect via: http://localhost:4500/mcp
|
|
@@ -1407,7 +1642,7 @@ async function runHttpMcpServer(port = exports.DEVSMIND_PORT) {
|
|
|
1407
1642
|
res.status(500).json({ error: err.message });
|
|
1408
1643
|
}
|
|
1409
1644
|
});
|
|
1410
|
-
// MCP endpoint
|
|
1645
|
+
// MCP endpoint — stateless: each request gets its own server + transport pair
|
|
1411
1646
|
app.all('/mcp', async (req, res) => {
|
|
1412
1647
|
try {
|
|
1413
1648
|
const server = createMcpServer();
|
|
@@ -1434,17 +1669,17 @@ async function runHttpMcpServer(port = exports.DEVSMIND_PORT) {
|
|
|
1434
1669
|
httpServer.listen(port, '127.0.0.1', () => resolve());
|
|
1435
1670
|
httpServer.once('error', reject);
|
|
1436
1671
|
});
|
|
1437
|
-
console.log(
|
|
1672
|
+
console.log(`🧠 DevsMind running → http://localhost:${port}/mcp`);
|
|
1438
1673
|
console.log(` press Ctrl+C to stop`);
|
|
1439
1674
|
registerShutdownHandlers(httpServer);
|
|
1440
1675
|
}
|
|
1441
|
-
// ── Stdio mode
|
|
1676
|
+
// ── Stdio mode — for direct IDE plugin injection ──────────────────────────────
|
|
1442
1677
|
/**
|
|
1443
1678
|
* Start DevsMind as a stdio MCP server.
|
|
1444
1679
|
* Used when an IDE manages the process directly (e.g. Cursor stdio plugin mode).
|
|
1445
1680
|
*/
|
|
1446
1681
|
function runStdioMcpServer() {
|
|
1447
|
-
// NOTE: do NOT write to stdout here
|
|
1682
|
+
// NOTE: do NOT write to stdout here — it is the JSON-RPC pipe.
|
|
1448
1683
|
const server = createMcpServer();
|
|
1449
1684
|
process.on('SIGINT', () => { cleanup(); process.exit(0); });
|
|
1450
1685
|
process.on('SIGTERM', () => { cleanup(); process.exit(0); });
|