@mandujs/mcp 0.30.0 → 0.31.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.
@@ -1,270 +1,270 @@
1
- import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
- import {
3
- searchDecisions,
4
- saveDecision,
5
- checkConsistency,
6
- getCompactArchitecture,
7
- getNextDecisionId,
8
- type ArchitectureDecision,
9
- type DecisionStatus,
10
- } from "@mandujs/core";
11
-
12
- export const decisionToolDefinitions: Tool[] = [
13
- {
14
- name: "mandu.decision.list",
15
- description:
16
- "Search architecture decisions (ADRs) by tags. Use before implementing features for consistency.",
17
- annotations: {
18
- readOnlyHint: true,
19
- },
20
- inputSchema: {
21
- type: "object",
22
- properties: {
23
- tags: {
24
- type: "array",
25
- items: { type: "string" },
26
- description: "Tags to search for (e.g., ['auth', 'cache', 'api'])",
27
- },
28
- },
29
- required: ["tags"],
30
- },
31
- },
32
- {
33
- name: "mandu.decision.save",
34
- description:
35
- "Save a new architecture decision record (ADR) for future consistency checks.",
36
- annotations: {
37
- readOnlyHint: false,
38
- },
39
- inputSchema: {
40
- type: "object",
41
- properties: {
42
- title: {
43
- type: "string",
44
- description: "Decision title (e.g., 'Use JWT for API Authentication')",
45
- },
46
- tags: {
47
- type: "array",
48
- items: { type: "string" },
49
- description: "Tags for searchability (e.g., ['auth', 'api', 'security'])",
50
- },
51
- context: {
52
- type: "string",
53
- description: "Why this decision was needed",
54
- },
55
- decision: {
56
- type: "string",
57
- description: "What was decided",
58
- },
59
- consequences: {
60
- type: "array",
61
- items: { type: "string" },
62
- description: "Impact and trade-offs of this decision",
63
- },
64
- status: {
65
- type: "string",
66
- enum: ["proposed", "accepted", "deprecated", "superseded"],
67
- description: "Decision status (default: proposed)",
68
- },
69
- },
70
- required: ["title", "tags", "context", "decision", "consequences"],
71
- },
72
- },
73
- {
74
- name: "mandu.decision.check",
75
- description:
76
- "Check if a proposed change conflicts with existing architecture decisions.",
77
- annotations: {
78
- readOnlyHint: true,
79
- },
80
- inputSchema: {
81
- type: "object",
82
- properties: {
83
- intent: {
84
- type: "string",
85
- description: "What you're trying to do (e.g., 'Add Redis caching layer')",
86
- },
87
- tags: {
88
- type: "array",
89
- items: { type: "string" },
90
- description: "Related tags to check against (e.g., ['cache', 'redis'])",
91
- },
92
- },
93
- required: ["intent", "tags"],
94
- },
95
- },
96
- {
97
- name: "mandu.decision.architecture",
98
- description:
99
- "Get a compact summary of project architecture decisions and rules.",
100
- annotations: {
101
- readOnlyHint: true,
102
- },
103
- inputSchema: {
104
- type: "object",
105
- properties: {},
106
- required: [],
107
- },
108
- },
109
- ];
110
-
111
- export function decisionTools(projectRoot: string) {
112
- const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
113
- "mandu.decision.list": async (args: Record<string, unknown>) => {
114
- const { tags } = args as { tags: string[] };
115
-
116
- if (!tags || tags.length === 0) {
117
- return {
118
- error: "Tags are required",
119
- tip: "Provide at least one tag to search for (e.g., ['auth', 'cache'])",
120
- };
121
- }
122
-
123
- const result = await searchDecisions(projectRoot, tags);
124
-
125
- if (result.decisions.length === 0) {
126
- return {
127
- found: false,
128
- message: `No decisions found for tags: ${tags.join(", ")}`,
129
- searchedTags: tags,
130
- tip: "Try broader tags or check spec/decisions/ directory",
131
- };
132
- }
133
-
134
- return {
135
- found: true,
136
- total: result.total,
137
- searchedTags: tags,
138
- decisions: result.decisions.map((d) => ({
139
- id: d.id,
140
- title: d.title,
141
- status: d.status,
142
- date: d.date,
143
- tags: d.tags,
144
- context: d.context.slice(0, 200) + (d.context.length > 200 ? "..." : ""),
145
- decision: d.decision,
146
- consequences: d.consequences,
147
- relatedDecisions: d.relatedDecisions,
148
- })),
149
- tip: "Follow these decisions for consistency. Use mandu.decision.save if you make a new architectural choice.",
150
- };
151
- },
152
-
153
- "mandu.decision.save": async (args: Record<string, unknown>) => {
154
- const { title, tags, context, decision, consequences, status } = args as {
155
- title: string;
156
- tags: string[];
157
- context: string;
158
- decision: string;
159
- consequences: string[];
160
- status?: DecisionStatus;
161
- };
162
-
163
- // Validate required fields
164
- if (!title || !tags || !context || !decision || !consequences) {
165
- return {
166
- error: "Missing required fields",
167
- required: ["title", "tags", "context", "decision", "consequences"],
168
- };
169
- }
170
-
171
- // Get next ID
172
- const id = await getNextDecisionId(projectRoot);
173
-
174
- // Save decision
175
- const newDecision: Omit<ArchitectureDecision, "date"> = {
176
- id,
177
- title,
178
- status: status || "proposed",
179
- tags: tags.map((t) => t.toLowerCase()),
180
- context,
181
- decision,
182
- consequences,
183
- };
184
-
185
- const result = await saveDecision(projectRoot, newDecision);
186
-
187
- return {
188
- success: result.success,
189
- decision: {
190
- id,
191
- title,
192
- status: status || "proposed",
193
- tags,
194
- },
195
- filePath: result.filePath,
196
- message: result.message,
197
- tip: "Decision saved. It will be found when searching for related tags.",
198
- };
199
- },
200
-
201
- "mandu.decision.check": async (args: Record<string, unknown>) => {
202
- const { intent, tags } = args as {
203
- intent: string;
204
- tags: string[];
205
- };
206
-
207
- if (!intent || !tags || tags.length === 0) {
208
- return {
209
- error: "Intent and tags are required",
210
- tip: "Describe what you're trying to do and provide related tags",
211
- };
212
- }
213
-
214
- const result = await checkConsistency(projectRoot, intent, tags);
215
-
216
- return {
217
- consistent: result.consistent,
218
- intent,
219
- checkedTags: tags,
220
- relatedDecisions: result.relatedDecisions.map((d) => ({
221
- id: d.id,
222
- title: d.title,
223
- status: d.status,
224
- decision: d.decision.slice(0, 150) + "...",
225
- })),
226
- warnings: result.warnings,
227
- suggestions: result.suggestions,
228
- tip: result.consistent
229
- ? "No conflicts found. Proceed with implementation following the suggestions."
230
- : "⚠️ Review warnings before proceeding. Some decisions may conflict.",
231
- };
232
- },
233
-
234
- "mandu.decision.architecture": async () => {
235
- const compact = await getCompactArchitecture(projectRoot);
236
-
237
- if (!compact) {
238
- return {
239
- found: false,
240
- message: "No architecture information found",
241
- tip: "Save some decisions first using mandu.decision.save",
242
- };
243
- }
244
-
245
- return {
246
- found: true,
247
- project: compact.project,
248
- lastUpdated: compact.lastUpdated,
249
- summary: {
250
- totalDecisions: compact.keyDecisions.length,
251
- topTags: Object.entries(compact.tagCounts)
252
- .sort(([, a], [, b]) => b - a)
253
- .slice(0, 10)
254
- .map(([tag, count]) => ({ tag, count })),
255
- },
256
- keyDecisions: compact.keyDecisions,
257
- rules: compact.rules,
258
- tip: "Use mandu.decision.list with specific tags for detailed information.",
259
- };
260
- },
261
- };
262
-
263
- // Backward-compatible aliases (deprecated)
264
- handlers["mandu_get_decisions"] = handlers["mandu.decision.list"];
265
- handlers["mandu_save_decision"] = handlers["mandu.decision.save"];
266
- handlers["mandu_check_consistency"] = handlers["mandu.decision.check"];
267
- handlers["mandu_get_architecture"] = handlers["mandu.decision.architecture"];
268
-
269
- return handlers;
270
- }
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import {
3
+ searchDecisions,
4
+ saveDecision,
5
+ checkConsistency,
6
+ getCompactArchitecture,
7
+ getNextDecisionId,
8
+ type ArchitectureDecision,
9
+ type DecisionStatus,
10
+ } from "@mandujs/core";
11
+
12
+ export const decisionToolDefinitions: Tool[] = [
13
+ {
14
+ name: "mandu.decision.list",
15
+ description:
16
+ "Search architecture decisions (ADRs) by tags. Use before implementing features for consistency.",
17
+ annotations: {
18
+ readOnlyHint: true,
19
+ },
20
+ inputSchema: {
21
+ type: "object",
22
+ properties: {
23
+ tags: {
24
+ type: "array",
25
+ items: { type: "string" },
26
+ description: "Tags to search for (e.g., ['auth', 'cache', 'api'])",
27
+ },
28
+ },
29
+ required: ["tags"],
30
+ },
31
+ },
32
+ {
33
+ name: "mandu.decision.save",
34
+ description:
35
+ "Save a new architecture decision record (ADR) for future consistency checks.",
36
+ annotations: {
37
+ readOnlyHint: false,
38
+ },
39
+ inputSchema: {
40
+ type: "object",
41
+ properties: {
42
+ title: {
43
+ type: "string",
44
+ description: "Decision title (e.g., 'Use JWT for API Authentication')",
45
+ },
46
+ tags: {
47
+ type: "array",
48
+ items: { type: "string" },
49
+ description: "Tags for searchability (e.g., ['auth', 'api', 'security'])",
50
+ },
51
+ context: {
52
+ type: "string",
53
+ description: "Why this decision was needed",
54
+ },
55
+ decision: {
56
+ type: "string",
57
+ description: "What was decided",
58
+ },
59
+ consequences: {
60
+ type: "array",
61
+ items: { type: "string" },
62
+ description: "Impact and trade-offs of this decision",
63
+ },
64
+ status: {
65
+ type: "string",
66
+ enum: ["proposed", "accepted", "deprecated", "superseded"],
67
+ description: "Decision status (default: proposed)",
68
+ },
69
+ },
70
+ required: ["title", "tags", "context", "decision", "consequences"],
71
+ },
72
+ },
73
+ {
74
+ name: "mandu.decision.check",
75
+ description:
76
+ "Check if a proposed change conflicts with existing architecture decisions.",
77
+ annotations: {
78
+ readOnlyHint: true,
79
+ },
80
+ inputSchema: {
81
+ type: "object",
82
+ properties: {
83
+ intent: {
84
+ type: "string",
85
+ description: "What you're trying to do (e.g., 'Add Redis caching layer')",
86
+ },
87
+ tags: {
88
+ type: "array",
89
+ items: { type: "string" },
90
+ description: "Related tags to check against (e.g., ['cache', 'redis'])",
91
+ },
92
+ },
93
+ required: ["intent", "tags"],
94
+ },
95
+ },
96
+ {
97
+ name: "mandu.decision.architecture",
98
+ description:
99
+ "Get a compact summary of project architecture decisions and rules.",
100
+ annotations: {
101
+ readOnlyHint: true,
102
+ },
103
+ inputSchema: {
104
+ type: "object",
105
+ properties: {},
106
+ required: [],
107
+ },
108
+ },
109
+ ];
110
+
111
+ export function decisionTools(projectRoot: string) {
112
+ const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
113
+ "mandu.decision.list": async (args: Record<string, unknown>) => {
114
+ const { tags } = args as { tags: string[] };
115
+
116
+ if (!tags || tags.length === 0) {
117
+ return {
118
+ error: "Tags are required",
119
+ tip: "Provide at least one tag to search for (e.g., ['auth', 'cache'])",
120
+ };
121
+ }
122
+
123
+ const result = await searchDecisions(projectRoot, tags);
124
+
125
+ if (result.decisions.length === 0) {
126
+ return {
127
+ found: false,
128
+ message: `No decisions found for tags: ${tags.join(", ")}`,
129
+ searchedTags: tags,
130
+ tip: "Try broader tags or check spec/decisions/ directory",
131
+ };
132
+ }
133
+
134
+ return {
135
+ found: true,
136
+ total: result.total,
137
+ searchedTags: tags,
138
+ decisions: result.decisions.map((d) => ({
139
+ id: d.id,
140
+ title: d.title,
141
+ status: d.status,
142
+ date: d.date,
143
+ tags: d.tags,
144
+ context: d.context.slice(0, 200) + (d.context.length > 200 ? "..." : ""),
145
+ decision: d.decision,
146
+ consequences: d.consequences,
147
+ relatedDecisions: d.relatedDecisions,
148
+ })),
149
+ tip: "Follow these decisions for consistency. Use mandu.decision.save if you make a new architectural choice.",
150
+ };
151
+ },
152
+
153
+ "mandu.decision.save": async (args: Record<string, unknown>) => {
154
+ const { title, tags, context, decision, consequences, status } = args as {
155
+ title: string;
156
+ tags: string[];
157
+ context: string;
158
+ decision: string;
159
+ consequences: string[];
160
+ status?: DecisionStatus;
161
+ };
162
+
163
+ // Validate required fields
164
+ if (!title || !tags || !context || !decision || !consequences) {
165
+ return {
166
+ error: "Missing required fields",
167
+ required: ["title", "tags", "context", "decision", "consequences"],
168
+ };
169
+ }
170
+
171
+ // Get next ID
172
+ const id = await getNextDecisionId(projectRoot);
173
+
174
+ // Save decision
175
+ const newDecision: Omit<ArchitectureDecision, "date"> = {
176
+ id,
177
+ title,
178
+ status: status || "proposed",
179
+ tags: tags.map((t) => t.toLowerCase()),
180
+ context,
181
+ decision,
182
+ consequences,
183
+ };
184
+
185
+ const result = await saveDecision(projectRoot, newDecision);
186
+
187
+ return {
188
+ success: result.success,
189
+ decision: {
190
+ id,
191
+ title,
192
+ status: status || "proposed",
193
+ tags,
194
+ },
195
+ filePath: result.filePath,
196
+ message: result.message,
197
+ tip: "Decision saved. It will be found when searching for related tags.",
198
+ };
199
+ },
200
+
201
+ "mandu.decision.check": async (args: Record<string, unknown>) => {
202
+ const { intent, tags } = args as {
203
+ intent: string;
204
+ tags: string[];
205
+ };
206
+
207
+ if (!intent || !tags || tags.length === 0) {
208
+ return {
209
+ error: "Intent and tags are required",
210
+ tip: "Describe what you're trying to do and provide related tags",
211
+ };
212
+ }
213
+
214
+ const result = await checkConsistency(projectRoot, intent, tags);
215
+
216
+ return {
217
+ consistent: result.consistent,
218
+ intent,
219
+ checkedTags: tags,
220
+ relatedDecisions: result.relatedDecisions.map((d) => ({
221
+ id: d.id,
222
+ title: d.title,
223
+ status: d.status,
224
+ decision: d.decision.slice(0, 150) + "...",
225
+ })),
226
+ warnings: result.warnings,
227
+ suggestions: result.suggestions,
228
+ tip: result.consistent
229
+ ? "No conflicts found. Proceed with implementation following the suggestions."
230
+ : "⚠️ Review warnings before proceeding. Some decisions may conflict.",
231
+ };
232
+ },
233
+
234
+ "mandu.decision.architecture": async () => {
235
+ const compact = await getCompactArchitecture(projectRoot);
236
+
237
+ if (!compact) {
238
+ return {
239
+ found: false,
240
+ message: "No architecture information found",
241
+ tip: "Save some decisions first using mandu.decision.save",
242
+ };
243
+ }
244
+
245
+ return {
246
+ found: true,
247
+ project: compact.project,
248
+ lastUpdated: compact.lastUpdated,
249
+ summary: {
250
+ totalDecisions: compact.keyDecisions.length,
251
+ topTags: Object.entries(compact.tagCounts)
252
+ .sort(([, a], [, b]) => b - a)
253
+ .slice(0, 10)
254
+ .map(([tag, count]) => ({ tag, count })),
255
+ },
256
+ keyDecisions: compact.keyDecisions,
257
+ rules: compact.rules,
258
+ tip: "Use mandu.decision.list with specific tags for detailed information.",
259
+ };
260
+ },
261
+ };
262
+
263
+ // Backward-compatible aliases (deprecated)
264
+ handlers["mandu_get_decisions"] = handlers["mandu.decision.list"];
265
+ handlers["mandu_save_decision"] = handlers["mandu.decision.save"];
266
+ handlers["mandu_check_consistency"] = handlers["mandu.decision.check"];
267
+ handlers["mandu_get_architecture"] = handlers["mandu.decision.architecture"];
268
+
269
+ return handlers;
270
+ }