devsmind-mcp 2.2.0 → 2.3.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 -527
- 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 +76 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/integrations/memory.d.ts +11 -0
- package/dist/cli/integrations/memory.js +156 -0
- package/dist/cli/integrations/memory.js.map +1 -0
- package/dist/cli/integrations/prompt.d.ts +5 -1
- package/dist/cli/integrations/prompt.js +29 -7
- package/dist/cli/integrations/prompt.js.map +1 -1
- package/dist/cli/integrations/registry.d.ts +35 -0
- package/dist/cli/integrations/registry.js +59 -0
- package/dist/cli/integrations/registry.js.map +1 -1
- package/dist/cli/rule.js +37 -46
- 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 +163 -0
- package/dist/db/analyze.js.map +1 -0
- package/dist/db/database.d.ts +166 -3
- package/dist/db/database.js +700 -64
- 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 +9 -0
- package/dist/mcp/server.js +417 -27
- package/dist/mcp/server.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/git.d.ts +14 -0
- package/dist/utils/git.js +43 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/scanner.d.ts +7 -0
- package/dist/utils/scanner.js +9 -3
- package/dist/utils/scanner.js.map +1 -1
- package/package.json +1 -1
package/dist/mcp/server.js
CHANGED
|
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.DEVSMIND_PORT = void 0;
|
|
39
|
+
exports.DEVSMIND_INSTRUCTIONS = exports.DEVSMIND_PORT = void 0;
|
|
40
40
|
exports.runHttpMcpServer = runHttpMcpServer;
|
|
41
41
|
exports.runStdioMcpServer = runStdioMcpServer;
|
|
42
42
|
exports.runMcpServer = runMcpServer;
|
|
@@ -53,8 +53,31 @@ const visualizer_1 = require("./visualizer");
|
|
|
53
53
|
const indexer_1 = require("../db/indexer");
|
|
54
54
|
const scanner_1 = require("../utils/scanner");
|
|
55
55
|
const staging_1 = require("../db/staging");
|
|
56
|
+
const analyze_1 = require("../db/analyze");
|
|
57
|
+
const workflow_import_1 = require("../db/workflow-import");
|
|
56
58
|
// ─── Port: devs→D(4)E(5)=45 + mind→M(13)=13 → 4513 ─────────────────────────
|
|
57
59
|
exports.DEVSMIND_PORT = 4513;
|
|
60
|
+
/**
|
|
61
|
+
* Sent to every MCP client at connection time via the protocol's `instructions`
|
|
62
|
+
* field (part of the initialize handshake) — the single source of truth for the
|
|
63
|
+
* cross-cutting workflow contract, independent of whatever the user did or didn't
|
|
64
|
+
* paste into a `devsmind rule` output. Keep this crisp: unlike a tool's own
|
|
65
|
+
* `description` (resent every call), this is easy for a long session to lose
|
|
66
|
+
* track of too, so it should carry only what's genuinely cross-cutting.
|
|
67
|
+
*/
|
|
68
|
+
exports.DEVSMIND_INSTRUCTIONS = `DevsMind is this team's persistent shared code memory — not a personal tool you reach for only when asked to search something. Every teammate's AI agent, in every session, reads from the SAME graph you are about to write to. There is no "your copy."
|
|
69
|
+
|
|
70
|
+
If you skip recording a change, you are not skipping a formality. You are leaving the whole team's graph stale for every other AI agent that queries this code later — tomorrow, on a different task, in a different session. And the reasoning behind your change (why it was made, what ticket drove it, what was broken before, what you tried and rejected) exists ONLY in this conversation, right now. It is not in the diff. It is not in the commit message. If it isn't captured this turn, it is gone forever — no reindex, no log, no git blame can recover it later.
|
|
71
|
+
|
|
72
|
+
Non-negotiable workflow:
|
|
73
|
+
1. Before any filesystem search or file read: call search_nodes first. It searches names/reasoning and automatically falls back to a full code-content search if nothing matches — one call covers both, never start with grep.
|
|
74
|
+
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
|
+
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
|
+
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. Stage as you go: call stage_change the moment you finish editing a node — one call per node, not per file, and 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.
|
|
78
|
+
6. Scope: stage_change is for source code only (functions/classes/logic) and 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.
|
|
79
|
+
7. 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.
|
|
80
|
+
8. 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).`;
|
|
58
81
|
// Shared node-type taxonomy description, reused by update_history and stage_change.
|
|
59
82
|
const NODE_TYPE_DESCRIPTION = 'The type of node. Be highly specific and framework-aware. Choose from the taxonomy below (or use a custom value if nothing fits).\n\n' +
|
|
60
83
|
'UNIVERSAL: function | method | class | abstract_class | interface | type_alias | enum | constant | variable | module | namespace | decorator\n\n' +
|
|
@@ -142,7 +165,10 @@ function cleanup() {
|
|
|
142
165
|
* Stateless — every call receives devmind_path and opens the db from there.
|
|
143
166
|
*/
|
|
144
167
|
function createMcpServer() {
|
|
145
|
-
const server = new index_js_1.Server({ name: 'devsmind-server', version: '1.0.0' }, {
|
|
168
|
+
const server = new index_js_1.Server({ name: 'devsmind-server', version: '1.0.0' }, {
|
|
169
|
+
capabilities: { tools: {} },
|
|
170
|
+
instructions: exports.DEVSMIND_INSTRUCTIONS
|
|
171
|
+
});
|
|
146
172
|
// ── Tool Definitions ────────────────────────────────────────────────────────
|
|
147
173
|
server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => {
|
|
148
174
|
return {
|
|
@@ -193,7 +219,7 @@ function createMcpServer() {
|
|
|
193
219
|
},
|
|
194
220
|
{
|
|
195
221
|
name: 'get_node_code',
|
|
196
|
-
description: "Get a single node's CURRENT source code, parsed live from its file on disk — token-efficient, since it returns only that function/class/route rather than the whole file. Call this instead of reading a file whenever you need one specific entity. Response fields: `source: \"live\"` means the code was read from disk and is current. `source: \"cached\"` means the symbol could not be located in its file (not a TS/JS file, or it was renamed/moved/deleted) so a possibly-stale cached snapshot was returned — verify it against the file before relying on it. `snapshot_outdated: true` means the stored graph has drifted from disk; re-stage the node with stage_change + commit_changes to bring the brain back in sync. To fetch a whole call flow at once, prefer get_node_graph with include_code instead of calling this repeatedly.",
|
|
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; re-stage the node with stage_change + commit_changes to bring the brain back in sync. To fetch a whole call flow at once, prefer get_node_graph with include_code instead of calling this repeatedly.",
|
|
197
223
|
inputSchema: {
|
|
198
224
|
type: 'object',
|
|
199
225
|
properties: {
|
|
@@ -270,7 +296,7 @@ function createMcpServer() {
|
|
|
270
296
|
},
|
|
271
297
|
{
|
|
272
298
|
name: 'stage_change',
|
|
273
|
-
description:
|
|
299
|
+
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.`,
|
|
274
300
|
inputSchema: {
|
|
275
301
|
type: 'object',
|
|
276
302
|
properties: {
|
|
@@ -288,7 +314,7 @@ function createMcpServer() {
|
|
|
288
314
|
requirement: { type: 'string', description: 'Ticket / issue / user request ID if applicable' },
|
|
289
315
|
previous_state: { type: 'string', description: 'What the code looked like before and why it was a problem' },
|
|
290
316
|
decision: { type: 'string', description: 'Architectural or implementation decision and why' },
|
|
291
|
-
developer: { type: 'string', description: 'Name of the developer' },
|
|
317
|
+
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)' },
|
|
292
318
|
model: { type: 'string', description: 'AI model name used' }
|
|
293
319
|
},
|
|
294
320
|
required: ['what_changed', 'why', 'goal']
|
|
@@ -303,7 +329,7 @@ function createMcpServer() {
|
|
|
303
329
|
},
|
|
304
330
|
{
|
|
305
331
|
name: 'commit_changes',
|
|
306
|
-
description: '
|
|
332
|
+
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.',
|
|
307
333
|
inputSchema: {
|
|
308
334
|
type: 'object',
|
|
309
335
|
properties: {
|
|
@@ -326,7 +352,7 @@ function createMcpServer() {
|
|
|
326
352
|
},
|
|
327
353
|
{
|
|
328
354
|
name: 'get_node_history',
|
|
329
|
-
description: 'Get the full version history of a code node, including all past code snapshots and change reasoning.',
|
|
355
|
+
description: 'Get the full version history of a code node, including all past code snapshots and change reasoning. Git blame tells you who and when; it never tells you why — the actual decision, ticket, and what was rejected only exists here. Skip this before refactoring and you risk re-breaking a bug that was already fixed once, or undoing a decision made for a reason you never saw.',
|
|
330
356
|
inputSchema: {
|
|
331
357
|
type: 'object',
|
|
332
358
|
properties: {
|
|
@@ -338,7 +364,7 @@ function createMcpServer() {
|
|
|
338
364
|
},
|
|
339
365
|
{
|
|
340
366
|
name: 'get_node_graph',
|
|
341
|
-
description: 'Get a node\'s dependency graph — connected nodes and the relationships between them. Set direction:"out" AND include_code:true to pull an ENTIRE CALL FLOW in a single call: the starting node plus everything it transitively calls, each with its current source code read from disk. Use that combination whenever you are tracing how a request, endpoint, or feature flows through the codebase — it replaces a long chain of get_node_code calls with one round trip. Use direction:"in" to find every caller of a node
|
|
367
|
+
description: 'Get a node\'s dependency graph — connected nodes and the relationships between them. Set direction:"out" AND include_code:true to pull an ENTIRE CALL FLOW in a single call: the starting node plus everything it transitively calls, each with its current source code read from disk. Use that combination whenever you are tracing how a request, endpoint, or feature flows through the codebase — it replaces a long chain of get_node_code calls with one round trip. Use direction:"in" to find every caller of a node before you change its signature — git shows you what changed, never what depends on it, so this is the only way to know what breaks before a teammate hits it. If `code_truncated` is true in the response, the character budget ran out and `nodes_without_code` nodes came back with metadata but no code — fetch those individually with get_node_code, or raise code_char_budget.',
|
|
342
368
|
inputSchema: {
|
|
343
369
|
type: 'object',
|
|
344
370
|
properties: {
|
|
@@ -364,12 +390,14 @@ function createMcpServer() {
|
|
|
364
390
|
},
|
|
365
391
|
{
|
|
366
392
|
name: 'search_nodes',
|
|
367
|
-
description: 'Search node
|
|
393
|
+
description: 'Search for code by name/id/reasoning first; if nothing matches, automatically falls back to a full code-content search (regex or substring) over every node\'s current code — so this is the ONE search tool to call, in ONE turn, whether the term is an identifier or only appears in the code body. Each result is tagged `matched_via: "identifier"` or `matched_via: "code"` so you know how it was found; code matches also include line-level `matches`, `match_count`, and `match_ratio`. 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.',
|
|
368
394
|
inputSchema: {
|
|
369
395
|
type: 'object',
|
|
370
396
|
properties: {
|
|
371
397
|
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
372
|
-
query: { type: 'string', description: 'Search term or
|
|
398
|
+
query: { type: 'string', description: 'Search term, substring, or (with is_regex) regex pattern' },
|
|
399
|
+
is_regex: { type: 'boolean', description: 'Treat query as a regex pattern when falling back to code search (default: false)' },
|
|
400
|
+
case_insensitive: { type: 'boolean', description: 'Case-insensitive code-fallback search (default: true)' }
|
|
373
401
|
},
|
|
374
402
|
required: ['devmind_path', 'query']
|
|
375
403
|
}
|
|
@@ -450,23 +478,24 @@ function createMcpServer() {
|
|
|
450
478
|
required: ['devmind_path', 'query']
|
|
451
479
|
}
|
|
452
480
|
},
|
|
481
|
+
// NOTE: `search_code` is intentionally NOT listed here anymore. `search_nodes` now
|
|
482
|
+
// falls back to the same code-content search automatically when the identifier
|
|
483
|
+
// match is empty, so there is no longer a reason to advertise two search tools.
|
|
484
|
+
// The handler below is retained so any direct/legacy call still works.
|
|
453
485
|
{
|
|
454
|
-
name: '
|
|
455
|
-
description: '
|
|
486
|
+
name: 'get_orphaned_nodes',
|
|
487
|
+
description: 'Find disconnected code nodes in the graph that have no incoming or outgoing connections.',
|
|
456
488
|
inputSchema: {
|
|
457
489
|
type: 'object',
|
|
458
490
|
properties: {
|
|
459
|
-
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' }
|
|
460
|
-
query: { type: 'string', description: 'Regex or substring pattern to search for in code' },
|
|
461
|
-
is_regex: { type: 'boolean', description: 'Whether the query is a regex pattern (default: false)' },
|
|
462
|
-
case_insensitive: { type: 'boolean', description: 'Perform case-insensitive search (default: true)' }
|
|
491
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' }
|
|
463
492
|
},
|
|
464
|
-
required: ['devmind_path'
|
|
493
|
+
required: ['devmind_path']
|
|
465
494
|
}
|
|
466
495
|
},
|
|
467
496
|
{
|
|
468
|
-
name: '
|
|
469
|
-
description: '
|
|
497
|
+
name: 'get_visualizer_url',
|
|
498
|
+
description: 'Get local URLs to open the interactive 2D and 3D code graph visualizer pages.',
|
|
470
499
|
inputSchema: {
|
|
471
500
|
type: 'object',
|
|
472
501
|
properties: {
|
|
@@ -476,8 +505,50 @@ function createMcpServer() {
|
|
|
476
505
|
}
|
|
477
506
|
},
|
|
478
507
|
{
|
|
479
|
-
name: '
|
|
480
|
-
description: '
|
|
508
|
+
name: 'analyze_graph',
|
|
509
|
+
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.',
|
|
510
|
+
inputSchema: {
|
|
511
|
+
type: 'object',
|
|
512
|
+
properties: {
|
|
513
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
514
|
+
fix: { type: 'boolean', description: 'If true, applies safe automatic fixes (default: false — dry run/report only)' },
|
|
515
|
+
god_entity_threshold: { type: 'number', description: 'Connection-degree threshold to flag a god entity (default: 15)' }
|
|
516
|
+
},
|
|
517
|
+
required: ['devmind_path']
|
|
518
|
+
}
|
|
519
|
+
},
|
|
520
|
+
{
|
|
521
|
+
name: 'workflow_create',
|
|
522
|
+
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.',
|
|
523
|
+
inputSchema: {
|
|
524
|
+
type: 'object',
|
|
525
|
+
properties: {
|
|
526
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
527
|
+
name: { type: 'string', description: 'Short human-readable name for the feature/workflow' },
|
|
528
|
+
description: { type: 'string', description: 'Brief description of the goal — used by you (the agent) to judge whether a later task relates to this workflow' }
|
|
529
|
+
},
|
|
530
|
+
required: ['devmind_path', 'name', 'description']
|
|
531
|
+
}
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
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/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
|
+
inputSchema: {
|
|
537
|
+
type: 'object',
|
|
538
|
+
properties: {
|
|
539
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
540
|
+
workflow_id: { type: 'string', description: 'Workflow to add this step to (optional — defaults to the currently active workflow)' },
|
|
541
|
+
summary: { type: 'string', description: 'Short summary of what this step accomplished' },
|
|
542
|
+
pending_tasks: { type: 'string', description: 'Optional note on what is still left to do' },
|
|
543
|
+
history_ids: { type: 'array', items: { type: 'string' }, description: 'Optional history row ids (from stage_change/commit_changes results) this step covers' },
|
|
544
|
+
session_id: { type: 'string', description: 'Optional session id grouping this step with related history entries' }
|
|
545
|
+
},
|
|
546
|
+
required: ['devmind_path', 'summary']
|
|
547
|
+
}
|
|
548
|
+
},
|
|
549
|
+
{
|
|
550
|
+
name: 'workflow_pause',
|
|
551
|
+
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.',
|
|
481
552
|
inputSchema: {
|
|
482
553
|
type: 'object',
|
|
483
554
|
properties: {
|
|
@@ -485,6 +556,139 @@ function createMcpServer() {
|
|
|
485
556
|
},
|
|
486
557
|
required: ['devmind_path']
|
|
487
558
|
}
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
name: 'workflow_resume',
|
|
562
|
+
description: 'Resumes a paused workflow, making it active again (auto-pausing whatever was active before).',
|
|
563
|
+
inputSchema: {
|
|
564
|
+
type: 'object',
|
|
565
|
+
properties: {
|
|
566
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
567
|
+
workflow_id: { type: 'string', description: 'The workflow id to resume' }
|
|
568
|
+
},
|
|
569
|
+
required: ['devmind_path', 'workflow_id']
|
|
570
|
+
}
|
|
571
|
+
},
|
|
572
|
+
{
|
|
573
|
+
name: 'workflow_list',
|
|
574
|
+
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.',
|
|
575
|
+
inputSchema: {
|
|
576
|
+
type: 'object',
|
|
577
|
+
properties: {
|
|
578
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
579
|
+
status: { type: 'string', enum: ['active', 'paused', 'completed'], description: 'Optional status filter (default: all)' }
|
|
580
|
+
},
|
|
581
|
+
required: ['devmind_path']
|
|
582
|
+
}
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
name: 'workflow_get_context',
|
|
586
|
+
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.',
|
|
587
|
+
inputSchema: {
|
|
588
|
+
type: 'object',
|
|
589
|
+
properties: {
|
|
590
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
591
|
+
workflow_id: { type: 'string', description: 'The workflow id to fetch context for' },
|
|
592
|
+
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.' }
|
|
593
|
+
},
|
|
594
|
+
required: ['devmind_path', 'workflow_id']
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
name: 'workflow_add_artifact',
|
|
599
|
+
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.',
|
|
600
|
+
inputSchema: {
|
|
601
|
+
type: 'object',
|
|
602
|
+
properties: {
|
|
603
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
604
|
+
workflow_id: { type: 'string', description: 'The target workflow id' },
|
|
605
|
+
step_id: { type: 'string', description: 'Optional step id this artifact relates to' },
|
|
606
|
+
type: { type: 'string', description: 'Artifact type, e.g. pm_doc, api_spec, web_snippet' },
|
|
607
|
+
source_name: { type: 'string', description: 'Title/filename of the artifact source' },
|
|
608
|
+
content: { type: 'string', description: 'Text or markdown content to save' }
|
|
609
|
+
},
|
|
610
|
+
required: ['devmind_path', 'workflow_id', 'type', 'source_name', 'content']
|
|
611
|
+
}
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
name: 'workflow_sync_retroactive',
|
|
615
|
+
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.',
|
|
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 sync into' },
|
|
621
|
+
steps: {
|
|
622
|
+
type: 'array',
|
|
623
|
+
description: 'Steps you extracted from the session, oldest first',
|
|
624
|
+
items: {
|
|
625
|
+
type: 'object',
|
|
626
|
+
properties: {
|
|
627
|
+
summary: { type: 'string' },
|
|
628
|
+
pending_tasks: { type: 'string' },
|
|
629
|
+
history_ids: { type: 'array', items: { type: 'string' } }
|
|
630
|
+
},
|
|
631
|
+
required: ['summary']
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
required: ['devmind_path', 'workflow_id', 'steps']
|
|
636
|
+
}
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
name: 'workflow_import',
|
|
640
|
+
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.',
|
|
641
|
+
inputSchema: {
|
|
642
|
+
type: 'object',
|
|
643
|
+
properties: {
|
|
644
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
645
|
+
folder_path: { type: 'string', description: 'Folder to import every .md file from (one workflow per file)' },
|
|
646
|
+
file_path: { type: 'string', description: 'A single .md file to import instead of a folder' }
|
|
647
|
+
},
|
|
648
|
+
required: ['devmind_path']
|
|
649
|
+
}
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
name: 'workflow_search',
|
|
653
|
+
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).',
|
|
654
|
+
inputSchema: {
|
|
655
|
+
type: 'object',
|
|
656
|
+
properties: {
|
|
657
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
658
|
+
query: { type: 'string', description: 'Keyword or phrase to search for' },
|
|
659
|
+
status: { type: 'string', enum: ['active', 'paused', 'completed'], description: 'Optional: only search within workflows of this status' },
|
|
660
|
+
include_artifact_content: { type: 'boolean', description: 'If true, also searches inside artifact file contents and includes a snippet of the matching text (default: false)' }
|
|
661
|
+
},
|
|
662
|
+
required: ['devmind_path', 'query']
|
|
663
|
+
}
|
|
664
|
+
},
|
|
665
|
+
{
|
|
666
|
+
name: 'workflow_read_artifact',
|
|
667
|
+
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.',
|
|
668
|
+
inputSchema: {
|
|
669
|
+
type: 'object',
|
|
670
|
+
properties: {
|
|
671
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
672
|
+
workflow_id: { type: 'string', description: 'The workflow id that owns the artifact' },
|
|
673
|
+
artifact_id: { type: 'string', description: 'The artifact id to read (from workflow_get_context or workflow_search results)' }
|
|
674
|
+
},
|
|
675
|
+
required: ['devmind_path', 'workflow_id', 'artifact_id']
|
|
676
|
+
}
|
|
677
|
+
},
|
|
678
|
+
{
|
|
679
|
+
name: 'workflow_get_steps',
|
|
680
|
+
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.',
|
|
681
|
+
inputSchema: {
|
|
682
|
+
type: 'object',
|
|
683
|
+
properties: {
|
|
684
|
+
devmind_path: { type: 'string', description: 'Absolute path to the .devmind directory' },
|
|
685
|
+
workflow_id: { type: 'string', description: 'The workflow id to read steps from' },
|
|
686
|
+
last_n: { type: 'number', description: 'Return only the last N steps (most recent). Recommended for catching up on long workflows.' },
|
|
687
|
+
limit: { type: 'number', description: 'Max steps to return (for forward pagination with offset)' },
|
|
688
|
+
offset: { type: 'number', description: 'Step offset for forward pagination (default: 0)' }
|
|
689
|
+
},
|
|
690
|
+
required: ['devmind_path', 'workflow_id']
|
|
691
|
+
}
|
|
488
692
|
}
|
|
489
693
|
]
|
|
490
694
|
};
|
|
@@ -550,8 +754,23 @@ function createMcpServer() {
|
|
|
550
754
|
}
|
|
551
755
|
case 'update_history': {
|
|
552
756
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
553
|
-
const
|
|
757
|
+
const rawFilePath = String(args.file_path);
|
|
554
758
|
const db = getDatabase(devmindPath);
|
|
759
|
+
const workspaceRoot = path.dirname(devmindPath);
|
|
760
|
+
const filePath = path.isAbsolute(rawFilePath) ? path.resolve(rawFilePath) : path.resolve(workspaceRoot, rawFilePath);
|
|
761
|
+
if (!db.isPathAllowed(filePath)) {
|
|
762
|
+
return {
|
|
763
|
+
isError: true,
|
|
764
|
+
content: [{
|
|
765
|
+
type: 'text',
|
|
766
|
+
text: JSON.stringify({
|
|
767
|
+
success: false,
|
|
768
|
+
error: `file_path resolves outside the project's configured repos — nothing was written.`,
|
|
769
|
+
resolved_path: filePath
|
|
770
|
+
})
|
|
771
|
+
}]
|
|
772
|
+
};
|
|
773
|
+
}
|
|
555
774
|
// Single-shot path: stage one entry and commit it immediately, so a lone edit still
|
|
556
775
|
// gets its node, history, AND outgoing edges resolved via the shared commit logic.
|
|
557
776
|
const entry = {
|
|
@@ -701,9 +920,42 @@ function createMcpServer() {
|
|
|
701
920
|
}
|
|
702
921
|
case 'stage_change': {
|
|
703
922
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
923
|
+
const rawFilePath = String(args.file_path);
|
|
924
|
+
const ext = path.extname(rawFilePath).toLowerCase();
|
|
925
|
+
if (!scanner_1.INDEXABLE_EXTENSIONS.has(ext)) {
|
|
926
|
+
return {
|
|
927
|
+
isError: true,
|
|
928
|
+
content: [{
|
|
929
|
+
type: 'text',
|
|
930
|
+
text: JSON.stringify({
|
|
931
|
+
staged: false,
|
|
932
|
+
error: `'${ext || '(no extension)'}' is not a supported node file type — nothing was staged.`,
|
|
933
|
+
reason: 'DevsMind models functions, classes, and logic entities in source code. Stylesheets (.css/.scss/.less), markup, JSON/config, docs, and other non-code assets are intentionally out of scope, not oversights — staging them would only bloat the graph with nodes that have no callers/callees to resolve. Do not retry this file.',
|
|
934
|
+
supported_extensions: Array.from(scanner_1.INDEXABLE_EXTENSIONS).sort()
|
|
935
|
+
})
|
|
936
|
+
}]
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
const workspaceRoot = path.dirname(devmindPath);
|
|
940
|
+
const filePath = path.isAbsolute(rawFilePath) ? path.resolve(rawFilePath) : path.resolve(workspaceRoot, rawFilePath);
|
|
941
|
+
const stageDb = getDatabase(devmindPath);
|
|
942
|
+
if (!stageDb.isPathAllowed(filePath)) {
|
|
943
|
+
return {
|
|
944
|
+
isError: true,
|
|
945
|
+
content: [{
|
|
946
|
+
type: 'text',
|
|
947
|
+
text: JSON.stringify({
|
|
948
|
+
staged: false,
|
|
949
|
+
error: `file_path resolves outside the project's configured repos — nothing was staged.`,
|
|
950
|
+
reason: 'stage_change only accepts paths inside a repo this project knows about, to prevent staging/reading files outside the project.',
|
|
951
|
+
resolved_path: filePath
|
|
952
|
+
})
|
|
953
|
+
}]
|
|
954
|
+
};
|
|
955
|
+
}
|
|
704
956
|
const entry = {
|
|
705
957
|
node_id: String(args.node_id),
|
|
706
|
-
file_path:
|
|
958
|
+
file_path: filePath,
|
|
707
959
|
code_snapshot: String(args.code_snapshot),
|
|
708
960
|
reasoning: args.reasoning,
|
|
709
961
|
name: args.name ? String(args.name) : undefined,
|
|
@@ -735,14 +987,28 @@ function createMcpServer() {
|
|
|
735
987
|
}
|
|
736
988
|
const summary = (0, staging_1.commitStagedChanges)(db, devmindPath, entries);
|
|
737
989
|
(0, staging_1.clearStaged)(devmindPath);
|
|
990
|
+
// If a workflow is active, auto-record this commit as a step — the agent doesn't
|
|
991
|
+
// need a separate workflow_add_step call for the common case. Call workflow_add_step
|
|
992
|
+
// directly only when you want to attach pending_tasks or a richer custom summary.
|
|
993
|
+
let workflowStepId = null;
|
|
994
|
+
const activeWorkflow = db.getActiveWorkflow();
|
|
995
|
+
if (activeWorkflow) {
|
|
996
|
+
const step = db.addWorkflowStep(activeWorkflow.id, {
|
|
997
|
+
summary: (0, staging_1.summarizeEntriesForWorkflow)(entries),
|
|
998
|
+
historyIds: summary.history_ids
|
|
999
|
+
});
|
|
1000
|
+
workflowStepId = step.id;
|
|
1001
|
+
}
|
|
738
1002
|
return {
|
|
739
1003
|
content: [{
|
|
740
1004
|
type: 'text',
|
|
741
1005
|
text: JSON.stringify({
|
|
742
1006
|
committed: true,
|
|
743
1007
|
message: `✅ Committed ${summary.nodes} node(s), ${summary.history_entries} history entr(ies), ${summary.edges_added} connection(s) resolved` +
|
|
744
|
-
(summary.missing_filled > 0 ? `, ${summary.missing_filled} missing node(s) auto-created.` : '.')
|
|
745
|
-
|
|
1008
|
+
(summary.missing_filled > 0 ? `, ${summary.missing_filled} missing node(s) auto-created.` : '.') +
|
|
1009
|
+
(workflowStepId ? ` Logged as a step on active workflow "${activeWorkflow.name}".` : ''),
|
|
1010
|
+
...summary,
|
|
1011
|
+
workflow_step_id: workflowStepId
|
|
746
1012
|
}, null, 2)
|
|
747
1013
|
}]
|
|
748
1014
|
};
|
|
@@ -806,7 +1072,8 @@ function createMcpServer() {
|
|
|
806
1072
|
case 'get_node_graph': {
|
|
807
1073
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
808
1074
|
const nodeId = String(args.node_id);
|
|
809
|
-
const
|
|
1075
|
+
const rawMaxDepth = args.max_depth ? Number(args.max_depth) : 6;
|
|
1076
|
+
const maxDepth = Number.isFinite(rawMaxDepth) ? Math.min(10, Math.max(1, Math.trunc(rawMaxDepth))) : 6;
|
|
810
1077
|
const direction = args.direction === 'out' || args.direction === 'in' || args.direction === 'both'
|
|
811
1078
|
? args.direction
|
|
812
1079
|
: 'both';
|
|
@@ -823,8 +1090,10 @@ function createMcpServer() {
|
|
|
823
1090
|
case 'search_nodes': {
|
|
824
1091
|
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
825
1092
|
const query = String(args.query);
|
|
1093
|
+
const isRegex = args.is_regex === true;
|
|
1094
|
+
const caseInsensitive = args.case_insensitive !== false;
|
|
826
1095
|
const db = getDatabase(devmindPath);
|
|
827
|
-
const results = db.searchNodes(query);
|
|
1096
|
+
const results = db.searchNodes(query, { is_regex: isRegex, case_insensitive: caseInsensitive });
|
|
828
1097
|
return {
|
|
829
1098
|
content: [{ type: 'text', text: JSON.stringify(results, null, 2) }]
|
|
830
1099
|
};
|
|
@@ -919,6 +1188,127 @@ function createMcpServer() {
|
|
|
919
1188
|
}]
|
|
920
1189
|
};
|
|
921
1190
|
}
|
|
1191
|
+
case 'analyze_graph': {
|
|
1192
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1193
|
+
const workspaceRoot = path.dirname(devmindPath);
|
|
1194
|
+
const db = getDatabase(devmindPath);
|
|
1195
|
+
const godEntityThreshold = args.god_entity_threshold ? Number(args.god_entity_threshold) : undefined;
|
|
1196
|
+
const report = (0, analyze_1.runAnalysis)(db, workspaceRoot, {
|
|
1197
|
+
fix: args.fix === true,
|
|
1198
|
+
godEntityThreshold: Number.isFinite(godEntityThreshold) ? godEntityThreshold : undefined
|
|
1199
|
+
});
|
|
1200
|
+
return {
|
|
1201
|
+
content: [{ type: 'text', text: JSON.stringify(report, null, 2) }]
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
case 'workflow_create': {
|
|
1205
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1206
|
+
const db = getDatabase(devmindPath);
|
|
1207
|
+
const workflow = db.createWorkflow(String(args.name), String(args.description));
|
|
1208
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'created', workflow }, null, 2) }] };
|
|
1209
|
+
}
|
|
1210
|
+
case 'workflow_add_step': {
|
|
1211
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1212
|
+
const db = getDatabase(devmindPath);
|
|
1213
|
+
const workflowId = args.workflow_id ? String(args.workflow_id) : db.getActiveWorkflow()?.id;
|
|
1214
|
+
if (!workflowId) {
|
|
1215
|
+
return {
|
|
1216
|
+
isError: true,
|
|
1217
|
+
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.' }) }]
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
const step = db.addWorkflowStep(workflowId, {
|
|
1221
|
+
summary: String(args.summary),
|
|
1222
|
+
pendingTasks: args.pending_tasks ? String(args.pending_tasks) : undefined,
|
|
1223
|
+
historyIds: Array.isArray(args.history_ids) ? args.history_ids.map(String) : undefined,
|
|
1224
|
+
sessionId: args.session_id ? String(args.session_id) : undefined
|
|
1225
|
+
});
|
|
1226
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'added', step }, null, 2) }] };
|
|
1227
|
+
}
|
|
1228
|
+
case 'workflow_pause': {
|
|
1229
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1230
|
+
const db = getDatabase(devmindPath);
|
|
1231
|
+
const paused = db.pauseWorkflow();
|
|
1232
|
+
return {
|
|
1233
|
+
content: [{ type: 'text', text: JSON.stringify(paused ? { status: 'paused', workflow: paused } : { status: 'no_active_workflow' }, null, 2) }]
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
case 'workflow_resume': {
|
|
1237
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1238
|
+
const db = getDatabase(devmindPath);
|
|
1239
|
+
const workflow = db.resumeWorkflow(String(args.workflow_id));
|
|
1240
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'active', workflow }, null, 2) }] };
|
|
1241
|
+
}
|
|
1242
|
+
case 'workflow_list': {
|
|
1243
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1244
|
+
const db = getDatabase(devmindPath);
|
|
1245
|
+
const status = args.status === 'active' || args.status === 'paused' || args.status === 'completed' ? args.status : undefined;
|
|
1246
|
+
const workflows = db.listWorkflows(status);
|
|
1247
|
+
return { content: [{ type: 'text', text: JSON.stringify({ workflows }, null, 2) }] };
|
|
1248
|
+
}
|
|
1249
|
+
case 'workflow_get_context': {
|
|
1250
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1251
|
+
const db = getDatabase(devmindPath);
|
|
1252
|
+
const context = db.getWorkflowContext(String(args.workflow_id), {
|
|
1253
|
+
includeArtifactContent: args.include_artifact_content === true
|
|
1254
|
+
});
|
|
1255
|
+
return { content: [{ type: 'text', text: JSON.stringify(context, null, 2) }] };
|
|
1256
|
+
}
|
|
1257
|
+
case 'workflow_add_artifact': {
|
|
1258
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1259
|
+
const db = getDatabase(devmindPath);
|
|
1260
|
+
const artifact = db.addWorkflowArtifact(String(args.workflow_id), {
|
|
1261
|
+
stepId: args.step_id ? String(args.step_id) : undefined,
|
|
1262
|
+
type: String(args.type),
|
|
1263
|
+
sourceName: String(args.source_name),
|
|
1264
|
+
content: String(args.content)
|
|
1265
|
+
});
|
|
1266
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'added', artifact }, null, 2) }] };
|
|
1267
|
+
}
|
|
1268
|
+
case 'workflow_sync_retroactive': {
|
|
1269
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1270
|
+
const db = getDatabase(devmindPath);
|
|
1271
|
+
const workflowId = String(args.workflow_id);
|
|
1272
|
+
const stepsInput = Array.isArray(args.steps) ? args.steps : [];
|
|
1273
|
+
const added = stepsInput.map((s) => db.addWorkflowStep(workflowId, {
|
|
1274
|
+
summary: String(s.summary),
|
|
1275
|
+
pendingTasks: s.pending_tasks ? String(s.pending_tasks) : undefined,
|
|
1276
|
+
historyIds: Array.isArray(s.history_ids) ? s.history_ids.map(String) : undefined
|
|
1277
|
+
}));
|
|
1278
|
+
return { content: [{ type: 'text', text: JSON.stringify({ status: 'synced', steps_added: added.length, steps: added }, null, 2) }] };
|
|
1279
|
+
}
|
|
1280
|
+
case 'workflow_import': {
|
|
1281
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1282
|
+
const db = getDatabase(devmindPath);
|
|
1283
|
+
const result = (0, workflow_import_1.importWorkflowDocs)(db, args.folder_path ? String(args.folder_path) : undefined, args.file_path ? String(args.file_path) : undefined);
|
|
1284
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1285
|
+
}
|
|
1286
|
+
case 'workflow_search': {
|
|
1287
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1288
|
+
const db = getDatabase(devmindPath);
|
|
1289
|
+
const status = args.status === 'active' || args.status === 'paused' || args.status === 'completed' ? args.status : undefined;
|
|
1290
|
+
const results = db.searchWorkflows(String(args.query), {
|
|
1291
|
+
include_artifact_content: args.include_artifact_content === true,
|
|
1292
|
+
status
|
|
1293
|
+
});
|
|
1294
|
+
return { content: [{ type: 'text', text: JSON.stringify({ results, total_workflows_matched: results.length }, null, 2) }] };
|
|
1295
|
+
}
|
|
1296
|
+
case 'workflow_read_artifact': {
|
|
1297
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1298
|
+
const db = getDatabase(devmindPath);
|
|
1299
|
+
const result = db.readWorkflowArtifact(String(args.workflow_id), String(args.artifact_id));
|
|
1300
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
1301
|
+
}
|
|
1302
|
+
case 'workflow_get_steps': {
|
|
1303
|
+
const devmindPath = resolveDevmindPath(args.devmind_path);
|
|
1304
|
+
const db = getDatabase(devmindPath);
|
|
1305
|
+
const steps = db.getWorkflowSteps(String(args.workflow_id), {
|
|
1306
|
+
last_n: args.last_n ? Number(args.last_n) : undefined,
|
|
1307
|
+
limit: args.limit ? Number(args.limit) : undefined,
|
|
1308
|
+
offset: args.offset ? Number(args.offset) : undefined
|
|
1309
|
+
});
|
|
1310
|
+
return { content: [{ type: 'text', text: JSON.stringify({ workflow_id: args.workflow_id, steps, count: steps.length }, null, 2) }] };
|
|
1311
|
+
}
|
|
922
1312
|
default:
|
|
923
1313
|
throw new Error(`Tool not found: ${name}`);
|
|
924
1314
|
}
|