create-mcp-forge 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/LICENSE +21 -0
- package/README.md +370 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +160 -0
- package/dist/templates/empty/README.md +16 -0
- package/dist/templates/empty/package.json +17 -0
- package/dist/templates/empty/src/index.ts +26 -0
- package/dist/templates/empty/tests/index.test.ts +9 -0
- package/dist/templates/empty/tsconfig.json +12 -0
- package/dist/templates/filesystem/.env.example +1 -0
- package/dist/templates/filesystem/README.md +37 -0
- package/dist/templates/filesystem/package.json +18 -0
- package/dist/templates/filesystem/src/index.ts +56 -0
- package/dist/templates/filesystem/src/tools/filesystem.ts +52 -0
- package/dist/templates/filesystem/tests/filesystem.test.ts +12 -0
- package/dist/templates/filesystem/tsconfig.json +12 -0
- package/dist/templates/postgresql/.env.example +1 -0
- package/dist/templates/postgresql/README.md +33 -0
- package/dist/templates/postgresql/package.json +19 -0
- package/dist/templates/postgresql/src/index.ts +56 -0
- package/dist/templates/postgresql/src/tools/database.ts +49 -0
- package/dist/templates/postgresql/tests/database.test.ts +32 -0
- package/dist/templates/postgresql/tsconfig.json +12 -0
- package/dist/templates/rest-api/.env.example +2 -0
- package/dist/templates/rest-api/README.md +32 -0
- package/dist/templates/rest-api/package.json +19 -0
- package/dist/templates/rest-api/src/index.ts +52 -0
- package/dist/templates/rest-api/src/tools/api.ts +66 -0
- package/dist/templates/rest-api/tests/api.test.ts +32 -0
- package/dist/templates/rest-api/tsconfig.json +12 -0
- package/package.json +49 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hiyilmaz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
# mcp-forge
|
|
2
|
+
|
|
3
|
+
⚡ **The fastest way to build MCP servers**
|
|
4
|
+
|
|
5
|
+
A powerful CLI tool for scaffolding [Model Context Protocol](https://modelcontextprotocol.io) servers with production-ready templates.
|
|
6
|
+
|
|
7
|
+
[](https://www.npmjs.com/package/mcp-forge)
|
|
8
|
+
[](https://opensource.org/licenses/MIT)
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- 🚀 **Instant Setup** - Create production-ready MCP servers in seconds
|
|
13
|
+
- 📦 **Multiple Templates** - PostgreSQL, REST API, FileSystem, or start from scratch
|
|
14
|
+
- 🔧 **TypeScript First** - Full type safety and modern tooling
|
|
15
|
+
- ✅ **Test Ready** - Pre-configured testing with Vitest
|
|
16
|
+
- 🎯 **Claude Desktop Integration** - Automatic registration with Claude Desktop
|
|
17
|
+
- 🛠️ **Best Practices** - Error handling, validation, and security built-in
|
|
18
|
+
|
|
19
|
+
## Quick Start
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx mcp-forge init my-server
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Follow the interactive prompts to:
|
|
26
|
+
|
|
27
|
+
1. Choose a template (empty, postgresql, rest-api, filesystem)
|
|
28
|
+
2. Select language (TypeScript)
|
|
29
|
+
3. Automatically register with Claude Desktop
|
|
30
|
+
|
|
31
|
+
Your MCP server is ready! 🎉
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
### Global Installation (Optional)
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm install -g mcp-forge
|
|
39
|
+
mcp-forge init my-server
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### One-Time Use (Recommended)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npx mcp-forge init my-server
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
### Interactive Mode
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npx mcp-forge init my-server
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The CLI will guide you through:
|
|
57
|
+
|
|
58
|
+
- Template selection
|
|
59
|
+
- Language preference
|
|
60
|
+
- Claude Desktop registration
|
|
61
|
+
|
|
62
|
+
### Non-Interactive Mode
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx mcp-forge init my-server \
|
|
66
|
+
--template postgresql \
|
|
67
|
+
--language typescript \
|
|
68
|
+
--no-register
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### CLI Options
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
mcp-forge init <project-name> [options]
|
|
75
|
+
|
|
76
|
+
Options:
|
|
77
|
+
-t, --template <name> Template to use (empty|postgresql|rest-api|filesystem)
|
|
78
|
+
-l, --language <lang> Language to use (typescript)
|
|
79
|
+
--no-register Skip Claude Desktop registration
|
|
80
|
+
-h, --help Display help
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Templates
|
|
84
|
+
|
|
85
|
+
### 🗂️ Empty Template
|
|
86
|
+
|
|
87
|
+
Minimal MCP server with basic structure. Perfect for custom implementations.
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx mcp-forge init my-server --template empty
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Includes:**
|
|
94
|
+
|
|
95
|
+
- Basic MCP server setup
|
|
96
|
+
- TypeScript configuration
|
|
97
|
+
- Test scaffolding
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
### 🐘 PostgreSQL Template
|
|
102
|
+
|
|
103
|
+
Full-featured database MCP server with connection pooling and query tools.
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npx mcp-forge init my-db-server --template postgresql
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**Tools Included:**
|
|
110
|
+
|
|
111
|
+
- `query_database` - Execute raw SQL queries
|
|
112
|
+
- `list_tables` - List all database tables
|
|
113
|
+
- `describe_table` - Get table schema information
|
|
114
|
+
|
|
115
|
+
**Features:**
|
|
116
|
+
|
|
117
|
+
- Connection pooling with `pg`
|
|
118
|
+
- Environment-based configuration
|
|
119
|
+
- Parameterized queries for security
|
|
120
|
+
- Comprehensive error handling
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
### 🌐 REST API Template
|
|
125
|
+
|
|
126
|
+
HTTP client MCP server for interacting with REST APIs.
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
npx mcp-forge init my-api-server --template rest-api
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Tools Included:**
|
|
133
|
+
|
|
134
|
+
- `fetch_api` - Make HTTP requests (GET, POST, PUT, DELETE)
|
|
135
|
+
- `get_api_status` - Check API health
|
|
136
|
+
|
|
137
|
+
**Features:**
|
|
138
|
+
|
|
139
|
+
- Built-in HTTP client
|
|
140
|
+
- Request/response validation
|
|
141
|
+
- Timeout handling
|
|
142
|
+
- Error recovery
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
### 📁 FileSystem Template
|
|
147
|
+
|
|
148
|
+
Secure file operations MCP server with path traversal protection.
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
npx mcp-forge init my-fs-server --template filesystem
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Tools Included:**
|
|
155
|
+
|
|
156
|
+
- `read_file` - Read file contents
|
|
157
|
+
- `write_file` - Write to files
|
|
158
|
+
- `list_directory` - List directory contents
|
|
159
|
+
|
|
160
|
+
**Features:**
|
|
161
|
+
|
|
162
|
+
- Path traversal protection
|
|
163
|
+
- Configurable allowed directories
|
|
164
|
+
- Safe file operations
|
|
165
|
+
- Comprehensive validation
|
|
166
|
+
|
|
167
|
+
## Project Structure
|
|
168
|
+
|
|
169
|
+
```
|
|
170
|
+
my-server/
|
|
171
|
+
├── src/
|
|
172
|
+
│ ├── index.ts # Main server entry point
|
|
173
|
+
│ └── tools/ # Tool implementations
|
|
174
|
+
│ └── *.ts
|
|
175
|
+
├── tests/
|
|
176
|
+
│ └── *.test.ts # Test files
|
|
177
|
+
├── .env.example # Environment template
|
|
178
|
+
├── package.json
|
|
179
|
+
├── tsconfig.json
|
|
180
|
+
└── README.md
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Development Workflow
|
|
184
|
+
|
|
185
|
+
### 1. Create Your Server
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
npx mcp-forge init my-server --template postgresql
|
|
189
|
+
cd my-server
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### 2. Configure Environment
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
cp .env.example .env
|
|
196
|
+
# Edit .env with your configuration
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### 3. Install Dependencies
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
npm install
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### 4. Start Development
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
npm run dev
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 5. Run Tests
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
npm test
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### 6. Build for Production
|
|
218
|
+
|
|
219
|
+
```bash
|
|
220
|
+
npm run build
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Claude Desktop Integration
|
|
224
|
+
|
|
225
|
+
mcp-forge automatically registers your server with Claude Desktop during initialization.
|
|
226
|
+
|
|
227
|
+
### Manual Registration
|
|
228
|
+
|
|
229
|
+
If you skipped registration or need to register later:
|
|
230
|
+
|
|
231
|
+
1. Open Claude Desktop configuration:
|
|
232
|
+
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
233
|
+
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
234
|
+
|
|
235
|
+
2. Add your server:
|
|
236
|
+
|
|
237
|
+
```json
|
|
238
|
+
{
|
|
239
|
+
"mcpServers": {
|
|
240
|
+
"my-server": {
|
|
241
|
+
"command": "node",
|
|
242
|
+
"args": ["/absolute/path/to/my-server/dist/index.js"]
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
3. Restart Claude Desktop
|
|
249
|
+
|
|
250
|
+
## Testing
|
|
251
|
+
|
|
252
|
+
All templates include pre-configured testing with Vitest.
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
# Run tests once
|
|
256
|
+
npm test
|
|
257
|
+
|
|
258
|
+
# Watch mode
|
|
259
|
+
npm run test:watch
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Example Test
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
import { describe, it, expect } from "vitest";
|
|
266
|
+
|
|
267
|
+
describe("My Tool", () => {
|
|
268
|
+
it("should process input correctly", () => {
|
|
269
|
+
const result = myTool({ input: "test" });
|
|
270
|
+
expect(result).toBe("expected");
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## Requirements
|
|
276
|
+
|
|
277
|
+
- **Node.js**: >= 18.0.0
|
|
278
|
+
- **npm**: >= 8.0.0
|
|
279
|
+
|
|
280
|
+
## Best Practices
|
|
281
|
+
|
|
282
|
+
### Error Handling
|
|
283
|
+
|
|
284
|
+
All templates include comprehensive error handling:
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
try {
|
|
288
|
+
// Tool logic
|
|
289
|
+
return { content: [{ type: "text", text: result }] };
|
|
290
|
+
} catch (error) {
|
|
291
|
+
return {
|
|
292
|
+
content: [
|
|
293
|
+
{
|
|
294
|
+
type: "text",
|
|
295
|
+
text: `Error: ${error.message}`,
|
|
296
|
+
},
|
|
297
|
+
],
|
|
298
|
+
isError: true,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Input Validation
|
|
304
|
+
|
|
305
|
+
Validate all tool inputs:
|
|
306
|
+
|
|
307
|
+
```typescript
|
|
308
|
+
if (!input || typeof input !== "string") {
|
|
309
|
+
throw new Error("Invalid input: expected string");
|
|
310
|
+
}
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Security
|
|
314
|
+
|
|
315
|
+
- Never expose sensitive data in error messages
|
|
316
|
+
- Use environment variables for credentials
|
|
317
|
+
- Validate and sanitize all user inputs
|
|
318
|
+
- Implement path traversal protection for file operations
|
|
319
|
+
|
|
320
|
+
## Troubleshooting
|
|
321
|
+
|
|
322
|
+
### Server Not Appearing in Claude Desktop
|
|
323
|
+
|
|
324
|
+
1. Check configuration file syntax (valid JSON)
|
|
325
|
+
2. Verify absolute paths in `args`
|
|
326
|
+
3. Ensure server is built (`npm run build`)
|
|
327
|
+
4. Restart Claude Desktop
|
|
328
|
+
|
|
329
|
+
### TypeScript Errors
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
# Clean and rebuild
|
|
333
|
+
rm -rf dist node_modules
|
|
334
|
+
npm install
|
|
335
|
+
npm run build
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
### Tests Failing
|
|
339
|
+
|
|
340
|
+
```bash
|
|
341
|
+
# Update dependencies
|
|
342
|
+
npm update
|
|
343
|
+
|
|
344
|
+
# Clear cache
|
|
345
|
+
npm cache clean --force
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## Contributing
|
|
349
|
+
|
|
350
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
351
|
+
|
|
352
|
+
## License
|
|
353
|
+
|
|
354
|
+
MIT © mcp-forge Maintainer
|
|
355
|
+
|
|
356
|
+
## Resources
|
|
357
|
+
|
|
358
|
+
- [Model Context Protocol Documentation](https://modelcontextprotocol.io)
|
|
359
|
+
- [MCP SDK](https://github.com/modelcontextprotocol/sdk)
|
|
360
|
+
- [Claude Desktop](https://claude.ai/desktop)
|
|
361
|
+
|
|
362
|
+
## Support
|
|
363
|
+
|
|
364
|
+
- 🐛 [Report Issues](https://github.com/hiyilmaz/mcp-forge/issues)
|
|
365
|
+
- 💬 [Discussions](https://github.com/hiyilmaz/mcp-forge/discussions)
|
|
366
|
+
- 📧 Email: mcpforge@grisis.com
|
|
367
|
+
|
|
368
|
+
---
|
|
369
|
+
|
|
370
|
+
**Built with ❤️ for the MCP community**
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// src/commands/init.ts
|
|
7
|
+
import * as clack2 from "@clack/prompts";
|
|
8
|
+
import { existsSync as existsSync2 } from "fs";
|
|
9
|
+
import { join as join3 } from "path";
|
|
10
|
+
|
|
11
|
+
// src/utils/scaffold.ts
|
|
12
|
+
import { cpSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
13
|
+
import { join, dirname } from "path";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
15
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
function scaffoldProject(projectName, template) {
|
|
17
|
+
const targetDir = join(process.cwd(), projectName);
|
|
18
|
+
const templateDir = join(__dirname, "../templates", template);
|
|
19
|
+
mkdirSync(targetDir, { recursive: true });
|
|
20
|
+
cpSync(templateDir, targetDir, { recursive: true });
|
|
21
|
+
replaceTokensInDirectory(targetDir, projectName);
|
|
22
|
+
}
|
|
23
|
+
function replaceTokensInDirectory(dir, projectName) {
|
|
24
|
+
const files = [
|
|
25
|
+
"package.json",
|
|
26
|
+
"README.md",
|
|
27
|
+
"src/index.ts",
|
|
28
|
+
"tests/index.test.ts"
|
|
29
|
+
];
|
|
30
|
+
for (const file of files) {
|
|
31
|
+
const filePath = join(dir, file);
|
|
32
|
+
try {
|
|
33
|
+
let content = readFileSync(filePath, "utf-8");
|
|
34
|
+
content = content.replace(/\{\{PROJECT_NAME\}\}/g, projectName);
|
|
35
|
+
writeFileSync(filePath, content, "utf-8");
|
|
36
|
+
} catch (error) {
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// src/utils/claude-config.ts
|
|
42
|
+
import { existsSync, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
43
|
+
import { join as join2, dirname as dirname2 } from "path";
|
|
44
|
+
import { homedir } from "os";
|
|
45
|
+
import * as clack from "@clack/prompts";
|
|
46
|
+
function getConfigPath() {
|
|
47
|
+
const platform = process.platform;
|
|
48
|
+
if (platform === "darwin") {
|
|
49
|
+
return join2(homedir(), "Library", "Application Support", "Claude", "claude_desktop_config.json");
|
|
50
|
+
} else if (platform === "win32") {
|
|
51
|
+
return join2(process.env.APPDATA || "", "Claude", "claude_desktop_config.json");
|
|
52
|
+
} else {
|
|
53
|
+
return join2(homedir(), ".config", "Claude", "claude_desktop_config.json");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async function registerWithClaudeDesktop(projectName, projectPath) {
|
|
57
|
+
const configPath = getConfigPath();
|
|
58
|
+
let config = { mcpServers: {} };
|
|
59
|
+
if (existsSync(configPath)) {
|
|
60
|
+
try {
|
|
61
|
+
const content = readFileSync2(configPath, "utf-8");
|
|
62
|
+
config = JSON.parse(content);
|
|
63
|
+
if (!config.mcpServers) {
|
|
64
|
+
config.mcpServers = {};
|
|
65
|
+
}
|
|
66
|
+
} catch (error) {
|
|
67
|
+
clack.log.warn("Could not parse existing config, creating new one");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
if (config.mcpServers[projectName]) {
|
|
71
|
+
const overwrite = await clack.confirm({
|
|
72
|
+
message: `Entry "${projectName}" already exists in Claude Desktop config. Overwrite?`,
|
|
73
|
+
initialValue: false
|
|
74
|
+
});
|
|
75
|
+
if (clack.isCancel(overwrite) || !overwrite) {
|
|
76
|
+
clack.log.info("Skipped Claude Desktop registration");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
config.mcpServers[projectName] = {
|
|
81
|
+
command: "node",
|
|
82
|
+
args: [join2(projectPath, "src", "index.js")]
|
|
83
|
+
};
|
|
84
|
+
try {
|
|
85
|
+
mkdirSync2(dirname2(configPath), { recursive: true });
|
|
86
|
+
writeFileSync2(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
87
|
+
clack.log.success("Registered with Claude Desktop");
|
|
88
|
+
clack.log.warn("\u26A0\uFE0F Restart Claude Desktop to apply changes");
|
|
89
|
+
} catch (error) {
|
|
90
|
+
clack.log.error(`Failed to register: ${error.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// src/commands/init.ts
|
|
95
|
+
async function init(projectName, options) {
|
|
96
|
+
clack2.intro("\u26A1 mcp-forge v0.1.0");
|
|
97
|
+
if (!projectName || projectName.length < 2 || !/^[a-z0-9-]+$/i.test(projectName)) {
|
|
98
|
+
clack2.outro("\u274C Invalid project name. Use alphanumeric characters and hyphens only (min 2 chars).");
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
const targetDir = join3(process.cwd(), projectName);
|
|
102
|
+
if (existsSync2(targetDir)) {
|
|
103
|
+
clack2.outro(`\u274C Directory "${projectName}" already exists.`);
|
|
104
|
+
process.exit(1);
|
|
105
|
+
}
|
|
106
|
+
const template = options.template || await clack2.select({
|
|
107
|
+
message: "Which template would you like to use?",
|
|
108
|
+
options: [
|
|
109
|
+
{ value: "empty", label: "Empty MCP (clean starting point)" },
|
|
110
|
+
{ value: "postgresql", label: "PostgreSQL MCP (database tools)" },
|
|
111
|
+
{ value: "rest-api", label: "REST API Wrapper MCP (HTTP tools)" },
|
|
112
|
+
{ value: "filesystem", label: "Local FileSystem MCP (file tools)" }
|
|
113
|
+
]
|
|
114
|
+
});
|
|
115
|
+
if (clack2.isCancel(template)) {
|
|
116
|
+
clack2.cancel("Operation cancelled.");
|
|
117
|
+
process.exit(0);
|
|
118
|
+
}
|
|
119
|
+
const language = options.language || await clack2.select({
|
|
120
|
+
message: "Which language?",
|
|
121
|
+
options: [
|
|
122
|
+
{ value: "typescript", label: "TypeScript" }
|
|
123
|
+
]
|
|
124
|
+
});
|
|
125
|
+
if (clack2.isCancel(language)) {
|
|
126
|
+
clack2.cancel("Operation cancelled.");
|
|
127
|
+
process.exit(0);
|
|
128
|
+
}
|
|
129
|
+
const autoRegister = options.register !== void 0 ? options.register : await clack2.confirm({
|
|
130
|
+
message: "Auto-register with Claude Desktop? (recommended)",
|
|
131
|
+
initialValue: true
|
|
132
|
+
});
|
|
133
|
+
if (clack2.isCancel(autoRegister)) {
|
|
134
|
+
clack2.cancel("Operation cancelled.");
|
|
135
|
+
process.exit(0);
|
|
136
|
+
}
|
|
137
|
+
const spinner2 = clack2.spinner();
|
|
138
|
+
spinner2.start("Creating your MCP server...");
|
|
139
|
+
scaffoldProject(projectName, template);
|
|
140
|
+
spinner2.stop("\u2714 Project scaffolded");
|
|
141
|
+
if (autoRegister) {
|
|
142
|
+
await registerWithClaudeDesktop(projectName, targetDir);
|
|
143
|
+
}
|
|
144
|
+
clack2.outro(`\u2705 ${projectName} is ready!
|
|
145
|
+
|
|
146
|
+
Next steps:
|
|
147
|
+
cd ${projectName}
|
|
148
|
+
npm install
|
|
149
|
+
npm run dev
|
|
150
|
+
|
|
151
|
+
Docs: https://github.com/org/mcp-forge`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/index.ts
|
|
155
|
+
var program = new Command();
|
|
156
|
+
program.name("mcp-forge").description("The fastest way to build MCP servers").version("0.1.0");
|
|
157
|
+
program.command("init <project-name>").description("Initialize a new MCP server project").option("--template <template>", "Template to use (empty, postgresql, rest-api, filesystem)").option("--language <language>", "Language to use (typescript)").option("--no-register", "Skip Claude Desktop auto-registration").action(async (projectName, options) => {
|
|
158
|
+
await init(projectName, options);
|
|
159
|
+
});
|
|
160
|
+
program.parse();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc",
|
|
7
|
+
"dev": "tsc && node src/index.js",
|
|
8
|
+
"test": "vitest run"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@modelcontextprotocol/sdk": "^0.5.0"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"typescript": "^5.3.3",
|
|
15
|
+
"vitest": "^1.2.0"
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
|
|
5
|
+
export const server = new Server(
|
|
6
|
+
{
|
|
7
|
+
name: '{{PROJECT_NAME}}',
|
|
8
|
+
version: '0.1.0',
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
capabilities: {
|
|
12
|
+
tools: {},
|
|
13
|
+
},
|
|
14
|
+
}
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
async function main() {
|
|
18
|
+
const transport = new StdioServerTransport();
|
|
19
|
+
await server.connect(transport);
|
|
20
|
+
console.error('MCP server running on stdio');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
main().catch((error) => {
|
|
24
|
+
console.error('Server error:', error);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALLOWED_BASE_PATH=/path/to/allowed/directory
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
Local FileSystem MCP server built with mcp-forge.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
1. Copy `.env.example` to `.env` and configure allowed path:
|
|
8
|
+
```bash
|
|
9
|
+
cp .env.example .env
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
2. Install dependencies:
|
|
13
|
+
```bash
|
|
14
|
+
npm install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Development
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm run dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Testing
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm test
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Tools
|
|
30
|
+
|
|
31
|
+
- `read_file`: Read file contents (restricted to ALLOWED_BASE_PATH)
|
|
32
|
+
- `write_file`: Write file contents (restricted to ALLOWED_BASE_PATH)
|
|
33
|
+
- `list_directory`: List directory contents (restricted to ALLOWED_BASE_PATH)
|
|
34
|
+
|
|
35
|
+
## Security
|
|
36
|
+
|
|
37
|
+
All file operations are restricted to the path specified in `ALLOWED_BASE_PATH` environment variable. Path traversal attacks are blocked.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc",
|
|
7
|
+
"dev": "tsc && node src/index.js",
|
|
8
|
+
"test": "vitest run"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@modelcontextprotocol/sdk": "^0.5.0"
|
|
12
|
+
},
|
|
13
|
+
"devDependencies": {
|
|
14
|
+
"@types/node": "^20.11.0",
|
|
15
|
+
"typescript": "^5.3.3",
|
|
16
|
+
"vitest": "^1.2.0"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { readFile, writeFile, listDirectory } from './tools/filesystem.js';
|
|
6
|
+
|
|
7
|
+
export const server = new Server(
|
|
8
|
+
{
|
|
9
|
+
name: '{{PROJECT_NAME}}',
|
|
10
|
+
version: '0.1.0',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
capabilities: {
|
|
14
|
+
tools: {},
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
20
|
+
try {
|
|
21
|
+
const { name, arguments: args } = request.params;
|
|
22
|
+
|
|
23
|
+
switch (name) {
|
|
24
|
+
case 'read_file': {
|
|
25
|
+
const content = readFile((args?.path as string) || '');
|
|
26
|
+
return { content: [{ type: 'text', text: content }] };
|
|
27
|
+
}
|
|
28
|
+
case 'write_file': {
|
|
29
|
+
writeFile((args?.path as string) || '', (args?.content as string) || '');
|
|
30
|
+
return { content: [{ type: 'text', text: 'File written successfully' }] };
|
|
31
|
+
}
|
|
32
|
+
case 'list_directory': {
|
|
33
|
+
const files = listDirectory((args?.path as string) || '');
|
|
34
|
+
return { content: [{ type: 'text', text: JSON.stringify(files, null, 2) }] };
|
|
35
|
+
}
|
|
36
|
+
default:
|
|
37
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
38
|
+
}
|
|
39
|
+
} catch (error: any) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
42
|
+
isError: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
async function main() {
|
|
48
|
+
const transport = new StdioServerTransport();
|
|
49
|
+
await server.connect(transport);
|
|
50
|
+
console.error('FileSystem MCP server running on stdio');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
main().catch((error) => {
|
|
54
|
+
console.error('Server error:', error);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, readdirSync, mkdirSync } from 'fs';
|
|
2
|
+
import { join, resolve, dirname } from 'path';
|
|
3
|
+
|
|
4
|
+
const ALLOWED_BASE_PATH = resolve(process.env.ALLOWED_BASE_PATH || process.cwd());
|
|
5
|
+
|
|
6
|
+
function isPathAllowed(filePath: string): boolean {
|
|
7
|
+
const resolvedPath = resolve(filePath);
|
|
8
|
+
return resolvedPath.startsWith(ALLOWED_BASE_PATH);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function readFile(path: string): string {
|
|
12
|
+
const fullPath = join(ALLOWED_BASE_PATH, path);
|
|
13
|
+
|
|
14
|
+
if (!isPathAllowed(fullPath)) {
|
|
15
|
+
throw new Error('Access denied: Path outside allowed directory');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
return readFileSync(fullPath, 'utf-8');
|
|
20
|
+
} catch (error: any) {
|
|
21
|
+
throw new Error(`File read error: ${error.message}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function writeFile(path: string, content: string): void {
|
|
26
|
+
const fullPath = join(ALLOWED_BASE_PATH, path);
|
|
27
|
+
|
|
28
|
+
if (!isPathAllowed(fullPath)) {
|
|
29
|
+
throw new Error('Access denied: Path outside allowed directory');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
34
|
+
writeFileSync(fullPath, content, 'utf-8');
|
|
35
|
+
} catch (error: any) {
|
|
36
|
+
throw new Error(`File write error: ${error.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function listDirectory(path: string): string[] {
|
|
41
|
+
const fullPath = join(ALLOWED_BASE_PATH, path);
|
|
42
|
+
|
|
43
|
+
if (!isPathAllowed(fullPath)) {
|
|
44
|
+
throw new Error('Access denied: Path outside allowed directory');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
return readdirSync(fullPath);
|
|
49
|
+
} catch (error: any) {
|
|
50
|
+
throw new Error(`Directory read error: ${error.message}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { readFile, writeFile } from '../src/tools/filesystem.js';
|
|
3
|
+
|
|
4
|
+
describe('FileSystem Tools', () => {
|
|
5
|
+
it('readFile blocks path traversal', () => {
|
|
6
|
+
expect(() => readFile('../../etc/passwd')).toThrow('Access denied');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('writeFile blocks path traversal', () => {
|
|
10
|
+
expect(() => writeFile('../../etc/passwd', 'test')).toThrow('Access denied');
|
|
11
|
+
});
|
|
12
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
DATABASE_URL=postgresql://user:password@localhost:5432/database
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
PostgreSQL MCP server built with mcp-forge.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
1. Copy `.env.example` to `.env` and configure your database:
|
|
8
|
+
```bash
|
|
9
|
+
cp .env.example .env
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
2. Install dependencies:
|
|
13
|
+
```bash
|
|
14
|
+
npm install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Development
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm run dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Testing
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm test
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Tools
|
|
30
|
+
|
|
31
|
+
- `query_database`: Execute raw SQL queries
|
|
32
|
+
- `list_tables`: List all tables in the database
|
|
33
|
+
- `describe_table`: Get column information for a table
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc",
|
|
7
|
+
"dev": "tsc && node src/index.js",
|
|
8
|
+
"test": "vitest run"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@modelcontextprotocol/sdk": "^0.5.0",
|
|
12
|
+
"pg": "^8.11.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/pg": "^8.11.0",
|
|
16
|
+
"typescript": "^5.3.3",
|
|
17
|
+
"vitest": "^1.2.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { queryDatabase, listTables, describeTable } from './tools/database.js';
|
|
6
|
+
|
|
7
|
+
export const server = new Server(
|
|
8
|
+
{
|
|
9
|
+
name: '{{PROJECT_NAME}}',
|
|
10
|
+
version: '0.1.0',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
capabilities: {
|
|
14
|
+
tools: {},
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
20
|
+
try {
|
|
21
|
+
const { name, arguments: args } = request.params;
|
|
22
|
+
|
|
23
|
+
switch (name) {
|
|
24
|
+
case 'query_database': {
|
|
25
|
+
const result = await queryDatabase((args?.sql as string) || '');
|
|
26
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
27
|
+
}
|
|
28
|
+
case 'list_tables': {
|
|
29
|
+
const tables = await listTables();
|
|
30
|
+
return { content: [{ type: 'text', text: JSON.stringify(tables, null, 2) }] };
|
|
31
|
+
}
|
|
32
|
+
case 'describe_table': {
|
|
33
|
+
const columns = await describeTable((args?.tableName as string) || '');
|
|
34
|
+
return { content: [{ type: 'text', text: JSON.stringify(columns, null, 2) }] };
|
|
35
|
+
}
|
|
36
|
+
default:
|
|
37
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
38
|
+
}
|
|
39
|
+
} catch (error: any) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
42
|
+
isError: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
async function main() {
|
|
48
|
+
const transport = new StdioServerTransport();
|
|
49
|
+
await server.connect(transport);
|
|
50
|
+
console.error('PostgreSQL MCP server running on stdio');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
main().catch((error) => {
|
|
54
|
+
console.error('Server error:', error);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Pool } from 'pg';
|
|
2
|
+
|
|
3
|
+
const pool = new Pool({
|
|
4
|
+
connectionString: process.env.DATABASE_URL,
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
export async function queryDatabase(sql: string) {
|
|
8
|
+
if (!sql || sql.trim().length === 0) {
|
|
9
|
+
throw new Error('SQL query cannot be empty');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
const result = await pool.query(sql);
|
|
14
|
+
return result.rows;
|
|
15
|
+
} catch (error: any) {
|
|
16
|
+
throw new Error(`Database error: ${error.message}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function listTables() {
|
|
21
|
+
try {
|
|
22
|
+
const result = await pool.query(`
|
|
23
|
+
SELECT tablename FROM pg_tables
|
|
24
|
+
WHERE schemaname = 'public'
|
|
25
|
+
ORDER BY tablename
|
|
26
|
+
`);
|
|
27
|
+
return result.rows.map(row => row.tablename);
|
|
28
|
+
} catch (error: any) {
|
|
29
|
+
throw new Error(`Database error: ${error.message}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function describeTable(tableName: string) {
|
|
34
|
+
if (!tableName || tableName.trim().length === 0) {
|
|
35
|
+
throw new Error('Table name cannot be empty');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
const result = await pool.query(`
|
|
40
|
+
SELECT column_name, data_type, is_nullable
|
|
41
|
+
FROM information_schema.columns
|
|
42
|
+
WHERE table_schema = 'public' AND table_name = $1
|
|
43
|
+
ORDER BY ordinal_position
|
|
44
|
+
`, [tableName]);
|
|
45
|
+
return result.rows;
|
|
46
|
+
} catch (error: any) {
|
|
47
|
+
throw new Error(`Database error: ${error.message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { queryDatabase, listTables, describeTable } from '../src/tools/database.js';
|
|
3
|
+
|
|
4
|
+
vi.mock('pg', () => ({
|
|
5
|
+
Pool: vi.fn(() => ({
|
|
6
|
+
query: vi.fn((sql: string) => {
|
|
7
|
+
if (sql.includes('pg_tables')) {
|
|
8
|
+
return Promise.resolve({ rows: [{ tablename: 'users' }, { tablename: 'posts' }] });
|
|
9
|
+
}
|
|
10
|
+
if (sql.includes('information_schema')) {
|
|
11
|
+
return Promise.resolve({ rows: [{ column_name: 'id', data_type: 'integer', is_nullable: 'NO' }] });
|
|
12
|
+
}
|
|
13
|
+
return Promise.resolve({ rows: [{ id: 1, name: 'test' }] });
|
|
14
|
+
}),
|
|
15
|
+
})),
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
describe('Database Tools', () => {
|
|
19
|
+
it('queryDatabase validates non-empty SQL', async () => {
|
|
20
|
+
await expect(queryDatabase('')).rejects.toThrow('SQL query cannot be empty');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('listTables returns array of table names', async () => {
|
|
24
|
+
const tables = await listTables();
|
|
25
|
+
expect(Array.isArray(tables)).toBe(true);
|
|
26
|
+
expect(tables).toContain('users');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('describeTable validates table name', async () => {
|
|
30
|
+
await expect(describeTable('')).rejects.toThrow('Table name cannot be empty');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
REST API Wrapper MCP server built with mcp-forge.
|
|
4
|
+
|
|
5
|
+
## Setup
|
|
6
|
+
|
|
7
|
+
1. Copy `.env.example` to `.env` and configure your API:
|
|
8
|
+
```bash
|
|
9
|
+
cp .env.example .env
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
2. Install dependencies:
|
|
13
|
+
```bash
|
|
14
|
+
npm install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Development
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm run dev
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Testing
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
npm test
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Tools
|
|
30
|
+
|
|
31
|
+
- `get_resource`: Make GET requests to the API
|
|
32
|
+
- `post_resource`: Make POST requests to the API
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"build": "tsc",
|
|
7
|
+
"dev": "tsc && node src/index.js",
|
|
8
|
+
"test": "vitest run"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@modelcontextprotocol/sdk": "^0.5.0",
|
|
12
|
+
"node-fetch": "^3.3.0"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"@types/node": "^20.11.0",
|
|
16
|
+
"typescript": "^5.3.3",
|
|
17
|
+
"vitest": "^1.2.0"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { getResource, postResource } from './tools/api.js';
|
|
6
|
+
|
|
7
|
+
export const server = new Server(
|
|
8
|
+
{
|
|
9
|
+
name: '{{PROJECT_NAME}}',
|
|
10
|
+
version: '0.1.0',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
capabilities: {
|
|
14
|
+
tools: {},
|
|
15
|
+
},
|
|
16
|
+
}
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
20
|
+
try {
|
|
21
|
+
const { name, arguments: args } = request.params;
|
|
22
|
+
|
|
23
|
+
switch (name) {
|
|
24
|
+
case 'get_resource': {
|
|
25
|
+
const result = await getResource((args?.path as string) || '');
|
|
26
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
27
|
+
}
|
|
28
|
+
case 'post_resource': {
|
|
29
|
+
const result = await postResource((args?.path as string) || '', args?.data);
|
|
30
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
31
|
+
}
|
|
32
|
+
default:
|
|
33
|
+
throw new Error(`Unknown tool: ${name}`);
|
|
34
|
+
}
|
|
35
|
+
} catch (error: any) {
|
|
36
|
+
return {
|
|
37
|
+
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
38
|
+
isError: true,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
async function main() {
|
|
44
|
+
const transport = new StdioServerTransport();
|
|
45
|
+
await server.connect(transport);
|
|
46
|
+
console.error('REST API MCP server running on stdio');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
main().catch((error) => {
|
|
50
|
+
console.error('Server error:', error);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
|
|
3
|
+
const BASE_URL = process.env.API_BASE_URL || '';
|
|
4
|
+
const API_KEY = process.env.API_KEY || '';
|
|
5
|
+
const TIMEOUT = 10000;
|
|
6
|
+
|
|
7
|
+
export async function getResource(path: string) {
|
|
8
|
+
const controller = new AbortController();
|
|
9
|
+
const timeout = setTimeout(() => controller.abort(), TIMEOUT);
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const response = await fetch(`${BASE_URL}${path}`, {
|
|
13
|
+
method: 'GET',
|
|
14
|
+
headers: {
|
|
15
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
},
|
|
18
|
+
signal: controller.signal,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
clearTimeout(timeout);
|
|
22
|
+
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return await response.json();
|
|
28
|
+
} catch (error: any) {
|
|
29
|
+
clearTimeout(timeout);
|
|
30
|
+
if (error.name === 'AbortError') {
|
|
31
|
+
throw new Error('Request timeout');
|
|
32
|
+
}
|
|
33
|
+
throw new Error(`API error: ${error.message}`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function postResource(path: string, data: any) {
|
|
38
|
+
const controller = new AbortController();
|
|
39
|
+
const timeout = setTimeout(() => controller.abort(), TIMEOUT);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(`${BASE_URL}${path}`, {
|
|
43
|
+
method: 'POST',
|
|
44
|
+
headers: {
|
|
45
|
+
'Authorization': `Bearer ${API_KEY}`,
|
|
46
|
+
'Content-Type': 'application/json',
|
|
47
|
+
},
|
|
48
|
+
body: JSON.stringify(data),
|
|
49
|
+
signal: controller.signal,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
clearTimeout(timeout);
|
|
53
|
+
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return await response.json();
|
|
59
|
+
} catch (error: any) {
|
|
60
|
+
clearTimeout(timeout);
|
|
61
|
+
if (error.name === 'AbortError') {
|
|
62
|
+
throw new Error('Request timeout');
|
|
63
|
+
}
|
|
64
|
+
throw new Error(`API error: ${error.message}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('node-fetch', () => ({
|
|
4
|
+
default: vi.fn((url: string, options: any) => {
|
|
5
|
+
if (options?.signal?.aborted) {
|
|
6
|
+
return Promise.reject(new Error('AbortError'));
|
|
7
|
+
}
|
|
8
|
+
return Promise.resolve({
|
|
9
|
+
ok: true,
|
|
10
|
+
status: 200,
|
|
11
|
+
json: () => Promise.resolve({ success: true }),
|
|
12
|
+
});
|
|
13
|
+
}),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
describe('API Tools', () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
vi.clearAllMocks();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('getResource makes HTTP request', async () => {
|
|
22
|
+
const { getResource } = await import('../src/tools/api.js');
|
|
23
|
+
const result = await getResource('/test');
|
|
24
|
+
expect(result).toEqual({ success: true });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('postResource makes HTTP POST request', async () => {
|
|
28
|
+
const { postResource } = await import('../src/tools/api.js');
|
|
29
|
+
const result = await postResource('/test', { data: 'test' });
|
|
30
|
+
expect(result).toEqual({ success: true });
|
|
31
|
+
});
|
|
32
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-mcp-forge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The fastest way to build MCP servers",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-mcp-forge": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsup && cp -r templates dist/",
|
|
11
|
+
"dev": "tsup --watch",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"test:watch": "vitest"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"mcp",
|
|
17
|
+
"cli",
|
|
18
|
+
"scaffold",
|
|
19
|
+
"model-context-protocol",
|
|
20
|
+
"anthropic",
|
|
21
|
+
"claude"
|
|
22
|
+
],
|
|
23
|
+
"author": "hiyilmaz",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/hiyilmaz/mcp-forge.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/hiyilmaz/mcp-forge/issues"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://github.com/hiyilmaz/mcp-forge#readme",
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@clack/prompts": "^0.7.0",
|
|
38
|
+
"commander": "^12.0.0"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^20.11.0",
|
|
42
|
+
"tsup": "^8.0.1",
|
|
43
|
+
"typescript": "^5.3.3",
|
|
44
|
+
"vitest": "^1.2.0"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18.0.0"
|
|
48
|
+
}
|
|
49
|
+
}
|