Skip to content

Commit a375033

Browse files
committed
feat: Add Vercel serverless function support for MCP server
- Add Vercel serverless function handler (api/mcp.ts) - Add serverless MCP implementation (mcp-serverless.ts) - Add Vercel configuration (vercel.json, .vercelignore) - Add environment variable example (.env.example) - Update TypeScript configuration for serverless build - Add documentation for Vercel HTTPS endpoint setup close #111
1 parent 44c6508 commit a375033

11 files changed

Lines changed: 2035 additions & 16 deletions

File tree

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,5 @@ coverage/
4848

4949
# AI
5050
.mcp.json
51-
52-
# codingbuddy.config.js
5351
codingbuddy.config.js
52+
custom.mdc

apps/mcp-server/.vercelignore

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Dependencies (installed during build)
2+
node_modules
3+
4+
# Source maps
5+
*.map
6+
7+
# Tests
8+
**/*.spec.ts
9+
**/*.test.ts
10+
__tests__
11+
12+
# Development files
13+
.env.local
14+
.env.*.local
15+
16+
# IDE
17+
.idea
18+
.vscode
19+
20+
# Build cache
21+
.turbo

apps/mcp-server/README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,43 @@ The server will start in SSE mode, exposing:
134134
- `GET /sse`: SSE Endpoint
135135
- `POST /messages`: Message Endpoint
136136

137+
### Option 5: Vercel Deployment
138+
139+
The MCP server can be deployed to Vercel as a serverless function:
140+
141+
#### Deploy
142+
143+
```bash
144+
cd apps/mcp-server
145+
npx vercel deploy
146+
```
147+
148+
#### Endpoint
149+
150+
- **URL**: `https://your-project.vercel.app/api/mcp`
151+
- **Method**: POST
152+
- **Content-Type**: application/json
153+
154+
#### Example Request
155+
156+
```bash
157+
curl -X POST https://your-project.vercel.app/api/mcp \
158+
-H "Content-Type: application/json" \
159+
-d '{
160+
"jsonrpc": "2.0",
161+
"method": "tools/list",
162+
"id": 1
163+
}'
164+
```
165+
166+
### Transport Modes
167+
168+
| Mode | Use Case | Command |
169+
|------|----------|---------|
170+
| Stdio | CLI integration | `yarn start` |
171+
| SSE | Self-hosted HTTP | `MCP_TRANSPORT=sse yarn start` |
172+
| Vercel | Serverless HTTPS | `npx vercel deploy` |
173+
137174
## Environment Variables
138175

139176
| Variable | Description | Default |

apps/mcp-server/api/mcp.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import type { VercelRequest, VercelResponse } from '@vercel/node';
2+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
3+
import { McpServerlessService } from '../src/mcp/mcp-serverless';
4+
5+
// Constants
6+
const MAX_BODY_SIZE = 1024 * 1024; // 1MB limit
7+
const ALLOWED_METHODS = ['GET', 'POST', 'DELETE', 'OPTIONS'];
8+
9+
// Singleton service instance (reused across warm invocations)
10+
let mcpService: McpServerlessService | null = null;
11+
12+
function getService(): McpServerlessService {
13+
if (!mcpService) {
14+
mcpService = new McpServerlessService();
15+
}
16+
return mcpService;
17+
}
18+
19+
function jsonRpcError(
20+
res: VercelResponse,
21+
code: number,
22+
message: string,
23+
status: number = 500,
24+
): void {
25+
res.status(status).json({
26+
jsonrpc: '2.0',
27+
error: { code, message },
28+
id: null,
29+
});
30+
}
31+
32+
async function parseBody(req: VercelRequest): Promise<unknown> {
33+
// Vercel may already parse body in some cases
34+
if (req.body && typeof req.body === 'object') {
35+
return req.body;
36+
}
37+
38+
// Manual parsing for streaming body
39+
const chunks: Buffer[] = [];
40+
let totalSize = 0;
41+
42+
for await (const chunk of req as unknown as AsyncIterable<Buffer>) {
43+
totalSize += chunk.length;
44+
if (totalSize > MAX_BODY_SIZE) {
45+
throw new Error('PAYLOAD_TOO_LARGE');
46+
}
47+
chunks.push(chunk);
48+
}
49+
50+
if (chunks.length === 0) {
51+
return undefined;
52+
}
53+
54+
const bodyStr = Buffer.concat(chunks).toString('utf-8');
55+
return JSON.parse(bodyStr);
56+
}
57+
58+
export default async function handler(
59+
req: VercelRequest,
60+
res: VercelResponse,
61+
): Promise<void> {
62+
// CORS headers
63+
res.setHeader('Access-Control-Allow-Origin', '*');
64+
res.setHeader('Access-Control-Allow-Methods', ALLOWED_METHODS.join(', '));
65+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id');
66+
67+
// Handle preflight
68+
if (req.method === 'OPTIONS') {
69+
res.status(200).end();
70+
return;
71+
}
72+
73+
// Validate method
74+
if (!req.method || !ALLOWED_METHODS.includes(req.method)) {
75+
return jsonRpcError(res, -32600, 'Method not allowed', 405);
76+
}
77+
78+
// Check Content-Length header for early rejection
79+
const contentLength = req.headers['content-length'];
80+
if (contentLength && parseInt(contentLength, 10) > MAX_BODY_SIZE) {
81+
return jsonRpcError(res, -32700, 'Request entity too large', 413);
82+
}
83+
84+
// Validate Content-Type for POST
85+
if (req.method === 'POST') {
86+
const contentType = req.headers['content-type'] || '';
87+
if (!contentType.includes('application/json')) {
88+
return jsonRpcError(
89+
res,
90+
-32700,
91+
'Unsupported Media Type: expected application/json',
92+
415,
93+
);
94+
}
95+
}
96+
97+
try {
98+
// Parse request body
99+
let body: unknown;
100+
if (req.method === 'POST') {
101+
try {
102+
body = await parseBody(req);
103+
} catch (parseError) {
104+
if (
105+
parseError instanceof Error &&
106+
parseError.message === 'PAYLOAD_TOO_LARGE'
107+
) {
108+
return jsonRpcError(res, -32700, 'Request entity too large', 413);
109+
}
110+
console.error('Body parse error:', parseError);
111+
return jsonRpcError(res, -32700, 'Parse error: invalid JSON', 400);
112+
}
113+
}
114+
115+
const service = getService();
116+
const server = service.getServer();
117+
118+
// Create stateless transport for this request
119+
const transport = new StreamableHTTPServerTransport({
120+
sessionIdGenerator: undefined, // Stateless mode
121+
enableJsonResponse: true,
122+
});
123+
124+
// Connect server to transport
125+
await server.connect(transport);
126+
127+
// Handle the request
128+
await transport.handleRequest(
129+
req as unknown as import('express').Request,
130+
res as unknown as import('express').Response,
131+
body,
132+
);
133+
} catch (error) {
134+
console.error('MCP handler error:', error);
135+
const message =
136+
error instanceof Error ? error.message : 'Internal server error';
137+
return jsonRpcError(res, -32603, message);
138+
}
139+
}

apps/mcp-server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
"@nestjs/schematics": "^11.0.9",
7777
"@types/express": "^5.0.6",
7878
"@types/node": "^25.0.3",
79+
"@vercel/node": "^5.5.16",
7980
"@vitest/coverage-v8": "^4.0.15",
8081
"eslint": "^9.39.2",
8182
"eslint-config-prettier": "^10.1.8",

0 commit comments

Comments
 (0)