@mandujs/mcp 0.18.9 → 0.19.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.
@@ -2,12 +2,12 @@
2
2
  * Mandu MCP - Brain Tools
3
3
  *
4
4
  * MCP tools for Brain functionality:
5
- * - mandu_doctor: Guard failure analysis + patch suggestions
6
- * - mandu_watch_start: Start file watching
7
- * - mandu_watch_status: Get watch status
8
- * - mandu_check_location: Check if file location is valid (v0.2)
9
- * - mandu_check_import: Check if imports are valid (v0.2)
10
- * - mandu_get_architecture: Get project architecture rules (v0.2)
5
+ * - mandu.brain.doctor: Guard failure analysis + patch suggestions
6
+ * - mandu.watch.start: Start file watching
7
+ * - mandu.watch.status: Get watch status
8
+ * - mandu.brain.checkLocation: Check if file location is valid (v0.2)
9
+ * - mandu.brain.checkImport: Check if imports are valid (v0.2)
10
+ * - mandu.brain.architecture: Get project architecture rules (v0.2)
11
11
  */
12
12
 
13
13
  import type { Tool } from "@modelcontextprotocol/sdk/types.js";
@@ -31,9 +31,12 @@ import { getProjectPaths } from "../utils/project.js";
31
31
 
32
32
  export const brainToolDefinitions: Tool[] = [
33
33
  {
34
- name: "mandu_doctor",
34
+ name: "mandu.brain.doctor",
35
35
  description:
36
36
  "Analyze Guard failures and suggest patches. Works with or without LLM - template-based analysis is always available.",
37
+ annotations: {
38
+ readOnlyHint: true,
39
+ },
37
40
  inputSchema: {
38
41
  type: "object",
39
42
  properties: {
@@ -47,9 +50,12 @@ export const brainToolDefinitions: Tool[] = [
47
50
  },
48
51
  },
49
52
  {
50
- name: "mandu_watch_start",
53
+ name: "mandu.watch.start",
51
54
  description:
52
55
  "Start file watching with architecture rule warnings. Watches for common mistakes and emits warnings (no blocking).",
56
+ annotations: {
57
+ readOnlyHint: false,
58
+ },
53
59
  inputSchema: {
54
60
  type: "object",
55
61
  properties: {
@@ -62,9 +68,12 @@ export const brainToolDefinitions: Tool[] = [
62
68
  },
63
69
  },
64
70
  {
65
- name: "mandu_watch_status",
71
+ name: "mandu.watch.status",
66
72
  description:
67
73
  "Get the current watch status including recent warnings and active rules.",
74
+ annotations: {
75
+ readOnlyHint: true,
76
+ },
68
77
  inputSchema: {
69
78
  type: "object",
70
79
  properties: {},
@@ -72,9 +81,12 @@ export const brainToolDefinitions: Tool[] = [
72
81
  },
73
82
  },
74
83
  {
75
- name: "mandu_watch_stop",
84
+ name: "mandu.watch.stop",
76
85
  description:
77
86
  "Stop file watching and clean up MCP notification subscriptions.",
87
+ annotations: {
88
+ readOnlyHint: false,
89
+ },
78
90
  inputSchema: {
79
91
  type: "object",
80
92
  properties: {},
@@ -83,9 +95,12 @@ export const brainToolDefinitions: Tool[] = [
83
95
  },
84
96
  // Architecture tools (v0.2)
85
97
  {
86
- name: "mandu_check_location",
98
+ name: "mandu.brain.checkLocation",
87
99
  description:
88
100
  "Check if a file location follows project architecture rules. Call this BEFORE creating or moving files to ensure proper placement.",
101
+ annotations: {
102
+ readOnlyHint: true,
103
+ },
89
104
  inputSchema: {
90
105
  type: "object",
91
106
  properties: {
@@ -102,9 +117,12 @@ export const brainToolDefinitions: Tool[] = [
102
117
  },
103
118
  },
104
119
  {
105
- name: "mandu_check_import",
120
+ name: "mandu.brain.checkImport",
106
121
  description:
107
122
  "Check if imports in a file follow architecture rules. Call this to validate imports before adding them.",
123
+ annotations: {
124
+ readOnlyHint: true,
125
+ },
108
126
  inputSchema: {
109
127
  type: "object",
110
128
  properties: {
@@ -122,9 +140,12 @@ export const brainToolDefinitions: Tool[] = [
122
140
  },
123
141
  },
124
142
  {
125
- name: "mandu_get_architecture",
143
+ name: "mandu.brain.architecture",
126
144
  description:
127
145
  "Get the project architecture rules and folder structure. Use this to understand where to place new files.",
146
+ annotations: {
147
+ readOnlyHint: true,
148
+ },
128
149
  inputSchema: {
129
150
  type: "object",
130
151
  properties: {
@@ -144,8 +165,8 @@ let mcpWarningUnsubscribe: (() => void) | null = null;
144
165
  export function brainTools(projectRoot: string, server?: Server, monitor?: ActivityMonitor) {
145
166
  const paths = getProjectPaths(projectRoot);
146
167
 
147
- return {
148
- mandu_doctor: async (args: Record<string, unknown>) => {
168
+ const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
169
+ "mandu.brain.doctor": async (args: Record<string, unknown>) => {
149
170
  const { useLLM = false } = args as { useLLM?: boolean };
150
171
 
151
172
  try {
@@ -213,7 +234,7 @@ export function brainTools(projectRoot: string, server?: Server, monitor?: Activ
213
234
  }
214
235
  },
215
236
 
216
- mandu_watch_start: async (args: Record<string, unknown>) => {
237
+ "mandu.watch.start": async (args: Record<string, unknown>) => {
217
238
  const { debounceMs } = args as { debounceMs?: number };
218
239
 
219
240
  try {
@@ -256,7 +277,7 @@ export function brainTools(projectRoot: string, server?: Server, monitor?: Activ
256
277
  );
257
278
  }
258
279
 
259
- // Push logging message (Claude Code receives in real-time)
280
+ // Push logging message (MCP client receives in real-time)
260
281
  server.sendLoggingMessage({
261
282
  level: "warning",
262
283
  logger: "mandu-watch",
@@ -313,7 +334,7 @@ export function brainTools(projectRoot: string, server?: Server, monitor?: Activ
313
334
  }
314
335
  },
315
336
 
316
- mandu_watch_status: async () => {
337
+ "mandu.watch.status": async () => {
317
338
  try {
318
339
  const watcher = getWatcher();
319
340
 
@@ -321,7 +342,7 @@ export function brainTools(projectRoot: string, server?: Server, monitor?: Activ
321
342
  return {
322
343
  active: false,
323
344
  message: "Watch is not running",
324
- tip: "Use mandu_watch_start to begin watching",
345
+ tip: "Use mandu.watch.start to begin watching",
325
346
  };
326
347
  }
327
348
 
@@ -352,7 +373,7 @@ export function brainTools(projectRoot: string, server?: Server, monitor?: Activ
352
373
  }
353
374
  },
354
375
 
355
- mandu_watch_stop: async () => {
376
+ "mandu.watch.stop": async () => {
356
377
  try {
357
378
  // Clean up MCP notification subscription
358
379
  if (mcpWarningUnsubscribe) {
@@ -375,7 +396,7 @@ export function brainTools(projectRoot: string, server?: Server, monitor?: Activ
375
396
  },
376
397
 
377
398
  // Architecture tools (v0.2)
378
- mandu_check_location: async (args: Record<string, unknown>) => {
399
+ "mandu.brain.checkLocation": async (args: Record<string, unknown>) => {
379
400
  const { path: filePath, content } = args as {
380
401
  path: string;
381
402
  content?: string;
@@ -423,7 +444,7 @@ export function brainTools(projectRoot: string, server?: Server, monitor?: Activ
423
444
  }
424
445
  },
425
446
 
426
- mandu_check_import: async (args: Record<string, unknown>) => {
447
+ "mandu.brain.checkImport": async (args: Record<string, unknown>) => {
427
448
  const { sourceFile, imports } = args as {
428
449
  sourceFile: string;
429
450
  imports: string[];
@@ -466,7 +487,7 @@ export function brainTools(projectRoot: string, server?: Server, monitor?: Activ
466
487
  }
467
488
  },
468
489
 
469
- mandu_get_architecture: async (args: Record<string, unknown>) => {
490
+ "mandu.brain.architecture": async (args: Record<string, unknown>) => {
470
491
  const { includeStructure = true } = args as {
471
492
  includeStructure?: boolean;
472
493
  };
@@ -515,7 +536,7 @@ export function brainTools(projectRoot: string, server?: Server, monitor?: Activ
515
536
  indexedAt: structure.indexedAt,
516
537
  }
517
538
  : null,
518
- tip: "이 규칙을 따라 파일을 생성하세요. mandu_check_location으로 검증할 수 있습니다.",
539
+ tip: "이 규칙을 따라 파일을 생성하세요. mandu.brain.checkLocation으로 검증할 수 있습니다.",
519
540
  };
520
541
  } catch (error) {
521
542
  return {
@@ -525,4 +546,15 @@ export function brainTools(projectRoot: string, server?: Server, monitor?: Activ
525
546
  }
526
547
  },
527
548
  };
549
+
550
+ // Backward-compatible aliases (deprecated)
551
+ handlers["mandu_doctor"] = handlers["mandu.brain.doctor"];
552
+ handlers["mandu_watch_start"] = handlers["mandu.watch.start"];
553
+ handlers["mandu_watch_stop"] = handlers["mandu.watch.stop"];
554
+ handlers["mandu_watch_status"] = handlers["mandu.watch.status"];
555
+ handlers["mandu_check_location"] = handlers["mandu.brain.checkLocation"];
556
+ handlers["mandu_check_import"] = handlers["mandu.brain.checkImport"];
557
+ handlers["mandu_get_architecture"] = handlers["mandu.brain.architecture"];
558
+
559
+ return handlers;
528
560
  }
@@ -1,185 +1,194 @@
1
- import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
- import path from "path";
3
- import fs from "fs/promises";
4
-
5
- export const componentToolDefinitions: Tool[] = [
6
- {
7
- name: "mandu_add_component",
8
- description:
9
- "Scaffold a new client-side component in the correct FSD (Feature-Sliced Design) layer. " +
10
- "Mandu projects organize client components under src/client/ following FSD layers: " +
11
- "shared (reusable primitives), entities (domain objects), features (user interactions), " +
12
- "widgets (composite blocks), pages (page-level controllers). " +
13
- "Creates the component file and updates the layer's public API index.ts. " +
14
- "Use this instead of manually creating files in app/ to maintain FSD architecture.",
15
- inputSchema: {
16
- type: "object",
17
- properties: {
18
- name: {
19
- type: "string",
20
- description: "Component name in PascalCase (e.g., 'ReactionBar', 'UserAvatar')",
21
- },
22
- layer: {
23
- type: "string",
24
- enum: ["shared", "entities", "features", "widgets", "pages"],
25
- description:
26
- "FSD layer: " +
27
- "'shared' (reusable UI primitives, utils — no business logic), " +
28
- "'entities' (domain models and their UI — User, Message, Post), " +
29
- "'features' (user interactions that change state — like, comment, follow), " +
30
- "'widgets' (composite sections combining entities+features), " +
31
- "'pages' (page-level client componentsuse sparingly, prefer features/entities)",
32
- },
33
- slice: {
34
- type: "string",
35
- description:
36
- "Feature slice name in kebab-case (required for features/entities/widgets). " +
37
- "Examples: 'chat-reaction', 'user-profile', 'post-feed'. " +
38
- "For 'shared' layer, use segment name like 'ui', 'lib', 'api'.",
39
- },
40
- segment: {
41
- type: "string",
42
- enum: ["ui", "model", "api", "lib", "config"],
43
- description: "Segment within the slice (default: 'ui'). 'ui' for React components, 'model' for hooks/store, 'api' for data fetching.",
44
- },
45
- description: {
46
- type: "string",
47
- description: "Brief description of what this component does (added as a comment)",
48
- },
49
- },
50
- required: ["name", "layer"],
51
- },
52
- },
53
- ];
54
-
55
- function toKebabCase(name: string): string {
56
- return name
57
- .replace(/([A-Z])/g, "-$1")
58
- .toLowerCase()
59
- .replace(/^-/, "");
60
- }
61
-
62
- export function componentTools(projectRoot: string) {
63
- return {
64
- mandu_add_component: async (args: Record<string, unknown>) => {
65
- const {
66
- name,
67
- layer,
68
- slice,
69
- segment = "ui",
70
- description = "",
71
- } = args as {
72
- name: string;
73
- layer: "shared" | "entities" | "features" | "widgets" | "pages";
74
- slice?: string;
75
- segment?: string;
76
- description?: string;
77
- };
78
-
79
- // Validate: features/entities/widgets require a slice
80
- if (["features", "entities", "widgets"].includes(layer) && !slice) {
81
- return {
82
- success: false,
83
- error: `The '${layer}' layer requires a 'slice' name (e.g., 'chat-reaction', 'user-profile').`,
84
- };
85
- }
86
-
87
- // Build the file path
88
- const clientBase = path.join(projectRoot, "src", "client");
89
- let componentDir: string;
90
- let indexPath: string;
91
-
92
- if (layer === "shared") {
93
- const sliceName = slice || "ui";
94
- componentDir = path.join(clientBase, "shared", sliceName);
95
- indexPath = path.join(clientBase, "shared", sliceName, "index.ts");
96
- } else if (layer === "pages") {
97
- componentDir = path.join(clientBase, "pages");
98
- indexPath = path.join(clientBase, "pages", "index.ts");
99
- } else {
100
- const sliceName = slice!;
101
- componentDir = path.join(clientBase, layer, sliceName, segment);
102
- indexPath = path.join(clientBase, layer, sliceName, "index.ts");
103
- }
104
-
105
- const kebabName = toKebabCase(name);
106
- const componentFile = path.join(componentDir, `${kebabName}.tsx`);
107
- const relativePath = path.relative(projectRoot, componentFile).replace(/\\/g, "/");
108
-
109
- // Check if file already exists
110
- try {
111
- await fs.access(componentFile);
112
- return {
113
- success: false,
114
- error: `Component file already exists: ${relativePath}`,
115
- };
116
- } catch {
117
- // Good - file doesn't exist
118
- }
119
-
120
- // Create directory
121
- await fs.mkdir(componentDir, { recursive: true });
122
-
123
- // Generate component template
124
- const descComment = description ? `\n * ${description}` : "";
125
- const template = `/**
126
- * ${name} Component${descComment}
127
- * Layer: ${layer}${slice ? ` / ${slice}` : ""}
128
- */
129
-
130
- import { useState } from "react";
131
-
132
- interface ${name}Props {
133
- // TODO: Define props
134
- className?: string;
135
- }
136
-
137
- export function ${name}({ className }: ${name}Props) {
138
- return (
139
- <div className={className}>
140
- {/* TODO: Implement ${name} */}
141
- </div>
142
- );
143
- }
144
- `;
145
-
146
- await fs.writeFile(componentFile, template, "utf-8");
147
-
148
- // Update index.ts (create or append export)
149
- const exportLine = `export { ${name} } from "./${segment}/${kebabName}.js";\n`;
150
- const simpleExportLine = `export { ${name} } from "./${kebabName}.js";\n`;
151
-
152
- try {
153
- let indexContent = "";
154
- try {
155
- indexContent = await fs.readFile(indexPath, "utf-8");
156
- } catch {
157
- // index.ts doesn't exist yet
158
- }
159
-
160
- const exportToAdd = layer === "shared" || layer === "pages" ? simpleExportLine : exportLine;
161
-
162
- if (!indexContent.includes(`{ ${name} }`)) {
163
- await fs.writeFile(indexPath, indexContent + exportToAdd, "utf-8");
164
- }
165
- } catch {
166
- // index update failed - not critical
167
- }
168
-
169
- return {
170
- success: true,
171
- component: name,
172
- layer,
173
- slice: slice || null,
174
- segment,
175
- createdFiles: [relativePath],
176
- updatedFiles: [path.relative(projectRoot, indexPath).replace(/\\/g, "/")],
177
- message: `Created ${name} in ${layer}${slice ? `/${slice}` : ""}/${segment}`,
178
- nextSteps: [
179
- `Edit ${relativePath} to implement the component`,
180
- `Import with: import { ${name} } from "@/client/${layer}${slice ? `/${slice}` : ""}"`,
181
- ],
182
- };
183
- },
184
- };
185
- }
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import path from "path";
3
+ import fs from "fs/promises";
4
+
5
+ export const componentToolDefinitions: Tool[] = [
6
+ {
7
+ name: "mandu.component.add",
8
+ annotations: {
9
+ destructiveHint: false,
10
+ readOnlyHint: false,
11
+ },
12
+ description:
13
+ "Scaffold a new client-side component in the correct FSD (Feature-Sliced Design) layer. " +
14
+ "Mandu projects organize client components under src/client/ following FSD layers: " +
15
+ "shared (reusable primitives), entities (domain objects), features (user interactions), " +
16
+ "widgets (composite blocks), pages (page-level controllers). " +
17
+ "Creates the component file and updates the layer's public API index.ts. " +
18
+ "Use this instead of manually creating files in app/ to maintain FSD architecture.",
19
+ inputSchema: {
20
+ type: "object",
21
+ properties: {
22
+ name: {
23
+ type: "string",
24
+ description: "Component name in PascalCase (e.g., 'ReactionBar', 'UserAvatar')",
25
+ },
26
+ layer: {
27
+ type: "string",
28
+ enum: ["shared", "entities", "features", "widgets", "pages"],
29
+ description:
30
+ "FSD layer: " +
31
+ "'shared' (reusable UI primitives, utils no business logic), " +
32
+ "'entities' (domain models and their UI — User, Message, Post), " +
33
+ "'features' (user interactions that change state — like, comment, follow), " +
34
+ "'widgets' (composite sections combining entities+features), " +
35
+ "'pages' (page-level client components — use sparingly, prefer features/entities)",
36
+ },
37
+ slice: {
38
+ type: "string",
39
+ description:
40
+ "Feature slice name in kebab-case (required for features/entities/widgets). " +
41
+ "Examples: 'chat-reaction', 'user-profile', 'post-feed'. " +
42
+ "For 'shared' layer, use segment name like 'ui', 'lib', 'api'.",
43
+ },
44
+ segment: {
45
+ type: "string",
46
+ enum: ["ui", "model", "api", "lib", "config"],
47
+ description: "Segment within the slice (default: 'ui'). 'ui' for React components, 'model' for hooks/store, 'api' for data fetching.",
48
+ },
49
+ description: {
50
+ type: "string",
51
+ description: "Brief description of what this component does (added as a comment)",
52
+ },
53
+ },
54
+ required: ["name", "layer"],
55
+ },
56
+ },
57
+ ];
58
+
59
+ function toKebabCase(name: string): string {
60
+ return name
61
+ .replace(/([A-Z])/g, "-$1")
62
+ .toLowerCase()
63
+ .replace(/^-/, "");
64
+ }
65
+
66
+ export function componentTools(projectRoot: string) {
67
+ const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
68
+ "mandu.component.add": async (args: Record<string, unknown>) => {
69
+ const {
70
+ name,
71
+ layer,
72
+ slice,
73
+ segment = "ui",
74
+ description = "",
75
+ } = args as {
76
+ name: string;
77
+ layer: "shared" | "entities" | "features" | "widgets" | "pages";
78
+ slice?: string;
79
+ segment?: string;
80
+ description?: string;
81
+ };
82
+
83
+ // Validate: features/entities/widgets require a slice
84
+ if (["features", "entities", "widgets"].includes(layer) && !slice) {
85
+ return {
86
+ success: false,
87
+ error: `The '${layer}' layer requires a 'slice' name (e.g., 'chat-reaction', 'user-profile').`,
88
+ };
89
+ }
90
+
91
+ // Build the file path
92
+ const clientBase = path.join(projectRoot, "src", "client");
93
+ let componentDir: string;
94
+ let indexPath: string;
95
+
96
+ if (layer === "shared") {
97
+ const sliceName = slice || "ui";
98
+ componentDir = path.join(clientBase, "shared", sliceName);
99
+ indexPath = path.join(clientBase, "shared", sliceName, "index.ts");
100
+ } else if (layer === "pages") {
101
+ componentDir = path.join(clientBase, "pages");
102
+ indexPath = path.join(clientBase, "pages", "index.ts");
103
+ } else {
104
+ const sliceName = slice!;
105
+ componentDir = path.join(clientBase, layer, sliceName, segment);
106
+ indexPath = path.join(clientBase, layer, sliceName, "index.ts");
107
+ }
108
+
109
+ const kebabName = toKebabCase(name);
110
+ const componentFile = path.join(componentDir, `${kebabName}.tsx`);
111
+ const relativePath = path.relative(projectRoot, componentFile).replace(/\\/g, "/");
112
+
113
+ // Check if file already exists
114
+ try {
115
+ await fs.access(componentFile);
116
+ return {
117
+ success: false,
118
+ error: `Component file already exists: ${relativePath}`,
119
+ };
120
+ } catch {
121
+ // Good - file doesn't exist
122
+ }
123
+
124
+ // Create directory
125
+ await fs.mkdir(componentDir, { recursive: true });
126
+
127
+ // Generate component template
128
+ const descComment = description ? `\n * ${description}` : "";
129
+ const template = `/**
130
+ * ${name} Component${descComment}
131
+ * Layer: ${layer}${slice ? ` / ${slice}` : ""}
132
+ */
133
+
134
+ import { useState } from "react";
135
+
136
+ interface ${name}Props {
137
+ // TODO: Define props
138
+ className?: string;
139
+ }
140
+
141
+ export function ${name}({ className }: ${name}Props) {
142
+ return (
143
+ <div className={className}>
144
+ {/* TODO: Implement ${name} */}
145
+ </div>
146
+ );
147
+ }
148
+ `;
149
+
150
+ await fs.writeFile(componentFile, template, "utf-8");
151
+
152
+ // Update index.ts (create or append export)
153
+ const exportLine = `export { ${name} } from "./${segment}/${kebabName}.js";\n`;
154
+ const simpleExportLine = `export { ${name} } from "./${kebabName}.js";\n`;
155
+
156
+ try {
157
+ let indexContent = "";
158
+ try {
159
+ indexContent = await fs.readFile(indexPath, "utf-8");
160
+ } catch {
161
+ // index.ts doesn't exist yet
162
+ }
163
+
164
+ const exportToAdd = layer === "shared" || layer === "pages" ? simpleExportLine : exportLine;
165
+
166
+ if (!indexContent.includes(`{ ${name} }`)) {
167
+ await fs.writeFile(indexPath, indexContent + exportToAdd, "utf-8");
168
+ }
169
+ } catch {
170
+ // index update failed - not critical
171
+ }
172
+
173
+ return {
174
+ success: true,
175
+ component: name,
176
+ layer,
177
+ slice: slice || null,
178
+ segment,
179
+ createdFiles: [relativePath],
180
+ updatedFiles: [path.relative(projectRoot, indexPath).replace(/\\/g, "/")],
181
+ message: `Created ${name} in ${layer}${slice ? `/${slice}` : ""}/${segment}`,
182
+ nextSteps: [
183
+ `Edit ${relativePath} to implement the component`,
184
+ `Import with: import { ${name} } from "@/client/${layer}${slice ? `/${slice}` : ""}"`,
185
+ ],
186
+ };
187
+ },
188
+ };
189
+
190
+ // Backward-compatible aliases (deprecated)
191
+ handlers["mandu_add_component"] = handlers["mandu.component.add"];
192
+
193
+ return handlers;
194
+ }