@amitshrivastava/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 +59 -0
- package/dist/index.js +138 -0
- package/package.json +40 -0
- package/templates/blank/README.md +69 -0
- package/templates/blank/TEMPLATE.md +1 -0
- package/templates/blank/_gitignore +3 -0
- package/templates/blank/package.json +28 -0
- package/templates/blank/src/index.ts +55 -0
- package/templates/blank/tsconfig.json +15 -0
- package/templates/markdown-rag/README.md +83 -0
- package/templates/markdown-rag/TEMPLATE.md +1 -0
- package/templates/markdown-rag/_gitignore +3 -0
- package/templates/markdown-rag/content/example.md +16 -0
- package/templates/markdown-rag/package.json +29 -0
- package/templates/markdown-rag/src/index.ts +172 -0
- package/templates/markdown-rag/tsconfig.json +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# create-mcp-server
|
|
2
|
+
|
|
3
|
+
```bash
|
|
4
|
+
npm create @amitshrivastava/mcp-server@latest my-server
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Scaffold a production-ready **Model Context Protocol** server in 30 seconds. Pick a template, answer two questions, get a runnable server with a Claude Desktop config snippet ready to paste.
|
|
8
|
+
|
|
9
|
+
Built by [Amit Shrivastava](https://www.ashrivastava.xyz), creator of the [portfolio-mcp](https://github.com/amitshrivastavaa/amit-shrivastava/tree/main/mcp-server) server. Companion to the [MCP Inspector demo](https://www.ashrivastava.xyz/ai-labs/mcp-inspector).
|
|
10
|
+
|
|
11
|
+
## What it generates
|
|
12
|
+
|
|
13
|
+
Each template is a complete, runnable TypeScript MCP server:
|
|
14
|
+
|
|
15
|
+
- `package.json` with the right `@modelcontextprotocol/sdk` version pinned
|
|
16
|
+
- `tsconfig.json` set up for stdio MCP servers (ES2022, Bundler resolution)
|
|
17
|
+
- `src/index.ts` — the server, with at least one working tool
|
|
18
|
+
- `README.md` explaining the server and how to wire it up
|
|
19
|
+
- `.gitignore`
|
|
20
|
+
|
|
21
|
+
Build with `npm run build`, point Claude Desktop / Cursor / Cline at `dist/index.js`, and it works.
|
|
22
|
+
|
|
23
|
+
## Templates
|
|
24
|
+
|
|
25
|
+
| Template | What it gives you |
|
|
26
|
+
|---|---|
|
|
27
|
+
| **`blank`** | A minimal MCP server with a single example tool (`greet`). Best when you want to add custom tools by hand. |
|
|
28
|
+
| **`markdown-rag`** | An MCP server that exposes a `./content/` directory of markdown files as resources, with a keyword `search` tool. Drop your own docs in and it serves them. |
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm create @amitshrivastava/mcp-server@latest
|
|
34
|
+
|
|
35
|
+
# or with a name + non-interactive
|
|
36
|
+
npm create @amitshrivastava/mcp-server@latest my-server
|
|
37
|
+
|
|
38
|
+
# equivalent
|
|
39
|
+
npx @amitshrivastava/create-mcp-server my-server
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
You'll be prompted for:
|
|
43
|
+
|
|
44
|
+
- **Project name** — any valid directory name
|
|
45
|
+
- **Template** — see the table above
|
|
46
|
+
- **Description** — written into `package.json`
|
|
47
|
+
- **Author** — written into `package.json`
|
|
48
|
+
|
|
49
|
+
After scaffolding it prints a complete Claude Desktop config snippet with the absolute path to your built `dist/index.js`.
|
|
50
|
+
|
|
51
|
+
## What MCP is
|
|
52
|
+
|
|
53
|
+
The [Model Context Protocol](https://modelcontextprotocol.io) is an open standard from Anthropic that lets any AI client talk to any tool, database, or service through one protocol. Instead of building custom integrations for Claude Desktop, Cursor, and every other client, you build one MCP server and they all consume it.
|
|
54
|
+
|
|
55
|
+
This CLI gets you from zero to "my server is plugged into Claude Desktop" in about a minute.
|
|
56
|
+
|
|
57
|
+
## License
|
|
58
|
+
|
|
59
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// create-mcp-server — interactive scaffolder for Model Context Protocol servers.
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// npm create @amitshrivastava/mcp-server@latest [project-name]
|
|
6
|
+
// # or
|
|
7
|
+
// npx @amitshrivastava/create-mcp-server [project-name]
|
|
8
|
+
//
|
|
9
|
+
// Reads templates from ./templates/<name>/, copies into ./<project-name>/,
|
|
10
|
+
// substitutes __NAME__ / __DESCRIPTION__ / __AUTHOR__ placeholders, prints
|
|
11
|
+
// next-step instructions including a Claude Desktop config snippet.
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import readline from "node:readline/promises";
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = dirname(__filename);
|
|
18
|
+
const TEMPLATES_DIR = join(__dirname, "..", "templates");
|
|
19
|
+
// ─── tiny terminal helpers ─────────────────────────────────────────────────────
|
|
20
|
+
const SUPPORTS_COLOR = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
21
|
+
const c = {
|
|
22
|
+
bold: (s) => (SUPPORTS_COLOR ? `\x1b[1m${s}\x1b[22m` : s),
|
|
23
|
+
dim: (s) => (SUPPORTS_COLOR ? `\x1b[2m${s}\x1b[22m` : s),
|
|
24
|
+
cyan: (s) => (SUPPORTS_COLOR ? `\x1b[36m${s}\x1b[39m` : s),
|
|
25
|
+
green: (s) => (SUPPORTS_COLOR ? `\x1b[32m${s}\x1b[39m` : s),
|
|
26
|
+
red: (s) => (SUPPORTS_COLOR ? `\x1b[31m${s}\x1b[39m` : s),
|
|
27
|
+
};
|
|
28
|
+
function die(message) {
|
|
29
|
+
console.error(c.red("✗ " + message));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
// ─── main ──────────────────────────────────────────────────────────────────────
|
|
33
|
+
async function main() {
|
|
34
|
+
const argName = process.argv[2];
|
|
35
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
36
|
+
console.log("");
|
|
37
|
+
console.log(c.bold("🔌 create-mcp-server"));
|
|
38
|
+
console.log(c.dim(" Scaffold a Model Context Protocol server in 30 seconds.\n"));
|
|
39
|
+
const name = await ask(rl, "Project name:", argName || "my-mcp-server");
|
|
40
|
+
if (!/^[a-z0-9_-]+$/i.test(name)) {
|
|
41
|
+
rl.close();
|
|
42
|
+
die("Project name must contain only letters, digits, hyphens, and underscores.");
|
|
43
|
+
}
|
|
44
|
+
const templates = readdirSync(TEMPLATES_DIR).filter((d) => statSync(join(TEMPLATES_DIR, d)).isDirectory());
|
|
45
|
+
if (templates.length === 0) {
|
|
46
|
+
rl.close();
|
|
47
|
+
die("No templates found. Did you install the package correctly?");
|
|
48
|
+
}
|
|
49
|
+
console.log("\n" + c.bold("Templates:"));
|
|
50
|
+
templates.forEach((t, i) => {
|
|
51
|
+
const meta = readTemplateMeta(join(TEMPLATES_DIR, t));
|
|
52
|
+
console.log(` ${c.cyan(String(i + 1))}. ${c.bold(t)}${meta ? c.dim(" — " + meta) : ""}`);
|
|
53
|
+
});
|
|
54
|
+
const choice = (await ask(rl, "\nTemplate number:", "1")).trim();
|
|
55
|
+
const tIdx = parseInt(choice, 10) - 1;
|
|
56
|
+
if (Number.isNaN(tIdx) || tIdx < 0 || tIdx >= templates.length) {
|
|
57
|
+
rl.close();
|
|
58
|
+
die(`Invalid template choice. Pick 1..${templates.length}.`);
|
|
59
|
+
}
|
|
60
|
+
const template = templates[tIdx];
|
|
61
|
+
const description = await ask(rl, "Description:", `MCP server: ${name}`);
|
|
62
|
+
const author = await ask(rl, "Author:", "");
|
|
63
|
+
rl.close();
|
|
64
|
+
const targetDir = join(process.cwd(), name);
|
|
65
|
+
if (existsSync(targetDir)) {
|
|
66
|
+
die(`Target directory ${name} already exists.`);
|
|
67
|
+
}
|
|
68
|
+
const answers = { name, template, description, author };
|
|
69
|
+
copyTemplate(join(TEMPLATES_DIR, template), targetDir, answers);
|
|
70
|
+
printSummary(targetDir, answers);
|
|
71
|
+
}
|
|
72
|
+
async function ask(rl, q, def) {
|
|
73
|
+
const prompt = def ? `${q} ${c.dim("(" + def + ")")} ` : `${q} `;
|
|
74
|
+
const a = (await rl.question(prompt)).trim();
|
|
75
|
+
return a || def;
|
|
76
|
+
}
|
|
77
|
+
function readTemplateMeta(dir) {
|
|
78
|
+
const meta = join(dir, "TEMPLATE.md");
|
|
79
|
+
if (!existsSync(meta))
|
|
80
|
+
return null;
|
|
81
|
+
return readFileSync(meta, "utf-8").trim().split("\n")[0];
|
|
82
|
+
}
|
|
83
|
+
function copyTemplate(src, dest, answers) {
|
|
84
|
+
mkdirSync(dest, { recursive: true });
|
|
85
|
+
for (const entry of readdirSync(src)) {
|
|
86
|
+
const srcPath = join(src, entry);
|
|
87
|
+
// npm strips real `.gitignore` files from published packages; the template
|
|
88
|
+
// ships it as `_gitignore` and we rename on copy.
|
|
89
|
+
const renamed = entry === "_gitignore" ? ".gitignore" : entry;
|
|
90
|
+
if (renamed === "TEMPLATE.md")
|
|
91
|
+
continue;
|
|
92
|
+
const destPath = join(dest, renamed);
|
|
93
|
+
const stat = statSync(srcPath);
|
|
94
|
+
if (stat.isDirectory()) {
|
|
95
|
+
copyTemplate(srcPath, destPath, answers);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const raw = readFileSync(srcPath, "utf-8");
|
|
99
|
+
writeFileSync(destPath, substitute(raw, answers));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function substitute(text, answers) {
|
|
104
|
+
return text
|
|
105
|
+
.replace(/__NAME__/g, answers.name)
|
|
106
|
+
.replace(/__DESCRIPTION__/g, answers.description)
|
|
107
|
+
.replace(/__AUTHOR__/g, answers.author || "")
|
|
108
|
+
.replace(/__TEMPLATE__/g, answers.template);
|
|
109
|
+
}
|
|
110
|
+
function printSummary(dir, answers) {
|
|
111
|
+
console.log("");
|
|
112
|
+
console.log(c.green("✓") + " Scaffolded " + c.bold(answers.name) + c.dim(` (${answers.template})`));
|
|
113
|
+
console.log(c.dim(" at " + dir));
|
|
114
|
+
console.log("");
|
|
115
|
+
console.log(c.bold("Next steps:"));
|
|
116
|
+
console.log(" " + c.cyan("cd " + answers.name));
|
|
117
|
+
console.log(" " + c.cyan("npm install"));
|
|
118
|
+
console.log(" " + c.cyan("npm run build"));
|
|
119
|
+
console.log("");
|
|
120
|
+
console.log(c.bold("Then add to your MCP client (e.g. Claude Desktop):"));
|
|
121
|
+
console.log(c.dim(" ~/Library/Application Support/Claude/claude_desktop_config.json"));
|
|
122
|
+
console.log("");
|
|
123
|
+
console.log(" {");
|
|
124
|
+
console.log(' "mcpServers": {');
|
|
125
|
+
console.log(` ${c.cyan(`"${answers.name}"`)}: {`);
|
|
126
|
+
console.log(' "command": "node",');
|
|
127
|
+
console.log(` "args": [${c.cyan(`"${dir}/dist/index.js"`)}]`);
|
|
128
|
+
console.log(" }");
|
|
129
|
+
console.log(" }");
|
|
130
|
+
console.log(" }");
|
|
131
|
+
console.log("");
|
|
132
|
+
console.log("Restart Claude Desktop. See " + c.bold(answers.name + "/README.md") + " for details.");
|
|
133
|
+
console.log("");
|
|
134
|
+
}
|
|
135
|
+
main().catch((err) => {
|
|
136
|
+
console.error(err);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@amitshrivastava/create-mcp-server",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a production-ready Model Context Protocol (MCP) server in 30 seconds. Pick a template, answer two questions, get a runnable server.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"bin": {
|
|
9
|
+
"create-mcp-server": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": ["dist", "templates", "README.md"],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"dev": "tsx src/index.ts",
|
|
15
|
+
"typecheck": "tsc --noEmit",
|
|
16
|
+
"prepack": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": ["mcp", "model-context-protocol", "anthropic", "claude", "scaffold", "cli", "ai", "agent"],
|
|
19
|
+
"author": "Amit Shrivastava",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/amitshrivastavaa/amit-shrivastava.git",
|
|
23
|
+
"directory": "create-mcp-server"
|
|
24
|
+
},
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/amitshrivastavaa/amit-shrivastava/issues"
|
|
27
|
+
},
|
|
28
|
+
"homepage": "https://github.com/amitshrivastavaa/amit-shrivastava/tree/main/create-mcp-server#readme",
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/node": "^22",
|
|
34
|
+
"tsx": "^4.19.0",
|
|
35
|
+
"typescript": "^5"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=18"
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# __NAME__
|
|
2
|
+
|
|
3
|
+
__DESCRIPTION__
|
|
4
|
+
|
|
5
|
+
A Model Context Protocol server, scaffolded by [`create-mcp-server`](https://github.com/amitshrivastavaa/amit-shrivastava/tree/main/create-mcp-server).
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
npm run build
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Wire it into Claude Desktop
|
|
15
|
+
|
|
16
|
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (or the equivalent on your OS):
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"mcpServers": {
|
|
21
|
+
"__NAME__": {
|
|
22
|
+
"command": "node",
|
|
23
|
+
"args": ["/absolute/path/to/__NAME__/dist/index.js"]
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Restart Claude Desktop. You should see `__NAME__` in the MCP menu (the plug icon).
|
|
30
|
+
|
|
31
|
+
## Try it
|
|
32
|
+
|
|
33
|
+
In Claude Desktop:
|
|
34
|
+
|
|
35
|
+
> Use __NAME__ to greet Sara
|
|
36
|
+
|
|
37
|
+
The client will call the `greet` tool and you'll see the response inline.
|
|
38
|
+
|
|
39
|
+
## Add your own tools
|
|
40
|
+
|
|
41
|
+
Open `src/index.ts`. The `ListToolsRequestSchema` handler returns your tool catalog; the `CallToolRequestSchema` handler dispatches to the right implementation.
|
|
42
|
+
|
|
43
|
+
Pattern:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
// in ListToolsRequestSchema
|
|
47
|
+
{
|
|
48
|
+
name: "my_tool",
|
|
49
|
+
description: "What the tool does",
|
|
50
|
+
inputSchema: {
|
|
51
|
+
type: "object",
|
|
52
|
+
properties: { input: { type: "string" } },
|
|
53
|
+
required: ["input"],
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// in CallToolRequestSchema
|
|
58
|
+
if (name === "my_tool") {
|
|
59
|
+
const input = String(args.input || "")
|
|
60
|
+
const result = await doSomething(input)
|
|
61
|
+
return { content: [{ type: "text", text: result }] }
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
After changing tools, run `npm run build` and restart Claude Desktop.
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Minimal MCP server. One example tool. Build your own from here.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "__DESCRIPTION__",
|
|
5
|
+
"author": "__AUTHOR__",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"__NAME__": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"start": "node dist/index.js",
|
|
15
|
+
"dev": "tsx src/index.ts"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^22",
|
|
22
|
+
"tsx": "^4.19.0",
|
|
23
|
+
"typescript": "^5"
|
|
24
|
+
},
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=18"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// __NAME__ — MCP server. Generated by create-mcp-server.
|
|
3
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
|
|
4
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
5
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js"
|
|
6
|
+
|
|
7
|
+
const server = new Server(
|
|
8
|
+
{ name: "__NAME__", version: "0.1.0" },
|
|
9
|
+
{ capabilities: { tools: {} } },
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
// ─── Tools ────────────────────────────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
15
|
+
tools: [
|
|
16
|
+
{
|
|
17
|
+
name: "greet",
|
|
18
|
+
description: "Say hello to someone.",
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: "object",
|
|
21
|
+
properties: {
|
|
22
|
+
name: { type: "string", description: "Who to greet" },
|
|
23
|
+
},
|
|
24
|
+
required: ["name"],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
// Add your tools here.
|
|
28
|
+
],
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
32
|
+
const { name, arguments: rawArgs } = request.params
|
|
33
|
+
const args = (rawArgs ?? {}) as Record<string, unknown>
|
|
34
|
+
|
|
35
|
+
if (name === "greet") {
|
|
36
|
+
const who = String(args.name || "world")
|
|
37
|
+
return { content: [{ type: "text", text: `Hello, ${who}!` }] }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
throw new Error(`Unknown tool: ${name}`)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
// ─── Boot ─────────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
async function main() {
|
|
46
|
+
// stderr — stdout is reserved for the JSON-RPC stdio transport.
|
|
47
|
+
console.error("__NAME__: ready")
|
|
48
|
+
const transport = new StdioServerTransport()
|
|
49
|
+
await server.connect(transport)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
main().catch((err) => {
|
|
53
|
+
console.error("__NAME__ failed to start:", err)
|
|
54
|
+
process.exit(1)
|
|
55
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"outDir": "dist",
|
|
11
|
+
"rootDir": "src"
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "dist"]
|
|
15
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# __NAME__
|
|
2
|
+
|
|
3
|
+
__DESCRIPTION__
|
|
4
|
+
|
|
5
|
+
A markdown-RAG Model Context Protocol server, scaffolded by [`create-mcp-server`](https://github.com/amitshrivastavaa/amit-shrivastava/tree/main/create-mcp-server). Exposes every `.md` file in `./content/` as an MCP resource plus a keyword `search` tool.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install
|
|
11
|
+
npm run build
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Add your content
|
|
15
|
+
|
|
16
|
+
Drop `.md` files into `./content/`. Each file becomes a resource at `doc://<filename-without-md>`. Frontmatter is optional but recommended:
|
|
17
|
+
|
|
18
|
+
```md
|
|
19
|
+
---
|
|
20
|
+
title: My Doc
|
|
21
|
+
description: One-line summary used in search results.
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
# My Doc
|
|
25
|
+
|
|
26
|
+
Body markdown here...
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
You can also point at a different content directory:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
MCP_CONTENT_DIR=/abs/path/to/docs npm start
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Wire it into Claude Desktop
|
|
36
|
+
|
|
37
|
+
Edit `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
38
|
+
|
|
39
|
+
```json
|
|
40
|
+
{
|
|
41
|
+
"mcpServers": {
|
|
42
|
+
"__NAME__": {
|
|
43
|
+
"command": "node",
|
|
44
|
+
"args": ["/absolute/path/to/__NAME__/dist/index.js"]
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Restart Claude Desktop. You should see `__NAME__` in the MCP menu.
|
|
51
|
+
|
|
52
|
+
## What it exposes
|
|
53
|
+
|
|
54
|
+
**Resources** (one per markdown file):
|
|
55
|
+
|
|
56
|
+
- `doc://<slug>` — full markdown content with frontmatter
|
|
57
|
+
|
|
58
|
+
**Tools**:
|
|
59
|
+
|
|
60
|
+
- `search(query, limit?)` — keyword-ranked search across titles, descriptions, and body
|
|
61
|
+
- `list()` — every doc with title + slug
|
|
62
|
+
|
|
63
|
+
## Try it
|
|
64
|
+
|
|
65
|
+
In Claude Desktop:
|
|
66
|
+
|
|
67
|
+
> Use __NAME__ to search for "introduction"
|
|
68
|
+
|
|
69
|
+
Claude will call `search`, then read the top hits via their `doc://` URIs and summarize.
|
|
70
|
+
|
|
71
|
+
## Going beyond keyword search
|
|
72
|
+
|
|
73
|
+
When keyword search isn't enough, swap it for embeddings:
|
|
74
|
+
|
|
75
|
+
1. Add `@huggingface/transformers` to `dependencies`.
|
|
76
|
+
2. At startup, embed each doc's body with `all-MiniLM-L6-v2`.
|
|
77
|
+
3. In `search`, embed the query and rank docs by cosine similarity (dot product on normalised vectors).
|
|
78
|
+
|
|
79
|
+
The portfolio-mcp server in the parent repo uses this pattern — see [its source](https://github.com/amitshrivastavaa/amit-shrivastava/tree/main/mcp-server) for a working example.
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Markdown RAG: exposes a ./content/*.md directory as resources + a keyword search tool.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Example Doc
|
|
3
|
+
description: Replace this file with your own markdown. Anything in ./content/ becomes a queryable MCP resource.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Hello from create-mcp-server
|
|
7
|
+
|
|
8
|
+
This file demonstrates how the `markdown-rag` template exposes content.
|
|
9
|
+
|
|
10
|
+
You can:
|
|
11
|
+
|
|
12
|
+
- Add as many `.md` files as you want to this `./content/` directory.
|
|
13
|
+
- Use frontmatter (`title`, `description`) to make search results nicer.
|
|
14
|
+
- Query them from Claude Desktop with prompts like *"Use __NAME__ to search for hello"*.
|
|
15
|
+
|
|
16
|
+
When you're ready to graduate beyond keyword search, the README explains how to swap in vector embeddings.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "__DESCRIPTION__",
|
|
5
|
+
"author": "__AUTHOR__",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "dist/index.js",
|
|
9
|
+
"bin": {
|
|
10
|
+
"__NAME__": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"start": "node dist/index.js",
|
|
15
|
+
"dev": "tsx src/index.ts"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
19
|
+
"gray-matter": "^4.0.3"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22",
|
|
23
|
+
"tsx": "^4.19.0",
|
|
24
|
+
"typescript": "^5"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// __NAME__ — markdown-RAG MCP server. Generated by create-mcp-server.
|
|
3
|
+
//
|
|
4
|
+
// Exposes every .md file in ./content/ as an MCP resource (uri = doc://<slug>)
|
|
5
|
+
// and provides a keyword `search` tool that ranks docs by term-overlap against
|
|
6
|
+
// title + description + body. No embedding model — keep it lean. Replace the
|
|
7
|
+
// scorer with cosine-over-embeddings if you outgrow keyword search.
|
|
8
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js"
|
|
9
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
10
|
+
import {
|
|
11
|
+
CallToolRequestSchema,
|
|
12
|
+
ListResourcesRequestSchema,
|
|
13
|
+
ListToolsRequestSchema,
|
|
14
|
+
ReadResourceRequestSchema,
|
|
15
|
+
} from "@modelcontextprotocol/sdk/types.js"
|
|
16
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs"
|
|
17
|
+
import { dirname, join } from "node:path"
|
|
18
|
+
import { fileURLToPath } from "node:url"
|
|
19
|
+
import matter from "gray-matter"
|
|
20
|
+
|
|
21
|
+
interface Doc {
|
|
22
|
+
slug: string
|
|
23
|
+
title: string
|
|
24
|
+
description: string
|
|
25
|
+
content: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
29
|
+
const __dirname = dirname(__filename)
|
|
30
|
+
|
|
31
|
+
// Candidate content directories, in priority order. Override with MCP_CONTENT_DIR.
|
|
32
|
+
function findContentDir(): string {
|
|
33
|
+
const candidates = [
|
|
34
|
+
process.env.MCP_CONTENT_DIR,
|
|
35
|
+
join(__dirname, "..", "content"),
|
|
36
|
+
join(__dirname, "..", "..", "content"),
|
|
37
|
+
].filter((c): c is string => typeof c === "string" && c.length > 0)
|
|
38
|
+
for (const dir of candidates) {
|
|
39
|
+
if (existsSync(dir)) return dir
|
|
40
|
+
}
|
|
41
|
+
throw new Error(
|
|
42
|
+
"Could not find content directory. Create ./content/*.md or set MCP_CONTENT_DIR to an absolute path.",
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadDocs(): Doc[] {
|
|
47
|
+
const dir = findContentDir()
|
|
48
|
+
const files = readdirSync(dir).filter((f) => f.endsWith(".md"))
|
|
49
|
+
return files.map((file) => {
|
|
50
|
+
const slug = file.replace(/\.md$/, "")
|
|
51
|
+
const raw = readFileSync(join(dir, file), "utf-8")
|
|
52
|
+
const { data, content } = matter(raw)
|
|
53
|
+
return {
|
|
54
|
+
slug,
|
|
55
|
+
title: typeof data.title === "string" ? data.title : slug,
|
|
56
|
+
description: typeof data.description === "string" ? data.description : "",
|
|
57
|
+
content,
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
let DOCS: Doc[] = []
|
|
63
|
+
|
|
64
|
+
const server = new Server(
|
|
65
|
+
{ name: "__NAME__", version: "0.1.0" },
|
|
66
|
+
{ capabilities: { resources: {}, tools: {} } },
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// ─── Resources ────────────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
72
|
+
resources: DOCS.map((d) => ({
|
|
73
|
+
uri: `doc://${d.slug}`,
|
|
74
|
+
name: d.title,
|
|
75
|
+
description: d.description,
|
|
76
|
+
mimeType: "text/markdown",
|
|
77
|
+
})),
|
|
78
|
+
}))
|
|
79
|
+
|
|
80
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
81
|
+
const uri = request.params.uri
|
|
82
|
+
const match = uri.match(/^doc:\/\/(.+)$/)
|
|
83
|
+
if (!match) throw new Error(`Unknown resource URI: ${uri}`)
|
|
84
|
+
const slug = match[1]
|
|
85
|
+
const doc = DOCS.find((d) => d.slug === slug)
|
|
86
|
+
if (!doc) throw new Error(`Doc not found: ${slug}`)
|
|
87
|
+
const text = [
|
|
88
|
+
"---",
|
|
89
|
+
`title: ${doc.title}`,
|
|
90
|
+
doc.description ? `description: ${doc.description}` : null,
|
|
91
|
+
"---",
|
|
92
|
+
"",
|
|
93
|
+
doc.content,
|
|
94
|
+
]
|
|
95
|
+
.filter((l): l is string => l !== null)
|
|
96
|
+
.join("\n")
|
|
97
|
+
return { contents: [{ uri, mimeType: "text/markdown", text }] }
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
// ─── Tools ────────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
103
|
+
tools: [
|
|
104
|
+
{
|
|
105
|
+
name: "search",
|
|
106
|
+
description: "Keyword search across all docs. Returns ranked results with doc:// URIs to read in full.",
|
|
107
|
+
inputSchema: {
|
|
108
|
+
type: "object",
|
|
109
|
+
properties: {
|
|
110
|
+
query: { type: "string", description: "Keywords to search for" },
|
|
111
|
+
limit: { type: "number", description: "Max results (default 5, max 20)" },
|
|
112
|
+
},
|
|
113
|
+
required: ["query"],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "list",
|
|
118
|
+
description: "List every doc with title and slug.",
|
|
119
|
+
inputSchema: { type: "object", properties: {} },
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
}))
|
|
123
|
+
|
|
124
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
125
|
+
const { name, arguments: rawArgs } = request.params
|
|
126
|
+
const args = (rawArgs ?? {}) as Record<string, unknown>
|
|
127
|
+
|
|
128
|
+
if (name === "list") {
|
|
129
|
+
const text = DOCS.map((d) => `- ${d.title} [${d.slug}] — ${d.description}`).join("\n")
|
|
130
|
+
return { content: [{ type: "text", text: `${DOCS.length} docs:\n\n${text}` }] }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (name === "search") {
|
|
134
|
+
const query = String(args.query || "").toLowerCase().trim()
|
|
135
|
+
const limit = Math.min(Math.max(1, Number(args.limit) || 5), 20)
|
|
136
|
+
if (!query) {
|
|
137
|
+
return { content: [{ type: "text", text: "Provide a non-empty `query`." }], isError: true }
|
|
138
|
+
}
|
|
139
|
+
const terms = query.split(/\s+/).filter(Boolean)
|
|
140
|
+
const scored = DOCS.map((d) => {
|
|
141
|
+
const text = `${d.title} ${d.description} ${d.content}`.toLowerCase()
|
|
142
|
+
const score = terms.reduce((s, t) => (text.includes(t) ? s + 1 : s), 0)
|
|
143
|
+
return { d, score }
|
|
144
|
+
})
|
|
145
|
+
.filter((x) => x.score > 0)
|
|
146
|
+
.sort((a, b) => b.score - a.score)
|
|
147
|
+
.slice(0, limit)
|
|
148
|
+
if (scored.length === 0) {
|
|
149
|
+
return { content: [{ type: "text", text: `No docs matched "${query}".` }] }
|
|
150
|
+
}
|
|
151
|
+
const lines = scored.map(({ d, score }) =>
|
|
152
|
+
`- [score ${score}] ${d.title} — doc://${d.slug}\n ${d.description}`,
|
|
153
|
+
)
|
|
154
|
+
return { content: [{ type: "text", text: `Top ${scored.length} matches for "${query}":\n\n${lines.join("\n\n")}` }] }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
throw new Error(`Unknown tool: ${name}`)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// ─── Boot ─────────────────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
async function main() {
|
|
163
|
+
DOCS = loadDocs()
|
|
164
|
+
console.error(`__NAME__: loaded ${DOCS.length} docs`)
|
|
165
|
+
const transport = new StdioServerTransport()
|
|
166
|
+
await server.connect(transport)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
main().catch((err) => {
|
|
170
|
+
console.error("__NAME__ failed to start:", err)
|
|
171
|
+
process.exit(1)
|
|
172
|
+
})
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"strict": true,
|
|
9
|
+
"skipLibCheck": true,
|
|
10
|
+
"outDir": "dist",
|
|
11
|
+
"rootDir": "src"
|
|
12
|
+
},
|
|
13
|
+
"include": ["src/**/*"],
|
|
14
|
+
"exclude": ["node_modules", "dist"]
|
|
15
|
+
}
|