@mandujs/mcp 0.4.2 → 0.5.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/package.json +2 -2
- package/src/index.ts +0 -0
- package/src/server.ts +3 -0
- package/src/tools/contract.ts +544 -0
- package/src/tools/index.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Mandu MCP Server - Agent-native interface for Mandu framework operations",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"access": "public"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
|
-
"@mandujs/core": "^0.
|
|
35
|
+
"@mandujs/core": "^0.5.0",
|
|
36
36
|
"@modelcontextprotocol/sdk": "^1.25.3"
|
|
37
37
|
},
|
|
38
38
|
"engines": {
|
package/src/index.ts
CHANGED
|
File without changes
|
package/src/server.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { historyTools, historyToolDefinitions } from "./tools/history.js";
|
|
|
16
16
|
import { guardTools, guardToolDefinitions } from "./tools/guard.js";
|
|
17
17
|
import { slotTools, slotToolDefinitions } from "./tools/slot.js";
|
|
18
18
|
import { hydrationTools, hydrationToolDefinitions } from "./tools/hydration.js";
|
|
19
|
+
import { contractTools, contractToolDefinitions } from "./tools/contract.js";
|
|
19
20
|
import { resourceHandlers, resourceDefinitions } from "./resources/handlers.js";
|
|
20
21
|
import { findProjectRoot } from "./utils/project.js";
|
|
21
22
|
|
|
@@ -51,6 +52,7 @@ export class ManduMcpServer {
|
|
|
51
52
|
...guardToolDefinitions,
|
|
52
53
|
...slotToolDefinitions,
|
|
53
54
|
...hydrationToolDefinitions,
|
|
55
|
+
...contractToolDefinitions,
|
|
54
56
|
];
|
|
55
57
|
}
|
|
56
58
|
|
|
@@ -63,6 +65,7 @@ export class ManduMcpServer {
|
|
|
63
65
|
...guardTools(this.projectRoot),
|
|
64
66
|
...slotTools(this.projectRoot),
|
|
65
67
|
...hydrationTools(this.projectRoot),
|
|
68
|
+
...contractTools(this.projectRoot),
|
|
66
69
|
};
|
|
67
70
|
}
|
|
68
71
|
|
|
@@ -0,0 +1,544 @@
|
|
|
1
|
+
import type { Tool } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import {
|
|
3
|
+
loadManifest,
|
|
4
|
+
writeLock,
|
|
5
|
+
runContractGuardCheck,
|
|
6
|
+
generateContractTemplate,
|
|
7
|
+
generateOpenAPIDocument,
|
|
8
|
+
openAPIToJSON,
|
|
9
|
+
type RouteSpec,
|
|
10
|
+
type RoutesManifest,
|
|
11
|
+
} from "@mandujs/core";
|
|
12
|
+
import { getProjectPaths, readJsonFile, writeJsonFile } from "../utils/project.js";
|
|
13
|
+
import path from "path";
|
|
14
|
+
import fs from "fs/promises";
|
|
15
|
+
|
|
16
|
+
export const contractToolDefinitions: Tool[] = [
|
|
17
|
+
{
|
|
18
|
+
name: "mandu_list_contracts",
|
|
19
|
+
description: "List all contracts in the current Mandu project",
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: "object",
|
|
22
|
+
properties: {},
|
|
23
|
+
required: [],
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "mandu_get_contract",
|
|
28
|
+
description: "Get details of a specific contract by route ID",
|
|
29
|
+
inputSchema: {
|
|
30
|
+
type: "object",
|
|
31
|
+
properties: {
|
|
32
|
+
routeId: {
|
|
33
|
+
type: "string",
|
|
34
|
+
description: "The route ID to retrieve contract for",
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
required: ["routeId"],
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: "mandu_create_contract",
|
|
42
|
+
description: "Create a new contract for a route",
|
|
43
|
+
inputSchema: {
|
|
44
|
+
type: "object",
|
|
45
|
+
properties: {
|
|
46
|
+
routeId: {
|
|
47
|
+
type: "string",
|
|
48
|
+
description: "The route ID to create contract for",
|
|
49
|
+
},
|
|
50
|
+
description: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: "API description (optional)",
|
|
53
|
+
},
|
|
54
|
+
methods: {
|
|
55
|
+
type: "array",
|
|
56
|
+
items: { type: "string" },
|
|
57
|
+
description: "HTTP methods to include (default: route methods or ['GET', 'POST'])",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
required: ["routeId"],
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "mandu_update_route_contract",
|
|
65
|
+
description: "Update a route to add or change its contract module",
|
|
66
|
+
inputSchema: {
|
|
67
|
+
type: "object",
|
|
68
|
+
properties: {
|
|
69
|
+
routeId: {
|
|
70
|
+
type: "string",
|
|
71
|
+
description: "The route ID to update",
|
|
72
|
+
},
|
|
73
|
+
contractModule: {
|
|
74
|
+
type: "string",
|
|
75
|
+
description: "Path to contract file (e.g., spec/contracts/users.contract.ts)",
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
required: ["routeId", "contractModule"],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "mandu_validate_contracts",
|
|
83
|
+
description: "Validate all contracts against their slot implementations",
|
|
84
|
+
inputSchema: {
|
|
85
|
+
type: "object",
|
|
86
|
+
properties: {},
|
|
87
|
+
required: [],
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
name: "mandu_sync_contract_slot",
|
|
92
|
+
description: "Sync contract and slot to resolve mismatches",
|
|
93
|
+
inputSchema: {
|
|
94
|
+
type: "object",
|
|
95
|
+
properties: {
|
|
96
|
+
routeId: {
|
|
97
|
+
type: "string",
|
|
98
|
+
description: "The route ID to sync",
|
|
99
|
+
},
|
|
100
|
+
direction: {
|
|
101
|
+
type: "string",
|
|
102
|
+
enum: ["contract-to-slot", "slot-to-contract"],
|
|
103
|
+
description: "Direction of sync: contract-to-slot adds slot stubs, slot-to-contract adds contract schemas",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
required: ["routeId", "direction"],
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: "mandu_generate_openapi",
|
|
111
|
+
description: "Generate OpenAPI 3.0 specification from contracts",
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: "object",
|
|
114
|
+
properties: {
|
|
115
|
+
output: {
|
|
116
|
+
type: "string",
|
|
117
|
+
description: "Output file path (default: openapi.json)",
|
|
118
|
+
},
|
|
119
|
+
title: {
|
|
120
|
+
type: "string",
|
|
121
|
+
description: "API title (default: Mandu API)",
|
|
122
|
+
},
|
|
123
|
+
version: {
|
|
124
|
+
type: "string",
|
|
125
|
+
description: "API version (default: from manifest)",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
required: [],
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
134
|
+
try {
|
|
135
|
+
await fs.access(filePath);
|
|
136
|
+
return true;
|
|
137
|
+
} catch {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function readFileContent(filePath: string): Promise<string | null> {
|
|
143
|
+
try {
|
|
144
|
+
return await Bun.file(filePath).text();
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function contractTools(projectRoot: string) {
|
|
151
|
+
const paths = getProjectPaths(projectRoot);
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
mandu_list_contracts: async () => {
|
|
155
|
+
const result = await loadManifest(paths.manifestPath);
|
|
156
|
+
if (!result.success || !result.data) {
|
|
157
|
+
return { error: result.errors };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const contracts = result.data.routes
|
|
161
|
+
.filter((r) => r.contractModule)
|
|
162
|
+
.map((r) => ({
|
|
163
|
+
routeId: r.id,
|
|
164
|
+
pattern: r.pattern,
|
|
165
|
+
contractModule: r.contractModule,
|
|
166
|
+
slotModule: r.slotModule,
|
|
167
|
+
}));
|
|
168
|
+
|
|
169
|
+
const routesWithoutContract = result.data.routes
|
|
170
|
+
.filter((r) => !r.contractModule && r.kind === "api")
|
|
171
|
+
.map((r) => r.id);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
contracts,
|
|
175
|
+
count: contracts.length,
|
|
176
|
+
routesWithoutContract,
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
mandu_get_contract: async (args: Record<string, unknown>) => {
|
|
181
|
+
const { routeId } = args as { routeId: string };
|
|
182
|
+
|
|
183
|
+
const result = await loadManifest(paths.manifestPath);
|
|
184
|
+
if (!result.success || !result.data) {
|
|
185
|
+
return { error: result.errors };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const route = result.data.routes.find((r) => r.id === routeId);
|
|
189
|
+
if (!route) {
|
|
190
|
+
return { error: `Route not found: ${routeId}` };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!route.contractModule) {
|
|
194
|
+
return {
|
|
195
|
+
routeId,
|
|
196
|
+
hasContract: false,
|
|
197
|
+
suggestion: `Create a contract with: mandu_create_contract({ routeId: "${routeId}" })`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Read contract file
|
|
202
|
+
const contractPath = path.join(projectRoot, route.contractModule);
|
|
203
|
+
const contractContent = await readFileContent(contractPath);
|
|
204
|
+
|
|
205
|
+
if (!contractContent) {
|
|
206
|
+
return {
|
|
207
|
+
routeId,
|
|
208
|
+
contractModule: route.contractModule,
|
|
209
|
+
error: "Contract file not found",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
routeId,
|
|
215
|
+
contractModule: route.contractModule,
|
|
216
|
+
hasContract: true,
|
|
217
|
+
content: contractContent,
|
|
218
|
+
};
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
mandu_create_contract: async (args: Record<string, unknown>) => {
|
|
222
|
+
const { routeId, description, methods } = args as {
|
|
223
|
+
routeId: string;
|
|
224
|
+
description?: string;
|
|
225
|
+
methods?: string[];
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
// Load manifest
|
|
229
|
+
const result = await loadManifest(paths.manifestPath);
|
|
230
|
+
if (!result.success || !result.data) {
|
|
231
|
+
return { error: result.errors };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Find route
|
|
235
|
+
const route = result.data.routes.find((r) => r.id === routeId);
|
|
236
|
+
if (!route) {
|
|
237
|
+
return { error: `Route not found: ${routeId}` };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Determine contract path
|
|
241
|
+
const contractPath = route.contractModule || `spec/contracts/${routeId}.contract.ts`;
|
|
242
|
+
const fullContractPath = path.join(projectRoot, contractPath);
|
|
243
|
+
|
|
244
|
+
// Check if contract already exists
|
|
245
|
+
if (await fileExists(fullContractPath)) {
|
|
246
|
+
return {
|
|
247
|
+
error: "Contract file already exists",
|
|
248
|
+
contractModule: contractPath,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Create directory if needed
|
|
253
|
+
const contractDir = path.dirname(fullContractPath);
|
|
254
|
+
await fs.mkdir(contractDir, { recursive: true });
|
|
255
|
+
|
|
256
|
+
// Generate contract with custom methods if provided
|
|
257
|
+
const routeWithMethods: RouteSpec = {
|
|
258
|
+
...route,
|
|
259
|
+
methods: methods as any || route.methods || ["GET", "POST"],
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const contractContent = generateContractTemplate(routeWithMethods);
|
|
263
|
+
await Bun.write(fullContractPath, contractContent);
|
|
264
|
+
|
|
265
|
+
// Update manifest if contractModule wasn't set
|
|
266
|
+
if (!route.contractModule) {
|
|
267
|
+
const routeIndex = result.data.routes.findIndex((r) => r.id === routeId);
|
|
268
|
+
result.data.routes[routeIndex] = {
|
|
269
|
+
...route,
|
|
270
|
+
contractModule: contractPath,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
await writeJsonFile(paths.manifestPath, result.data);
|
|
274
|
+
await writeLock(paths.lockPath, result.data);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
success: true,
|
|
279
|
+
contractModule: contractPath,
|
|
280
|
+
message: `Contract created for route '${routeId}'`,
|
|
281
|
+
nextSteps: [
|
|
282
|
+
`Edit ${contractPath} to define your API schema`,
|
|
283
|
+
"Run mandu_generate to regenerate handlers with validation",
|
|
284
|
+
"Run mandu_validate_contracts to check consistency",
|
|
285
|
+
],
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
mandu_update_route_contract: async (args: Record<string, unknown>) => {
|
|
290
|
+
const { routeId, contractModule } = args as {
|
|
291
|
+
routeId: string;
|
|
292
|
+
contractModule: string;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// Load manifest
|
|
296
|
+
const result = await loadManifest(paths.manifestPath);
|
|
297
|
+
if (!result.success || !result.data) {
|
|
298
|
+
return { error: result.errors };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Find route
|
|
302
|
+
const routeIndex = result.data.routes.findIndex((r) => r.id === routeId);
|
|
303
|
+
if (routeIndex === -1) {
|
|
304
|
+
return { error: `Route not found: ${routeId}` };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Update route
|
|
308
|
+
result.data.routes[routeIndex] = {
|
|
309
|
+
...result.data.routes[routeIndex],
|
|
310
|
+
contractModule,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
// Write updated manifest
|
|
314
|
+
await writeJsonFile(paths.manifestPath, result.data);
|
|
315
|
+
await writeLock(paths.lockPath, result.data);
|
|
316
|
+
|
|
317
|
+
return {
|
|
318
|
+
success: true,
|
|
319
|
+
route: result.data.routes[routeIndex],
|
|
320
|
+
message: `Route '${routeId}' updated with contract: ${contractModule}`,
|
|
321
|
+
};
|
|
322
|
+
},
|
|
323
|
+
|
|
324
|
+
mandu_validate_contracts: async () => {
|
|
325
|
+
const result = await loadManifest(paths.manifestPath);
|
|
326
|
+
if (!result.success || !result.data) {
|
|
327
|
+
return { error: result.errors };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const violations = await runContractGuardCheck(result.data, projectRoot);
|
|
331
|
+
|
|
332
|
+
if (violations.length === 0) {
|
|
333
|
+
const contractCount = result.data.routes.filter((r) => r.contractModule).length;
|
|
334
|
+
return {
|
|
335
|
+
valid: true,
|
|
336
|
+
contractCount,
|
|
337
|
+
totalRoutes: result.data.routes.length,
|
|
338
|
+
message: "All contracts are valid",
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
valid: false,
|
|
344
|
+
violations: violations.map((v) => ({
|
|
345
|
+
ruleId: v.ruleId,
|
|
346
|
+
routeId: v.routeId,
|
|
347
|
+
file: v.file,
|
|
348
|
+
message: v.message,
|
|
349
|
+
suggestion: v.suggestion,
|
|
350
|
+
})),
|
|
351
|
+
count: violations.length,
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
|
|
355
|
+
mandu_sync_contract_slot: async (args: Record<string, unknown>) => {
|
|
356
|
+
const { routeId, direction } = args as {
|
|
357
|
+
routeId: string;
|
|
358
|
+
direction: "contract-to-slot" | "slot-to-contract";
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const result = await loadManifest(paths.manifestPath);
|
|
362
|
+
if (!result.success || !result.data) {
|
|
363
|
+
return { error: result.errors };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const route = result.data.routes.find((r) => r.id === routeId);
|
|
367
|
+
if (!route) {
|
|
368
|
+
return { error: `Route not found: ${routeId}` };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (!route.contractModule || !route.slotModule) {
|
|
372
|
+
return {
|
|
373
|
+
error: "Route must have both contractModule and slotModule",
|
|
374
|
+
contractModule: route.contractModule,
|
|
375
|
+
slotModule: route.slotModule,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Read files
|
|
380
|
+
const contractPath = path.join(projectRoot, route.contractModule);
|
|
381
|
+
const slotPath = path.join(projectRoot, route.slotModule);
|
|
382
|
+
|
|
383
|
+
const contractContent = await readFileContent(contractPath);
|
|
384
|
+
const slotContent = await readFileContent(slotPath);
|
|
385
|
+
|
|
386
|
+
if (!contractContent) {
|
|
387
|
+
return { error: `Contract file not found: ${route.contractModule}` };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (!slotContent) {
|
|
391
|
+
return { error: `Slot file not found: ${route.slotModule}` };
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
// Extract methods
|
|
395
|
+
const contractMethods: string[] = [];
|
|
396
|
+
const contractMethodRegex = /\b(GET|POST|PUT|PATCH|DELETE)\s*:\s*\{/g;
|
|
397
|
+
let match;
|
|
398
|
+
while ((match = contractMethodRegex.exec(contractContent)) !== null) {
|
|
399
|
+
if (!contractMethods.includes(match[1])) {
|
|
400
|
+
contractMethods.push(match[1]);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const slotMethods: string[] = [];
|
|
405
|
+
const slotMethodRegex = /\.(get|post|put|patch|delete)\s*\(/gi;
|
|
406
|
+
while ((match = slotMethodRegex.exec(slotContent)) !== null) {
|
|
407
|
+
const method = match[1].toUpperCase();
|
|
408
|
+
if (!slotMethods.includes(method)) {
|
|
409
|
+
slotMethods.push(method);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (direction === "contract-to-slot") {
|
|
414
|
+
// Add missing methods to slot
|
|
415
|
+
const missingInSlot = contractMethods.filter((m) => !slotMethods.includes(m));
|
|
416
|
+
|
|
417
|
+
if (missingInSlot.length === 0) {
|
|
418
|
+
return {
|
|
419
|
+
success: true,
|
|
420
|
+
message: "Slot already has all contract methods",
|
|
421
|
+
contractMethods,
|
|
422
|
+
slotMethods,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Generate slot stubs
|
|
427
|
+
const stubs = missingInSlot
|
|
428
|
+
.map((m) => {
|
|
429
|
+
const method = m.toLowerCase();
|
|
430
|
+
return `
|
|
431
|
+
// 📋 ${m} ${route.pattern}
|
|
432
|
+
.${method}((ctx) => {
|
|
433
|
+
// TODO: Implement ${m} handler
|
|
434
|
+
return ctx.ok({ message: "${m} not implemented yet" });
|
|
435
|
+
})`;
|
|
436
|
+
})
|
|
437
|
+
.join("\n");
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
success: true,
|
|
441
|
+
message: `Add these handlers to ${route.slotModule}:`,
|
|
442
|
+
missingMethods: missingInSlot,
|
|
443
|
+
stubCode: stubs,
|
|
444
|
+
contractMethods,
|
|
445
|
+
slotMethods,
|
|
446
|
+
};
|
|
447
|
+
} else {
|
|
448
|
+
// Add missing methods to contract
|
|
449
|
+
const undocumented = slotMethods.filter((m) => !contractMethods.includes(m));
|
|
450
|
+
|
|
451
|
+
if (undocumented.length === 0) {
|
|
452
|
+
return {
|
|
453
|
+
success: true,
|
|
454
|
+
message: "Contract already documents all slot methods",
|
|
455
|
+
contractMethods,
|
|
456
|
+
slotMethods,
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Generate contract schemas
|
|
461
|
+
const schemas = undocumented
|
|
462
|
+
.map((m) => {
|
|
463
|
+
if (m === "GET" || m === "DELETE") {
|
|
464
|
+
return ` ${m}: {
|
|
465
|
+
// Query parameters
|
|
466
|
+
query: z.object({}).optional(),
|
|
467
|
+
}`;
|
|
468
|
+
}
|
|
469
|
+
return ` ${m}: {
|
|
470
|
+
// Request body
|
|
471
|
+
body: z.object({
|
|
472
|
+
// TODO: Define your schema
|
|
473
|
+
}),
|
|
474
|
+
}`;
|
|
475
|
+
})
|
|
476
|
+
.join(",\n\n");
|
|
477
|
+
|
|
478
|
+
return {
|
|
479
|
+
success: true,
|
|
480
|
+
message: `Add these schemas to ${route.contractModule} request object:`,
|
|
481
|
+
undocumentedMethods: undocumented,
|
|
482
|
+
schemaCode: schemas,
|
|
483
|
+
contractMethods,
|
|
484
|
+
slotMethods,
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
},
|
|
488
|
+
|
|
489
|
+
mandu_generate_openapi: async (args: Record<string, unknown>) => {
|
|
490
|
+
const { output, title, version } = args as {
|
|
491
|
+
output?: string;
|
|
492
|
+
title?: string;
|
|
493
|
+
version?: string;
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
const result = await loadManifest(paths.manifestPath);
|
|
497
|
+
if (!result.success || !result.data) {
|
|
498
|
+
return { error: result.errors };
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Count routes with contracts
|
|
502
|
+
const contractRoutes = result.data.routes.filter((r) => r.contractModule);
|
|
503
|
+
if (contractRoutes.length === 0) {
|
|
504
|
+
return {
|
|
505
|
+
error: "No routes with contracts found",
|
|
506
|
+
suggestion: "Add contractModule to your routes to generate OpenAPI docs",
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
try {
|
|
511
|
+
const doc = await generateOpenAPIDocument(result.data, projectRoot, {
|
|
512
|
+
title,
|
|
513
|
+
version,
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
const json = openAPIToJSON(doc);
|
|
517
|
+
const outputPath = output || path.join(projectRoot, "openapi.json");
|
|
518
|
+
|
|
519
|
+
// Ensure directory exists
|
|
520
|
+
const outputDir = path.dirname(outputPath);
|
|
521
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
522
|
+
|
|
523
|
+
await Bun.write(outputPath, json);
|
|
524
|
+
|
|
525
|
+
const pathCount = Object.keys(doc.paths).length;
|
|
526
|
+
const tagCount = doc.tags?.length || 0;
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
success: true,
|
|
530
|
+
outputPath: path.relative(projectRoot, outputPath),
|
|
531
|
+
summary: {
|
|
532
|
+
paths: pathCount,
|
|
533
|
+
tags: tagCount,
|
|
534
|
+
version: doc.info.version,
|
|
535
|
+
},
|
|
536
|
+
};
|
|
537
|
+
} catch (error) {
|
|
538
|
+
return {
|
|
539
|
+
error: `Failed to generate OpenAPI: ${error instanceof Error ? error.message : String(error)}`,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
},
|
|
543
|
+
};
|
|
544
|
+
}
|
package/src/tools/index.ts
CHANGED
|
@@ -5,3 +5,4 @@ export { historyTools, historyToolDefinitions } from "./history.js";
|
|
|
5
5
|
export { guardTools, guardToolDefinitions } from "./guard.js";
|
|
6
6
|
export { slotTools, slotToolDefinitions } from "./slot.js";
|
|
7
7
|
export { hydrationTools, hydrationToolDefinitions } from "./hydration.js";
|
|
8
|
+
export { contractTools, contractToolDefinitions } from "./contract.js";
|