@mandujs/mcp 0.18.8 → 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.
@@ -0,0 +1,200 @@
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import {
3
+ validateSlotConstraints,
4
+ validateSlots,
5
+ DEFAULT_SLOT_CONSTRAINTS,
6
+ API_SLOT_CONSTRAINTS,
7
+ READONLY_SLOT_CONSTRAINTS,
8
+ type SlotConstraints,
9
+ } from "@mandujs/core";
10
+
11
+ export const slotValidationToolDefinitions: Tool[] = [
12
+ {
13
+ name: "mandu.slot.validate",
14
+ description:
15
+ "Validate a slot file against semantic constraints (lines, complexity, patterns, imports).",
16
+ annotations: {
17
+ readOnlyHint: true,
18
+ },
19
+ inputSchema: {
20
+ type: "object",
21
+ properties: {
22
+ file: {
23
+ type: "string",
24
+ description: "Path to the slot file to validate",
25
+ },
26
+ preset: {
27
+ type: "string",
28
+ enum: ["default", "api", "readonly"],
29
+ description: "Constraint preset to use (default: 'default')",
30
+ },
31
+ constraints: {
32
+ type: "object",
33
+ description: "Custom constraints (overrides preset)",
34
+ properties: {
35
+ maxLines: { type: "number" },
36
+ maxCyclomaticComplexity: { type: "number" },
37
+ requiredPatterns: { type: "array", items: { type: "string" } },
38
+ forbiddenPatterns: { type: "array", items: { type: "string" } },
39
+ allowedImports: { type: "array", items: { type: "string" } },
40
+ },
41
+ },
42
+ },
43
+ required: ["file"],
44
+ },
45
+ },
46
+ {
47
+ name: "mandu.slot.constraints",
48
+ description:
49
+ "Get recommended slot constraint presets (default, api, readonly).",
50
+ annotations: {
51
+ readOnlyHint: true,
52
+ },
53
+ inputSchema: {
54
+ type: "object",
55
+ properties: {
56
+ preset: {
57
+ type: "string",
58
+ enum: ["default", "api", "readonly"],
59
+ description: "Constraint preset to retrieve",
60
+ },
61
+ },
62
+ required: [],
63
+ },
64
+ },
65
+ ];
66
+
67
+ export function slotValidationTools(projectRoot: string) {
68
+ const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
69
+ "mandu.slot.validate": async (args: Record<string, unknown>) => {
70
+ const { file, preset, constraints: customConstraints } = args as {
71
+ file: string;
72
+ preset?: "default" | "api" | "readonly";
73
+ constraints?: SlotConstraints;
74
+ };
75
+
76
+ if (!file) {
77
+ return {
78
+ error: "File path is required",
79
+ tip: "Provide the path to the slot file to validate",
80
+ };
81
+ }
82
+
83
+ // 프리셋 선택
84
+ let constraints: SlotConstraints;
85
+ if (customConstraints) {
86
+ constraints = customConstraints;
87
+ } else {
88
+ switch (preset) {
89
+ case "api":
90
+ constraints = API_SLOT_CONSTRAINTS;
91
+ break;
92
+ case "readonly":
93
+ constraints = READONLY_SLOT_CONSTRAINTS;
94
+ break;
95
+ default:
96
+ constraints = DEFAULT_SLOT_CONSTRAINTS;
97
+ }
98
+ }
99
+
100
+ // 파일 경로 정규화 및 보안 검증 (LFI 방지)
101
+ const path = await import("path");
102
+ const rawPath = file.startsWith("/") || file.includes(":")
103
+ ? file
104
+ : path.join(projectRoot, file);
105
+ const filePath = path.normalize(path.resolve(rawPath));
106
+ const normalizedRoot = path.normalize(path.resolve(projectRoot));
107
+
108
+ // 경로가 프로젝트 루트 내에 있는지 검증
109
+ if (!filePath.startsWith(normalizedRoot)) {
110
+ return {
111
+ error: "Access denied: File path is outside project root",
112
+ tip: "Only files within the project directory can be validated",
113
+ requestedPath: file,
114
+ projectRoot: projectRoot,
115
+ };
116
+ }
117
+
118
+ const result = await validateSlotConstraints(filePath, constraints);
119
+
120
+ return {
121
+ valid: result.valid,
122
+ file: result.filePath,
123
+ stats: result.stats,
124
+ violations: result.violations.map((v) => ({
125
+ type: v.type,
126
+ severity: v.severity,
127
+ message: v.message,
128
+ suggestion: v.suggestion,
129
+ line: v.line,
130
+ })),
131
+ suggestions: result.suggestions,
132
+ constraintsUsed: constraints,
133
+ tip: result.valid
134
+ ? "✅ Slot passes all constraints"
135
+ : "Fix violations before deployment. Use mandu.slot.constraints for guidance.",
136
+ };
137
+ },
138
+
139
+ "mandu.slot.constraints": async (args: Record<string, unknown>) => {
140
+ const { preset } = args as { preset?: "default" | "api" | "readonly" };
141
+
142
+ const presets = {
143
+ default: {
144
+ name: "Default",
145
+ description: "Basic constraints for general slots",
146
+ constraints: DEFAULT_SLOT_CONSTRAINTS,
147
+ },
148
+ api: {
149
+ name: "API Slot",
150
+ description: "Constraints for API handlers with validation requirements",
151
+ constraints: API_SLOT_CONSTRAINTS,
152
+ },
153
+ readonly: {
154
+ name: "Read-only Slot",
155
+ description: "Strict constraints for read-only operations (no DB writes)",
156
+ constraints: READONLY_SLOT_CONSTRAINTS,
157
+ },
158
+ };
159
+
160
+ if (preset) {
161
+ const selected = presets[preset];
162
+ return {
163
+ preset: preset,
164
+ ...selected,
165
+ usage: `
166
+ .constraints(${JSON.stringify(selected.constraints, null, 2)})
167
+ `.trim(),
168
+ };
169
+ }
170
+
171
+ return {
172
+ available: Object.entries(presets).map(([key, value]) => ({
173
+ preset: key,
174
+ name: value.name,
175
+ description: value.description,
176
+ constraints: value.constraints,
177
+ })),
178
+ tip: "Use these constraints with Mandu.filling().constraints({...}) to enforce slot rules.",
179
+ example: `
180
+ Mandu.filling()
181
+ .purpose("사용자 목록 조회 API")
182
+ .constraints({
183
+ maxLines: 50,
184
+ maxCyclomaticComplexity: 10,
185
+ requiredPatterns: ["input-validation", "error-handling"],
186
+ forbiddenPatterns: ["direct-db-write"],
187
+ allowedImports: ["server/domain/*", "shared/utils/*"],
188
+ })
189
+ .get(async (ctx) => { ... });
190
+ `.trim(),
191
+ };
192
+ },
193
+ };
194
+
195
+ // Backward-compatible aliases
196
+ handlers["mandu_validate_slot"] = handlers["mandu.slot.validate"];
197
+ handlers["mandu_get_slot_constraints"] = handlers["mandu.slot.constraints"];
198
+
199
+ return handlers;
200
+ }
package/src/tools/slot.ts CHANGED
@@ -10,14 +10,12 @@ import path from "path";
10
10
 
11
11
  export const slotToolDefinitions: Tool[] = [
12
12
  {
13
- name: "mandu_read_slot",
13
+ name: "mandu.slot.read",
14
+ annotations: {
15
+ readOnlyHint: true,
16
+ },
14
17
  description:
15
- "Read the TypeScript source of a route's slot file and validate its structure. " +
16
- "In Mandu, a 'slot' is the server-side data loader for a route: " +
17
- "it runs on every request before rendering and returns a typed object " +
18
- "that is injected into the page component as props (for pages) or as handler context (for API routes). " +
19
- "Slot files live at spec/slots/{routeId}.slot.ts and are auto-linked by generateManifest(). " +
20
- "Returns the raw source, line count, and any structural validation issues.",
18
+ "Read the TypeScript source of a route's slot file and validate its structure.",
21
19
  inputSchema: {
22
20
  type: "object",
23
21
  properties: {
@@ -30,17 +28,12 @@ export const slotToolDefinitions: Tool[] = [
30
28
  },
31
29
  },
32
30
  {
33
- name: "mandu_validate_slot",
31
+ name: "mandu.slot.validate",
32
+ annotations: {
33
+ readOnlyHint: true,
34
+ },
34
35
  description:
35
- "Validate TypeScript slot content against Mandu's structural rules without writing any files. " +
36
- "A valid slot must export a default function (or use the slot() builder) that accepts a Request " +
37
- "and returns a plain serializable object (becomes the typed props injected into the page). " +
38
- "Returns: " +
39
- "errors (must fix before use), " +
40
- "warnings (best-practice suggestions), " +
41
- "autoFixable issues (with corrected code preview), " +
42
- "manualFixRequired items (issues needing human review). " +
43
- "Use this before writing a slot file with the Edit tool to catch structural problems early.",
36
+ "Validate TypeScript slot content against structural rules without writing files. Returns errors, warnings, and auto-fix previews.",
44
37
  inputSchema: {
45
38
  type: "object",
46
39
  properties: {
@@ -57,8 +50,8 @@ export const slotToolDefinitions: Tool[] = [
57
50
  export function slotTools(projectRoot: string) {
58
51
  const paths = getProjectPaths(projectRoot);
59
52
 
60
- return {
61
- mandu_read_slot: async (args: Record<string, unknown>) => {
53
+ const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
54
+ "mandu.slot.read": async (args: Record<string, unknown>) => {
62
55
  const { routeId } = args as { routeId: string };
63
56
 
64
57
  // Load manifest to find the route
@@ -92,7 +85,7 @@ export function slotTools(projectRoot: string) {
92
85
  return {
93
86
  exists: false,
94
87
  slotPath: route.slotModule,
95
- message: "Slot file does not exist. Run mandu_generate to create it.",
88
+ message: "Slot file does not exist. Run mandu.generate to create it.",
96
89
  };
97
90
  }
98
91
 
@@ -119,7 +112,7 @@ export function slotTools(projectRoot: string) {
119
112
  }
120
113
  },
121
114
 
122
- mandu_validate_slot: async (args: Record<string, unknown>) => {
115
+ "mandu.slot.validate": async (args: Record<string, unknown>) => {
123
116
  const { content } = args as { content: string };
124
117
 
125
118
  const validation = validateSlotContent(content);
@@ -167,4 +160,10 @@ export function slotTools(projectRoot: string) {
167
160
  };
168
161
  },
169
162
  };
163
+
164
+ // Backward-compatible aliases
165
+ handlers["mandu_read_slot"] = handlers["mandu.slot.read"];
166
+ handlers["mandu_validate_slot"] = handlers["mandu.slot.validate"];
167
+
168
+ return handlers;
170
169
  }
package/src/tools/spec.ts CHANGED
@@ -2,7 +2,6 @@ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
2
  import {
3
3
  loadManifest,
4
4
  validateManifest,
5
- writeLock,
6
5
  generateManifest,
7
6
  GENERATED_RELATIVE_PATHS,
8
7
  type RouteSpec,
@@ -14,17 +13,12 @@ import fs from "fs/promises";
14
13
 
15
14
  export const specToolDefinitions: Tool[] = [
16
15
  {
17
- name: "mandu_list_routes",
16
+ name: "mandu.route.list",
18
17
  description:
19
- "List all routes registered in the Mandu project, read from .mandu/routes.manifest.json. " +
20
- "Route kinds: " +
21
- "'api' (REST endpoint — app/**/route.ts, exports named GET/POST/PUT/PATCH/DELETE handler functions), " +
22
- "'page' (SSR page — app/**/page.tsx, React component supporting client-side hydration islands). " +
23
- "Special files auto-detected by the filesystem router (not user-created routes): " +
24
- "layout.tsx (shared wrapper rendered around child routes), " +
25
- "error.tsx (error boundary for the route subtree), " +
26
- "loading.tsx (suspense fallback shown while page data loads). " +
27
- "Each route may have an associated slotModule (server data loader) and contractModule (Zod API schema).",
18
+ "List all routes from .mandu/routes.manifest.json with their kind, pattern, slotModule, and contractModule.",
19
+ annotations: {
20
+ readOnlyHint: true,
21
+ },
28
22
  inputSchema: {
29
23
  type: "object",
30
24
  properties: {},
@@ -32,34 +26,31 @@ export const specToolDefinitions: Tool[] = [
32
26
  },
33
27
  },
34
28
  {
35
- name: "mandu_get_route",
29
+ name: "mandu.route.get",
36
30
  description:
37
- "Get full details of a specific route by its ID. " +
38
- "Returns the complete route spec: kind, URL pattern, module paths (app, slot, contract, component), " +
39
- "HTTP methods, and hydration configuration (for page routes with client islands). " +
40
- "Use this before modifying a route to understand its current configuration.",
31
+ "Get full details of a specific route by its ID. Use before modifying a route.",
32
+ annotations: {
33
+ readOnlyHint: true,
34
+ },
41
35
  inputSchema: {
42
36
  type: "object",
43
37
  properties: {
44
38
  routeId: {
45
39
  type: "string",
46
- description: "The route ID to retrieve (use mandu_list_routes to see all IDs)",
40
+ description: "The route ID to retrieve (use mandu.route.list to see all IDs)",
47
41
  },
48
42
  },
49
43
  required: ["routeId"],
50
44
  },
51
45
  },
52
46
  {
53
- name: "mandu_add_route",
47
+ name: "mandu.route.add",
54
48
  description:
55
- "Scaffold a new route by creating source files in app/ and registering it in the manifest. " +
56
- "For 'api' routes: creates app/{path}/route.ts with a GET handler stub. " +
57
- "For 'page' routes: creates app/{path}/page.tsx with a React component stub. " +
58
- "withSlot=true (default): also creates spec/slots/{routeId}.slot.ts — " +
59
- "the server-side data loader that runs on every request before rendering and injects typed props into the page. " +
60
- "withContract=true: also creates spec/contracts/{routeId}.contract.ts — " +
61
- "Zod schemas for request/response validation, enabling typed handlers, OpenAPI generation, and ATE L2/L3 testing. " +
62
- "Automatically runs generateManifest() after creation to link all files.",
49
+ "Scaffold a new route in app/ with optional slot and contract files, then regenerate the manifest.",
50
+ annotations: {
51
+ destructiveHint: false,
52
+ readOnlyHint: false,
53
+ },
63
54
  inputSchema: {
64
55
  type: "object",
65
56
  properties: {
@@ -85,30 +76,31 @@ export const specToolDefinitions: Tool[] = [
85
76
  },
86
77
  },
87
78
  {
88
- name: "mandu_delete_route",
79
+ name: "mandu.route.delete",
89
80
  description:
90
- "Delete a route's app/ source file and regenerate the manifest. " +
91
- "Only removes the app/{path}/route.ts or page.tsx file — " +
92
- "slot files (spec/slots/) and contract files (spec/contracts/) are intentionally preserved, " +
93
- "as they may be reused when the route is recreated. " +
94
- "Use mandu_list_routes before deleting to confirm the correct routeId.",
81
+ "Delete a route's app/ source file and regenerate the manifest. Slot and contract files are preserved.",
82
+ annotations: {
83
+ destructiveHint: true,
84
+ readOnlyHint: false,
85
+ },
95
86
  inputSchema: {
96
87
  type: "object",
97
88
  properties: {
98
89
  routeId: {
99
90
  type: "string",
100
- description: "The route ID to delete (use mandu_list_routes to find it)",
91
+ description: "The route ID to delete (use mandu.route.list to find it)",
101
92
  },
102
93
  },
103
94
  required: ["routeId"],
104
95
  },
105
96
  },
106
97
  {
107
- name: "mandu_validate_manifest",
98
+ name: "mandu.manifest.validate",
108
99
  description:
109
- "Validate the routes manifest (.mandu/routes.manifest.json) for structural integrity. " +
110
- "Checks required fields, valid route kinds, correct module paths, and manifest schema version. " +
111
- "Run this after manual manifest edits, after upgrading Mandu, or when routes behave unexpectedly.",
100
+ "Validate the routes manifest for structural integrity. Run after manual edits or when routes behave unexpectedly.",
101
+ annotations: {
102
+ readOnlyHint: true,
103
+ },
112
104
  inputSchema: {
113
105
  type: "object",
114
106
  properties: {},
@@ -120,8 +112,8 @@ export const specToolDefinitions: Tool[] = [
120
112
  export function specTools(projectRoot: string) {
121
113
  const paths = getProjectPaths(projectRoot);
122
114
 
123
- return {
124
- mandu_list_routes: async () => {
115
+ const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
116
+ "mandu.route.list": async () => {
125
117
  const result = await loadManifest(paths.manifestPath);
126
118
  if (!result.success || !result.data) {
127
119
  return { error: result.errors };
@@ -141,7 +133,7 @@ export function specTools(projectRoot: string) {
141
133
  };
142
134
  },
143
135
 
144
- mandu_get_route: async (args: Record<string, unknown>) => {
136
+ "mandu.route.get": async (args: Record<string, unknown>) => {
145
137
  const { routeId } = args as { routeId: string };
146
138
 
147
139
  const result = await loadManifest(paths.manifestPath);
@@ -157,7 +149,7 @@ export function specTools(projectRoot: string) {
157
149
  return { route };
158
150
  },
159
151
 
160
- mandu_add_route: async (args: Record<string, unknown>) => {
152
+ "mandu.route.add": async (args: Record<string, unknown>) => {
161
153
  const { path: routePath, kind, withSlot = true, withContract = false } = args as {
162
154
  path: string;
163
155
  kind: "api" | "page";
@@ -213,10 +205,11 @@ export function specTools(projectRoot: string) {
213
205
  createdFiles,
214
206
  totalRoutes: genResult.manifest.routes.length,
215
207
  message: `Route '${routeId}' scaffolded successfully`,
208
+ relatedSkills: ["mandu-create-feature"],
216
209
  };
217
210
  },
218
211
 
219
- mandu_delete_route: async (args: Record<string, unknown>) => {
212
+ "mandu.route.delete": async (args: Record<string, unknown>) => {
220
213
  const { routeId } = args as { routeId: string };
221
214
 
222
215
  // Load current manifest to find the route
@@ -253,7 +246,7 @@ export function specTools(projectRoot: string) {
253
246
  };
254
247
  },
255
248
 
256
- mandu_validate_manifest: async () => {
249
+ "mandu.manifest.validate": async () => {
257
250
  const result = await loadManifest(paths.manifestPath);
258
251
  if (!result.success) {
259
252
  return {
@@ -269,4 +262,13 @@ export function specTools(projectRoot: string) {
269
262
  };
270
263
  },
271
264
  };
265
+
266
+ // Backward-compatible aliases (deprecated)
267
+ handlers["mandu_list_routes"] = handlers["mandu.route.list"];
268
+ handlers["mandu_get_route"] = handlers["mandu.route.get"];
269
+ handlers["mandu_add_route"] = handlers["mandu.route.add"];
270
+ handlers["mandu_delete_route"] = handlers["mandu.route.delete"];
271
+ handlers["mandu_validate_manifest"] = handlers["mandu.manifest.validate"];
272
+
273
+ return handlers;
272
274
  }
@@ -6,12 +6,16 @@ import {
6
6
  getTransactionStatus,
7
7
  hasActiveTransaction,
8
8
  } from "@mandujs/core";
9
+ import { acquireLock, releaseLock, checkLock } from "../tx-lock.js";
9
10
 
10
11
  export const transactionToolDefinitions: Tool[] = [
11
12
  {
12
- name: "mandu_begin",
13
+ name: "mandu.tx.begin",
13
14
  description:
14
15
  "Begin a new transaction. Creates a snapshot of the current spec state for safe rollback.",
16
+ annotations: {
17
+ readOnlyHint: false,
18
+ },
15
19
  inputSchema: {
16
20
  type: "object",
17
21
  properties: {
@@ -19,22 +23,35 @@ export const transactionToolDefinitions: Tool[] = [
19
23
  type: "string",
20
24
  description: "Description of the changes being made",
21
25
  },
26
+ sessionId: {
27
+ type: "string",
28
+ description: "Caller session identifier for the concurrency lock",
29
+ },
22
30
  },
23
31
  required: [],
24
32
  },
25
33
  },
26
34
  {
27
- name: "mandu_commit",
35
+ name: "mandu.tx.commit",
28
36
  description: "Commit the current transaction, finalizing all changes",
37
+ annotations: {
38
+ readOnlyHint: false,
39
+ },
29
40
  inputSchema: {
30
41
  type: "object",
31
- properties: {},
42
+ properties: {
43
+ lockId: { type: "string", description: "Lock ID returned by mandu.tx.begin" },
44
+ },
32
45
  required: [],
33
46
  },
34
47
  },
35
48
  {
36
- name: "mandu_rollback",
49
+ name: "mandu.tx.rollback",
37
50
  description: "Rollback the current transaction, restoring the previous state",
51
+ annotations: {
52
+ destructiveHint: true,
53
+ readOnlyHint: false,
54
+ },
38
55
  inputSchema: {
39
56
  type: "object",
40
57
  properties: {
@@ -42,13 +59,17 @@ export const transactionToolDefinitions: Tool[] = [
42
59
  type: "string",
43
60
  description: "Specific change ID to rollback (optional, defaults to active transaction)",
44
61
  },
62
+ lockId: { type: "string", description: "Lock ID returned by mandu.tx.begin" },
45
63
  },
46
64
  required: [],
47
65
  },
48
66
  },
49
67
  {
50
- name: "mandu_tx_status",
68
+ name: "mandu.tx.status",
51
69
  description: "Get the current transaction status",
70
+ annotations: {
71
+ readOnlyHint: true,
72
+ },
52
73
  inputSchema: {
53
74
  type: "object",
54
75
  properties: {},
@@ -58,9 +79,9 @@ export const transactionToolDefinitions: Tool[] = [
58
79
  ];
59
80
 
60
81
  export function transactionTools(projectRoot: string) {
61
- return {
62
- mandu_begin: async (args: Record<string, unknown>) => {
63
- const { message } = args as { message?: string };
82
+ const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
83
+ "mandu.tx.begin": async (args: Record<string, unknown>) => {
84
+ const { message, sessionId } = args as { message?: string; sessionId?: string };
64
85
 
65
86
  // Check if there's already an active transaction
66
87
  const isActive = await hasActiveTransaction(projectRoot);
@@ -72,6 +93,12 @@ export function transactionTools(projectRoot: string) {
72
93
  };
73
94
  }
74
95
 
96
+ // Acquire concurrency lock
97
+ const lock = acquireLock(sessionId || "anonymous");
98
+ if (!lock.success) {
99
+ return { error: lock.error };
100
+ }
101
+
75
102
  const change = await beginChange(projectRoot, {
76
103
  message: message || "MCP transaction",
77
104
  });
@@ -79,14 +106,16 @@ export function transactionTools(projectRoot: string) {
79
106
  return {
80
107
  success: true,
81
108
  changeId: change.id,
109
+ lockId: lock.lockId,
82
110
  snapshotId: change.snapshotId,
83
111
  message: change.message,
84
112
  createdAt: change.createdAt,
85
- tip: "Use mandu_commit to finalize or mandu_rollback to revert changes",
113
+ tip: "Use mandu.tx.commit to finalize or mandu.tx.rollback to revert changes. Pass lockId to subsequent calls.",
86
114
  };
87
115
  },
88
116
 
89
- mandu_commit: async () => {
117
+ "mandu.tx.commit": async (args: Record<string, unknown>) => {
118
+ const { lockId } = args as { lockId?: string };
90
119
  const isActive = await hasActiveTransaction(projectRoot);
91
120
  if (!isActive) {
92
121
  return {
@@ -95,6 +124,7 @@ export function transactionTools(projectRoot: string) {
95
124
  }
96
125
 
97
126
  const result = await commitChange(projectRoot);
127
+ if (lockId) releaseLock(lockId);
98
128
 
99
129
  return {
100
130
  success: result.success,
@@ -103,8 +133,8 @@ export function transactionTools(projectRoot: string) {
103
133
  };
104
134
  },
105
135
 
106
- mandu_rollback: async (args: Record<string, unknown>) => {
107
- const { changeId } = args as { changeId?: string };
136
+ "mandu.tx.rollback": async (args: Record<string, unknown>) => {
137
+ const { changeId, lockId } = args as { changeId?: string; lockId?: string };
108
138
 
109
139
  const isActive = await hasActiveTransaction(projectRoot);
110
140
  if (!isActive && !changeId) {
@@ -114,6 +144,7 @@ export function transactionTools(projectRoot: string) {
114
144
  }
115
145
 
116
146
  const result = await rollbackChange(projectRoot, changeId);
147
+ if (lockId) releaseLock(lockId);
117
148
 
118
149
  return {
119
150
  success: result.success,
@@ -126,13 +157,15 @@ export function transactionTools(projectRoot: string) {
126
157
  };
127
158
  },
128
159
 
129
- mandu_tx_status: async () => {
160
+ "mandu.tx.status": async () => {
130
161
  const { state, change } = await getTransactionStatus(projectRoot);
162
+ const lock = checkLock();
131
163
 
132
164
  if (!state.active) {
133
165
  return {
134
166
  hasActiveTransaction: false,
135
167
  message: "No active transaction",
168
+ lock,
136
169
  };
137
170
  }
138
171
 
@@ -140,6 +173,7 @@ export function transactionTools(projectRoot: string) {
140
173
  hasActiveTransaction: true,
141
174
  changeId: state.changeId,
142
175
  snapshotId: state.snapshotId,
176
+ lock,
143
177
  change: change
144
178
  ? {
145
179
  id: change.id,
@@ -151,4 +185,12 @@ export function transactionTools(projectRoot: string) {
151
185
  };
152
186
  },
153
187
  };
188
+
189
+ // Backward-compatible aliases (deprecated)
190
+ handlers["mandu_begin"] = handlers["mandu.tx.begin"];
191
+ handlers["mandu_commit"] = handlers["mandu.tx.commit"];
192
+ handlers["mandu_rollback"] = handlers["mandu.tx.rollback"];
193
+ handlers["mandu_tx_status"] = handlers["mandu.tx.status"];
194
+
195
+ return handlers;
154
196
  }