@agentvalet/mcp-server 0.1.0 → 0.2.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 +67 -0
- package/dist/config.js +34 -0
- package/dist/index.js +31 -122
- package/dist/pem.js +34 -0
- package/dist/transports/http.js +38 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# @agentvalet/mcp-server
|
|
2
|
+
|
|
3
|
+
MCP server that lets AI agents (Claude Code, Cursor, Codex CLI, etc.) call approved external platforms through the AgentValet proxy.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @agentvalet/register # registers this machine as an agent, writes config
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The CLI writes env vars and optionally updates `.mcp.json` or `.cursor/mcp.json` automatically.
|
|
12
|
+
|
|
13
|
+
## Manual configuration
|
|
14
|
+
|
|
15
|
+
Add to your `.mcp.json` or equivalent:
|
|
16
|
+
|
|
17
|
+
```json
|
|
18
|
+
{
|
|
19
|
+
"mcpServers": {
|
|
20
|
+
"agentvalet": {
|
|
21
|
+
"command": "npx",
|
|
22
|
+
"args": ["-y", "@agentvalet/mcp-server"],
|
|
23
|
+
"env": {
|
|
24
|
+
"AGENT_ID": "agt_...",
|
|
25
|
+
"OWNER_ID": "...",
|
|
26
|
+
"PROXY_URL": "https://api.agentvalet.ai",
|
|
27
|
+
"AGENT_PRIVATE_KEY_PATH": "/path/to/agent.key"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Environment variables
|
|
35
|
+
|
|
36
|
+
| Variable | Required | Description |
|
|
37
|
+
|----------|----------|-------------|
|
|
38
|
+
| `AGENT_ID` | Yes | Agent ID from registration |
|
|
39
|
+
| `OWNER_ID` | Yes | Owner ID from registration |
|
|
40
|
+
| `PROXY_URL` | Yes | AgentValet proxy URL |
|
|
41
|
+
| `AGENT_PRIVATE_KEY_PATH` | One of three | Path to PEM private key file |
|
|
42
|
+
| `AGENT_PRIVATE_KEY` | One of three | Raw PEM or `\n`-escaped single-line PEM |
|
|
43
|
+
| `AGENT_PRIVATE_KEY_B64` | One of three | Base64-encoded PEM |
|
|
44
|
+
| `MCP_TRANSPORT` | No | Set to `http` to use HTTP transport instead of STDIO |
|
|
45
|
+
| `MCP_PORT` | No | Port for HTTP transport (default: `3100`) |
|
|
46
|
+
|
|
47
|
+
## Tools
|
|
48
|
+
|
|
49
|
+
| Tool | Auth | Description |
|
|
50
|
+
|------|------|-------------|
|
|
51
|
+
| `list_platforms` | JWT | List platforms and scopes this agent has access to |
|
|
52
|
+
| `use_platform` | JWT | Call an external platform API through the proxy |
|
|
53
|
+
| `agent_register` | None | Self-register a new agent with an owner |
|
|
54
|
+
| `agent_status` | None | Poll registration approval status |
|
|
55
|
+
| `authzen_evaluate` | None | Check if this agent has access to a platform scope |
|
|
56
|
+
|
|
57
|
+
## Transports
|
|
58
|
+
|
|
59
|
+
**STDIO (default)** — compatible with all MCP hosts (Claude Code, Cursor, Codex CLI).
|
|
60
|
+
|
|
61
|
+
**HTTP** — set `MCP_TRANSPORT=http` and optionally `MCP_PORT=3100`. Uses `StreamableHTTPServerTransport`.
|
|
62
|
+
|
|
63
|
+
## Architecture
|
|
64
|
+
|
|
65
|
+
Each tool call signs a short-lived RS256 JWT (`exp: 60s`) using the agent's private key. The JWT is verified by the AgentValet proxy, which checks permissions and forwards the request to the target platform using stored credentials.
|
|
66
|
+
|
|
67
|
+
The agent never sees platform credentials. The proxy decrypts them in-memory at call time and discards them after the upstream request completes.
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readPrivateKeyFromEnv } from "./pem.js";
|
|
2
|
+
import { importPKCS8 } from "jose";
|
|
3
|
+
export async function validateConfig() {
|
|
4
|
+
const missing = [];
|
|
5
|
+
for (const key of ["AGENT_ID", "OWNER_ID", "PROXY_URL"]) {
|
|
6
|
+
if (!process.env[key])
|
|
7
|
+
missing.push(key);
|
|
8
|
+
}
|
|
9
|
+
if (missing.length > 0) {
|
|
10
|
+
process.stderr.write(`[mcp-server] Missing required environment variables: ${missing.join(", ")}\n`);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
const agentId = process.env.AGENT_ID;
|
|
14
|
+
const ownerId = process.env.OWNER_ID;
|
|
15
|
+
const proxyUrl = process.env.PROXY_URL.replace(/\/$/, "");
|
|
16
|
+
let privateKeyPem = null;
|
|
17
|
+
let privateKey = null;
|
|
18
|
+
try {
|
|
19
|
+
privateKeyPem = readPrivateKeyFromEnv();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
// No key provided — tools will return pending-activation response
|
|
23
|
+
}
|
|
24
|
+
if (privateKeyPem !== null) {
|
|
25
|
+
try {
|
|
26
|
+
privateKey = await importPKCS8(privateKeyPem, "RS256");
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
process.stderr.write(`[mcp-server] Invalid private key: ${err instanceof Error ? err.message : err}\n`);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return { agentId, ownerId, proxyUrl, privateKeyPem, privateKey };
|
|
34
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,48 +1,12 @@
|
|
|
1
1
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
3
|
import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
-
import { SignJWT
|
|
5
|
-
import {
|
|
4
|
+
import { SignJWT } from "jose";
|
|
5
|
+
import { validateConfig } from "./config.js";
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
// Startup env validation
|
|
8
8
|
// ---------------------------------------------------------------------------
|
|
9
|
-
|
|
10
|
-
if (!process.env.AGENT_PRIVATE_KEY && process.env.AGENT_PRIVATE_KEY_PATH) {
|
|
11
|
-
try {
|
|
12
|
-
process.env.AGENT_PRIVATE_KEY = readFileSync(process.env.AGENT_PRIVATE_KEY_PATH, "utf-8").trim();
|
|
13
|
-
}
|
|
14
|
-
catch (err) {
|
|
15
|
-
process.stderr.write(`[mcp-server] Cannot read AGENT_PRIVATE_KEY_PATH: ${err instanceof Error ? err.message : err}\n`);
|
|
16
|
-
process.exit(1);
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
const REQUIRED_ENV = ["AGENT_PRIVATE_KEY", "AGENT_ID", "OWNER_ID", "PROXY_URL"];
|
|
20
|
-
for (const key of REQUIRED_ENV) {
|
|
21
|
-
if (!process.env[key]) {
|
|
22
|
-
process.stderr.write(`[mcp-server] Missing required environment variable: ${key} (or AGENT_PRIVATE_KEY_PATH)\n`);
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
const AGENT_PRIVATE_KEY_RAW = process.env.AGENT_PRIVATE_KEY;
|
|
27
|
-
const AGENT_ID = process.env.AGENT_ID;
|
|
28
|
-
const OWNER_ID = process.env.OWNER_ID;
|
|
29
|
-
const PROXY_URL = process.env.PROXY_URL.replace(/\/$/, "");
|
|
30
|
-
// If the key is the sentinel value, defer key import — the guard in each tool
|
|
31
|
-
// handler will return a user-friendly error before any signing attempt.
|
|
32
|
-
let privateKey = null;
|
|
33
|
-
if (AGENT_PRIVATE_KEY_RAW !== "PENDING_FIRST_CALL") {
|
|
34
|
-
const rawKey = AGENT_PRIVATE_KEY_RAW.trim();
|
|
35
|
-
const AGENT_PRIVATE_KEY = rawKey.startsWith("-----")
|
|
36
|
-
? rawKey.replace(/\\n/g, "\n")
|
|
37
|
-
: `-----BEGIN PRIVATE KEY-----\n${rawKey}\n-----END PRIVATE KEY-----`;
|
|
38
|
-
try {
|
|
39
|
-
privateKey = await importPKCS8(AGENT_PRIVATE_KEY, "RS256");
|
|
40
|
-
}
|
|
41
|
-
catch (err) {
|
|
42
|
-
process.stderr.write(`[mcp-server] Invalid AGENT_PRIVATE_KEY: ${err instanceof Error ? err.message : err}\n`);
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
9
|
+
const { agentId: AGENT_ID, ownerId: OWNER_ID, proxyUrl: PROXY_URL, privateKeyPem: AGENT_PRIVATE_KEY_RAW, privateKey } = await validateConfig();
|
|
46
10
|
// ---------------------------------------------------------------------------
|
|
47
11
|
// JWT signing
|
|
48
12
|
// ---------------------------------------------------------------------------
|
|
@@ -162,11 +126,6 @@ const AGENT_STATUS_TOOL = {
|
|
|
162
126
|
required: ["token"],
|
|
163
127
|
},
|
|
164
128
|
};
|
|
165
|
-
const AGENT_PERMISSIONS_TOOL = {
|
|
166
|
-
name: "agent_permissions",
|
|
167
|
-
description: "agent_permissions: List all platforms and permission scopes granted to this agent.\nInput: None.\nReturns: platforms array with platformId, platformName, scopes, requireApproval.\nAuth: Bearer JWT.",
|
|
168
|
-
inputSchema: { type: "object", properties: {} },
|
|
169
|
-
};
|
|
170
129
|
const AUTHZEN_EVALUATE_TOOL = {
|
|
171
130
|
name: "authzen_evaluate",
|
|
172
131
|
description: "authzen_evaluate: Evaluate whether this agent has access to a specific platform scope.\nInput: platform_id (string), scope (string).\nReturns: decision (boolean), reason (\"approved\"|\"denied\"|\"revoked\"|\"scope_not_granted\").\nAuth: None.",
|
|
@@ -185,24 +144,7 @@ const AUTHZEN_EVALUATE_TOOL = {
|
|
|
185
144
|
required: ["platform_id", "scope"],
|
|
186
145
|
},
|
|
187
146
|
};
|
|
188
|
-
|
|
189
|
-
name: "intent_resolve",
|
|
190
|
-
description: "intent_resolve: Resolve a natural-language intent to the exact AgentValet API action to call.\nInput: intent (string, required), context (object, optional).\nReturns: resolved (boolean), capability object with endpoint, auth_required, example_request, or suggestions array.\nAuth: None.",
|
|
191
|
-
inputSchema: {
|
|
192
|
-
type: "object",
|
|
193
|
-
properties: {
|
|
194
|
-
intent: {
|
|
195
|
-
type: "string",
|
|
196
|
-
description: "Natural-language description of what the agent wants to do",
|
|
197
|
-
},
|
|
198
|
-
context: {
|
|
199
|
-
type: "object",
|
|
200
|
-
description: "Optional context object to aid resolution",
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
required: ["intent"],
|
|
204
|
-
},
|
|
205
|
-
};
|
|
147
|
+
// TODO: intent_resolve tool — planned for future release
|
|
206
148
|
// ---------------------------------------------------------------------------
|
|
207
149
|
// MCP server setup
|
|
208
150
|
// ---------------------------------------------------------------------------
|
|
@@ -213,9 +155,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
213
155
|
USE_PLATFORM_TOOL,
|
|
214
156
|
AGENT_REGISTER_TOOL,
|
|
215
157
|
AGENT_STATUS_TOOL,
|
|
216
|
-
AGENT_PERMISSIONS_TOOL,
|
|
217
158
|
AUTHZEN_EVALUATE_TOOL,
|
|
218
|
-
INTENT_RESOLVE_TOOL,
|
|
219
159
|
],
|
|
220
160
|
}));
|
|
221
161
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -260,21 +200,12 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
260
200
|
}
|
|
261
201
|
return await handleAgentStatus(args.token);
|
|
262
202
|
}
|
|
263
|
-
if (name === "agent_permissions") {
|
|
264
|
-
return await handleAgentPermissions();
|
|
265
|
-
}
|
|
266
203
|
if (name === "authzen_evaluate") {
|
|
267
204
|
if (!args || typeof args.platform_id !== "string" || typeof args.scope !== "string") {
|
|
268
205
|
return errorContent("Invalid or missing arguments: platform_id and scope are required");
|
|
269
206
|
}
|
|
270
207
|
return await handleAuthzenEvaluate(args.platform_id, args.scope);
|
|
271
208
|
}
|
|
272
|
-
if (name === "intent_resolve") {
|
|
273
|
-
if (!args || typeof args.intent !== "string") {
|
|
274
|
-
return errorContent("Invalid or missing argument: intent is required");
|
|
275
|
-
}
|
|
276
|
-
return await handleIntentResolve(args.intent, args.context);
|
|
277
|
-
}
|
|
278
209
|
return {
|
|
279
210
|
content: [{ type: "text", text: `Unknown tool: ${name}` }],
|
|
280
211
|
isError: true,
|
|
@@ -288,12 +219,20 @@ function fetchWithTimeout(url, init, timeoutMs = 15_000) {
|
|
|
288
219
|
const timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
289
220
|
return fetch(url, { ...init, signal: ac.signal }).finally(() => clearTimeout(timer));
|
|
290
221
|
}
|
|
291
|
-
async function
|
|
292
|
-
const
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
222
|
+
async function fetchWithAuth(url, init) {
|
|
223
|
+
const makeRequest = async () => {
|
|
224
|
+
const token = await signJWT();
|
|
225
|
+
return fetchWithTimeout(url, {
|
|
226
|
+
...init,
|
|
227
|
+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...init.headers },
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
const response = await makeRequest();
|
|
231
|
+
// Retry once on 401 — the JWT may have been issued just before clock skew threshold
|
|
232
|
+
if (response.status === 401) {
|
|
233
|
+
return makeRequest();
|
|
234
|
+
}
|
|
235
|
+
return response;
|
|
297
236
|
}
|
|
298
237
|
function errorContent(message) {
|
|
299
238
|
return { content: [{ type: "text", text: message }], isError: true };
|
|
@@ -302,13 +241,13 @@ function errorContent(message) {
|
|
|
302
241
|
// Tool handlers
|
|
303
242
|
// ---------------------------------------------------------------------------
|
|
304
243
|
async function handleListPlatforms() {
|
|
305
|
-
if (AGENT_PRIVATE_KEY_RAW ===
|
|
244
|
+
if (AGENT_PRIVATE_KEY_RAW === null) {
|
|
306
245
|
await notifyBindSecret();
|
|
307
246
|
return pendingFirstCallResponse();
|
|
308
247
|
}
|
|
309
248
|
let response;
|
|
310
249
|
try {
|
|
311
|
-
response = await
|
|
250
|
+
response = await fetchWithAuth(`${PROXY_URL}/v1/agent/permissions`, { method: "GET", headers: {} });
|
|
312
251
|
}
|
|
313
252
|
catch (err) {
|
|
314
253
|
return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
|
|
@@ -319,7 +258,7 @@ async function handleListPlatforms() {
|
|
|
319
258
|
return { content: [{ type: "text", text: body }] };
|
|
320
259
|
}
|
|
321
260
|
async function handleUsePlatform(params) {
|
|
322
|
-
if (AGENT_PRIVATE_KEY_RAW ===
|
|
261
|
+
if (AGENT_PRIVATE_KEY_RAW === null) {
|
|
323
262
|
await notifyBindSecret();
|
|
324
263
|
return pendingFirstCallResponse();
|
|
325
264
|
}
|
|
@@ -332,7 +271,7 @@ async function handleUsePlatform(params) {
|
|
|
332
271
|
};
|
|
333
272
|
let response;
|
|
334
273
|
try {
|
|
335
|
-
response = await
|
|
274
|
+
response = await fetchWithAuth(`${PROXY_URL}/v1/actions`, {
|
|
336
275
|
method: "POST",
|
|
337
276
|
body: JSON.stringify(requestBody),
|
|
338
277
|
});
|
|
@@ -378,23 +317,6 @@ async function handleAgentStatus(token) {
|
|
|
378
317
|
return errorContent(`Proxy error ${response.status}: ${body}`);
|
|
379
318
|
return { content: [{ type: "text", text: body }] };
|
|
380
319
|
}
|
|
381
|
-
async function handleAgentPermissions() {
|
|
382
|
-
if (AGENT_PRIVATE_KEY_RAW === "PENDING_FIRST_CALL") {
|
|
383
|
-
await notifyBindSecret();
|
|
384
|
-
return pendingFirstCallResponse();
|
|
385
|
-
}
|
|
386
|
-
let response;
|
|
387
|
-
try {
|
|
388
|
-
response = await bearerFetch(`${PROXY_URL}/v1/agent/permissions`, { method: "GET", headers: {} });
|
|
389
|
-
}
|
|
390
|
-
catch (err) {
|
|
391
|
-
return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
|
|
392
|
-
}
|
|
393
|
-
const body = await response.text();
|
|
394
|
-
if (!response.ok)
|
|
395
|
-
return errorContent(`Proxy error ${response.status}: ${body}`);
|
|
396
|
-
return { content: [{ type: "text", text: body }] };
|
|
397
|
-
}
|
|
398
320
|
async function handleAuthzenEvaluate(platformId, scope) {
|
|
399
321
|
const authzenBody = {
|
|
400
322
|
subject: { type: "agent", id: AGENT_ID },
|
|
@@ -417,28 +339,15 @@ async function handleAuthzenEvaluate(platformId, scope) {
|
|
|
417
339
|
return errorContent(`Proxy error ${response.status}: ${body}`);
|
|
418
340
|
return { content: [{ type: "text", text: body }] };
|
|
419
341
|
}
|
|
420
|
-
async function handleIntentResolve(intent, context) {
|
|
421
|
-
const requestBody = { intent };
|
|
422
|
-
if (context !== undefined)
|
|
423
|
-
requestBody.context = context;
|
|
424
|
-
let response;
|
|
425
|
-
try {
|
|
426
|
-
response = await fetchWithTimeout(`${PROXY_URL}/v1/intent/resolve`, {
|
|
427
|
-
method: "POST",
|
|
428
|
-
headers: { "Content-Type": "application/json" },
|
|
429
|
-
body: JSON.stringify(requestBody),
|
|
430
|
-
});
|
|
431
|
-
}
|
|
432
|
-
catch (err) {
|
|
433
|
-
return errorContent(`Network error: ${err instanceof Error ? err.message : err}`);
|
|
434
|
-
}
|
|
435
|
-
const body = await response.text();
|
|
436
|
-
if (!response.ok)
|
|
437
|
-
return errorContent(`Proxy error ${response.status}: ${body}`);
|
|
438
|
-
return { content: [{ type: "text", text: body }] };
|
|
439
|
-
}
|
|
440
342
|
// ---------------------------------------------------------------------------
|
|
441
343
|
// Connect transport
|
|
442
344
|
// ---------------------------------------------------------------------------
|
|
443
|
-
|
|
444
|
-
await
|
|
345
|
+
if (process.env.MCP_TRANSPORT === "http") {
|
|
346
|
+
const { startHttpTransport } = await import("./transports/http.js");
|
|
347
|
+
const port = parseInt(process.env.MCP_PORT ?? "3100", 10);
|
|
348
|
+
await startHttpTransport(server, port);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
const transport = new StdioServerTransport();
|
|
352
|
+
await server.connect(transport);
|
|
353
|
+
}
|
package/dist/pem.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readFileSync } from "fs";
|
|
2
|
+
/**
|
|
3
|
+
* Reads the RS256 private key from environment variables, supporting four formats:
|
|
4
|
+
* - AGENT_PRIVATE_KEY_B64: base64-encoded PEM
|
|
5
|
+
* - AGENT_PRIVATE_KEY_PATH: path to a PEM file
|
|
6
|
+
* - AGENT_PRIVATE_KEY: raw multi-line PEM or \n-escaped single-line PEM
|
|
7
|
+
*/
|
|
8
|
+
export function readPrivateKeyFromEnv() {
|
|
9
|
+
// 1. Base64-encoded PEM
|
|
10
|
+
if (process.env.AGENT_PRIVATE_KEY_B64) {
|
|
11
|
+
return Buffer.from(process.env.AGENT_PRIVATE_KEY_B64, "base64").toString("utf-8").trim();
|
|
12
|
+
}
|
|
13
|
+
// 2. Path to PEM file
|
|
14
|
+
if (process.env.AGENT_PRIVATE_KEY_PATH) {
|
|
15
|
+
try {
|
|
16
|
+
return readFileSync(process.env.AGENT_PRIVATE_KEY_PATH, "utf-8").trim();
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
throw new Error(`Cannot read AGENT_PRIVATE_KEY_PATH: ${err instanceof Error ? err.message : err}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
// 3. Raw PEM content (multi-line or \n-escaped)
|
|
23
|
+
if (process.env.AGENT_PRIVATE_KEY) {
|
|
24
|
+
const raw = process.env.AGENT_PRIVATE_KEY.trim();
|
|
25
|
+
// Unescape \n sequences
|
|
26
|
+
const unescaped = raw.replace(/\\n/g, "\n");
|
|
27
|
+
// Wrap bare base64 blob without headers
|
|
28
|
+
if (!unescaped.startsWith("-----")) {
|
|
29
|
+
return `-----BEGIN PRIVATE KEY-----\n${unescaped}\n-----END PRIVATE KEY-----`;
|
|
30
|
+
}
|
|
31
|
+
return unescaped;
|
|
32
|
+
}
|
|
33
|
+
throw new Error("No private key provided. Set AGENT_PRIVATE_KEY, AGENT_PRIVATE_KEY_PATH, or AGENT_PRIVATE_KEY_B64.");
|
|
34
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
3
|
+
export async function startHttpTransport(server, port) {
|
|
4
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
5
|
+
const httpServer = createServer(async (req, res) => {
|
|
6
|
+
try {
|
|
7
|
+
await transport.handleRequest(req, res, await readBody(req));
|
|
8
|
+
}
|
|
9
|
+
catch (err) {
|
|
10
|
+
res.writeHead(500);
|
|
11
|
+
res.end(JSON.stringify({ error: "Internal server error" }));
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
await server.connect(transport);
|
|
15
|
+
httpServer.listen(port, () => {
|
|
16
|
+
process.stderr.write(`[mcp-server] HTTP transport listening on port ${port}\n`);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function readBody(req) {
|
|
20
|
+
return new Promise((resolve, reject) => {
|
|
21
|
+
const chunks = [];
|
|
22
|
+
req.on("data", (chunk) => chunks.push(chunk));
|
|
23
|
+
req.on("end", () => {
|
|
24
|
+
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
25
|
+
if (!raw) {
|
|
26
|
+
resolve(undefined);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
resolve(JSON.parse(raw));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
resolve(undefined);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
req.on("error", reject);
|
|
37
|
+
});
|
|
38
|
+
}
|