@mandujs/mcp 0.18.9 → 0.18.10

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.
@@ -8,8 +8,11 @@ import {
8
8
 
9
9
  export const historyToolDefinitions: Tool[] = [
10
10
  {
11
- name: "mandu_list_history",
11
+ name: "mandu.history.list",
12
12
  description: "List the change history with snapshots",
13
+ annotations: {
14
+ readOnlyHint: true,
15
+ },
13
16
  inputSchema: {
14
17
  type: "object",
15
18
  properties: {
@@ -22,8 +25,11 @@ export const historyToolDefinitions: Tool[] = [
22
25
  },
23
26
  },
24
27
  {
25
- name: "mandu_get_snapshot",
28
+ name: "mandu.history.snapshot",
26
29
  description: "Get details of a specific snapshot",
30
+ annotations: {
31
+ readOnlyHint: true,
32
+ },
27
33
  inputSchema: {
28
34
  type: "object",
29
35
  properties: {
@@ -36,8 +42,12 @@ export const historyToolDefinitions: Tool[] = [
36
42
  },
37
43
  },
38
44
  {
39
- name: "mandu_prune_history",
45
+ name: "mandu.history.prune",
40
46
  description: "Remove old snapshots to free up space",
47
+ annotations: {
48
+ destructiveHint: true,
49
+ readOnlyHint: false,
50
+ },
41
51
  inputSchema: {
42
52
  type: "object",
43
53
  properties: {
@@ -52,8 +62,8 @@ export const historyToolDefinitions: Tool[] = [
52
62
  ];
53
63
 
54
64
  export function historyTools(projectRoot: string) {
55
- return {
56
- mandu_list_history: async (args: Record<string, unknown>) => {
65
+ const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
66
+ "mandu.history.list": async (args: Record<string, unknown>) => {
57
67
  const { limit = 10 } = args as { limit?: number };
58
68
 
59
69
  const changes = await listChanges(projectRoot);
@@ -83,7 +93,7 @@ export function historyTools(projectRoot: string) {
83
93
  };
84
94
  },
85
95
 
86
- mandu_get_snapshot: async (args: Record<string, unknown>) => {
96
+ "mandu.history.snapshot": async (args: Record<string, unknown>) => {
87
97
  const { snapshotId } = args as { snapshotId: string };
88
98
 
89
99
  const snapshot = await readSnapshotById(projectRoot, snapshotId);
@@ -117,7 +127,7 @@ export function historyTools(projectRoot: string) {
117
127
  };
118
128
  },
119
129
 
120
- mandu_prune_history: async (args: Record<string, unknown>) => {
130
+ "mandu.history.prune": async (args: Record<string, unknown>) => {
121
131
  const { keepCount = 5 } = args as { keepCount?: number };
122
132
 
123
133
  const deletedIds = await pruneHistory(projectRoot, keepCount);
@@ -135,4 +145,11 @@ export function historyTools(projectRoot: string) {
135
145
  };
136
146
  },
137
147
  };
148
+
149
+ // Backward-compatible aliases (deprecated)
150
+ handlers["mandu_list_history"] = handlers["mandu.history.list"];
151
+ handlers["mandu_get_snapshot"] = handlers["mandu.history.snapshot"];
152
+ handlers["mandu_prune_history"] = handlers["mandu.history.prune"];
153
+
154
+ return handlers;
138
155
  }
@@ -15,9 +15,14 @@ import path from "path";
15
15
 
16
16
  export const hydrationToolDefinitions: Tool[] = [
17
17
  {
18
- name: "mandu_build",
18
+ name: "mandu.build",
19
19
  description:
20
20
  "Build client bundles for hydration. Compiles client slots (.client.ts) into browser-ready JavaScript bundles.",
21
+ annotations: {
22
+ destructiveHint: true,
23
+ readOnlyHint: false,
24
+ idempotentHint: true,
25
+ },
21
26
  inputSchema: {
22
27
  type: "object",
23
28
  properties: {
@@ -40,9 +45,12 @@ export const hydrationToolDefinitions: Tool[] = [
40
45
  },
41
46
  },
42
47
  {
43
- name: "mandu_build_status",
48
+ name: "mandu.build.status",
44
49
  description:
45
50
  "Get the current build status, bundle manifest, and statistics for client bundles.",
51
+ annotations: {
52
+ readOnlyHint: true,
53
+ },
46
54
  inputSchema: {
47
55
  type: "object",
48
56
  properties: {},
@@ -50,9 +58,12 @@ export const hydrationToolDefinitions: Tool[] = [
50
58
  },
51
59
  },
52
60
  {
53
- name: "mandu_list_islands",
61
+ name: "mandu.island.list",
54
62
  description:
55
63
  "List all routes that have client-side hydration (islands). Shows hydration strategy and priority for each.",
64
+ annotations: {
65
+ readOnlyHint: true,
66
+ },
56
67
  inputSchema: {
57
68
  type: "object",
58
69
  properties: {},
@@ -60,9 +71,12 @@ export const hydrationToolDefinitions: Tool[] = [
60
71
  },
61
72
  },
62
73
  {
63
- name: "mandu_set_hydration",
74
+ name: "mandu.hydration.set",
64
75
  description:
65
76
  "Set hydration configuration for a specific route. Updates the route's hydration strategy and priority.",
77
+ annotations: {
78
+ readOnlyHint: false,
79
+ },
66
80
  inputSchema: {
67
81
  type: "object",
68
82
  properties: {
@@ -91,9 +105,13 @@ export const hydrationToolDefinitions: Tool[] = [
91
105
  },
92
106
  },
93
107
  {
94
- name: "mandu_add_client_slot",
108
+ name: "mandu.hydration.addClientSlot",
95
109
  description:
96
110
  "Add a client slot file for a route to enable hydration. Creates the .client.ts file and updates the manifest.",
111
+ annotations: {
112
+ destructiveHint: false,
113
+ readOnlyHint: false,
114
+ },
97
115
  inputSchema: {
98
116
  type: "object",
99
117
  properties: {
@@ -120,8 +138,8 @@ export const hydrationToolDefinitions: Tool[] = [
120
138
  export function hydrationTools(projectRoot: string) {
121
139
  const paths = getProjectPaths(projectRoot);
122
140
 
123
- return {
124
- mandu_build: async (args: Record<string, unknown>) => {
141
+ const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
142
+ "mandu.build": async (args: Record<string, unknown>) => {
125
143
  const { minify, sourcemap, targetRouteIds } = args as {
126
144
  minify?: boolean;
127
145
  sourcemap?: boolean;
@@ -163,7 +181,7 @@ export function hydrationTools(projectRoot: string) {
163
181
  };
164
182
  },
165
183
 
166
- mandu_build_status: async () => {
184
+ "mandu.build.status": async () => {
167
185
  // Read bundle manifest
168
186
  const manifestPath = path.join(projectRoot, ".mandu/manifest.json");
169
187
  const manifest = await readJsonFile<BundleManifest>(manifestPath);
@@ -171,7 +189,7 @@ export function hydrationTools(projectRoot: string) {
171
189
  if (!manifest) {
172
190
  return {
173
191
  hasBundles: false,
174
- message: "No bundle manifest found. Run mandu_build first.",
192
+ message: "No bundle manifest found. Run mandu.build first.",
175
193
  };
176
194
  }
177
195
 
@@ -197,7 +215,7 @@ export function hydrationTools(projectRoot: string) {
197
215
  };
198
216
  },
199
217
 
200
- mandu_list_islands: async () => {
218
+ "mandu.island.list": async () => {
201
219
  // Load manifest
202
220
  const manifestResult = await loadManifest(paths.manifestPath);
203
221
  if (!manifestResult.success || !manifestResult.data) {
@@ -236,7 +254,7 @@ export function hydrationTools(projectRoot: string) {
236
254
  };
237
255
  },
238
256
 
239
- mandu_set_hydration: async (args: Record<string, unknown>) => {
257
+ "mandu.hydration.set": async (args: Record<string, unknown>) => {
240
258
  const { routeId, strategy, priority, preload } = args as {
241
259
  routeId: string;
242
260
  strategy?: SpecHydrationStrategy;
@@ -295,7 +313,7 @@ export function hydrationTools(projectRoot: string) {
295
313
  };
296
314
  },
297
315
 
298
- mandu_add_client_slot: async (args: Record<string, unknown>) => {
316
+ "mandu.hydration.addClientSlot": async (args: Record<string, unknown>) => {
299
317
  const { routeId, strategy = "island", priority = "visible" } = args as {
300
318
  routeId: string;
301
319
  strategy?: SpecHydrationStrategy;
@@ -370,12 +388,21 @@ export function hydrationTools(projectRoot: string) {
370
388
  message: `Created client slot: ${clientModulePath}`,
371
389
  nextSteps: [
372
390
  `Edit ${clientModulePath} to add client-side logic`,
373
- `Run mandu_build to compile the client bundle`,
391
+ `Run mandu.build to compile the client bundle`,
374
392
  `The page will now hydrate in the browser`,
375
393
  ],
376
394
  };
377
395
  },
378
396
  };
397
+
398
+ // Backward-compatible aliases (deprecated)
399
+ handlers["mandu_build"] = handlers["mandu.build"];
400
+ handlers["mandu_build_status"] = handlers["mandu.build.status"];
401
+ handlers["mandu_list_islands"] = handlers["mandu.island.list"];
402
+ handlers["mandu_set_hydration"] = handlers["mandu.hydration.set"];
403
+ handlers["mandu_add_client_slot"] = handlers["mandu.hydration.addClientSlot"];
404
+
405
+ return handlers;
379
406
  }
380
407
 
381
408
  /**
@@ -9,6 +9,7 @@ import type { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
9
  import type { ActivityMonitor } from "../activity-monitor.js";
10
10
  import { mcpToolRegistry } from "../registry/mcp-tool-registry.js";
11
11
  import { moduleToPlugins } from "../adapters/tool-adapter.js";
12
+ import { type McpProfile, getProfileCategories } from "../profiles.js";
12
13
 
13
14
  // 도구 모듈 export
14
15
  export { specTools, specToolDefinitions } from "./spec.js";
@@ -16,16 +17,21 @@ export { generateTools, generateToolDefinitions } from "./generate.js";
16
17
  export { transactionTools, transactionToolDefinitions } from "./transaction.js";
17
18
  export { historyTools, historyToolDefinitions } from "./history.js";
18
19
  export { guardTools, guardToolDefinitions } from "./guard.js";
20
+ export { decisionTools, decisionToolDefinitions } from "./decisions.js";
21
+ export { negotiateTools, negotiateToolDefinitions } from "./negotiate.js";
22
+ export { slotValidationTools, slotValidationToolDefinitions } from "./slot-validation.js";
19
23
  export { slotTools, slotToolDefinitions } from "./slot.js";
20
24
  export { hydrationTools, hydrationToolDefinitions } from "./hydration.js";
21
25
  export { contractTools, contractToolDefinitions } from "./contract.js";
22
26
  export { brainTools, brainToolDefinitions } from "./brain.js";
23
27
  export { runtimeTools, runtimeToolDefinitions } from "./runtime.js";
24
28
  export { seoTools, seoToolDefinitions } from "./seo.js";
25
- export { projectTools, projectToolDefinitions } from "./project.js";
29
+ export { projectTools, projectToolDefinitions, getDevServerState } from "./project.js";
26
30
  export { ateTools, ateToolDefinitions } from "./ate.js";
27
31
  export { resourceTools, resourceToolDefinitions } from "./resource.js";
28
32
  export { componentTools, componentToolDefinitions } from "./component.js";
33
+ export { kitchenTools, kitchenToolDefinitions } from "./kitchen.js";
34
+ export { compositeTools, compositeToolDefinitions } from "./composite.js";
29
35
 
30
36
  // 도구 모듈 import (등록용)
31
37
  import { specTools, specToolDefinitions } from "./spec.js";
@@ -33,6 +39,9 @@ import { generateTools, generateToolDefinitions } from "./generate.js";
33
39
  import { transactionTools, transactionToolDefinitions } from "./transaction.js";
34
40
  import { historyTools, historyToolDefinitions } from "./history.js";
35
41
  import { guardTools, guardToolDefinitions } from "./guard.js";
42
+ import { decisionTools, decisionToolDefinitions } from "./decisions.js";
43
+ import { negotiateTools, negotiateToolDefinitions } from "./negotiate.js";
44
+ import { slotValidationTools, slotValidationToolDefinitions } from "./slot-validation.js";
36
45
  import { slotTools, slotToolDefinitions } from "./slot.js";
37
46
  import { hydrationTools, hydrationToolDefinitions } from "./hydration.js";
38
47
  import { contractTools, contractToolDefinitions } from "./contract.js";
@@ -43,6 +52,8 @@ import { projectTools, projectToolDefinitions } from "./project.js";
43
52
  import { ateTools, ateToolDefinitions } from "./ate.js";
44
53
  import { resourceTools, resourceToolDefinitions } from "./resource.js";
45
54
  import { componentTools, componentToolDefinitions } from "./component.js";
55
+ import { kitchenTools, kitchenToolDefinitions } from "./kitchen.js";
56
+ import { compositeTools, compositeToolDefinitions } from "./composite.js";
46
57
 
47
58
  /**
48
59
  * 도구 모듈 정보
@@ -67,6 +78,9 @@ const TOOL_MODULES: ToolModule[] = [
67
78
  { category: "transaction", definitions: transactionToolDefinitions, handlers: transactionTools },
68
79
  { category: "history", definitions: historyToolDefinitions, handlers: historyTools },
69
80
  { category: "guard", definitions: guardToolDefinitions, handlers: guardTools },
81
+ { category: "decisions", definitions: decisionToolDefinitions, handlers: decisionTools },
82
+ { category: "negotiate", definitions: negotiateToolDefinitions, handlers: negotiateTools },
83
+ { category: "slot-validation", definitions: slotValidationToolDefinitions, handlers: slotValidationTools },
70
84
  { category: "slot", definitions: slotToolDefinitions, handlers: slotTools },
71
85
  { category: "hydration", definitions: hydrationToolDefinitions, handlers: hydrationTools },
72
86
  { category: "contract", definitions: contractToolDefinitions, handlers: contractTools },
@@ -77,6 +91,8 @@ const TOOL_MODULES: ToolModule[] = [
77
91
  { category: "ate", definitions: ateToolDefinitions, handlers: ateTools as ToolModule["handlers"] },
78
92
  { category: "resource", definitions: resourceToolDefinitions, handlers: resourceTools },
79
93
  { category: "component", definitions: componentToolDefinitions, handlers: componentTools },
94
+ { category: "kitchen", definitions: kitchenToolDefinitions, handlers: kitchenTools },
95
+ { category: "composite", definitions: compositeToolDefinitions, handlers: compositeTools },
80
96
  ];
81
97
 
82
98
  /**
@@ -98,9 +114,19 @@ const TOOL_MODULES: ToolModule[] = [
98
114
  export function registerBuiltinTools(
99
115
  projectRoot: string,
100
116
  server?: Server,
101
- monitor?: ActivityMonitor
117
+ monitor?: ActivityMonitor,
118
+ options?: { profile?: McpProfile }
102
119
  ): void {
120
+ const allowedCategories = options?.profile
121
+ ? getProfileCategories(options.profile)
122
+ : null;
123
+
103
124
  for (const module of TOOL_MODULES) {
125
+ // Profile filtering: skip categories not in the allowed list
126
+ if (allowedCategories && !allowedCategories.includes(module.category)) {
127
+ continue;
128
+ }
129
+
104
130
  // Server가 필요한 모듈은 Server가 있을 때만 등록
105
131
  if (module.requiresServer && !server) {
106
132
  continue;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Mandu MCP Kitchen Tools
3
+ * Bridge between Kitchen DevTools (browser) and MCP protocol.
4
+ * Enables any MCP-compatible agent to read client-side errors in real-time.
5
+ */
6
+
7
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
8
+ import { loadManduConfig } from "@mandujs/core";
9
+ import { getDevServerState } from "./project.js";
10
+
11
+ export const kitchenToolDefinitions: Tool[] = [
12
+ {
13
+ name: "mandu.kitchen.errors",
14
+ description:
15
+ "Read client-side errors captured by Kitchen DevTools. Use clear=true to clear after reading.",
16
+ annotations: {
17
+ readOnlyHint: true,
18
+ },
19
+ inputSchema: {
20
+ type: "object",
21
+ properties: {
22
+ clear: {
23
+ type: "boolean",
24
+ description: "Clear errors after reading (default: false)",
25
+ },
26
+ },
27
+ required: [],
28
+ },
29
+ },
30
+ ];
31
+
32
+ export function kitchenTools(projectRoot: string) {
33
+ const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
34
+ "mandu.kitchen.errors": async (args: Record<string, unknown>) => {
35
+ const { clear = false } = args as { clear?: boolean };
36
+
37
+ // Detect port from the running dev server output first, then fall back to config
38
+ let port: number | undefined;
39
+
40
+ const serverState = getDevServerState();
41
+ if (serverState) {
42
+ // Parse the actual port from dev server stdout (e.g. "http://localhost:3333")
43
+ for (const line of serverState.output) {
44
+ const portMatch = line.match(/https?:\/\/localhost:(\d+)/);
45
+ if (portMatch) {
46
+ port = parseInt(portMatch[1], 10);
47
+ }
48
+ }
49
+ }
50
+
51
+ // Fall back to config if we couldn't detect from running server
52
+ if (!port) {
53
+ const config = await loadManduConfig(projectRoot);
54
+ port = config.server?.port ?? 3333;
55
+ }
56
+
57
+ const baseUrl = `http://localhost:${port}`;
58
+
59
+ try {
60
+ // Fetch errors from Kitchen API
61
+ const res = await fetch(`${baseUrl}/__kitchen/api/errors`);
62
+ if (!res.ok) {
63
+ return {
64
+ success: false,
65
+ message: `Dev server not reachable at ${baseUrl}. Is 'mandu dev' running?`,
66
+ errors: [],
67
+ };
68
+ }
69
+
70
+ const data = await res.json() as { errors: unknown[]; count: number };
71
+
72
+ // Clear if requested
73
+ if (clear && data.count > 0) {
74
+ await fetch(`${baseUrl}/__kitchen/api/errors`, { method: "DELETE" });
75
+ }
76
+
77
+ if (data.count === 0) {
78
+ return {
79
+ success: true,
80
+ message: "No client-side errors detected.",
81
+ errors: [],
82
+ count: 0,
83
+ };
84
+ }
85
+
86
+ return {
87
+ success: true,
88
+ message: `${data.count} client-side error(s) captured.${clear ? " Errors cleared." : ""}`,
89
+ errors: data.errors,
90
+ count: data.count,
91
+ relatedSkills: ["mandu-debug"],
92
+ };
93
+ } catch {
94
+ return {
95
+ success: false,
96
+ message: `Cannot connect to dev server at ${baseUrl}. Make sure 'mandu dev' is running.`,
97
+ errors: [],
98
+ };
99
+ }
100
+ },
101
+ };
102
+
103
+ // Backward-compatible alias
104
+ handlers["mandu_kitchen_errors"] = handlers["mandu.kitchen.errors"];
105
+
106
+ return handlers;
107
+ }
@@ -0,0 +1,263 @@
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import {
3
+ negotiate,
4
+ generateScaffold,
5
+ analyzeExistingStructure,
6
+ type NegotiationRequest,
7
+ type FeatureCategory,
8
+ type GuardPreset,
9
+ } from "@mandujs/core";
10
+
11
+ export const negotiateToolDefinitions: Tool[] = [
12
+ {
13
+ name: "mandu.negotiate",
14
+ description:
15
+ "Negotiate recommended structure and file templates before implementing a feature. " +
16
+ "IMPORTANT: featureName must be a short English kebab-case slug.",
17
+ annotations: {
18
+ readOnlyHint: true,
19
+ },
20
+ inputSchema: {
21
+ type: "object",
22
+ properties: {
23
+ intent: {
24
+ type: "string",
25
+ description: "What you want to implement, in any language (e.g., '사용자 인증 기능 추가', 'Add payment integration')",
26
+ },
27
+ featureName: {
28
+ type: "string",
29
+ description: "REQUIRED: Short English slug for the feature name (e.g., 'chat', 'user-auth', 'payment', 'file-upload'). " +
30
+ "You MUST translate the user's intent to a concise English identifier. " +
31
+ "Use lowercase kebab-case. This becomes the directory/module name.",
32
+ },
33
+ requirements: {
34
+ type: "array",
35
+ items: { type: "string" },
36
+ description: "Specific requirements (e.g., ['JWT-based', 'OAuth support'])",
37
+ },
38
+ constraints: {
39
+ type: "array",
40
+ items: { type: "string" },
41
+ description: "Constraints to respect (e.g., ['use existing User model', 'Redis sessions'])",
42
+ },
43
+ category: {
44
+ type: "string",
45
+ enum: ["auth", "crud", "api", "ui", "integration", "data", "util", "config", "other"],
46
+ description: "Feature category (auto-detected if not specified)",
47
+ },
48
+ preset: {
49
+ type: "string",
50
+ enum: ["fsd", "clean", "hexagonal", "atomic", "cqrs", "mandu"],
51
+ description: "Architecture preset (default: mandu). Use 'cqrs' for Command/Query separation.",
52
+ },
53
+ },
54
+ required: ["intent"],
55
+ },
56
+ },
57
+ {
58
+ name: "mandu.negotiate.scaffold",
59
+ description:
60
+ "Generate scaffold files from a negotiation plan. Use after mandu.negotiate.",
61
+ annotations: {
62
+ destructiveHint: true,
63
+ readOnlyHint: false,
64
+ },
65
+ inputSchema: {
66
+ type: "object",
67
+ properties: {
68
+ intent: {
69
+ type: "string",
70
+ description: "Feature intent (used to get the structure plan)",
71
+ },
72
+ category: {
73
+ type: "string",
74
+ enum: ["auth", "crud", "api", "ui", "integration", "data", "util", "config", "other"],
75
+ description: "Feature category",
76
+ },
77
+ dryRun: {
78
+ type: "boolean",
79
+ description: "If true, only show what would be created without actually creating files",
80
+ },
81
+ overwrite: {
82
+ type: "boolean",
83
+ description: "If true, overwrite existing files (default: false)",
84
+ },
85
+ preset: {
86
+ type: "string",
87
+ enum: ["fsd", "clean", "hexagonal", "atomic", "cqrs", "mandu"],
88
+ description: "Architecture preset (default: mandu)",
89
+ },
90
+ },
91
+ required: ["intent"],
92
+ },
93
+ },
94
+ {
95
+ name: "mandu.negotiate.analyze",
96
+ description:
97
+ "Analyze the existing project structure and return detected layers, features, and recommendations.",
98
+ annotations: {
99
+ readOnlyHint: true,
100
+ },
101
+ inputSchema: {
102
+ type: "object",
103
+ properties: {},
104
+ required: [],
105
+ },
106
+ },
107
+ ];
108
+
109
+ export function negotiateTools(projectRoot: string) {
110
+ const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
111
+ "mandu.negotiate": async (args: Record<string, unknown>) => {
112
+ const { intent, featureName, requirements, constraints, category, preset } = args as {
113
+ intent: string;
114
+ featureName?: string;
115
+ requirements?: string[];
116
+ constraints?: string[];
117
+ category?: FeatureCategory;
118
+ preset?: GuardPreset;
119
+ };
120
+
121
+ if (!intent) {
122
+ return {
123
+ error: "Intent is required",
124
+ tip: "Describe what you want to implement (e.g., '사용자 인증 기능 추가')",
125
+ };
126
+ }
127
+
128
+ const request: NegotiationRequest = {
129
+ intent,
130
+ featureName,
131
+ requirements,
132
+ constraints,
133
+ category,
134
+ preset,
135
+ };
136
+
137
+ const result = await negotiate(request, projectRoot);
138
+
139
+ return {
140
+ approved: result.approved,
141
+ intent,
142
+ detectedCategory: category || "auto",
143
+ preset: result.preset,
144
+
145
+ // Structure summary
146
+ structure: result.structure.map((dir) => ({
147
+ path: dir.path,
148
+ purpose: dir.purpose,
149
+ layer: dir.layer,
150
+ files: dir.files.map((f) => ({
151
+ name: f.name,
152
+ purpose: f.purpose,
153
+ isSlot: f.isSlot || false,
154
+ })),
155
+ })),
156
+
157
+ // Slots to implement
158
+ slots: result.slots,
159
+
160
+ // Context
161
+ relatedDecisions: result.relatedDecisions,
162
+ warnings: result.warnings,
163
+ recommendations: result.recommendations,
164
+
165
+ // Summary
166
+ summary: {
167
+ estimatedFiles: result.estimatedFiles,
168
+ slotsToImplement: result.slots.length,
169
+ relatedDecisionsCount: result.relatedDecisions.length,
170
+ },
171
+
172
+ // Next steps
173
+ nextSteps: result.nextSteps,
174
+ tip: "Use mandu.negotiate.scaffold to create the file structure, then implement the TODO sections.",
175
+ relatedSkills: ["mandu-create-feature", "mandu-guard-guide"],
176
+ };
177
+ },
178
+
179
+ "mandu.negotiate.scaffold": async (args: Record<string, unknown>) => {
180
+ const { intent, featureName, category, dryRun = false, overwrite = false, preset } = args as {
181
+ intent: string;
182
+ featureName?: string;
183
+ category?: FeatureCategory;
184
+ dryRun?: boolean;
185
+ overwrite?: boolean;
186
+ preset?: GuardPreset;
187
+ };
188
+
189
+ if (!intent) {
190
+ return {
191
+ error: "Intent is required",
192
+ tip: "Provide the same intent you used with mandu.negotiate",
193
+ };
194
+ }
195
+
196
+ // 먼저 협상하여 구조 계획 얻기
197
+ const plan = await negotiate({ intent, featureName, category, preset }, projectRoot);
198
+
199
+ if (!plan.approved) {
200
+ return {
201
+ error: "Negotiation not approved",
202
+ reason: plan.rejectionReason,
203
+ };
204
+ }
205
+
206
+ // Scaffold 생성
207
+ const result = await generateScaffold(plan.structure, projectRoot, {
208
+ dryRun,
209
+ overwrite,
210
+ });
211
+
212
+ return {
213
+ success: result.success,
214
+ dryRun,
215
+ created: {
216
+ directories: result.createdDirs,
217
+ files: result.createdFiles,
218
+ },
219
+ skipped: result.skippedFiles,
220
+ errors: result.errors,
221
+ summary: {
222
+ dirsCreated: result.createdDirs.length,
223
+ filesCreated: result.createdFiles.length,
224
+ filesSkipped: result.skippedFiles.length,
225
+ },
226
+ nextSteps: [
227
+ "1. Review the generated files",
228
+ "2. Implement the TODO sections in each file",
229
+ "3. Run mandu_guard_heal to verify architecture compliance",
230
+ "4. Add tests for your implementation",
231
+ ],
232
+ tip: dryRun
233
+ ? "This was a dry run. Remove dryRun: true to actually create files."
234
+ : "Files created! Start implementing the TODO sections.",
235
+ };
236
+ },
237
+
238
+ "mandu.negotiate.analyze": async () => {
239
+ const result = await analyzeExistingStructure(projectRoot);
240
+
241
+ return {
242
+ projectRoot,
243
+ detected: {
244
+ layers: result.layers,
245
+ layerCount: result.layers.length,
246
+ existingFeatures: result.existingFeatures,
247
+ featureCount: result.existingFeatures.length,
248
+ },
249
+ recommendations: result.recommendations,
250
+ tip: result.layers.length > 0
251
+ ? "Use mandu.negotiate to add new features following the existing structure."
252
+ : "Use mandu.negotiate to establish your project structure.",
253
+ };
254
+ },
255
+ };
256
+
257
+ // Backward-compatible aliases
258
+ handlers["mandu_negotiate"] = handlers["mandu.negotiate"];
259
+ handlers["mandu_generate_scaffold"] = handlers["mandu.negotiate.scaffold"];
260
+ handlers["mandu_analyze_structure"] = handlers["mandu.negotiate.analyze"];
261
+
262
+ return handlers;
263
+ }