@nexus-lab/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/README.md +105 -0
- package/dist/generator.d.ts +2 -0
- package/dist/generator.js +61 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +46 -0
- package/dist/prompts.d.ts +12 -0
- package/dist/prompts.js +53 -0
- package/package.json +43 -0
- package/templates/full/_gitignore +5 -0
- package/templates/full/package.json +22 -0
- package/templates/full/src/index.ts +28 -0
- package/templates/full/src/prompts.ts +32 -0
- package/templates/full/src/resources.ts +34 -0
- package/templates/full/src/tools.ts +59 -0
- package/templates/full/tests/tools.test.ts +46 -0
- package/templates/full/tsconfig.json +18 -0
- package/templates/full/vitest.config.ts +8 -0
- package/templates/http/_gitignore +4 -0
- package/templates/http/package.json +22 -0
- package/templates/http/src/index.ts +106 -0
- package/templates/http/tsconfig.json +15 -0
- package/templates/minimal/_gitignore +4 -0
- package/templates/minimal/package.json +20 -0
- package/templates/minimal/src/index.ts +27 -0
- package/templates/minimal/tsconfig.json +14 -0
package/README.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# create-mcp-server
|
|
2
|
+
|
|
3
|
+
Scaffold a new [Model Context Protocol](https://modelcontextprotocol.io/) server in seconds.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm create mcp-server my-server
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- **TypeScript + ESM** — Modern setup out of the box
|
|
12
|
+
- **Secure defaults** — Zod schema validation for all inputs
|
|
13
|
+
- **Multiple templates** — Choose what fits your use case
|
|
14
|
+
- **Test-ready** — Vitest included in the `full` template
|
|
15
|
+
- **Zero config** — Works immediately after generation
|
|
16
|
+
|
|
17
|
+
## Templates
|
|
18
|
+
|
|
19
|
+
### `minimal`
|
|
20
|
+
The simplest possible MCP server. One tool, stdio transport.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm create mcp-server my-server -- --template minimal
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### `full`
|
|
27
|
+
Tools, resources, prompts, and testing all wired up.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm create mcp-server my-server -- --template full
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
### `http`
|
|
34
|
+
Streamable HTTP transport with Express. Ready for remote deployment.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npm create mcp-server my-server -- --template http
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Interactive mode
|
|
44
|
+
npm create mcp-server
|
|
45
|
+
|
|
46
|
+
# With project name
|
|
47
|
+
npm create mcp-server my-server
|
|
48
|
+
|
|
49
|
+
# With template
|
|
50
|
+
npm create mcp-server my-server -- --template full
|
|
51
|
+
|
|
52
|
+
# Skip npm install
|
|
53
|
+
npm create mcp-server my-server -- --no-install
|
|
54
|
+
|
|
55
|
+
# Skip git init
|
|
56
|
+
npm create mcp-server my-server -- --no-git
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## What you get
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
my-server/
|
|
63
|
+
├── src/
|
|
64
|
+
│ └── index.ts # Server entry point
|
|
65
|
+
├── package.json
|
|
66
|
+
├── tsconfig.json
|
|
67
|
+
└── .gitignore
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The `full` template also includes:
|
|
71
|
+
```
|
|
72
|
+
├── src/
|
|
73
|
+
│ ├── index.ts # Server entry point
|
|
74
|
+
│ ├── tools.ts # Tool definitions
|
|
75
|
+
│ ├── resources.ts # Resource definitions
|
|
76
|
+
│ └── prompts.ts # Prompt definitions
|
|
77
|
+
├── tests/
|
|
78
|
+
│ └── tools.test.ts # Example tests
|
|
79
|
+
└── vitest.config.ts
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## After scaffolding
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
cd my-server
|
|
86
|
+
npm run build
|
|
87
|
+
node dist/index.js
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
To use with Claude Code, add to your MCP config:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
{
|
|
94
|
+
"mcpServers": {
|
|
95
|
+
"my-server": {
|
|
96
|
+
"command": "node",
|
|
97
|
+
"args": ["/path/to/my-server/dist/index.js"]
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## License
|
|
104
|
+
|
|
105
|
+
MIT — [Nexus Lab](https://github.com/nexus-lab)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "fs-extra";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
function getTemplatesDir() {
|
|
8
|
+
// In development: src/templates, in production: dist/../templates
|
|
9
|
+
const devPath = path.resolve(__dirname, "..", "templates");
|
|
10
|
+
const srcPath = path.resolve(__dirname, "templates");
|
|
11
|
+
return fs.existsSync(devPath) ? devPath : srcPath;
|
|
12
|
+
}
|
|
13
|
+
export async function generateProject(config) {
|
|
14
|
+
const targetDir = path.resolve(process.cwd(), config.projectName);
|
|
15
|
+
if (await fs.pathExists(targetDir)) {
|
|
16
|
+
const files = await fs.readdir(targetDir);
|
|
17
|
+
if (files.length > 0) {
|
|
18
|
+
throw new Error(`Directory "${config.projectName}" already exists and is not empty`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
console.log(chalk.dim(` Creating project in ${targetDir}...`));
|
|
22
|
+
// Copy template
|
|
23
|
+
const templatesDir = getTemplatesDir();
|
|
24
|
+
const templateDir = path.join(templatesDir, config.template);
|
|
25
|
+
if (!(await fs.pathExists(templateDir))) {
|
|
26
|
+
throw new Error(`Template "${config.template}" not found at ${templateDir}`);
|
|
27
|
+
}
|
|
28
|
+
await fs.copy(templateDir, targetDir);
|
|
29
|
+
// Update package.json with user config
|
|
30
|
+
const pkgPath = path.join(targetDir, "package.json");
|
|
31
|
+
const pkg = await fs.readJson(pkgPath);
|
|
32
|
+
pkg.name = config.projectName;
|
|
33
|
+
pkg.description = config.description;
|
|
34
|
+
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
|
|
35
|
+
// Rename _gitignore to .gitignore (npm strips .gitignore from packages)
|
|
36
|
+
const gitignoreSrc = path.join(targetDir, "_gitignore");
|
|
37
|
+
if (await fs.pathExists(gitignoreSrc)) {
|
|
38
|
+
await fs.rename(gitignoreSrc, path.join(targetDir, ".gitignore"));
|
|
39
|
+
}
|
|
40
|
+
// Git init
|
|
41
|
+
if (config.git) {
|
|
42
|
+
try {
|
|
43
|
+
execSync("git init", { cwd: targetDir, stdio: "ignore" });
|
|
44
|
+
console.log(chalk.dim(" Initialized git repository"));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Git not available, skip
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// npm install
|
|
51
|
+
if (config.install) {
|
|
52
|
+
console.log(chalk.dim(" Installing dependencies..."));
|
|
53
|
+
try {
|
|
54
|
+
execSync("npm install", { cwd: targetDir, stdio: "ignore" });
|
|
55
|
+
console.log(chalk.dim(" Dependencies installed"));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
console.log(chalk.yellow(" Could not install dependencies. Run 'npm install' manually."));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { runPrompts } from "./prompts.js";
|
|
4
|
+
import { generateProject } from "./generator.js";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
const program = new Command();
|
|
7
|
+
program
|
|
8
|
+
.name("create-mcp-server")
|
|
9
|
+
.description("Scaffold a new MCP server project with TypeScript and secure defaults")
|
|
10
|
+
.version("0.1.0")
|
|
11
|
+
.argument("[project-name]", "Name of the project to create")
|
|
12
|
+
.option("-t, --template <template>", "Template to use (minimal, full, http)", "minimal")
|
|
13
|
+
.option("--no-install", "Skip npm install")
|
|
14
|
+
.option("--no-git", "Skip git init")
|
|
15
|
+
.action(async (projectName, options) => {
|
|
16
|
+
console.log();
|
|
17
|
+
console.log(chalk.bold.cyan(" ⚡ create-mcp-server"));
|
|
18
|
+
console.log(chalk.dim(" Scaffold a new MCP server in seconds"));
|
|
19
|
+
console.log();
|
|
20
|
+
try {
|
|
21
|
+
const config = await runPrompts(projectName, options);
|
|
22
|
+
await generateProject(config);
|
|
23
|
+
console.log();
|
|
24
|
+
console.log(chalk.green(" ✓ Project created successfully!"));
|
|
25
|
+
console.log();
|
|
26
|
+
console.log(` ${chalk.dim("$")} cd ${config.projectName}`);
|
|
27
|
+
if (options.install !== false) {
|
|
28
|
+
console.log(` ${chalk.dim("$")} npm run build`);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
console.log(` ${chalk.dim("$")} npm install`);
|
|
32
|
+
console.log(` ${chalk.dim("$")} npm run build`);
|
|
33
|
+
}
|
|
34
|
+
console.log(` ${chalk.dim("$")} node dist/index.js`);
|
|
35
|
+
console.log();
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error instanceof Error && error.message === "cancelled") {
|
|
39
|
+
console.log(chalk.yellow("\n Cancelled."));
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
console.error(chalk.red(`\n Error: ${error}`));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
program.parse();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ProjectConfig {
|
|
2
|
+
projectName: string;
|
|
3
|
+
description: string;
|
|
4
|
+
template: "minimal" | "full" | "http";
|
|
5
|
+
install: boolean;
|
|
6
|
+
git: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare function runPrompts(projectName: string | undefined, options: {
|
|
9
|
+
template?: string;
|
|
10
|
+
install?: boolean;
|
|
11
|
+
git?: boolean;
|
|
12
|
+
}): Promise<ProjectConfig>;
|
package/dist/prompts.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import prompts from "prompts";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
const TEMPLATES = [
|
|
4
|
+
{
|
|
5
|
+
title: `${chalk.bold("minimal")} ${chalk.dim("— Single tool, stdio transport")}`,
|
|
6
|
+
value: "minimal",
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
title: `${chalk.bold("full")} ${chalk.dim("— Tools + Resources + Prompts, Vitest included")}`,
|
|
10
|
+
value: "full",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
title: `${chalk.bold("http")} ${chalk.dim("— Streamable HTTP transport with Express")}`,
|
|
14
|
+
value: "http",
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
export async function runPrompts(projectName, options) {
|
|
18
|
+
const questions = [];
|
|
19
|
+
if (!projectName) {
|
|
20
|
+
questions.push({
|
|
21
|
+
type: "text",
|
|
22
|
+
name: "projectName",
|
|
23
|
+
message: "Project name:",
|
|
24
|
+
initial: "my-mcp-server",
|
|
25
|
+
validate: (value) => /^[a-z0-9-_]+$/.test(value) || "Only lowercase letters, numbers, hyphens, and underscores",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (!options.template) {
|
|
29
|
+
questions.push({
|
|
30
|
+
type: "select",
|
|
31
|
+
name: "template",
|
|
32
|
+
message: "Template:",
|
|
33
|
+
choices: [...TEMPLATES],
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
questions.push({
|
|
37
|
+
type: "text",
|
|
38
|
+
name: "description",
|
|
39
|
+
message: "Description:",
|
|
40
|
+
initial: "A Model Context Protocol server",
|
|
41
|
+
});
|
|
42
|
+
const onCancel = () => {
|
|
43
|
+
throw new Error("cancelled");
|
|
44
|
+
};
|
|
45
|
+
const answers = await prompts(questions, { onCancel });
|
|
46
|
+
return {
|
|
47
|
+
projectName: projectName || answers.projectName,
|
|
48
|
+
description: answers.description || "A Model Context Protocol server",
|
|
49
|
+
template: options.template || answers.template || "minimal",
|
|
50
|
+
install: options.install !== false,
|
|
51
|
+
git: options.git !== false,
|
|
52
|
+
};
|
|
53
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nexus-lab/create-mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a new MCP server project with TypeScript, testing, and secure defaults",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-mcp-server": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"templates"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"dev": "tsc --watch",
|
|
16
|
+
"test": "vitest",
|
|
17
|
+
"prepublishOnly": "npm run build"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"model-context-protocol",
|
|
22
|
+
"claude",
|
|
23
|
+
"scaffold",
|
|
24
|
+
"template",
|
|
25
|
+
"cli",
|
|
26
|
+
"create"
|
|
27
|
+
],
|
|
28
|
+
"author": "Nexus Lab",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"commander": "^13.0.0",
|
|
32
|
+
"prompts": "^2.4.2",
|
|
33
|
+
"chalk": "^5.4.0",
|
|
34
|
+
"fs-extra": "^11.2.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/prompts": "^2.4.9",
|
|
38
|
+
"@types/fs-extra": "^11.0.4",
|
|
39
|
+
"@types/node": "^22.0.0",
|
|
40
|
+
"typescript": "^5.7.0",
|
|
41
|
+
"vitest": "^3.0.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "A full-featured MCP server with tools, resources, and prompts",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsc --watch",
|
|
10
|
+
"start": "node dist/index.js",
|
|
11
|
+
"test": "vitest run"
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
15
|
+
"zod": "^3.23.0"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@types/node": "^22.0.0",
|
|
19
|
+
"typescript": "^5.6.0",
|
|
20
|
+
"vitest": "^2.1.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { registerTools } from "./tools.js";
|
|
4
|
+
import { registerResources } from "./resources.js";
|
|
5
|
+
import { registerPrompts } from "./prompts.js";
|
|
6
|
+
|
|
7
|
+
// Create the MCP server instance
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: "my-mcp-server",
|
|
10
|
+
version: "0.1.0",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// Register all capabilities
|
|
14
|
+
registerTools(server);
|
|
15
|
+
registerResources(server);
|
|
16
|
+
registerPrompts(server);
|
|
17
|
+
|
|
18
|
+
// Connect via stdio transport
|
|
19
|
+
async function main() {
|
|
20
|
+
const transport = new StdioServerTransport();
|
|
21
|
+
await server.connect(transport);
|
|
22
|
+
console.error("MCP server running on stdio");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
main().catch((error) => {
|
|
26
|
+
console.error("Fatal error:", error);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
// Register prompts on the MCP server
|
|
5
|
+
export function registerPrompts(server: McpServer): void {
|
|
6
|
+
server.prompt(
|
|
7
|
+
"review-code",
|
|
8
|
+
"Review the provided code and suggest improvements",
|
|
9
|
+
{ code: z.string().describe("The source code to review") },
|
|
10
|
+
({ code }) => ({
|
|
11
|
+
messages: [
|
|
12
|
+
{
|
|
13
|
+
role: "user" as const,
|
|
14
|
+
content: {
|
|
15
|
+
type: "text" as const,
|
|
16
|
+
text: [
|
|
17
|
+
"Please review the following code and provide feedback on:",
|
|
18
|
+
"1. Code quality and readability",
|
|
19
|
+
"2. Potential bugs or edge cases",
|
|
20
|
+
"3. Performance considerations",
|
|
21
|
+
"4. Suggested improvements",
|
|
22
|
+
"",
|
|
23
|
+
"```",
|
|
24
|
+
code,
|
|
25
|
+
"```",
|
|
26
|
+
].join("\n"),
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
})
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
|
|
3
|
+
// Example application config returned as a resource
|
|
4
|
+
const appConfig = {
|
|
5
|
+
appName: "my-mcp-server",
|
|
6
|
+
version: "0.1.0",
|
|
7
|
+
features: {
|
|
8
|
+
tools: true,
|
|
9
|
+
resources: true,
|
|
10
|
+
prompts: true,
|
|
11
|
+
},
|
|
12
|
+
settings: {
|
|
13
|
+
maxRetries: 3,
|
|
14
|
+
timeoutMs: 5000,
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Register resources on the MCP server
|
|
19
|
+
export function registerResources(server: McpServer): void {
|
|
20
|
+
server.resource(
|
|
21
|
+
"app-config",
|
|
22
|
+
"config://app",
|
|
23
|
+
"Application configuration data",
|
|
24
|
+
async () => ({
|
|
25
|
+
contents: [
|
|
26
|
+
{
|
|
27
|
+
uri: "config://app",
|
|
28
|
+
mimeType: "application/json",
|
|
29
|
+
text: JSON.stringify(appConfig, null, 2),
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
// Greeting logic — exported for direct testing
|
|
5
|
+
export function greet(name: string): string {
|
|
6
|
+
return `Hello, ${name}! Welcome to the MCP server.`;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Safe expression evaluator — exported for direct testing
|
|
10
|
+
export function calculate(expression: string): string {
|
|
11
|
+
// Allow only digits, operators, parentheses, dots, and spaces
|
|
12
|
+
const sanitized = expression.replace(/\s/g, "");
|
|
13
|
+
if (!/^[\d+\-*/().]+$/.test(sanitized)) {
|
|
14
|
+
throw new Error(
|
|
15
|
+
"Invalid expression. Only numbers and +, -, *, /, (, ) are allowed."
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Use Function constructor for safe-ish evaluation of arithmetic
|
|
20
|
+
const result = new Function(`"use strict"; return (${sanitized});`)();
|
|
21
|
+
|
|
22
|
+
if (typeof result !== "number" || !Number.isFinite(result)) {
|
|
23
|
+
throw new Error("Expression did not evaluate to a finite number.");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return String(result);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Register tools on the MCP server
|
|
30
|
+
export function registerTools(server: McpServer): void {
|
|
31
|
+
server.tool(
|
|
32
|
+
"greet",
|
|
33
|
+
"Generate a greeting for the given name",
|
|
34
|
+
{ name: z.string().describe("The name to greet") },
|
|
35
|
+
async ({ name }) => ({
|
|
36
|
+
content: [{ type: "text", text: greet(name) }],
|
|
37
|
+
})
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
server.tool(
|
|
41
|
+
"calculate",
|
|
42
|
+
"Safely evaluate a mathematical expression",
|
|
43
|
+
{
|
|
44
|
+
expression: z
|
|
45
|
+
.string()
|
|
46
|
+
.describe("Arithmetic expression (e.g. '2 + 3 * 4')"),
|
|
47
|
+
},
|
|
48
|
+
async ({ expression }) => {
|
|
49
|
+
try {
|
|
50
|
+
const result = calculate(expression);
|
|
51
|
+
return { content: [{ type: "text", text: result }] };
|
|
52
|
+
} catch (error) {
|
|
53
|
+
const message =
|
|
54
|
+
error instanceof Error ? error.message : "Unknown error";
|
|
55
|
+
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { greet, calculate } from "../src/tools.js";
|
|
3
|
+
|
|
4
|
+
describe("greet", () => {
|
|
5
|
+
it("returns a greeting with the given name", () => {
|
|
6
|
+
const result = greet("Alice");
|
|
7
|
+
expect(result).toBe("Hello, Alice! Welcome to the MCP server.");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("handles empty string", () => {
|
|
11
|
+
const result = greet("");
|
|
12
|
+
expect(result).toBe("Hello, ! Welcome to the MCP server.");
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("calculate", () => {
|
|
17
|
+
it("evaluates simple addition", () => {
|
|
18
|
+
expect(calculate("2 + 3")).toBe("5");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("evaluates multiplication with precedence", () => {
|
|
22
|
+
expect(calculate("2 + 3 * 4")).toBe("14");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("evaluates expressions with parentheses", () => {
|
|
26
|
+
expect(calculate("(2 + 3) * 4")).toBe("20");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("evaluates decimal numbers", () => {
|
|
30
|
+
expect(calculate("1.5 + 2.5")).toBe("4");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("rejects expressions with invalid characters", () => {
|
|
34
|
+
expect(() => calculate("require('fs')")).toThrow("Invalid expression");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("rejects alphabetic input", () => {
|
|
38
|
+
expect(() => calculate("abc")).toThrow("Invalid expression");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("rejects division by zero (Infinity)", () => {
|
|
42
|
+
expect(() => calculate("1/0")).toThrow(
|
|
43
|
+
"Expression did not evaluate to a finite number"
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"outDir": "dist",
|
|
11
|
+
"rootDir": "src",
|
|
12
|
+
"declaration": true,
|
|
13
|
+
"declarationMap": true,
|
|
14
|
+
"sourceMap": true
|
|
15
|
+
},
|
|
16
|
+
"include": ["src/**/*.ts"],
|
|
17
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc",
|
|
7
|
+
"dev": "tsc --watch",
|
|
8
|
+
"start": "node dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
12
|
+
"cors": "^2.8.5",
|
|
13
|
+
"express": "^4.21.2",
|
|
14
|
+
"zod": "^3.24.4"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/cors": "^2.8.17",
|
|
18
|
+
"@types/express": "^5.0.2",
|
|
19
|
+
"@types/node": "^22.15.3",
|
|
20
|
+
"typescript": "^5.8.3"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import express from "express";
|
|
2
|
+
import cors from "cors";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
|
|
8
|
+
const app = express();
|
|
9
|
+
|
|
10
|
+
app.use(cors());
|
|
11
|
+
app.use(express.json());
|
|
12
|
+
|
|
13
|
+
// Store active transports keyed by session ID
|
|
14
|
+
const transports = new Map<string, StreamableHTTPServerTransport>();
|
|
15
|
+
|
|
16
|
+
/** Create a new MCP server instance and register tools */
|
|
17
|
+
function createServer(): McpServer {
|
|
18
|
+
const server = new McpServer({
|
|
19
|
+
name: "my-mcp-server",
|
|
20
|
+
version: "0.1.0",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Example tool: hello
|
|
24
|
+
server.tool(
|
|
25
|
+
"hello",
|
|
26
|
+
"Returns a greeting for the given name",
|
|
27
|
+
{ name: z.string().describe("Name to greet") },
|
|
28
|
+
async ({ name }) => ({
|
|
29
|
+
content: [{ type: "text", text: `Hello, ${name}!` }],
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
return server;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// POST /mcp — handle JSON-RPC requests (initialize + all subsequent calls)
|
|
37
|
+
app.post("/mcp", async (req, res) => {
|
|
38
|
+
try {
|
|
39
|
+
// Check for existing session
|
|
40
|
+
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
41
|
+
|
|
42
|
+
if (sessionId && transports.has(sessionId)) {
|
|
43
|
+
// Route to existing transport
|
|
44
|
+
const transport = transports.get(sessionId)!;
|
|
45
|
+
await transport.handleRequest(req, res);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// No valid session — create a new one (expects an initialize request)
|
|
50
|
+
const newSessionId = randomUUID();
|
|
51
|
+
const transport = new StreamableHTTPServerTransport({
|
|
52
|
+
sessionId: newSessionId,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
transports.set(newSessionId, transport);
|
|
56
|
+
|
|
57
|
+
// Clean up on close
|
|
58
|
+
transport.onclose = () => {
|
|
59
|
+
transports.delete(newSessionId);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Connect a fresh server to this transport
|
|
63
|
+
const server = createServer();
|
|
64
|
+
await server.connect(transport);
|
|
65
|
+
|
|
66
|
+
// Handle the incoming request
|
|
67
|
+
await transport.handleRequest(req, res);
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error("Error handling POST /mcp:", err);
|
|
70
|
+
if (!res.headersSent) {
|
|
71
|
+
res.status(500).json({ error: "Internal server error" });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// GET /mcp — SSE stream for server-to-client notifications
|
|
77
|
+
app.get("/mcp", async (req, res) => {
|
|
78
|
+
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
79
|
+
|
|
80
|
+
if (!sessionId || !transports.has(sessionId)) {
|
|
81
|
+
res.status(400).json({ error: "Invalid or missing session ID" });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const transport = transports.get(sessionId)!;
|
|
86
|
+
await transport.handleRequest(req, res);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// DELETE /mcp — terminate a session
|
|
90
|
+
app.delete("/mcp", async (req, res) => {
|
|
91
|
+
const sessionId = req.headers["mcp-session-id"] as string | undefined;
|
|
92
|
+
|
|
93
|
+
if (!sessionId || !transports.has(sessionId)) {
|
|
94
|
+
res.status(400).json({ error: "Invalid or missing session ID" });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const transport = transports.get(sessionId)!;
|
|
99
|
+
await transport.handleRequest(req, res);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const PORT = parseInt(process.env.PORT || "3000", 10);
|
|
103
|
+
|
|
104
|
+
app.listen(PORT, () => {
|
|
105
|
+
console.log(`MCP server listening on http://localhost:${PORT}/mcp`);
|
|
106
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"outDir": "dist",
|
|
11
|
+
"rootDir": "src",
|
|
12
|
+
"declaration": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A minimal MCP server",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"dev": "tsc --watch",
|
|
10
|
+
"start": "node dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
14
|
+
"zod": "^3.24.4"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@types/node": "^22.15.3",
|
|
18
|
+
"typescript": "^5.8.3"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
// Create the MCP server
|
|
6
|
+
const server = new McpServer({
|
|
7
|
+
name: "my-mcp-server",
|
|
8
|
+
version: "0.1.0",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// Register a simple greeting tool
|
|
12
|
+
server.tool(
|
|
13
|
+
"hello",
|
|
14
|
+
"Greet someone by name",
|
|
15
|
+
{ name: z.string().describe("Name of the person to greet") },
|
|
16
|
+
async ({ name }) => ({
|
|
17
|
+
content: [{ type: "text", text: `Hello, ${name}!` }],
|
|
18
|
+
})
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
// Start the server with stdio transport
|
|
22
|
+
async function main() {
|
|
23
|
+
const transport = new StdioServerTransport();
|
|
24
|
+
await server.connect(transport);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
main().catch(console.error);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"outDir": "dist",
|
|
10
|
+
"rootDir": "src",
|
|
11
|
+
"declaration": true
|
|
12
|
+
},
|
|
13
|
+
"include": ["src"]
|
|
14
|
+
}
|