@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.
- package/README.md +0 -1
- package/package.json +2 -2
- package/src/activity-adapter.ts +23 -0
- package/src/activity-monitor.ts +9 -8
- package/src/adapters/tool-adapter.ts +2 -0
- package/src/executor/error-handler.ts +268 -250
- package/src/index.ts +8 -0
- package/src/new-resources.ts +119 -0
- package/src/profiles.ts +34 -0
- package/src/prompts.ts +104 -0
- package/src/resources/handlers.ts +0 -23
- package/src/server.ts +78 -5
- package/src/tools/ate.ts +28 -0
- package/src/tools/brain.ts +56 -24
- package/src/tools/component.ts +194 -185
- package/src/tools/composite.ts +440 -0
- package/src/tools/contract.ts +58 -58
- package/src/tools/decisions.ts +270 -0
- package/src/tools/generate.ts +23 -21
- package/src/tools/guard.ts +32 -708
- package/src/tools/history.ts +24 -7
- package/src/tools/hydration.ts +40 -13
- package/src/tools/index.ts +28 -2
- package/src/tools/kitchen.ts +107 -0
- package/src/tools/negotiate.ts +263 -0
- package/src/tools/project.ts +464 -382
- package/src/tools/resource.ts +19 -2
- package/src/tools/runtime.ts +533 -508
- package/src/tools/seo.ts +446 -417
- package/src/tools/slot-validation.ts +200 -0
- package/src/tools/slot.ts +20 -25
- package/src/tools/spec.ts +45 -43
- package/src/tools/transaction.ts +55 -13
- package/src/tx-lock.ts +73 -0
- package/src/utils/project.ts +48 -9
- package/src/utils/runtime-control.ts +52 -0
- package/src/utils/withWarnings.ts +2 -1
|
@@ -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,18 +10,12 @@ import path from "path";
|
|
|
10
10
|
|
|
11
11
|
export const slotToolDefinitions: Tool[] = [
|
|
12
12
|
{
|
|
13
|
-
name: "
|
|
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
|
-
"The loader receives a ManduContext (ctx) with access to ctx.cookies for reading/setting cookies — " +
|
|
20
|
-
"cookies set in the loader are automatically applied to the SSR Response via Set-Cookie headers. " +
|
|
21
|
-
"Advanced: ctx.cookies.getSigned(name, secret) for HMAC-SHA256 signed cookies, " +
|
|
22
|
-
"ctx.cookies.getParsed(name, zodSchema) for Zod-validated JSON cookies. " +
|
|
23
|
-
"Slot files live at spec/slots/{routeId}.slot.ts and are auto-linked by generateManifest(). " +
|
|
24
|
-
"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.",
|
|
25
19
|
inputSchema: {
|
|
26
20
|
type: "object",
|
|
27
21
|
properties: {
|
|
@@ -34,17 +28,12 @@ export const slotToolDefinitions: Tool[] = [
|
|
|
34
28
|
},
|
|
35
29
|
},
|
|
36
30
|
{
|
|
37
|
-
name: "
|
|
31
|
+
name: "mandu.slot.validate",
|
|
32
|
+
annotations: {
|
|
33
|
+
readOnlyHint: true,
|
|
34
|
+
},
|
|
38
35
|
description:
|
|
39
|
-
"Validate TypeScript slot content against
|
|
40
|
-
"A valid slot must export a default function (or use the slot() builder) that accepts a Request " +
|
|
41
|
-
"and returns a plain serializable object (becomes the typed props injected into the page). " +
|
|
42
|
-
"Returns: " +
|
|
43
|
-
"errors (must fix before use), " +
|
|
44
|
-
"warnings (best-practice suggestions), " +
|
|
45
|
-
"autoFixable issues (with corrected code preview), " +
|
|
46
|
-
"manualFixRequired items (issues needing human review). " +
|
|
47
|
-
"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.",
|
|
48
37
|
inputSchema: {
|
|
49
38
|
type: "object",
|
|
50
39
|
properties: {
|
|
@@ -61,8 +50,8 @@ export const slotToolDefinitions: Tool[] = [
|
|
|
61
50
|
export function slotTools(projectRoot: string) {
|
|
62
51
|
const paths = getProjectPaths(projectRoot);
|
|
63
52
|
|
|
64
|
-
|
|
65
|
-
|
|
53
|
+
const handlers: Record<string, (args: Record<string, unknown>) => Promise<unknown>> = {
|
|
54
|
+
"mandu.slot.read": async (args: Record<string, unknown>) => {
|
|
66
55
|
const { routeId } = args as { routeId: string };
|
|
67
56
|
|
|
68
57
|
// Load manifest to find the route
|
|
@@ -96,7 +85,7 @@ export function slotTools(projectRoot: string) {
|
|
|
96
85
|
return {
|
|
97
86
|
exists: false,
|
|
98
87
|
slotPath: route.slotModule,
|
|
99
|
-
message: "Slot file does not exist. Run
|
|
88
|
+
message: "Slot file does not exist. Run mandu.generate to create it.",
|
|
100
89
|
};
|
|
101
90
|
}
|
|
102
91
|
|
|
@@ -123,7 +112,7 @@ export function slotTools(projectRoot: string) {
|
|
|
123
112
|
}
|
|
124
113
|
},
|
|
125
114
|
|
|
126
|
-
|
|
115
|
+
"mandu.slot.validate": async (args: Record<string, unknown>) => {
|
|
127
116
|
const { content } = args as { content: string };
|
|
128
117
|
|
|
129
118
|
const validation = validateSlotContent(content);
|
|
@@ -171,4 +160,10 @@ export function slotTools(projectRoot: string) {
|
|
|
171
160
|
};
|
|
172
161
|
},
|
|
173
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;
|
|
174
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: "
|
|
16
|
+
name: "mandu.route.list",
|
|
18
17
|
description:
|
|
19
|
-
"List all routes
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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: "
|
|
29
|
+
name: "mandu.route.get",
|
|
36
30
|
description:
|
|
37
|
-
"Get full details of a specific route by its ID. "
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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: "
|
|
47
|
+
name: "mandu.route.add",
|
|
54
48
|
description:
|
|
55
|
-
"Scaffold a new route
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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: "
|
|
79
|
+
name: "mandu.route.delete",
|
|
89
80
|
description:
|
|
90
|
-
"Delete a route's app/ source file and regenerate the manifest. "
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
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: "
|
|
98
|
+
name: "mandu.manifest.validate",
|
|
108
99
|
description:
|
|
109
|
-
"Validate the routes manifest
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/tools/transaction.ts
CHANGED
|
@@ -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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
-
|
|
62
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|