@webbywisp/create-mcp-server 0.1.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/dist/cli.js +1806 -0
- package/dist/cli.js.map +1 -0
- package/package.json +49 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1806 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import chalk3 from "chalk";
|
|
6
|
+
|
|
7
|
+
// src/commands/init.ts
|
|
8
|
+
import path2 from "path";
|
|
9
|
+
import chalk2 from "chalk";
|
|
10
|
+
import ora from "ora";
|
|
11
|
+
import fs2 from "fs-extra";
|
|
12
|
+
|
|
13
|
+
// src/utils/prompts.ts
|
|
14
|
+
import inquirer from "inquirer";
|
|
15
|
+
async function runWizard(defaults) {
|
|
16
|
+
return inquirer.prompt([
|
|
17
|
+
{
|
|
18
|
+
type: "input",
|
|
19
|
+
name: "serverName",
|
|
20
|
+
message: "Server name (used in MCP config):",
|
|
21
|
+
default: defaults?.serverName ?? "my-mcp-server",
|
|
22
|
+
validate: (input) => {
|
|
23
|
+
if (!input.trim()) return "Server name cannot be empty";
|
|
24
|
+
if (!/^[a-z0-9-]+$/.test(input)) return "Use lowercase letters, numbers, and hyphens only";
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
type: "input",
|
|
30
|
+
name: "serverDescription",
|
|
31
|
+
message: "Short description:",
|
|
32
|
+
default: defaults?.serverDescription ?? "An MCP server"
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
type: "list",
|
|
36
|
+
name: "template",
|
|
37
|
+
message: "Choose a template:",
|
|
38
|
+
default: defaults?.template ?? "tool-server",
|
|
39
|
+
choices: [
|
|
40
|
+
{
|
|
41
|
+
name: "tool-server \u2014 expose tools (functions AI can call)",
|
|
42
|
+
value: "tool-server",
|
|
43
|
+
short: "tool-server"
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
name: "resource-server \u2014 expose data/files as addressable resources",
|
|
47
|
+
value: "resource-server",
|
|
48
|
+
short: "resource-server"
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "prompt-server \u2014 provide structured prompt templates",
|
|
52
|
+
value: "prompt-server",
|
|
53
|
+
short: "prompt-server"
|
|
54
|
+
}
|
|
55
|
+
]
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
type: "input",
|
|
59
|
+
name: "packageName",
|
|
60
|
+
message: "npm package name:",
|
|
61
|
+
default: (answers) => defaults?.packageName ?? answers.serverName,
|
|
62
|
+
validate: (input) => {
|
|
63
|
+
if (!input.trim()) return "Package name cannot be empty";
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: "input",
|
|
69
|
+
name: "author",
|
|
70
|
+
message: "Author name:",
|
|
71
|
+
default: defaults?.author ?? ""
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: "confirm",
|
|
75
|
+
name: "gitInit",
|
|
76
|
+
message: "Initialize a git repository?",
|
|
77
|
+
default: defaults?.gitInit ?? true
|
|
78
|
+
}
|
|
79
|
+
]);
|
|
80
|
+
}
|
|
81
|
+
function buildDefaultConfig(targetDir, dirName, overrides = {}) {
|
|
82
|
+
return {
|
|
83
|
+
targetDir,
|
|
84
|
+
dirName,
|
|
85
|
+
serverName: dirName,
|
|
86
|
+
serverDescription: `An MCP server`,
|
|
87
|
+
template: "tool-server",
|
|
88
|
+
packageName: dirName,
|
|
89
|
+
author: "",
|
|
90
|
+
gitInit: true,
|
|
91
|
+
dryRun: false,
|
|
92
|
+
yes: false,
|
|
93
|
+
...overrides
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// src/utils/files.ts
|
|
98
|
+
import fs from "fs-extra";
|
|
99
|
+
import path from "path";
|
|
100
|
+
import chalk from "chalk";
|
|
101
|
+
async function writeFiles(targetDir, files, force) {
|
|
102
|
+
const results = [];
|
|
103
|
+
for (const file of files) {
|
|
104
|
+
const filePath = path.join(targetDir, file.path);
|
|
105
|
+
if (file.isDirectory) {
|
|
106
|
+
await fs.ensureDir(filePath);
|
|
107
|
+
results.push({ path: file.path, status: "created" });
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
await fs.ensureDir(path.dirname(filePath));
|
|
111
|
+
const exists = await fs.pathExists(filePath);
|
|
112
|
+
if (exists && !force) {
|
|
113
|
+
results.push({ path: file.path, status: "skipped" });
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
await fs.writeFile(filePath, file.content, "utf8");
|
|
117
|
+
results.push({ path: file.path, status: exists ? "overwritten" : "created" });
|
|
118
|
+
}
|
|
119
|
+
return results;
|
|
120
|
+
}
|
|
121
|
+
function printDryRunTree(targetDir, files) {
|
|
122
|
+
console.log(chalk.bold(`\u{1F4C1} ${targetDir}/`));
|
|
123
|
+
for (const file of files) {
|
|
124
|
+
const icon = file.isDirectory ? "\u{1F4C1}" : "\u{1F4C4}";
|
|
125
|
+
console.log(chalk.dim(` ${icon} ${file.path}`));
|
|
126
|
+
}
|
|
127
|
+
console.log();
|
|
128
|
+
}
|
|
129
|
+
function printWriteResults(results) {
|
|
130
|
+
for (const result of results) {
|
|
131
|
+
if (result.status === "created") {
|
|
132
|
+
console.log(` ${chalk.green("+")} ${result.path}`);
|
|
133
|
+
} else if (result.status === "overwritten") {
|
|
134
|
+
console.log(` ${chalk.yellow("~")} ${result.path} ${chalk.dim("(overwritten)")}`);
|
|
135
|
+
} else {
|
|
136
|
+
console.log(` ${chalk.dim("-")} ${result.path} ${chalk.dim("(skipped)")}`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/templates/tool-server.ts
|
|
142
|
+
function generateToolServer(config) {
|
|
143
|
+
const { serverName, serverDescription, packageName, author } = config;
|
|
144
|
+
return [
|
|
145
|
+
{
|
|
146
|
+
path: "src/index.ts",
|
|
147
|
+
content: `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
148
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
149
|
+
import { z } from 'zod';
|
|
150
|
+
import { readFileTool } from './tools/readFile.js';
|
|
151
|
+
import { writeFileTool } from './tools/writeFile.js';
|
|
152
|
+
import { httpFetchTool } from './tools/httpFetch.js';
|
|
153
|
+
import { execCommandTool } from './tools/execCommand.js';
|
|
154
|
+
|
|
155
|
+
const server = new McpServer({
|
|
156
|
+
name: '${serverName}',
|
|
157
|
+
version: '0.1.0',
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Register all tools
|
|
161
|
+
server.tool(
|
|
162
|
+
readFileTool.name,
|
|
163
|
+
readFileTool.description,
|
|
164
|
+
readFileTool.inputSchema,
|
|
165
|
+
readFileTool.handler,
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
server.tool(
|
|
169
|
+
writeFileTool.name,
|
|
170
|
+
writeFileTool.description,
|
|
171
|
+
writeFileTool.inputSchema,
|
|
172
|
+
writeFileTool.handler,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
server.tool(
|
|
176
|
+
httpFetchTool.name,
|
|
177
|
+
httpFetchTool.description,
|
|
178
|
+
httpFetchTool.inputSchema,
|
|
179
|
+
httpFetchTool.handler,
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
server.tool(
|
|
183
|
+
execCommandTool.name,
|
|
184
|
+
execCommandTool.description,
|
|
185
|
+
execCommandTool.inputSchema,
|
|
186
|
+
execCommandTool.handler,
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
async function main() {
|
|
190
|
+
const transport = new StdioServerTransport();
|
|
191
|
+
await server.connect(transport);
|
|
192
|
+
console.error('${serverName} MCP server running on stdio');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
main().catch((err) => {
|
|
196
|
+
console.error('Fatal error:', err);
|
|
197
|
+
process.exit(1);
|
|
198
|
+
});
|
|
199
|
+
`
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
path: "src/tools/readFile.ts",
|
|
203
|
+
content: `import { z } from 'zod';
|
|
204
|
+
import fs from 'fs/promises';
|
|
205
|
+
import path from 'path';
|
|
206
|
+
|
|
207
|
+
export const readFileTool = {
|
|
208
|
+
name: 'read_file',
|
|
209
|
+
description: 'Read the contents of a file. Returns the file content as a string.',
|
|
210
|
+
inputSchema: {
|
|
211
|
+
path: z.string().describe('Absolute or relative path to the file'),
|
|
212
|
+
encoding: z.enum(['utf8', 'base64']).optional().default('utf8').describe('File encoding'),
|
|
213
|
+
maxBytes: z.number().optional().default(1048576).describe('Max bytes to read (default 1MB)'),
|
|
214
|
+
},
|
|
215
|
+
handler: async ({ path: filePath, encoding, maxBytes }: {
|
|
216
|
+
path: string;
|
|
217
|
+
encoding?: 'utf8' | 'base64';
|
|
218
|
+
maxBytes?: number;
|
|
219
|
+
}) => {
|
|
220
|
+
const resolvedPath = path.resolve(filePath);
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const stat = await fs.stat(resolvedPath);
|
|
224
|
+
|
|
225
|
+
if (!stat.isFile()) {
|
|
226
|
+
return {
|
|
227
|
+
content: [{ type: 'text' as const, text: \`Error: \${filePath} is not a file\` }],
|
|
228
|
+
isError: true,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (stat.size > (maxBytes ?? 1048576)) {
|
|
233
|
+
return {
|
|
234
|
+
content: [{
|
|
235
|
+
type: 'text' as const,
|
|
236
|
+
text: \`Error: File too large (\${stat.size} bytes). Max is \${maxBytes} bytes.\`
|
|
237
|
+
}],
|
|
238
|
+
isError: true,
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const content = await fs.readFile(resolvedPath, encoding ?? 'utf8');
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
content: [{ type: 'text' as const, text: String(content) }],
|
|
246
|
+
};
|
|
247
|
+
} catch (err: any) {
|
|
248
|
+
return {
|
|
249
|
+
content: [{ type: 'text' as const, text: \`Error reading file: \${err.message}\` }],
|
|
250
|
+
isError: true,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
`
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
path: "src/tools/writeFile.ts",
|
|
259
|
+
content: `import { z } from 'zod';
|
|
260
|
+
import fs from 'fs/promises';
|
|
261
|
+
import path from 'path';
|
|
262
|
+
|
|
263
|
+
export const writeFileTool = {
|
|
264
|
+
name: 'write_file',
|
|
265
|
+
description: 'Write content to a file. Creates parent directories if needed.',
|
|
266
|
+
inputSchema: {
|
|
267
|
+
path: z.string().describe('Absolute or relative path to write to'),
|
|
268
|
+
content: z.string().describe('Content to write'),
|
|
269
|
+
encoding: z.enum(['utf8', 'base64']).optional().default('utf8').describe('File encoding'),
|
|
270
|
+
createDirs: z.boolean().optional().default(true).describe('Create parent directories if missing'),
|
|
271
|
+
overwrite: z.boolean().optional().default(true).describe('Overwrite if file exists'),
|
|
272
|
+
},
|
|
273
|
+
handler: async ({ path: filePath, content, encoding, createDirs, overwrite }: {
|
|
274
|
+
path: string;
|
|
275
|
+
content: string;
|
|
276
|
+
encoding?: 'utf8' | 'base64';
|
|
277
|
+
createDirs?: boolean;
|
|
278
|
+
overwrite?: boolean;
|
|
279
|
+
}) => {
|
|
280
|
+
const resolvedPath = path.resolve(filePath);
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
// Check if file exists
|
|
284
|
+
if (!overwrite) {
|
|
285
|
+
const exists = await fs.access(resolvedPath).then(() => true).catch(() => false);
|
|
286
|
+
if (exists) {
|
|
287
|
+
return {
|
|
288
|
+
content: [{ type: 'text' as const, text: \`Error: File already exists at \${filePath}. Set overwrite=true to overwrite.\` }],
|
|
289
|
+
isError: true,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (createDirs) {
|
|
295
|
+
await fs.mkdir(path.dirname(resolvedPath), { recursive: true });
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
await fs.writeFile(resolvedPath, content, encoding ?? 'utf8');
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
content: [{ type: 'text' as const, text: \`Successfully wrote \${content.length} bytes to \${filePath}\` }],
|
|
302
|
+
};
|
|
303
|
+
} catch (err: any) {
|
|
304
|
+
return {
|
|
305
|
+
content: [{ type: 'text' as const, text: \`Error writing file: \${err.message}\` }],
|
|
306
|
+
isError: true,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
},
|
|
310
|
+
};
|
|
311
|
+
`
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
path: "src/tools/httpFetch.ts",
|
|
315
|
+
content: `import { z } from 'zod';
|
|
316
|
+
|
|
317
|
+
export const httpFetchTool = {
|
|
318
|
+
name: 'http_fetch',
|
|
319
|
+
description: 'Make an HTTP request and return the response. Supports GET, POST, PUT, DELETE.',
|
|
320
|
+
inputSchema: {
|
|
321
|
+
url: z.string().url().describe('URL to fetch'),
|
|
322
|
+
method: z.enum(['GET', 'POST', 'PUT', 'DELETE', 'PATCH']).optional().default('GET').describe('HTTP method'),
|
|
323
|
+
headers: z.record(z.string()).optional().describe('Request headers as key-value pairs'),
|
|
324
|
+
body: z.string().optional().describe('Request body (for POST/PUT/PATCH)'),
|
|
325
|
+
timeoutMs: z.number().optional().default(30000).describe('Timeout in milliseconds'),
|
|
326
|
+
maxBytes: z.number().optional().default(524288).describe('Max response bytes (default 512KB)'),
|
|
327
|
+
},
|
|
328
|
+
handler: async ({ url, method, headers, body, timeoutMs, maxBytes }: {
|
|
329
|
+
url: string;
|
|
330
|
+
method?: string;
|
|
331
|
+
headers?: Record<string, string>;
|
|
332
|
+
body?: string;
|
|
333
|
+
timeoutMs?: number;
|
|
334
|
+
maxBytes?: number;
|
|
335
|
+
}) => {
|
|
336
|
+
try {
|
|
337
|
+
const controller = new AbortController();
|
|
338
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs ?? 30000);
|
|
339
|
+
|
|
340
|
+
const response = await fetch(url, {
|
|
341
|
+
method: method ?? 'GET',
|
|
342
|
+
headers: headers,
|
|
343
|
+
body: body,
|
|
344
|
+
signal: controller.signal,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
clearTimeout(timeout);
|
|
348
|
+
|
|
349
|
+
const responseHeaders: Record<string, string> = {};
|
|
350
|
+
response.headers.forEach((value, key) => {
|
|
351
|
+
responseHeaders[key] = value;
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
const contentType = response.headers.get('content-type') ?? '';
|
|
355
|
+
let responseText: string;
|
|
356
|
+
|
|
357
|
+
if (contentType.includes('application/json') || contentType.includes('text/')) {
|
|
358
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
359
|
+
const bytes = arrayBuffer.byteLength;
|
|
360
|
+
if (bytes > (maxBytes ?? 524288)) {
|
|
361
|
+
responseText = \`[Response truncated: \${bytes} bytes exceeds maxBytes limit]\`;
|
|
362
|
+
} else {
|
|
363
|
+
responseText = new TextDecoder().decode(arrayBuffer);
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
responseText = \`[Binary content: \${contentType}]\`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const summary = [
|
|
370
|
+
\`Status: \${response.status} \${response.statusText}\`,
|
|
371
|
+
\`Content-Type: \${contentType}\`,
|
|
372
|
+
\`\\n\${responseText}\`,
|
|
373
|
+
].join('\\n');
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
content: [{ type: 'text' as const, text: summary }],
|
|
377
|
+
isError: !response.ok,
|
|
378
|
+
};
|
|
379
|
+
} catch (err: any) {
|
|
380
|
+
return {
|
|
381
|
+
content: [{ type: 'text' as const, text: \`HTTP request failed: \${err.message}\` }],
|
|
382
|
+
isError: true,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
`
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
path: "src/tools/execCommand.ts",
|
|
391
|
+
content: `import { z } from 'zod';
|
|
392
|
+
import { exec } from 'child_process';
|
|
393
|
+
import { promisify } from 'util';
|
|
394
|
+
|
|
395
|
+
const execAsync = promisify(exec);
|
|
396
|
+
|
|
397
|
+
// Safety: block dangerous commands by default
|
|
398
|
+
const BLOCKED_PATTERNS = [
|
|
399
|
+
/rm\\s+-rf\\s+\\//,
|
|
400
|
+
/mkfs/,
|
|
401
|
+
/dd\\s+if=/,
|
|
402
|
+
/:(\\s*)\\{\\s*:\\|:&\\s*\\}/, // fork bomb
|
|
403
|
+
/>\\/dev\\/(sd|hd|nvme)/,
|
|
404
|
+
];
|
|
405
|
+
|
|
406
|
+
export const execCommandTool = {
|
|
407
|
+
name: 'exec_command',
|
|
408
|
+
description: 'Execute a shell command and return stdout/stderr. Use with caution.',
|
|
409
|
+
inputSchema: {
|
|
410
|
+
command: z.string().describe('Shell command to execute'),
|
|
411
|
+
cwd: z.string().optional().describe('Working directory (defaults to process cwd)'),
|
|
412
|
+
timeoutMs: z.number().optional().default(30000).describe('Timeout in milliseconds'),
|
|
413
|
+
env: z.record(z.string()).optional().describe('Additional environment variables'),
|
|
414
|
+
},
|
|
415
|
+
handler: async ({ command, cwd, timeoutMs, env }: {
|
|
416
|
+
command: string;
|
|
417
|
+
cwd?: string;
|
|
418
|
+
timeoutMs?: number;
|
|
419
|
+
env?: Record<string, string>;
|
|
420
|
+
}) => {
|
|
421
|
+
// Safety check
|
|
422
|
+
for (const pattern of BLOCKED_PATTERNS) {
|
|
423
|
+
if (pattern.test(command)) {
|
|
424
|
+
return {
|
|
425
|
+
content: [{ type: 'text' as const, text: \`Error: Command blocked for safety: \${command}\` }],
|
|
426
|
+
isError: true,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
const { stdout, stderr } = await execAsync(command, {
|
|
433
|
+
cwd: cwd ?? process.cwd(),
|
|
434
|
+
timeout: timeoutMs ?? 30000,
|
|
435
|
+
env: { ...process.env, ...env },
|
|
436
|
+
maxBuffer: 1024 * 1024, // 1MB
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
const output = [
|
|
440
|
+
stdout && \`stdout:\\n\${stdout.trim()}\`,
|
|
441
|
+
stderr && \`stderr:\\n\${stderr.trim()}\`,
|
|
442
|
+
].filter(Boolean).join('\\n\\n') || '(no output)';
|
|
443
|
+
|
|
444
|
+
return {
|
|
445
|
+
content: [{ type: 'text' as const, text: output }],
|
|
446
|
+
};
|
|
447
|
+
} catch (err: any) {
|
|
448
|
+
const output = [
|
|
449
|
+
\`Exit code: \${err.code ?? 'unknown'}\`,
|
|
450
|
+
err.stdout && \`stdout:\\n\${err.stdout.trim()}\`,
|
|
451
|
+
err.stderr && \`stderr:\\n\${err.stderr.trim()}\`,
|
|
452
|
+
\`Error: \${err.message}\`,
|
|
453
|
+
].filter(Boolean).join('\\n');
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
content: [{ type: 'text' as const, text: output }],
|
|
457
|
+
isError: true,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
`
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
path: "package.json",
|
|
466
|
+
content: JSON.stringify({
|
|
467
|
+
name: packageName,
|
|
468
|
+
version: "0.1.0",
|
|
469
|
+
description: serverDescription,
|
|
470
|
+
type: "module",
|
|
471
|
+
scripts: {
|
|
472
|
+
build: "tsc",
|
|
473
|
+
dev: "tsc --watch",
|
|
474
|
+
start: "node dist/index.js",
|
|
475
|
+
prepublishOnly: "npm run build"
|
|
476
|
+
},
|
|
477
|
+
engines: { node: ">=18.0.0" },
|
|
478
|
+
dependencies: {
|
|
479
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
480
|
+
zod: "^3.22.0"
|
|
481
|
+
},
|
|
482
|
+
devDependencies: {
|
|
483
|
+
"@types/node": "^22.0.0",
|
|
484
|
+
typescript: "^5.0.0"
|
|
485
|
+
}
|
|
486
|
+
}, null, 2)
|
|
487
|
+
},
|
|
488
|
+
{
|
|
489
|
+
path: "tsconfig.json",
|
|
490
|
+
content: JSON.stringify({
|
|
491
|
+
compilerOptions: {
|
|
492
|
+
target: "ES2022",
|
|
493
|
+
module: "NodeNext",
|
|
494
|
+
moduleResolution: "NodeNext",
|
|
495
|
+
lib: ["ES2022"],
|
|
496
|
+
outDir: "dist",
|
|
497
|
+
rootDir: "src",
|
|
498
|
+
strict: true,
|
|
499
|
+
esModuleInterop: true,
|
|
500
|
+
skipLibCheck: true,
|
|
501
|
+
declaration: true,
|
|
502
|
+
sourceMap: true,
|
|
503
|
+
resolveJsonModule: true
|
|
504
|
+
},
|
|
505
|
+
include: ["src/**/*"],
|
|
506
|
+
exclude: ["node_modules", "dist"]
|
|
507
|
+
}, null, 2)
|
|
508
|
+
},
|
|
509
|
+
{
|
|
510
|
+
path: ".gitignore",
|
|
511
|
+
content: `node_modules/
|
|
512
|
+
dist/
|
|
513
|
+
*.js.map
|
|
514
|
+
*.d.ts.map
|
|
515
|
+
.env
|
|
516
|
+
.env.local
|
|
517
|
+
*.log
|
|
518
|
+
.DS_Store
|
|
519
|
+
`
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
path: "README.md",
|
|
523
|
+
content: `# ${serverName}
|
|
524
|
+
|
|
525
|
+
${serverDescription}
|
|
526
|
+
|
|
527
|
+
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) tool server that exposes file system, HTTP, and shell execution capabilities to AI models.
|
|
528
|
+
|
|
529
|
+
## Tools
|
|
530
|
+
|
|
531
|
+
| Tool | Description |
|
|
532
|
+
|------|-------------|
|
|
533
|
+
| \`read_file\` | Read file contents with encoding and size limits |
|
|
534
|
+
| \`write_file\` | Write files, creating directories as needed |
|
|
535
|
+
| \`http_fetch\` | Make HTTP requests (GET, POST, PUT, DELETE) |
|
|
536
|
+
| \`exec_command\` | Execute shell commands with safety checks |
|
|
537
|
+
|
|
538
|
+
## Setup
|
|
539
|
+
|
|
540
|
+
\`\`\`bash
|
|
541
|
+
npm install
|
|
542
|
+
npm run build
|
|
543
|
+
\`\`\`
|
|
544
|
+
|
|
545
|
+
## Usage with Claude Desktop
|
|
546
|
+
|
|
547
|
+
Add to your Claude Desktop config (\`~/Library/Application Support/Claude/claude_desktop_config.json\`):
|
|
548
|
+
|
|
549
|
+
\`\`\`json
|
|
550
|
+
{
|
|
551
|
+
"mcpServers": {
|
|
552
|
+
"${serverName}": {
|
|
553
|
+
"command": "node",
|
|
554
|
+
"args": ["${config.targetDir}/dist/index.js"]
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
\`\`\`
|
|
559
|
+
|
|
560
|
+
## Development
|
|
561
|
+
|
|
562
|
+
\`\`\`bash
|
|
563
|
+
npm run dev # Watch mode
|
|
564
|
+
npm start # Run the server
|
|
565
|
+
\`\`\`
|
|
566
|
+
|
|
567
|
+
## Adding Tools
|
|
568
|
+
|
|
569
|
+
Create a new file in \`src/tools/\` following the existing pattern:
|
|
570
|
+
|
|
571
|
+
\`\`\`typescript
|
|
572
|
+
import { z } from 'zod';
|
|
573
|
+
|
|
574
|
+
export const myTool = {
|
|
575
|
+
name: 'my_tool',
|
|
576
|
+
description: 'Description of what this tool does',
|
|
577
|
+
inputSchema: {
|
|
578
|
+
param1: z.string().describe('First parameter'),
|
|
579
|
+
param2: z.number().optional().describe('Optional number'),
|
|
580
|
+
},
|
|
581
|
+
handler: async ({ param1, param2 }) => {
|
|
582
|
+
// Your implementation
|
|
583
|
+
return {
|
|
584
|
+
content: [{ type: 'text' as const, text: 'result' }],
|
|
585
|
+
};
|
|
586
|
+
},
|
|
587
|
+
};
|
|
588
|
+
\`\`\`
|
|
589
|
+
|
|
590
|
+
Then register it in \`src/index.ts\`.
|
|
591
|
+
|
|
592
|
+
## Author
|
|
593
|
+
|
|
594
|
+
${author}
|
|
595
|
+
`
|
|
596
|
+
}
|
|
597
|
+
];
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// src/templates/resource-server.ts
|
|
601
|
+
function generateResourceServer(config) {
|
|
602
|
+
const { serverName, serverDescription, packageName, author } = config;
|
|
603
|
+
return [
|
|
604
|
+
{
|
|
605
|
+
path: "src/index.ts",
|
|
606
|
+
content: `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
607
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
608
|
+
import { fileSystemResources } from './resources/fileSystem.js';
|
|
609
|
+
import { databaseResources } from './resources/database.js';
|
|
610
|
+
import { configResources } from './resources/config.js';
|
|
611
|
+
|
|
612
|
+
const server = new McpServer({
|
|
613
|
+
name: '${serverName}',
|
|
614
|
+
version: '0.1.0',
|
|
615
|
+
});
|
|
616
|
+
|
|
617
|
+
// Register resource providers
|
|
618
|
+
fileSystemResources.register(server);
|
|
619
|
+
databaseResources.register(server);
|
|
620
|
+
configResources.register(server);
|
|
621
|
+
|
|
622
|
+
async function main() {
|
|
623
|
+
const transport = new StdioServerTransport();
|
|
624
|
+
await server.connect(transport);
|
|
625
|
+
console.error('${serverName} MCP resource server running on stdio');
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
main().catch((err) => {
|
|
629
|
+
console.error('Fatal error:', err);
|
|
630
|
+
process.exit(1);
|
|
631
|
+
});
|
|
632
|
+
`
|
|
633
|
+
},
|
|
634
|
+
{
|
|
635
|
+
path: "src/resources/fileSystem.ts",
|
|
636
|
+
content: `import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
637
|
+
import fs from 'fs/promises';
|
|
638
|
+
import path from 'path';
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* File system resources \u2014 expose files and directories as MCP resources.
|
|
642
|
+
* URI pattern: file:///absolute/path/to/file
|
|
643
|
+
*/
|
|
644
|
+
export const fileSystemResources = {
|
|
645
|
+
register(server: McpServer) {
|
|
646
|
+
// Static resource: list files in a directory
|
|
647
|
+
server.resource(
|
|
648
|
+
'workspace-files',
|
|
649
|
+
'file:///workspace',
|
|
650
|
+
async (uri) => {
|
|
651
|
+
const dir = process.cwd();
|
|
652
|
+
try {
|
|
653
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
654
|
+
const listing = entries
|
|
655
|
+
.map((e) => \`\${e.isDirectory() ? '[dir]' : '[file]'} \${e.name}\`)
|
|
656
|
+
.join('\\n');
|
|
657
|
+
|
|
658
|
+
return {
|
|
659
|
+
contents: [{
|
|
660
|
+
uri: uri.href,
|
|
661
|
+
mimeType: 'text/plain',
|
|
662
|
+
text: \`Files in \${dir}:\\n\\n\${listing}\`,
|
|
663
|
+
}],
|
|
664
|
+
};
|
|
665
|
+
} catch (err: any) {
|
|
666
|
+
throw new Error(\`Failed to list directory: \${err.message}\`);
|
|
667
|
+
}
|
|
668
|
+
},
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
// Dynamic resource template: read any file by URI
|
|
672
|
+
server.resource(
|
|
673
|
+
'file',
|
|
674
|
+
new ResourceTemplate('file://{path}', { list: undefined }),
|
|
675
|
+
async (uri, params) => {
|
|
676
|
+
const filePath = decodeURIComponent(params.path as string);
|
|
677
|
+
const resolvedPath = path.isAbsolute(filePath)
|
|
678
|
+
? filePath
|
|
679
|
+
: path.resolve(process.cwd(), filePath);
|
|
680
|
+
|
|
681
|
+
try {
|
|
682
|
+
const stat = await fs.stat(resolvedPath);
|
|
683
|
+
|
|
684
|
+
if (stat.isDirectory()) {
|
|
685
|
+
const entries = await fs.readdir(resolvedPath, { withFileTypes: true });
|
|
686
|
+
const listing = entries
|
|
687
|
+
.map((e) => \`\${e.isDirectory() ? '[dir] ' : '[file]'} \${e.name}\`)
|
|
688
|
+
.join('\\n');
|
|
689
|
+
return {
|
|
690
|
+
contents: [{
|
|
691
|
+
uri: uri.href,
|
|
692
|
+
mimeType: 'text/plain',
|
|
693
|
+
text: listing,
|
|
694
|
+
}],
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const ext = path.extname(resolvedPath).toLowerCase();
|
|
699
|
+
const textExtensions = new Set([
|
|
700
|
+
'.ts', '.tsx', '.js', '.jsx', '.json', '.md', '.txt',
|
|
701
|
+
'.yaml', '.yml', '.toml', '.env', '.sh', '.bash', '.zsh',
|
|
702
|
+
'.py', '.rb', '.go', '.rs', '.java', '.c', '.cpp', '.h',
|
|
703
|
+
'.html', '.css', '.scss', '.less', '.xml', '.svg',
|
|
704
|
+
]);
|
|
705
|
+
|
|
706
|
+
if (textExtensions.has(ext) || stat.size < 100000) {
|
|
707
|
+
const content = await fs.readFile(resolvedPath, 'utf8');
|
|
708
|
+
const mimeType = getMimeType(ext);
|
|
709
|
+
|
|
710
|
+
return {
|
|
711
|
+
contents: [{
|
|
712
|
+
uri: uri.href,
|
|
713
|
+
mimeType,
|
|
714
|
+
text: content,
|
|
715
|
+
}],
|
|
716
|
+
};
|
|
717
|
+
} else {
|
|
718
|
+
const data = await fs.readFile(resolvedPath);
|
|
719
|
+
return {
|
|
720
|
+
contents: [{
|
|
721
|
+
uri: uri.href,
|
|
722
|
+
mimeType: 'application/octet-stream',
|
|
723
|
+
blob: data.toString('base64'),
|
|
724
|
+
}],
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
} catch (err: any) {
|
|
728
|
+
throw new Error(\`Failed to read file \${resolvedPath}: \${err.message}\`);
|
|
729
|
+
}
|
|
730
|
+
},
|
|
731
|
+
);
|
|
732
|
+
},
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
function getMimeType(ext: string): string {
|
|
736
|
+
const map: Record<string, string> = {
|
|
737
|
+
'.ts': 'text/typescript',
|
|
738
|
+
'.tsx': 'text/typescript',
|
|
739
|
+
'.js': 'text/javascript',
|
|
740
|
+
'.jsx': 'text/javascript',
|
|
741
|
+
'.json': 'application/json',
|
|
742
|
+
'.md': 'text/markdown',
|
|
743
|
+
'.txt': 'text/plain',
|
|
744
|
+
'.yaml': 'text/yaml',
|
|
745
|
+
'.yml': 'text/yaml',
|
|
746
|
+
'.html': 'text/html',
|
|
747
|
+
'.css': 'text/css',
|
|
748
|
+
'.svg': 'image/svg+xml',
|
|
749
|
+
'.xml': 'application/xml',
|
|
750
|
+
'.py': 'text/x-python',
|
|
751
|
+
'.sh': 'text/x-sh',
|
|
752
|
+
};
|
|
753
|
+
return map[ext] ?? 'text/plain';
|
|
754
|
+
}
|
|
755
|
+
`
|
|
756
|
+
},
|
|
757
|
+
{
|
|
758
|
+
path: "src/resources/database.ts",
|
|
759
|
+
content: `import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
760
|
+
|
|
761
|
+
// Example: In-memory key-value store as a resource.
|
|
762
|
+
// Replace with your actual data source (SQLite, Postgres, Redis, etc.)
|
|
763
|
+
const store = new Map<string, unknown>([
|
|
764
|
+
['example:config', { version: '1.0', feature_flags: { new_ui: true } }],
|
|
765
|
+
['example:stats', { requests: 0, errors: 0, uptime: Date.now() }],
|
|
766
|
+
]);
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Database/store resources \u2014 expose structured data as MCP resources.
|
|
770
|
+
* URI pattern: db://namespace/key
|
|
771
|
+
*
|
|
772
|
+
* Replace the in-memory store with your actual database client.
|
|
773
|
+
*/
|
|
774
|
+
export const databaseResources = {
|
|
775
|
+
register(server: McpServer) {
|
|
776
|
+
// List all available keys
|
|
777
|
+
server.resource(
|
|
778
|
+
'db-index',
|
|
779
|
+
'db://index',
|
|
780
|
+
async (uri) => {
|
|
781
|
+
const keys = Array.from(store.keys());
|
|
782
|
+
const listing = keys.map(k => \` \${k}\`).join('\\n');
|
|
783
|
+
|
|
784
|
+
return {
|
|
785
|
+
contents: [{
|
|
786
|
+
uri: uri.href,
|
|
787
|
+
mimeType: 'text/plain',
|
|
788
|
+
text: \`Available keys (\${keys.length}):\\n\${listing}\`,
|
|
789
|
+
}],
|
|
790
|
+
};
|
|
791
|
+
},
|
|
792
|
+
);
|
|
793
|
+
|
|
794
|
+
// Dynamic resource: fetch any key
|
|
795
|
+
server.resource(
|
|
796
|
+
'db-record',
|
|
797
|
+
new ResourceTemplate('db://{namespace}/{key}', { list: undefined }),
|
|
798
|
+
async (uri, params) => {
|
|
799
|
+
const recordKey = \`\${params.namespace}:\${params.key}\`;
|
|
800
|
+
const value = store.get(recordKey);
|
|
801
|
+
|
|
802
|
+
if (value === undefined) {
|
|
803
|
+
throw new Error(\`Key not found: \${recordKey}\`);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
return {
|
|
807
|
+
contents: [{
|
|
808
|
+
uri: uri.href,
|
|
809
|
+
mimeType: 'application/json',
|
|
810
|
+
text: JSON.stringify(value, null, 2),
|
|
811
|
+
}],
|
|
812
|
+
};
|
|
813
|
+
},
|
|
814
|
+
);
|
|
815
|
+
},
|
|
816
|
+
};
|
|
817
|
+
`
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
path: "src/resources/config.ts",
|
|
821
|
+
content: `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
822
|
+
import fs from 'fs/promises';
|
|
823
|
+
import path from 'path';
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* Config resources \u2014 expose runtime config, env vars, and app settings.
|
|
827
|
+
*
|
|
828
|
+
* These are useful for giving the AI context about the environment
|
|
829
|
+
* without exposing sensitive credentials.
|
|
830
|
+
*/
|
|
831
|
+
export const configResources = {
|
|
832
|
+
register(server: McpServer) {
|
|
833
|
+
// Expose safe environment info
|
|
834
|
+
server.resource(
|
|
835
|
+
'environment',
|
|
836
|
+
'config://environment',
|
|
837
|
+
async (uri) => {
|
|
838
|
+
const safeEnv: Record<string, string> = {};
|
|
839
|
+
|
|
840
|
+
// Only expose non-sensitive env vars
|
|
841
|
+
const allowedPrefixes = ['NODE_', 'npm_', 'PATH', 'HOME', 'USER', 'SHELL', 'LANG'];
|
|
842
|
+
const blockedKeys = ['TOKEN', 'SECRET', 'PASSWORD', 'KEY', 'PRIVATE', 'AUTH', 'CREDENTIAL'];
|
|
843
|
+
|
|
844
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
845
|
+
if (value === undefined) continue;
|
|
846
|
+
|
|
847
|
+
const isBlocked = blockedKeys.some(b => key.toUpperCase().includes(b));
|
|
848
|
+
if (isBlocked) continue;
|
|
849
|
+
|
|
850
|
+
safeEnv[key] = value;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
return {
|
|
854
|
+
contents: [{
|
|
855
|
+
uri: uri.href,
|
|
856
|
+
mimeType: 'application/json',
|
|
857
|
+
text: JSON.stringify({
|
|
858
|
+
nodeVersion: process.version,
|
|
859
|
+
platform: process.platform,
|
|
860
|
+
arch: process.arch,
|
|
861
|
+
cwd: process.cwd(),
|
|
862
|
+
env: safeEnv,
|
|
863
|
+
}, null, 2),
|
|
864
|
+
}],
|
|
865
|
+
};
|
|
866
|
+
},
|
|
867
|
+
);
|
|
868
|
+
|
|
869
|
+
// Expose package.json info
|
|
870
|
+
server.resource(
|
|
871
|
+
'package-info',
|
|
872
|
+
'config://package',
|
|
873
|
+
async (uri) => {
|
|
874
|
+
try {
|
|
875
|
+
const pkgPath = path.resolve(process.cwd(), 'package.json');
|
|
876
|
+
const pkg = JSON.parse(await fs.readFile(pkgPath, 'utf8'));
|
|
877
|
+
|
|
878
|
+
// Strip sensitive fields
|
|
879
|
+
const { _authToken, publishConfig, ...safePkg } = pkg;
|
|
880
|
+
|
|
881
|
+
return {
|
|
882
|
+
contents: [{
|
|
883
|
+
uri: uri.href,
|
|
884
|
+
mimeType: 'application/json',
|
|
885
|
+
text: JSON.stringify(safePkg, null, 2),
|
|
886
|
+
}],
|
|
887
|
+
};
|
|
888
|
+
} catch {
|
|
889
|
+
return {
|
|
890
|
+
contents: [{
|
|
891
|
+
uri: uri.href,
|
|
892
|
+
mimeType: 'text/plain',
|
|
893
|
+
text: 'No package.json found in current directory',
|
|
894
|
+
}],
|
|
895
|
+
};
|
|
896
|
+
}
|
|
897
|
+
},
|
|
898
|
+
);
|
|
899
|
+
},
|
|
900
|
+
};
|
|
901
|
+
`
|
|
902
|
+
},
|
|
903
|
+
{
|
|
904
|
+
path: "package.json",
|
|
905
|
+
content: JSON.stringify({
|
|
906
|
+
name: packageName,
|
|
907
|
+
version: "0.1.0",
|
|
908
|
+
description: serverDescription,
|
|
909
|
+
type: "module",
|
|
910
|
+
scripts: {
|
|
911
|
+
build: "tsc",
|
|
912
|
+
dev: "tsc --watch",
|
|
913
|
+
start: "node dist/index.js",
|
|
914
|
+
prepublishOnly: "npm run build"
|
|
915
|
+
},
|
|
916
|
+
engines: { node: ">=18.0.0" },
|
|
917
|
+
dependencies: {
|
|
918
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
919
|
+
},
|
|
920
|
+
devDependencies: {
|
|
921
|
+
"@types/node": "^22.0.0",
|
|
922
|
+
typescript: "^5.0.0"
|
|
923
|
+
}
|
|
924
|
+
}, null, 2)
|
|
925
|
+
},
|
|
926
|
+
{
|
|
927
|
+
path: "tsconfig.json",
|
|
928
|
+
content: JSON.stringify({
|
|
929
|
+
compilerOptions: {
|
|
930
|
+
target: "ES2022",
|
|
931
|
+
module: "NodeNext",
|
|
932
|
+
moduleResolution: "NodeNext",
|
|
933
|
+
lib: ["ES2022"],
|
|
934
|
+
outDir: "dist",
|
|
935
|
+
rootDir: "src",
|
|
936
|
+
strict: true,
|
|
937
|
+
esModuleInterop: true,
|
|
938
|
+
skipLibCheck: true,
|
|
939
|
+
declaration: true,
|
|
940
|
+
sourceMap: true,
|
|
941
|
+
resolveJsonModule: true
|
|
942
|
+
},
|
|
943
|
+
include: ["src/**/*"],
|
|
944
|
+
exclude: ["node_modules", "dist"]
|
|
945
|
+
}, null, 2)
|
|
946
|
+
},
|
|
947
|
+
{
|
|
948
|
+
path: ".gitignore",
|
|
949
|
+
content: `node_modules/
|
|
950
|
+
dist/
|
|
951
|
+
*.js.map
|
|
952
|
+
*.d.ts.map
|
|
953
|
+
.env
|
|
954
|
+
.env.local
|
|
955
|
+
*.log
|
|
956
|
+
.DS_Store
|
|
957
|
+
`
|
|
958
|
+
},
|
|
959
|
+
{
|
|
960
|
+
path: "README.md",
|
|
961
|
+
content: `# ${serverName}
|
|
962
|
+
|
|
963
|
+
${serverDescription}
|
|
964
|
+
|
|
965
|
+
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) resource server that exposes file system, database, and configuration data as addressable resources.
|
|
966
|
+
|
|
967
|
+
## Resources
|
|
968
|
+
|
|
969
|
+
| URI Pattern | Description |
|
|
970
|
+
|-------------|-------------|
|
|
971
|
+
| \`file:///workspace\` | List files in working directory |
|
|
972
|
+
| \`file://{path}\` | Read any file by path |
|
|
973
|
+
| \`db://index\` | List all database keys |
|
|
974
|
+
| \`db://{namespace}/{key}\` | Fetch a specific record |
|
|
975
|
+
| \`config://environment\` | Safe environment info |
|
|
976
|
+
| \`config://package\` | Package.json info |
|
|
977
|
+
|
|
978
|
+
## Setup
|
|
979
|
+
|
|
980
|
+
\`\`\`bash
|
|
981
|
+
npm install
|
|
982
|
+
npm run build
|
|
983
|
+
\`\`\`
|
|
984
|
+
|
|
985
|
+
## Usage with Claude Desktop
|
|
986
|
+
|
|
987
|
+
Add to your Claude Desktop config:
|
|
988
|
+
|
|
989
|
+
\`\`\`json
|
|
990
|
+
{
|
|
991
|
+
"mcpServers": {
|
|
992
|
+
"${serverName}": {
|
|
993
|
+
"command": "node",
|
|
994
|
+
"args": ["${config.targetDir}/dist/index.js"]
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
\`\`\`
|
|
999
|
+
|
|
1000
|
+
## Development
|
|
1001
|
+
|
|
1002
|
+
\`\`\`bash
|
|
1003
|
+
npm run dev # Watch mode
|
|
1004
|
+
npm start # Run the server
|
|
1005
|
+
\`\`\`
|
|
1006
|
+
|
|
1007
|
+
## Connecting a Real Database
|
|
1008
|
+
|
|
1009
|
+
Replace the in-memory store in \`src/resources/database.ts\` with your actual data source:
|
|
1010
|
+
|
|
1011
|
+
\`\`\`typescript
|
|
1012
|
+
// Example with SQLite
|
|
1013
|
+
import Database from 'better-sqlite3';
|
|
1014
|
+
const db = new Database('./data.db');
|
|
1015
|
+
|
|
1016
|
+
// Example with Postgres
|
|
1017
|
+
import { Pool } from 'pg';
|
|
1018
|
+
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
|
|
1019
|
+
\`\`\`
|
|
1020
|
+
|
|
1021
|
+
## Author
|
|
1022
|
+
|
|
1023
|
+
${author}
|
|
1024
|
+
`
|
|
1025
|
+
}
|
|
1026
|
+
];
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// src/templates/prompt-server.ts
|
|
1030
|
+
function generatePromptServer(config) {
|
|
1031
|
+
const { serverName, serverDescription, packageName, author } = config;
|
|
1032
|
+
return [
|
|
1033
|
+
{
|
|
1034
|
+
path: "src/index.ts",
|
|
1035
|
+
content: `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
1036
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
1037
|
+
import { codeReviewPrompts } from './prompts/codeReview.js';
|
|
1038
|
+
import { documentationPrompts } from './prompts/documentation.js';
|
|
1039
|
+
import { debuggingPrompts } from './prompts/debugging.js';
|
|
1040
|
+
import { refactoringPrompts } from './prompts/refactoring.js';
|
|
1041
|
+
|
|
1042
|
+
const server = new McpServer({
|
|
1043
|
+
name: '${serverName}',
|
|
1044
|
+
version: '0.1.0',
|
|
1045
|
+
});
|
|
1046
|
+
|
|
1047
|
+
// Register prompt collections
|
|
1048
|
+
codeReviewPrompts.register(server);
|
|
1049
|
+
documentationPrompts.register(server);
|
|
1050
|
+
debuggingPrompts.register(server);
|
|
1051
|
+
refactoringPrompts.register(server);
|
|
1052
|
+
|
|
1053
|
+
async function main() {
|
|
1054
|
+
const transport = new StdioServerTransport();
|
|
1055
|
+
await server.connect(transport);
|
|
1056
|
+
console.error('${serverName} MCP prompt server running on stdio');
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
main().catch((err) => {
|
|
1060
|
+
console.error('Fatal error:', err);
|
|
1061
|
+
process.exit(1);
|
|
1062
|
+
});
|
|
1063
|
+
`
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
path: "src/prompts/codeReview.ts",
|
|
1067
|
+
content: `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
1068
|
+
import { z } from 'zod';
|
|
1069
|
+
|
|
1070
|
+
/**
|
|
1071
|
+
* Code review prompt templates.
|
|
1072
|
+
* Structured prompts that guide thorough, consistent code reviews.
|
|
1073
|
+
*/
|
|
1074
|
+
export const codeReviewPrompts = {
|
|
1075
|
+
register(server: McpServer) {
|
|
1076
|
+
server.prompt(
|
|
1077
|
+
'code-review',
|
|
1078
|
+
'Comprehensive code review with focus areas',
|
|
1079
|
+
{
|
|
1080
|
+
code: z.string().describe('The code to review'),
|
|
1081
|
+
language: z.string().optional().describe('Programming language (e.g. TypeScript, Python)'),
|
|
1082
|
+
context: z.string().optional().describe('What does this code do? Any relevant context?'),
|
|
1083
|
+
focus: z.enum(['security', 'performance', 'readability', 'correctness', 'all'])
|
|
1084
|
+
.optional()
|
|
1085
|
+
.default('all')
|
|
1086
|
+
.describe('Area to focus the review on'),
|
|
1087
|
+
},
|
|
1088
|
+
({ code, language, context, focus }) => {
|
|
1089
|
+
const lang = language ?? 'unknown';
|
|
1090
|
+
const focusInstructions: Record<string, string> = {
|
|
1091
|
+
security: \`Focus specifically on security vulnerabilities:
|
|
1092
|
+
- Input validation and sanitization
|
|
1093
|
+
- Authentication and authorization flaws
|
|
1094
|
+
- Injection vulnerabilities (SQL, XSS, command)
|
|
1095
|
+
- Sensitive data exposure
|
|
1096
|
+
- Insecure dependencies\`,
|
|
1097
|
+
performance: \`Focus specifically on performance:
|
|
1098
|
+
- Time complexity of algorithms (Big O)
|
|
1099
|
+
- Unnecessary re-renders or recomputations
|
|
1100
|
+
- Memory leaks or excessive allocations
|
|
1101
|
+
- N+1 query problems
|
|
1102
|
+
- Missing indexes or caching opportunities\`,
|
|
1103
|
+
readability: \`Focus specifically on readability and maintainability:
|
|
1104
|
+
- Naming clarity (variables, functions, classes)
|
|
1105
|
+
- Function/method length and single responsibility
|
|
1106
|
+
- Comment quality (explain why, not what)
|
|
1107
|
+
- Consistent style and conventions
|
|
1108
|
+
- Dead code or confusing abstractions\`,
|
|
1109
|
+
correctness: \`Focus specifically on correctness:
|
|
1110
|
+
- Edge cases and boundary conditions
|
|
1111
|
+
- Error handling completeness
|
|
1112
|
+
- Race conditions or concurrency issues
|
|
1113
|
+
- Incorrect assumptions in logic
|
|
1114
|
+
- Missing null/undefined checks\`,
|
|
1115
|
+
all: \`Review all aspects: security, performance, readability, correctness, and design.\`,
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
return {
|
|
1119
|
+
messages: [
|
|
1120
|
+
{
|
|
1121
|
+
role: 'user',
|
|
1122
|
+
content: {
|
|
1123
|
+
type: 'text',
|
|
1124
|
+
text: \`Please perform a detailed code review of the following \${lang} code.
|
|
1125
|
+
|
|
1126
|
+
\${context ? \`Context: \${context}\\n\\n\` : ''}\${focusInstructions[focus ?? 'all']}
|
|
1127
|
+
|
|
1128
|
+
For each issue found:
|
|
1129
|
+
1. Quote the specific code
|
|
1130
|
+
2. Explain the problem
|
|
1131
|
+
3. Provide a concrete fix
|
|
1132
|
+
|
|
1133
|
+
Rate severity as: \u{1F534} Critical | \u{1F7E0} High | \u{1F7E1} Medium | \u{1F535} Low | \u{1F4A1} Suggestion
|
|
1134
|
+
|
|
1135
|
+
Code to review:
|
|
1136
|
+
\\\`\\\`\\\`\${lang}
|
|
1137
|
+
\${code}
|
|
1138
|
+
\\\`\\\`\\\`\`,
|
|
1139
|
+
},
|
|
1140
|
+
},
|
|
1141
|
+
],
|
|
1142
|
+
};
|
|
1143
|
+
},
|
|
1144
|
+
);
|
|
1145
|
+
|
|
1146
|
+
server.prompt(
|
|
1147
|
+
'security-audit',
|
|
1148
|
+
'Security-focused code audit',
|
|
1149
|
+
{
|
|
1150
|
+
code: z.string().describe('Code to audit'),
|
|
1151
|
+
threat_model: z.string().optional().describe('Who are the potential attackers? What are they after?'),
|
|
1152
|
+
},
|
|
1153
|
+
({ code, threat_model }) => ({
|
|
1154
|
+
messages: [
|
|
1155
|
+
{
|
|
1156
|
+
role: 'user',
|
|
1157
|
+
content: {
|
|
1158
|
+
type: 'text',
|
|
1159
|
+
text: \`Perform a security audit on this code.
|
|
1160
|
+
|
|
1161
|
+
\${threat_model ? \`Threat model: \${threat_model}\\n\\n\` : ''}
|
|
1162
|
+
Check for:
|
|
1163
|
+
- OWASP Top 10 vulnerabilities
|
|
1164
|
+
- Authentication/authorization bypasses
|
|
1165
|
+
- Data validation and sanitization
|
|
1166
|
+
- Cryptographic weaknesses
|
|
1167
|
+
- Dependency vulnerabilities
|
|
1168
|
+
- Secrets or credentials in code
|
|
1169
|
+
- Information disclosure
|
|
1170
|
+
|
|
1171
|
+
For each finding, provide:
|
|
1172
|
+
- CVE category (if applicable)
|
|
1173
|
+
- Severity (Critical/High/Medium/Low)
|
|
1174
|
+
- Attack vector
|
|
1175
|
+
- Remediation steps
|
|
1176
|
+
|
|
1177
|
+
Code:
|
|
1178
|
+
\\\`\\\`\\\`
|
|
1179
|
+
\${code}
|
|
1180
|
+
\\\`\\\`\\\`\`,
|
|
1181
|
+
},
|
|
1182
|
+
},
|
|
1183
|
+
],
|
|
1184
|
+
}),
|
|
1185
|
+
);
|
|
1186
|
+
},
|
|
1187
|
+
};
|
|
1188
|
+
`
|
|
1189
|
+
},
|
|
1190
|
+
{
|
|
1191
|
+
path: "src/prompts/documentation.ts",
|
|
1192
|
+
content: `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
1193
|
+
import { z } from 'zod';
|
|
1194
|
+
|
|
1195
|
+
/**
|
|
1196
|
+
* Documentation generation prompts.
|
|
1197
|
+
*/
|
|
1198
|
+
export const documentationPrompts = {
|
|
1199
|
+
register(server: McpServer) {
|
|
1200
|
+
server.prompt(
|
|
1201
|
+
'generate-docs',
|
|
1202
|
+
'Generate comprehensive documentation for code',
|
|
1203
|
+
{
|
|
1204
|
+
code: z.string().describe('Code to document'),
|
|
1205
|
+
style: z.enum(['jsdoc', 'tsdoc', 'docstring', 'markdown']).optional().default('tsdoc'),
|
|
1206
|
+
audience: z.enum(['internal', 'public-api', 'tutorial']).optional().default('public-api'),
|
|
1207
|
+
},
|
|
1208
|
+
({ code, style, audience }) => {
|
|
1209
|
+
const styleGuides: Record<string, string> = {
|
|
1210
|
+
jsdoc: 'Use JSDoc format with @param, @returns, @throws, @example tags',
|
|
1211
|
+
tsdoc: 'Use TSDoc format with @param, @returns, @throws, @example, @remarks tags',
|
|
1212
|
+
docstring: 'Use Python docstring format (Google style)',
|
|
1213
|
+
markdown: 'Generate a Markdown documentation section',
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
const audienceGuides: Record<string, string> = {
|
|
1217
|
+
internal: 'This is for internal developers. Focus on implementation details and gotchas.',
|
|
1218
|
+
'public-api': 'This is a public API. Focus on usage, parameters, and examples. Hide implementation details.',
|
|
1219
|
+
tutorial: 'This is for beginners. Explain concepts, provide step-by-step examples.',
|
|
1220
|
+
};
|
|
1221
|
+
|
|
1222
|
+
return {
|
|
1223
|
+
messages: [
|
|
1224
|
+
{
|
|
1225
|
+
role: 'user',
|
|
1226
|
+
content: {
|
|
1227
|
+
type: 'text',
|
|
1228
|
+
text: \`Generate documentation for the following code.
|
|
1229
|
+
|
|
1230
|
+
Format: \${styleGuides[style ?? 'tsdoc']}
|
|
1231
|
+
Audience: \${audienceGuides[audience ?? 'public-api']}
|
|
1232
|
+
|
|
1233
|
+
Include:
|
|
1234
|
+
- Overview/description
|
|
1235
|
+
- Parameter documentation with types
|
|
1236
|
+
- Return value documentation
|
|
1237
|
+
- Thrown errors/exceptions
|
|
1238
|
+
- At least one usage example
|
|
1239
|
+
- Any important notes or caveats
|
|
1240
|
+
|
|
1241
|
+
Code:
|
|
1242
|
+
\\\`\\\`\\\`
|
|
1243
|
+
\${code}
|
|
1244
|
+
\\\`\\\`\\\`\`,
|
|
1245
|
+
},
|
|
1246
|
+
},
|
|
1247
|
+
],
|
|
1248
|
+
};
|
|
1249
|
+
},
|
|
1250
|
+
);
|
|
1251
|
+
|
|
1252
|
+
server.prompt(
|
|
1253
|
+
'readme-generator',
|
|
1254
|
+
'Generate a README.md for a project',
|
|
1255
|
+
{
|
|
1256
|
+
project_name: z.string().describe('Project name'),
|
|
1257
|
+
description: z.string().describe('What does this project do?'),
|
|
1258
|
+
tech_stack: z.string().describe('Technologies used (e.g. Node.js, TypeScript, React)'),
|
|
1259
|
+
main_features: z.string().describe('Key features, one per line'),
|
|
1260
|
+
},
|
|
1261
|
+
({ project_name, description, tech_stack, main_features }) => ({
|
|
1262
|
+
messages: [
|
|
1263
|
+
{
|
|
1264
|
+
role: 'user',
|
|
1265
|
+
content: {
|
|
1266
|
+
type: 'text',
|
|
1267
|
+
text: \`Generate a professional README.md for this project.
|
|
1268
|
+
|
|
1269
|
+
Project: \${project_name}
|
|
1270
|
+
Description: \${description}
|
|
1271
|
+
Tech Stack: \${tech_stack}
|
|
1272
|
+
Features:
|
|
1273
|
+
\${main_features}
|
|
1274
|
+
|
|
1275
|
+
Include these sections:
|
|
1276
|
+
1. Title and badges (placeholder)
|
|
1277
|
+
2. Description
|
|
1278
|
+
3. Features list
|
|
1279
|
+
4. Installation
|
|
1280
|
+
5. Quick start / usage example
|
|
1281
|
+
6. API reference (if applicable)
|
|
1282
|
+
7. Configuration
|
|
1283
|
+
8. Contributing
|
|
1284
|
+
9. License
|
|
1285
|
+
|
|
1286
|
+
Make it clear, engaging, and developer-friendly.\`,
|
|
1287
|
+
},
|
|
1288
|
+
},
|
|
1289
|
+
],
|
|
1290
|
+
}),
|
|
1291
|
+
);
|
|
1292
|
+
},
|
|
1293
|
+
};
|
|
1294
|
+
`
|
|
1295
|
+
},
|
|
1296
|
+
{
|
|
1297
|
+
path: "src/prompts/debugging.ts",
|
|
1298
|
+
content: `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
1299
|
+
import { z } from 'zod';
|
|
1300
|
+
|
|
1301
|
+
/**
|
|
1302
|
+
* Debugging assistance prompts.
|
|
1303
|
+
*/
|
|
1304
|
+
export const debuggingPrompts = {
|
|
1305
|
+
register(server: McpServer) {
|
|
1306
|
+
server.prompt(
|
|
1307
|
+
'debug-error',
|
|
1308
|
+
'Debug an error with full context analysis',
|
|
1309
|
+
{
|
|
1310
|
+
error_message: z.string().describe('The error message or stack trace'),
|
|
1311
|
+
code: z.string().optional().describe('Relevant code where the error occurs'),
|
|
1312
|
+
expected: z.string().optional().describe('What you expected to happen'),
|
|
1313
|
+
actual: z.string().optional().describe('What actually happened'),
|
|
1314
|
+
environment: z.string().optional().describe('Runtime environment (e.g. Node 18, Chrome 120)'),
|
|
1315
|
+
},
|
|
1316
|
+
({ error_message, code, expected, actual, environment }) => ({
|
|
1317
|
+
messages: [
|
|
1318
|
+
{
|
|
1319
|
+
role: 'user',
|
|
1320
|
+
content: {
|
|
1321
|
+
type: 'text',
|
|
1322
|
+
text: \`Help me debug this error.
|
|
1323
|
+
|
|
1324
|
+
Error:
|
|
1325
|
+
\\\`\\\`\\\`
|
|
1326
|
+
\${error_message}
|
|
1327
|
+
\\\`\\\`\\\`
|
|
1328
|
+
\${code ? \`\\nRelevant code:\\n\\\`\\\`\\\`\\n\${code}\\n\\\`\\\`\\\`\` : ''}
|
|
1329
|
+
\${expected ? \`\\nExpected: \${expected}\` : ''}
|
|
1330
|
+
\${actual ? \`\\nActual: \${actual}\` : ''}
|
|
1331
|
+
\${environment ? \`\\nEnvironment: \${environment}\` : ''}
|
|
1332
|
+
|
|
1333
|
+
Please:
|
|
1334
|
+
1. Identify the root cause
|
|
1335
|
+
2. Explain why this error occurs
|
|
1336
|
+
3. Provide a step-by-step fix
|
|
1337
|
+
4. Suggest how to prevent this in the future\`,
|
|
1338
|
+
},
|
|
1339
|
+
},
|
|
1340
|
+
],
|
|
1341
|
+
}),
|
|
1342
|
+
);
|
|
1343
|
+
|
|
1344
|
+
server.prompt(
|
|
1345
|
+
'rubber-duck',
|
|
1346
|
+
'Explain code logic to find bugs through explanation',
|
|
1347
|
+
{
|
|
1348
|
+
code: z.string().describe('Code you want to explain/debug'),
|
|
1349
|
+
problem: z.string().describe('What problem are you trying to solve with this code?'),
|
|
1350
|
+
},
|
|
1351
|
+
({ code, problem }) => ({
|
|
1352
|
+
messages: [
|
|
1353
|
+
{
|
|
1354
|
+
role: 'user',
|
|
1355
|
+
content: {
|
|
1356
|
+
type: 'text',
|
|
1357
|
+
text: \`I'll explain my code to you and you help me find bugs through questioning.
|
|
1358
|
+
|
|
1359
|
+
Problem I'm trying to solve: \${problem}
|
|
1360
|
+
|
|
1361
|
+
My code:
|
|
1362
|
+
\\\`\\\`\\\`
|
|
1363
|
+
\${code}
|
|
1364
|
+
\\\`\\\`\\\`
|
|
1365
|
+
|
|
1366
|
+
As I explain my code, please:
|
|
1367
|
+
1. Ask clarifying questions about assumptions I'm making
|
|
1368
|
+
2. Point out any logical inconsistencies
|
|
1369
|
+
3. Highlight edge cases I might not be handling
|
|
1370
|
+
4. Don't give me the answer directly \u2014 help me discover it through questions
|
|
1371
|
+
|
|
1372
|
+
Let's start: what questions do you have about my approach?\`,
|
|
1373
|
+
},
|
|
1374
|
+
},
|
|
1375
|
+
],
|
|
1376
|
+
}),
|
|
1377
|
+
);
|
|
1378
|
+
},
|
|
1379
|
+
};
|
|
1380
|
+
`
|
|
1381
|
+
},
|
|
1382
|
+
{
|
|
1383
|
+
path: "src/prompts/refactoring.ts",
|
|
1384
|
+
content: `import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
1385
|
+
import { z } from 'zod';
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* Code refactoring prompts.
|
|
1389
|
+
*/
|
|
1390
|
+
export const refactoringPrompts = {
|
|
1391
|
+
register(server: McpServer) {
|
|
1392
|
+
server.prompt(
|
|
1393
|
+
'refactor',
|
|
1394
|
+
'Refactor code with specific goals',
|
|
1395
|
+
{
|
|
1396
|
+
code: z.string().describe('Code to refactor'),
|
|
1397
|
+
goals: z.string().describe('What do you want to improve? (e.g. reduce complexity, improve testability)'),
|
|
1398
|
+
constraints: z.string().optional().describe('Any constraints (e.g. must keep same API, cannot use classes)'),
|
|
1399
|
+
language: z.string().optional().describe('Programming language'),
|
|
1400
|
+
},
|
|
1401
|
+
({ code, goals, constraints, language }) => ({
|
|
1402
|
+
messages: [
|
|
1403
|
+
{
|
|
1404
|
+
role: 'user',
|
|
1405
|
+
content: {
|
|
1406
|
+
type: 'text',
|
|
1407
|
+
text: \`Refactor this \${language ?? ''} code to achieve the following goals:
|
|
1408
|
+
|
|
1409
|
+
Goals: \${goals}
|
|
1410
|
+
\${constraints ? \`Constraints: \${constraints}\` : ''}
|
|
1411
|
+
|
|
1412
|
+
Original code:
|
|
1413
|
+
\\\`\\\`\\\`\${language ?? ''}
|
|
1414
|
+
\${code}
|
|
1415
|
+
\\\`\\\`\\\`
|
|
1416
|
+
|
|
1417
|
+
Please:
|
|
1418
|
+
1. Explain your refactoring strategy
|
|
1419
|
+
2. Provide the refactored code
|
|
1420
|
+
3. Highlight what changed and why
|
|
1421
|
+
4. Note any tradeoffs\`,
|
|
1422
|
+
},
|
|
1423
|
+
},
|
|
1424
|
+
],
|
|
1425
|
+
}),
|
|
1426
|
+
);
|
|
1427
|
+
|
|
1428
|
+
server.prompt(
|
|
1429
|
+
'extract-function',
|
|
1430
|
+
'Extract logic into well-named functions',
|
|
1431
|
+
{
|
|
1432
|
+
code: z.string().describe('Code containing logic to extract'),
|
|
1433
|
+
description: z.string().describe('Describe the logic you want to extract'),
|
|
1434
|
+
},
|
|
1435
|
+
({ code, description }) => ({
|
|
1436
|
+
messages: [
|
|
1437
|
+
{
|
|
1438
|
+
role: 'user',
|
|
1439
|
+
content: {
|
|
1440
|
+
type: 'text',
|
|
1441
|
+
text: \`Help me extract the following logic into a well-named function:
|
|
1442
|
+
|
|
1443
|
+
Logic to extract: \${description}
|
|
1444
|
+
|
|
1445
|
+
From this code:
|
|
1446
|
+
\\\`\\\`\\\`
|
|
1447
|
+
\${code}
|
|
1448
|
+
\\\`\\\`\\\`
|
|
1449
|
+
|
|
1450
|
+
Please provide:
|
|
1451
|
+
1. The extracted function with a clear, descriptive name
|
|
1452
|
+
2. The modified original code calling the new function
|
|
1453
|
+
3. Any parameters the function needs
|
|
1454
|
+
4. Proper TypeScript types if applicable\`,
|
|
1455
|
+
},
|
|
1456
|
+
},
|
|
1457
|
+
],
|
|
1458
|
+
}),
|
|
1459
|
+
);
|
|
1460
|
+
},
|
|
1461
|
+
};
|
|
1462
|
+
`
|
|
1463
|
+
},
|
|
1464
|
+
{
|
|
1465
|
+
path: "package.json",
|
|
1466
|
+
content: JSON.stringify({
|
|
1467
|
+
name: packageName,
|
|
1468
|
+
version: "0.1.0",
|
|
1469
|
+
description: serverDescription,
|
|
1470
|
+
type: "module",
|
|
1471
|
+
scripts: {
|
|
1472
|
+
build: "tsc",
|
|
1473
|
+
dev: "tsc --watch",
|
|
1474
|
+
start: "node dist/index.js",
|
|
1475
|
+
prepublishOnly: "npm run build"
|
|
1476
|
+
},
|
|
1477
|
+
engines: { node: ">=18.0.0" },
|
|
1478
|
+
dependencies: {
|
|
1479
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
1480
|
+
zod: "^3.22.0"
|
|
1481
|
+
},
|
|
1482
|
+
devDependencies: {
|
|
1483
|
+
"@types/node": "^22.0.0",
|
|
1484
|
+
typescript: "^5.0.0"
|
|
1485
|
+
}
|
|
1486
|
+
}, null, 2)
|
|
1487
|
+
},
|
|
1488
|
+
{
|
|
1489
|
+
path: "tsconfig.json",
|
|
1490
|
+
content: JSON.stringify({
|
|
1491
|
+
compilerOptions: {
|
|
1492
|
+
target: "ES2022",
|
|
1493
|
+
module: "NodeNext",
|
|
1494
|
+
moduleResolution: "NodeNext",
|
|
1495
|
+
lib: ["ES2022"],
|
|
1496
|
+
outDir: "dist",
|
|
1497
|
+
rootDir: "src",
|
|
1498
|
+
strict: true,
|
|
1499
|
+
esModuleInterop: true,
|
|
1500
|
+
skipLibCheck: true,
|
|
1501
|
+
declaration: true,
|
|
1502
|
+
sourceMap: true,
|
|
1503
|
+
resolveJsonModule: true
|
|
1504
|
+
},
|
|
1505
|
+
include: ["src/**/*"],
|
|
1506
|
+
exclude: ["node_modules", "dist"]
|
|
1507
|
+
}, null, 2)
|
|
1508
|
+
},
|
|
1509
|
+
{
|
|
1510
|
+
path: ".gitignore",
|
|
1511
|
+
content: `node_modules/
|
|
1512
|
+
dist/
|
|
1513
|
+
*.js.map
|
|
1514
|
+
*.d.ts.map
|
|
1515
|
+
.env
|
|
1516
|
+
.env.local
|
|
1517
|
+
*.log
|
|
1518
|
+
.DS_Store
|
|
1519
|
+
`
|
|
1520
|
+
},
|
|
1521
|
+
{
|
|
1522
|
+
path: "README.md",
|
|
1523
|
+
content: `# ${serverName}
|
|
1524
|
+
|
|
1525
|
+
${serverDescription}
|
|
1526
|
+
|
|
1527
|
+
A [Model Context Protocol (MCP)](https://modelcontextprotocol.io) prompt server that provides structured, high-quality prompts for software development tasks.
|
|
1528
|
+
|
|
1529
|
+
## Prompts
|
|
1530
|
+
|
|
1531
|
+
### Code Review
|
|
1532
|
+
| Prompt | Description |
|
|
1533
|
+
|--------|-------------|
|
|
1534
|
+
| \`code-review\` | Comprehensive review with severity ratings |
|
|
1535
|
+
| \`security-audit\` | Security-focused code audit |
|
|
1536
|
+
|
|
1537
|
+
### Documentation
|
|
1538
|
+
| Prompt | Description |
|
|
1539
|
+
|--------|-------------|
|
|
1540
|
+
| \`generate-docs\` | Generate JSDoc/TSDoc/docstrings |
|
|
1541
|
+
| \`readme-generator\` | Generate a full README.md |
|
|
1542
|
+
|
|
1543
|
+
### Debugging
|
|
1544
|
+
| Prompt | Description |
|
|
1545
|
+
|--------|-------------|
|
|
1546
|
+
| \`debug-error\` | Debug errors with full context |
|
|
1547
|
+
| \`rubber-duck\` | Guided explanation-based debugging |
|
|
1548
|
+
|
|
1549
|
+
### Refactoring
|
|
1550
|
+
| Prompt | Description |
|
|
1551
|
+
|--------|-------------|
|
|
1552
|
+
| \`refactor\` | Refactor with specific goals and constraints |
|
|
1553
|
+
| \`extract-function\` | Extract logic into named functions |
|
|
1554
|
+
|
|
1555
|
+
## Setup
|
|
1556
|
+
|
|
1557
|
+
\`\`\`bash
|
|
1558
|
+
npm install
|
|
1559
|
+
npm run build
|
|
1560
|
+
\`\`\`
|
|
1561
|
+
|
|
1562
|
+
## Usage with Claude Desktop
|
|
1563
|
+
|
|
1564
|
+
\`\`\`json
|
|
1565
|
+
{
|
|
1566
|
+
"mcpServers": {
|
|
1567
|
+
"${serverName}": {
|
|
1568
|
+
"command": "node",
|
|
1569
|
+
"args": ["${config.targetDir}/dist/index.js"]
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
\`\`\`
|
|
1574
|
+
|
|
1575
|
+
## Development
|
|
1576
|
+
|
|
1577
|
+
\`\`\`bash
|
|
1578
|
+
npm run dev # Watch mode
|
|
1579
|
+
npm start # Run the server
|
|
1580
|
+
\`\`\`
|
|
1581
|
+
|
|
1582
|
+
## Adding Prompts
|
|
1583
|
+
|
|
1584
|
+
Create a new file in \`src/prompts/\` and register it in \`src/index.ts\`:
|
|
1585
|
+
|
|
1586
|
+
\`\`\`typescript
|
|
1587
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
1588
|
+
import { z } from 'zod';
|
|
1589
|
+
|
|
1590
|
+
export const myPrompts = {
|
|
1591
|
+
register(server: McpServer) {
|
|
1592
|
+
server.prompt(
|
|
1593
|
+
'my-prompt-name',
|
|
1594
|
+
'Description of what this prompt does',
|
|
1595
|
+
{
|
|
1596
|
+
input: z.string().describe('User input'),
|
|
1597
|
+
},
|
|
1598
|
+
({ input }) => ({
|
|
1599
|
+
messages: [
|
|
1600
|
+
{
|
|
1601
|
+
role: 'user',
|
|
1602
|
+
content: { type: 'text', text: \`Do something with: \${input}\` },
|
|
1603
|
+
},
|
|
1604
|
+
],
|
|
1605
|
+
}),
|
|
1606
|
+
);
|
|
1607
|
+
},
|
|
1608
|
+
};
|
|
1609
|
+
\`\`\`
|
|
1610
|
+
|
|
1611
|
+
## Author
|
|
1612
|
+
|
|
1613
|
+
${author}
|
|
1614
|
+
`
|
|
1615
|
+
}
|
|
1616
|
+
];
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
// src/commands/init.ts
|
|
1620
|
+
async function initCommand(dirArg, opts) {
|
|
1621
|
+
const targetName = dirArg ?? ".";
|
|
1622
|
+
const targetDir = path2.resolve(process.cwd(), targetName);
|
|
1623
|
+
const dirName = path2.basename(targetDir);
|
|
1624
|
+
console.log();
|
|
1625
|
+
console.log(chalk2.bold.cyan("\u26A1 create-mcp-server") + chalk2.dim(" \u2014 scaffold your MCP server"));
|
|
1626
|
+
console.log();
|
|
1627
|
+
const targetExists = await fs2.pathExists(targetDir);
|
|
1628
|
+
if (targetExists && !opts.dryRun) {
|
|
1629
|
+
const entries = await fs2.readdir(targetDir).catch(() => []);
|
|
1630
|
+
const hasContent = entries.some((e) => !e.startsWith("."));
|
|
1631
|
+
if (hasContent && !opts.force) {
|
|
1632
|
+
console.log(chalk2.yellow(`\u26A0\uFE0F Directory ${chalk2.bold(targetDir)} already has files.`));
|
|
1633
|
+
console.log(chalk2.dim(" Existing files will be skipped. Use --force to overwrite."));
|
|
1634
|
+
console.log();
|
|
1635
|
+
}
|
|
1636
|
+
}
|
|
1637
|
+
let config;
|
|
1638
|
+
const isInteractive = !opts.yes && process.stdin.isTTY;
|
|
1639
|
+
if (isInteractive) {
|
|
1640
|
+
const answers = await runWizard({
|
|
1641
|
+
serverName: dirName !== "." ? dirName : void 0,
|
|
1642
|
+
template: opts.template
|
|
1643
|
+
});
|
|
1644
|
+
config = buildDefaultConfig(targetDir, dirName, {
|
|
1645
|
+
serverName: answers.serverName,
|
|
1646
|
+
serverDescription: answers.serverDescription,
|
|
1647
|
+
template: answers.template,
|
|
1648
|
+
packageName: answers.packageName,
|
|
1649
|
+
author: answers.author,
|
|
1650
|
+
gitInit: answers.gitInit,
|
|
1651
|
+
dryRun: opts.dryRun ?? false
|
|
1652
|
+
});
|
|
1653
|
+
} else {
|
|
1654
|
+
config = buildDefaultConfig(targetDir, dirName, {
|
|
1655
|
+
serverName: dirName !== "." ? dirName : "my-mcp-server",
|
|
1656
|
+
packageName: dirName !== "." ? dirName : "my-mcp-server",
|
|
1657
|
+
template: opts.template ?? "tool-server",
|
|
1658
|
+
dryRun: opts.dryRun ?? false,
|
|
1659
|
+
yes: true
|
|
1660
|
+
});
|
|
1661
|
+
console.log(chalk2.dim(`Running with defaults. Template: ${config.template}`));
|
|
1662
|
+
console.log();
|
|
1663
|
+
}
|
|
1664
|
+
const files = getTemplateFiles(config);
|
|
1665
|
+
if (config.dryRun) {
|
|
1666
|
+
console.log(chalk2.bold.yellow("\u{1F50D} Dry run \u2014 no files will be written\n"));
|
|
1667
|
+
printDryRunTree(config.targetDir, files);
|
|
1668
|
+
console.log(chalk2.dim(` ${files.length} files would be created`));
|
|
1669
|
+
console.log();
|
|
1670
|
+
console.log(chalk2.dim("Run without --dry-run to scaffold the server."));
|
|
1671
|
+
return;
|
|
1672
|
+
}
|
|
1673
|
+
const spinner = ora("Scaffolding MCP server...").start();
|
|
1674
|
+
let results;
|
|
1675
|
+
try {
|
|
1676
|
+
results = await writeFiles(config.targetDir, files, opts.force ?? false);
|
|
1677
|
+
spinner.succeed("Server scaffolded");
|
|
1678
|
+
} catch (err) {
|
|
1679
|
+
spinner.fail("Failed to write files");
|
|
1680
|
+
throw err;
|
|
1681
|
+
}
|
|
1682
|
+
if (config.gitInit) {
|
|
1683
|
+
const gitSpinner = ora("Initializing git...").start();
|
|
1684
|
+
try {
|
|
1685
|
+
const { simpleGit } = await import("simple-git");
|
|
1686
|
+
const git = simpleGit(config.targetDir);
|
|
1687
|
+
const isRepo = await git.checkIsRepo().catch(() => false);
|
|
1688
|
+
if (!isRepo) {
|
|
1689
|
+
await git.init();
|
|
1690
|
+
gitSpinner.succeed("Git repository initialized");
|
|
1691
|
+
} else {
|
|
1692
|
+
gitSpinner.info("Git already initialized, skipped");
|
|
1693
|
+
}
|
|
1694
|
+
} catch {
|
|
1695
|
+
gitSpinner.warn("Git init failed (git may not be installed)");
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
const created = results.filter((r) => r.status === "created");
|
|
1699
|
+
const skipped = results.filter((r) => r.status === "skipped");
|
|
1700
|
+
console.log();
|
|
1701
|
+
console.log(chalk2.bold.green("\u2705 MCP server scaffolded at ") + chalk2.bold(config.targetDir));
|
|
1702
|
+
console.log();
|
|
1703
|
+
printWriteResults(results);
|
|
1704
|
+
console.log();
|
|
1705
|
+
console.log(chalk2.dim(` ${created.length} file(s) created${skipped.length > 0 ? `, ${skipped.length} skipped` : ""}`));
|
|
1706
|
+
console.log();
|
|
1707
|
+
console.log(chalk2.bold("Next steps:"));
|
|
1708
|
+
const rel = path2.relative(process.cwd(), config.targetDir);
|
|
1709
|
+
if (rel && rel !== ".") {
|
|
1710
|
+
console.log(` ${chalk2.cyan(`cd ${rel}`)}`);
|
|
1711
|
+
}
|
|
1712
|
+
console.log(` ${chalk2.cyan("npm install")}`);
|
|
1713
|
+
console.log(` ${chalk2.cyan("npm run build")}`);
|
|
1714
|
+
console.log(` ${chalk2.cyan("npm start")}`);
|
|
1715
|
+
console.log();
|
|
1716
|
+
console.log(chalk2.dim(" Then add to Claude Desktop config:"));
|
|
1717
|
+
console.log(chalk2.dim(` "command": "node", "args": ["${config.targetDir}/dist/index.js"]`));
|
|
1718
|
+
console.log();
|
|
1719
|
+
console.log(chalk2.dim(" See README.md for full setup instructions."));
|
|
1720
|
+
console.log();
|
|
1721
|
+
}
|
|
1722
|
+
function getTemplateFiles(config) {
|
|
1723
|
+
switch (config.template) {
|
|
1724
|
+
case "tool-server":
|
|
1725
|
+
return generateToolServer(config);
|
|
1726
|
+
case "resource-server":
|
|
1727
|
+
return generateResourceServer(config);
|
|
1728
|
+
case "prompt-server":
|
|
1729
|
+
return generatePromptServer(config);
|
|
1730
|
+
default:
|
|
1731
|
+
return generateToolServer(config);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// src/cli.ts
|
|
1736
|
+
var VERSION = "0.1.0";
|
|
1737
|
+
var program = new Command();
|
|
1738
|
+
program.name("create-mcp-server").description("Scaffold production-ready MCP (Model Context Protocol) servers").version(VERSION, "-v, --version", "Show version").addHelpText(
|
|
1739
|
+
"after",
|
|
1740
|
+
`
|
|
1741
|
+
${chalk3.bold("Examples:")}
|
|
1742
|
+
${chalk3.cyan("npx @webbywisp/create-mcp-server my-server")} Scaffold with prompts
|
|
1743
|
+
${chalk3.cyan("npx @webbywisp/create-mcp-server my-server --yes")} Use all defaults (tool-server)
|
|
1744
|
+
${chalk3.cyan("npx @webbywisp/create-mcp-server my-server -t resource-server")} Use resource-server template
|
|
1745
|
+
${chalk3.cyan("npx @webbywisp/create-mcp-server my-server --dry-run")} Preview without writing files
|
|
1746
|
+
|
|
1747
|
+
${chalk3.bold("Templates:")}
|
|
1748
|
+
${chalk3.cyan("tool-server")} Expose tools (functions AI can call) \u2014 file I/O, HTTP, exec
|
|
1749
|
+
${chalk3.cyan("resource-server")} Expose data as addressable URIs \u2014 files, DB, config
|
|
1750
|
+
${chalk3.cyan("prompt-server")} Provide structured prompt templates \u2014 review, debug, refactor
|
|
1751
|
+
`
|
|
1752
|
+
);
|
|
1753
|
+
program.command("init [directory]", { isDefault: true }).description("Scaffold a new MCP server (default command)").option("-t, --template <name>", "Template: tool-server | resource-server | prompt-server", "tool-server").option("-y, --yes", "Skip prompts, use all defaults").option("--dry-run", "Preview what would be created without writing files").option("--force", "Overwrite existing files").action(async (dir, opts) => {
|
|
1754
|
+
try {
|
|
1755
|
+
await initCommand(dir, {
|
|
1756
|
+
template: opts.template,
|
|
1757
|
+
yes: opts.yes,
|
|
1758
|
+
dryRun: opts.dryRun,
|
|
1759
|
+
force: opts.force
|
|
1760
|
+
});
|
|
1761
|
+
} catch (err) {
|
|
1762
|
+
handleError(err);
|
|
1763
|
+
}
|
|
1764
|
+
});
|
|
1765
|
+
program.command("list").description("Show available templates").action(() => {
|
|
1766
|
+
console.log();
|
|
1767
|
+
console.log(chalk3.bold("Available Templates:"));
|
|
1768
|
+
console.log();
|
|
1769
|
+
const templates = [
|
|
1770
|
+
{
|
|
1771
|
+
name: "tool-server",
|
|
1772
|
+
desc: "Expose callable tools to AI models",
|
|
1773
|
+
includes: "read_file, write_file, http_fetch, exec_command"
|
|
1774
|
+
},
|
|
1775
|
+
{
|
|
1776
|
+
name: "resource-server",
|
|
1777
|
+
desc: "Expose data as addressable resources",
|
|
1778
|
+
includes: "file://, db://, config:// URI schemes"
|
|
1779
|
+
},
|
|
1780
|
+
{
|
|
1781
|
+
name: "prompt-server",
|
|
1782
|
+
desc: "Provide structured prompt templates",
|
|
1783
|
+
includes: "code-review, debug-error, generate-docs, refactor"
|
|
1784
|
+
}
|
|
1785
|
+
];
|
|
1786
|
+
templates.forEach(({ name, desc, includes }) => {
|
|
1787
|
+
console.log(` ${chalk3.cyan.bold(name.padEnd(18))} ${desc}`);
|
|
1788
|
+
console.log(` ${" ".repeat(18)} ${chalk3.dim("Includes: " + includes)}`);
|
|
1789
|
+
console.log();
|
|
1790
|
+
});
|
|
1791
|
+
});
|
|
1792
|
+
function handleError(err) {
|
|
1793
|
+
if (err instanceof Error) {
|
|
1794
|
+
console.error(chalk3.red(`
|
|
1795
|
+
\u274C Error: ${err.message}`));
|
|
1796
|
+
if (process.env.DEBUG) {
|
|
1797
|
+
console.error(chalk3.dim(err.stack ?? ""));
|
|
1798
|
+
}
|
|
1799
|
+
} else {
|
|
1800
|
+
console.error(chalk3.red("\n\u274C An unexpected error occurred"));
|
|
1801
|
+
console.error(err);
|
|
1802
|
+
}
|
|
1803
|
+
process.exit(1);
|
|
1804
|
+
}
|
|
1805
|
+
program.parseAsync(process.argv).catch(handleError);
|
|
1806
|
+
//# sourceMappingURL=cli.js.map
|