@mandujs/mcp 0.3.3

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 ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@mandujs/mcp",
3
+ "version": "0.3.3",
4
+ "description": "Mandu MCP Server - Agent-native interface for Mandu framework operations",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "bin": {
8
+ "mandu-mcp": "./src/index.ts"
9
+ },
10
+ "exports": {
11
+ ".": "./src/index.ts"
12
+ },
13
+ "files": [
14
+ "src/**/*"
15
+ ],
16
+ "keywords": [
17
+ "mandu",
18
+ "mcp",
19
+ "model-context-protocol",
20
+ "ai",
21
+ "agent",
22
+ "code-generation"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/konamgil/mandu.git",
27
+ "directory": "packages/mcp"
28
+ },
29
+ "author": "konamgil",
30
+ "license": "MIT",
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "dependencies": {
35
+ "@mandujs/core": "^0.3.3",
36
+ "@modelcontextprotocol/sdk": "^1.25.3"
37
+ },
38
+ "engines": {
39
+ "bun": ">=1.0.0"
40
+ }
41
+ }
package/src/index.ts ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { startServer } from "./server.js";
4
+
5
+ // Start the MCP server
6
+ startServer().catch((error) => {
7
+ console.error("Failed to start Mandu MCP server:", error);
8
+ process.exit(1);
9
+ });
@@ -0,0 +1,176 @@
1
+ import type { Resource } from "@modelcontextprotocol/sdk/types.js";
2
+ import {
3
+ loadManifest,
4
+ getTransactionStatus,
5
+ type GeneratedMap,
6
+ type SpecLock,
7
+ } from "@mandujs/core";
8
+ import { getProjectPaths, readJsonFile } from "../utils/project.js";
9
+ import path from "path";
10
+
11
+ export const resourceDefinitions: Resource[] = [
12
+ {
13
+ uri: "mandu://spec/manifest",
14
+ name: "Routes Manifest",
15
+ description: "Current routes.manifest.json content",
16
+ mimeType: "application/json",
17
+ },
18
+ {
19
+ uri: "mandu://spec/lock",
20
+ name: "Spec Lock",
21
+ description: "Current spec.lock.json with hash verification",
22
+ mimeType: "application/json",
23
+ },
24
+ {
25
+ uri: "mandu://generated/map",
26
+ name: "Generated Map",
27
+ description: "Map of generated files to their source routes",
28
+ mimeType: "application/json",
29
+ },
30
+ {
31
+ uri: "mandu://transaction/active",
32
+ name: "Active Transaction",
33
+ description: "Current active transaction information",
34
+ mimeType: "application/json",
35
+ },
36
+ {
37
+ uri: "mandu://slots/{routeId}",
38
+ name: "Route Slot",
39
+ description: "Slot file content for a specific route",
40
+ mimeType: "text/typescript",
41
+ },
42
+ ];
43
+
44
+ type ResourceHandler = (params: Record<string, string>) => Promise<unknown>;
45
+
46
+ export function resourceHandlers(
47
+ projectRoot: string
48
+ ): Record<string, ResourceHandler> {
49
+ const paths = getProjectPaths(projectRoot);
50
+
51
+ return {
52
+ "mandu://spec/manifest": async () => {
53
+ const result = await loadManifest(paths.manifestPath);
54
+ if (!result.success || !result.data) {
55
+ return {
56
+ error: "Failed to load manifest",
57
+ details: result.errors,
58
+ };
59
+ }
60
+
61
+ return {
62
+ version: result.data.version,
63
+ routeCount: result.data.routes.length,
64
+ routes: result.data.routes,
65
+ };
66
+ },
67
+
68
+ "mandu://spec/lock": async () => {
69
+ const lock = await readJsonFile<SpecLock>(paths.lockPath);
70
+ if (!lock) {
71
+ return {
72
+ exists: false,
73
+ message: "spec.lock.json not found",
74
+ };
75
+ }
76
+
77
+ return {
78
+ exists: true,
79
+ routesHash: lock.routesHash,
80
+ updatedAt: lock.updatedAt,
81
+ };
82
+ },
83
+
84
+ "mandu://generated/map": async () => {
85
+ const generatedMap = await readJsonFile<GeneratedMap>(paths.generatedMapPath);
86
+ if (!generatedMap) {
87
+ return {
88
+ exists: false,
89
+ message: "generated.map.json not found. Run mandu generate first.",
90
+ };
91
+ }
92
+
93
+ return {
94
+ exists: true,
95
+ version: generatedMap.version,
96
+ generatedAt: generatedMap.generatedAt,
97
+ specSource: generatedMap.specSource,
98
+ fileCount: Object.keys(generatedMap.files).length,
99
+ files: generatedMap.files,
100
+ frameworkPaths: generatedMap.frameworkPaths,
101
+ };
102
+ },
103
+
104
+ "mandu://transaction/active": async () => {
105
+ const { state, change } = await getTransactionStatus(projectRoot);
106
+
107
+ if (!state.active) {
108
+ return {
109
+ hasActive: false,
110
+ message: "No active transaction",
111
+ };
112
+ }
113
+
114
+ return {
115
+ hasActive: true,
116
+ changeId: state.changeId,
117
+ snapshotId: state.snapshotId,
118
+ change: change
119
+ ? {
120
+ id: change.id,
121
+ message: change.message,
122
+ status: change.status,
123
+ createdAt: change.createdAt,
124
+ autoGenerated: change.autoGenerated,
125
+ }
126
+ : null,
127
+ };
128
+ },
129
+
130
+ "mandu://slots/{routeId}": async (params: Record<string, string>) => {
131
+ const { routeId } = params;
132
+
133
+ if (!routeId) {
134
+ return { error: "routeId parameter is required" };
135
+ }
136
+
137
+ // Load manifest to find the route
138
+ const result = await loadManifest(paths.manifestPath);
139
+ if (!result.success || !result.data) {
140
+ return { error: result.errors };
141
+ }
142
+
143
+ const route = result.data.routes.find((r) => r.id === routeId);
144
+ if (!route) {
145
+ return { error: `Route not found: ${routeId}` };
146
+ }
147
+
148
+ if (!route.slotModule) {
149
+ return {
150
+ error: `Route '${routeId}' does not have a slotModule`,
151
+ };
152
+ }
153
+
154
+ const slotPath = path.join(projectRoot, route.slotModule);
155
+ const file = Bun.file(slotPath);
156
+
157
+ if (!(await file.exists())) {
158
+ return {
159
+ exists: false,
160
+ slotModule: route.slotModule,
161
+ message: "Slot file does not exist",
162
+ };
163
+ }
164
+
165
+ const content = await file.text();
166
+ return {
167
+ exists: true,
168
+ slotModule: route.slotModule,
169
+ routeId,
170
+ kind: route.kind,
171
+ pattern: route.pattern,
172
+ content,
173
+ };
174
+ },
175
+ };
176
+ }
@@ -0,0 +1 @@
1
+ export { resourceHandlers, resourceDefinitions } from "./handlers.js";
package/src/server.ts ADDED
@@ -0,0 +1,232 @@
1
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import {
4
+ CallToolRequestSchema,
5
+ ListToolsRequestSchema,
6
+ ListResourcesRequestSchema,
7
+ ReadResourceRequestSchema,
8
+ type Tool,
9
+ type Resource,
10
+ } from "@modelcontextprotocol/sdk/types.js";
11
+
12
+ import { specTools, specToolDefinitions } from "./tools/spec.js";
13
+ import { generateTools, generateToolDefinitions } from "./tools/generate.js";
14
+ import { transactionTools, transactionToolDefinitions } from "./tools/transaction.js";
15
+ import { historyTools, historyToolDefinitions } from "./tools/history.js";
16
+ import { guardTools, guardToolDefinitions } from "./tools/guard.js";
17
+ import { slotTools, slotToolDefinitions } from "./tools/slot.js";
18
+ import { resourceHandlers, resourceDefinitions } from "./resources/handlers.js";
19
+ import { findProjectRoot } from "./utils/project.js";
20
+
21
+ export class ManduMcpServer {
22
+ private server: Server;
23
+ private projectRoot: string;
24
+
25
+ constructor(projectRoot: string) {
26
+ this.projectRoot = projectRoot;
27
+ this.server = new Server(
28
+ {
29
+ name: "mandu-mcp",
30
+ version: "0.1.0",
31
+ },
32
+ {
33
+ capabilities: {
34
+ tools: {},
35
+ resources: {},
36
+ },
37
+ }
38
+ );
39
+
40
+ this.registerToolHandlers();
41
+ this.registerResourceHandlers();
42
+ }
43
+
44
+ private getAllToolDefinitions(): Tool[] {
45
+ return [
46
+ ...specToolDefinitions,
47
+ ...generateToolDefinitions,
48
+ ...transactionToolDefinitions,
49
+ ...historyToolDefinitions,
50
+ ...guardToolDefinitions,
51
+ ...slotToolDefinitions,
52
+ ];
53
+ }
54
+
55
+ private getAllToolHandlers(): Record<string, (args: Record<string, unknown>) => Promise<unknown>> {
56
+ return {
57
+ ...specTools(this.projectRoot),
58
+ ...generateTools(this.projectRoot),
59
+ ...transactionTools(this.projectRoot),
60
+ ...historyTools(this.projectRoot),
61
+ ...guardTools(this.projectRoot),
62
+ ...slotTools(this.projectRoot),
63
+ };
64
+ }
65
+
66
+ private registerToolHandlers(): void {
67
+ const toolHandlers = this.getAllToolHandlers();
68
+
69
+ this.server.setRequestHandler(ListToolsRequestSchema, async () => {
70
+ return {
71
+ tools: this.getAllToolDefinitions(),
72
+ };
73
+ });
74
+
75
+ this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
76
+ const { name, arguments: args } = request.params;
77
+
78
+ const handler = toolHandlers[name];
79
+ if (!handler) {
80
+ return {
81
+ content: [
82
+ {
83
+ type: "text",
84
+ text: JSON.stringify({ error: `Unknown tool: ${name}` }),
85
+ },
86
+ ],
87
+ isError: true,
88
+ };
89
+ }
90
+
91
+ try {
92
+ const result = await handler(args || {});
93
+ return {
94
+ content: [
95
+ {
96
+ type: "text",
97
+ text: JSON.stringify(result, null, 2),
98
+ },
99
+ ],
100
+ };
101
+ } catch (error) {
102
+ return {
103
+ content: [
104
+ {
105
+ type: "text",
106
+ text: JSON.stringify({
107
+ error: error instanceof Error ? error.message : String(error),
108
+ }),
109
+ },
110
+ ],
111
+ isError: true,
112
+ };
113
+ }
114
+ });
115
+ }
116
+
117
+ private registerResourceHandlers(): void {
118
+ const handlers = resourceHandlers(this.projectRoot);
119
+
120
+ this.server.setRequestHandler(ListResourcesRequestSchema, async () => {
121
+ return {
122
+ resources: resourceDefinitions,
123
+ };
124
+ });
125
+
126
+ this.server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
127
+ const { uri } = request.params;
128
+
129
+ const handler = handlers[uri];
130
+ if (!handler) {
131
+ // Try pattern matching for dynamic resources
132
+ for (const [pattern, h] of Object.entries(handlers)) {
133
+ if (pattern.includes("{") && matchResourcePattern(pattern, uri)) {
134
+ const params = extractResourceParams(pattern, uri);
135
+ const result = await h(params);
136
+ return {
137
+ contents: [
138
+ {
139
+ uri,
140
+ mimeType: "application/json",
141
+ text: JSON.stringify(result, null, 2),
142
+ },
143
+ ],
144
+ };
145
+ }
146
+ }
147
+
148
+ return {
149
+ contents: [
150
+ {
151
+ uri,
152
+ mimeType: "application/json",
153
+ text: JSON.stringify({ error: `Unknown resource: ${uri}` }),
154
+ },
155
+ ],
156
+ };
157
+ }
158
+
159
+ try {
160
+ const result = await handler({});
161
+ return {
162
+ contents: [
163
+ {
164
+ uri,
165
+ mimeType: "application/json",
166
+ text: JSON.stringify(result, null, 2),
167
+ },
168
+ ],
169
+ };
170
+ } catch (error) {
171
+ return {
172
+ contents: [
173
+ {
174
+ uri,
175
+ mimeType: "application/json",
176
+ text: JSON.stringify({
177
+ error: error instanceof Error ? error.message : String(error),
178
+ }),
179
+ },
180
+ ],
181
+ };
182
+ }
183
+ });
184
+ }
185
+
186
+ async run(): Promise<void> {
187
+ const transport = new StdioServerTransport();
188
+ await this.server.connect(transport);
189
+ console.error(`Mandu MCP Server running for project: ${this.projectRoot}`);
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Match a resource pattern like "mandu://slots/{routeId}" against a URI
195
+ */
196
+ function matchResourcePattern(pattern: string, uri: string): boolean {
197
+ const regexPattern = pattern.replace(/\{[^}]+\}/g, "([^/]+)");
198
+ const regex = new RegExp(`^${regexPattern}$`);
199
+ return regex.test(uri);
200
+ }
201
+
202
+ /**
203
+ * Extract parameters from a URI based on a pattern
204
+ */
205
+ function extractResourceParams(pattern: string, uri: string): Record<string, string> {
206
+ const paramNames: string[] = [];
207
+ const regexPattern = pattern.replace(/\{([^}]+)\}/g, (_, name) => {
208
+ paramNames.push(name);
209
+ return "([^/]+)";
210
+ });
211
+
212
+ const regex = new RegExp(`^${regexPattern}$`);
213
+ const match = uri.match(regex);
214
+
215
+ if (!match) return {};
216
+
217
+ const params: Record<string, string> = {};
218
+ paramNames.forEach((name, index) => {
219
+ params[name] = match[index + 1];
220
+ });
221
+
222
+ return params;
223
+ }
224
+
225
+ /**
226
+ * Create and start the MCP server
227
+ */
228
+ export async function startServer(projectRoot?: string): Promise<void> {
229
+ const root = projectRoot || (await findProjectRoot()) || process.cwd();
230
+ const server = new ManduMcpServer(root);
231
+ await server.run();
232
+ }
@@ -0,0 +1,127 @@
1
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
+ import { loadManifest, generateRoutes, type GeneratedMap } from "@mandujs/core";
3
+ import { getProjectPaths, readJsonFile } from "../utils/project.js";
4
+
5
+ export const generateToolDefinitions: Tool[] = [
6
+ {
7
+ name: "mandu_generate",
8
+ description:
9
+ "Generate route handlers and components from the spec manifest. Creates server handlers, page components, and slot files.",
10
+ inputSchema: {
11
+ type: "object",
12
+ properties: {
13
+ dryRun: {
14
+ type: "boolean",
15
+ description: "If true, show what would be generated without writing files",
16
+ },
17
+ },
18
+ required: [],
19
+ },
20
+ },
21
+ {
22
+ name: "mandu_generate_status",
23
+ description: "Get the current generation status and generated file map",
24
+ inputSchema: {
25
+ type: "object",
26
+ properties: {},
27
+ required: [],
28
+ },
29
+ },
30
+ ];
31
+
32
+ export function generateTools(projectRoot: string) {
33
+ const paths = getProjectPaths(projectRoot);
34
+
35
+ return {
36
+ mandu_generate: async (args: Record<string, unknown>) => {
37
+ const { dryRun } = args as { dryRun?: boolean };
38
+
39
+ // Load manifest
40
+ const manifestResult = await loadManifest(paths.manifestPath);
41
+ if (!manifestResult.success || !manifestResult.data) {
42
+ return { error: manifestResult.errors };
43
+ }
44
+
45
+ if (dryRun) {
46
+ // Dry run - just show what would be generated
47
+ const routes = manifestResult.data.routes;
48
+ const wouldCreate: string[] = [];
49
+ const wouldSkip: string[] = [];
50
+
51
+ for (const route of routes) {
52
+ // Server handler
53
+ wouldCreate.push(`apps/server/generated/routes/${route.id}.route.ts`);
54
+
55
+ // Page component (for page kind)
56
+ if (route.kind === "page") {
57
+ wouldCreate.push(`apps/web/generated/routes/${route.id}.route.tsx`);
58
+ }
59
+
60
+ // Slot file (only if not exists)
61
+ if (route.slotModule) {
62
+ const slotFile = Bun.file(`${projectRoot}/${route.slotModule}`);
63
+ if (await slotFile.exists()) {
64
+ wouldSkip.push(route.slotModule);
65
+ } else {
66
+ wouldCreate.push(route.slotModule);
67
+ }
68
+ }
69
+ }
70
+
71
+ return {
72
+ dryRun: true,
73
+ wouldCreate,
74
+ wouldSkip,
75
+ routeCount: routes.length,
76
+ };
77
+ }
78
+
79
+ // Actually generate
80
+ const result = await generateRoutes(manifestResult.data, projectRoot);
81
+
82
+ return {
83
+ success: result.success,
84
+ created: result.created,
85
+ deleted: result.deleted,
86
+ skipped: result.skipped,
87
+ errors: result.errors,
88
+ summary: {
89
+ createdCount: result.created.length,
90
+ deletedCount: result.deleted.length,
91
+ skippedCount: result.skipped.length,
92
+ },
93
+ };
94
+ },
95
+
96
+ mandu_generate_status: async () => {
97
+ // Read generated map
98
+ const generatedMap = await readJsonFile<GeneratedMap>(paths.generatedMapPath);
99
+
100
+ if (!generatedMap) {
101
+ return {
102
+ hasGeneratedFiles: false,
103
+ message: "No generated.map.json found. Run mandu_generate first.",
104
+ };
105
+ }
106
+
107
+ const fileCount = Object.keys(generatedMap.files).length;
108
+ const routeIds = Object.values(generatedMap.files).map((f) => f.routeId);
109
+ const uniqueRoutes = [...new Set(routeIds)];
110
+
111
+ return {
112
+ hasGeneratedFiles: true,
113
+ version: generatedMap.version,
114
+ generatedAt: generatedMap.generatedAt,
115
+ specSource: generatedMap.specSource,
116
+ fileCount,
117
+ routeCount: uniqueRoutes.length,
118
+ files: Object.entries(generatedMap.files).map(([path, info]) => ({
119
+ path,
120
+ routeId: info.routeId,
121
+ kind: info.kind,
122
+ hasSlot: !!info.slotMapping,
123
+ })),
124
+ };
125
+ },
126
+ };
127
+ }