@studiometa/forge-mcp 0.2.2 → 0.2.4
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 +61 -19
- package/dist/auth.d.ts +12 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +19 -1
- package/dist/auth.js.map +1 -1
- package/dist/crypto.d.ts +56 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +110 -0
- package/dist/crypto.js.map +1 -0
- package/dist/handlers/auto-resolve.d.ts +24 -0
- package/dist/handlers/auto-resolve.d.ts.map +1 -0
- package/dist/handlers/batch.d.ts +14 -0
- package/dist/handlers/batch.d.ts.map +1 -0
- package/dist/handlers/context.d.ts +14 -0
- package/dist/handlers/context.d.ts.map +1 -0
- package/dist/handlers/help.d.ts.map +1 -1
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/schema.d.ts.map +1 -1
- package/dist/handlers/servers.d.ts +6 -1
- package/dist/handlers/servers.d.ts.map +1 -1
- package/dist/handlers/sites.d.ts +6 -1
- package/dist/handlers/sites.d.ts.map +1 -1
- package/dist/{http-BMOiJdyw.js → http-DN0I5GR6.js} +16 -4
- package/dist/http-DN0I5GR6.js.map +1 -0
- package/dist/http.d.ts +1 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +2 -2
- package/dist/index.js +1 -1
- package/dist/oauth.d.ts +118 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +571 -0
- package/dist/oauth.js.map +1 -0
- package/dist/server.d.ts +24 -6
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +33 -9
- package/dist/server.js.map +1 -1
- package/dist/tools.d.ts +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/{version-D3OFS3DQ.js → version-Gjth4BwC.js} +2799 -2450
- package/dist/version-Gjth4BwC.js.map +1 -0
- package/package.json +9 -1
- package/skills/SKILL.md +152 -29
- package/dist/http-BMOiJdyw.js.map +0 -1
- package/dist/version-D3OFS3DQ.js.map +0 -1
package/skills/SKILL.md
CHANGED
|
@@ -22,9 +22,9 @@ Before your first interaction with any resource, call `action="help"` with that
|
|
|
22
22
|
|
|
23
23
|
### `forge` — Read Operations
|
|
24
24
|
|
|
25
|
-
Safe, read-only queries. Use for listing, getting details, help, and schema.
|
|
25
|
+
Safe, read-only queries. Use for listing, getting details, resolving names, fetching context snapshots, help, and schema.
|
|
26
26
|
|
|
27
|
-
**Actions**: `list`, `get`, `help`, `schema`
|
|
27
|
+
**Actions**: `list`, `get`, `resolve`, `context`, `help`, `schema`
|
|
28
28
|
|
|
29
29
|
```
|
|
30
30
|
forge(resource, action, [parameters...])
|
|
@@ -42,24 +42,29 @@ forge_write(resource, action, [parameters...])
|
|
|
42
42
|
|
|
43
43
|
### Resources & Actions
|
|
44
44
|
|
|
45
|
-
| Resource | Read Actions (`forge`)
|
|
46
|
-
| ----------------- |
|
|
47
|
-
| `servers` | `list`, `get`
|
|
48
|
-
| `sites` | `list`, `get`
|
|
49
|
-
| `deployments` | `list`
|
|
50
|
-
| `env` | `get`
|
|
51
|
-
| `nginx` | `get`
|
|
52
|
-
| `certificates` | `list`, `get`
|
|
53
|
-
| `databases` | `list`, `get`
|
|
54
|
-
| `database-users` | `list`, `get`
|
|
55
|
-
| `daemons` | `list`, `get`
|
|
56
|
-
| `firewall-rules` | `list`, `get`
|
|
57
|
-
| `ssh-keys` | `list`, `get`
|
|
58
|
-
| `security-rules` | `list`, `get`
|
|
59
|
-
| `redirect-rules` | `list`, `get`
|
|
60
|
-
| `monitors` | `list`, `get`
|
|
61
|
-
| `nginx-templates` | `list`, `get`
|
|
62
|
-
| `recipes` | `list`, `get`
|
|
45
|
+
| Resource | Read Actions (`forge`) | Write Actions (`forge_write`) | Scope |
|
|
46
|
+
| ----------------- | ----------------------------------- | ------------------------------ | ------ |
|
|
47
|
+
| `servers` | `list`, `get`, `resolve`, `context` | `create`, `delete`, `reboot` | global |
|
|
48
|
+
| `sites` | `list`, `get`, `resolve`, `context` | `create`, `delete` | server |
|
|
49
|
+
| `deployments` | `list` | `deploy`, `update` | site |
|
|
50
|
+
| `env` | `get` | `update` | site |
|
|
51
|
+
| `nginx` | `get` | `update` | site |
|
|
52
|
+
| `certificates` | `list`, `get` | `create`, `delete`, `activate` | site |
|
|
53
|
+
| `databases` | `list`, `get` | `create`, `delete` | server |
|
|
54
|
+
| `database-users` | `list`, `get` | `create`, `delete` | server |
|
|
55
|
+
| `daemons` | `list`, `get` | `create`, `delete`, `restart` | server |
|
|
56
|
+
| `firewall-rules` | `list`, `get` | `create`, `delete` | server |
|
|
57
|
+
| `ssh-keys` | `list`, `get` | `create`, `delete` | server |
|
|
58
|
+
| `security-rules` | `list`, `get` | `create`, `delete` | site |
|
|
59
|
+
| `redirect-rules` | `list`, `get` | `create`, `delete` | site |
|
|
60
|
+
| `monitors` | `list`, `get` | `create`, `delete` | server |
|
|
61
|
+
| `nginx-templates` | `list`, `get` | `create`, `update`, `delete` | server |
|
|
62
|
+
| `recipes` | `list`, `get` | `create`, `delete`, `run` | global |
|
|
63
|
+
| `scheduled-jobs` | `list`, `get` | `create`, `delete` | server |
|
|
64
|
+
| `backups` | `list`, `get` | `create`, `delete` | server |
|
|
65
|
+
| `commands` | `list`, `get` | `create` | site |
|
|
66
|
+
| `user` | `get` | — | global |
|
|
67
|
+
| `batch` | `run` | — | global |
|
|
63
68
|
|
|
64
69
|
### Scope Guide
|
|
65
70
|
|
|
@@ -67,6 +72,31 @@ forge_write(resource, action, [parameters...])
|
|
|
67
72
|
- **server**: Requires `server_id` (e.g. `sites`, `databases`, `daemons`)
|
|
68
73
|
- **site**: Requires `server_id` + `site_id` (e.g. `deployments`, `env`, `certificates`)
|
|
69
74
|
|
|
75
|
+
### Auto-Resolve: Names Instead of IDs
|
|
76
|
+
|
|
77
|
+
`server_id` and `site_id` accept **name strings** in addition to numeric IDs. When a non-numeric value is provided, the server automatically performs a case-insensitive partial match against known resources:
|
|
78
|
+
|
|
79
|
+
- **`server_id`**: Matched against server names (e.g. `"prod"` resolves `"production-web-01"`)
|
|
80
|
+
- **`site_id`**: Matched against site domains (e.g. `"myapp"` resolves `"myapp.example.com"`)
|
|
81
|
+
|
|
82
|
+
Resolution rules:
|
|
83
|
+
|
|
84
|
+
- **Exact single match** → resolved silently, the call proceeds normally
|
|
85
|
+
- **Ambiguous (multiple matches)** → error listing all candidates; use a more specific name or numeric ID
|
|
86
|
+
- **No match** → error with a hint to use `action: "resolve"` to search
|
|
87
|
+
|
|
88
|
+
This applies to **every** resource and action automatically — no special syntax needed.
|
|
89
|
+
|
|
90
|
+
Use `action: "resolve"` explicitly when you need to search or preview matches before acting:
|
|
91
|
+
|
|
92
|
+
```json
|
|
93
|
+
// Search servers by name
|
|
94
|
+
{ "resource": "servers", "action": "resolve", "query": "prod" }
|
|
95
|
+
|
|
96
|
+
// Search sites on a server by domain
|
|
97
|
+
{ "resource": "sites", "action": "resolve", "server_id": "123", "query": "myapp" }
|
|
98
|
+
```
|
|
99
|
+
|
|
70
100
|
### Getting Help
|
|
71
101
|
|
|
72
102
|
Use `action: "help"` to get detailed documentation for any resource:
|
|
@@ -84,15 +114,17 @@ Use `action: "schema"` for a compact machine-readable spec:
|
|
|
84
114
|
|
|
85
115
|
## Common Parameters
|
|
86
116
|
|
|
87
|
-
| Parameter
|
|
88
|
-
|
|
|
89
|
-
| `resource`
|
|
90
|
-
| `action`
|
|
91
|
-
| `id`
|
|
92
|
-
| `server_id`
|
|
93
|
-
| `site_id`
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
117
|
+
| Parameter | Type | Description |
|
|
118
|
+
| ------------ | ------- | ------------------------------------------------------------------------------------------------------- |
|
|
119
|
+
| `resource` | string | **Required**. Resource type (see table above) |
|
|
120
|
+
| `action` | string | **Required**. Action to perform |
|
|
121
|
+
| `id` | string | Resource ID (for `get`, `delete`, `activate`, `restart`) |
|
|
122
|
+
| `server_id` | string | Server ID **or name** — numeric IDs are used as-is; names are auto-resolved via partial match |
|
|
123
|
+
| `site_id` | string | Site ID **or domain name** — auto-resolved via partial match; requires `server_id` |
|
|
124
|
+
| `query` | string | Search query for `resolve` action (partial, case-insensitive match against resource names/domains) |
|
|
125
|
+
| `compact` | boolean | Compact output (default: true) |
|
|
126
|
+
| `content` | string | Content for env/nginx `update` and deployment script `update` |
|
|
127
|
+
| `operations` | array | Array of operations for `batch` `run` (max 10). Each item needs `resource`, `action`, and extra params. |
|
|
96
128
|
|
|
97
129
|
## Common Workflows
|
|
98
130
|
|
|
@@ -172,6 +204,93 @@ Use `action: "schema"` for a compact machine-readable spec:
|
|
|
172
204
|
{ "resource": "recipes", "action": "run", "id": "456", "servers": [1, 2, 3] }
|
|
173
205
|
```
|
|
174
206
|
|
|
207
|
+
### Auto-Resolve: Deploy Using Names Instead of IDs
|
|
208
|
+
|
|
209
|
+
No need to look up numeric IDs first — just pass names directly:
|
|
210
|
+
|
|
211
|
+
```json
|
|
212
|
+
// Deploy using server name and site domain (forge_write)
|
|
213
|
+
// "prod" and "myapp.example.com" are auto-resolved to numeric IDs
|
|
214
|
+
{ "resource": "deployments", "action": "deploy", "server_id": "prod", "site_id": "myapp.example.com" }
|
|
215
|
+
|
|
216
|
+
// Update env using a partial server name and domain fragment
|
|
217
|
+
{ "resource": "env", "action": "get", "server_id": "production", "site_id": "myapp" }
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Resolve: Find Resources by Name
|
|
221
|
+
|
|
222
|
+
Use `action: "resolve"` to search resources by name before acting:
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
// Find servers matching "prod" (forge)
|
|
226
|
+
{ "resource": "servers", "action": "resolve", "query": "prod" }
|
|
227
|
+
// → returns matches: [{ id: 123, name: "production-web-01" }, ...]
|
|
228
|
+
|
|
229
|
+
// Find sites on a server matching a domain fragment (forge)
|
|
230
|
+
{ "resource": "sites", "action": "resolve", "server_id": "123", "query": "myapp" }
|
|
231
|
+
// → returns matches: [{ id: 456, name: "myapp.example.com" }, ...]
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Context: Get a Full Snapshot in One Call
|
|
235
|
+
|
|
236
|
+
`action: "context"` fetches a resource plus all its sub-resources in a single parallel call — ideal for orientation at the start of a session:
|
|
237
|
+
|
|
238
|
+
```json
|
|
239
|
+
// Server context (forge) — returns server + sites, databases, database_users,
|
|
240
|
+
// daemons, firewall_rules, scheduled_jobs
|
|
241
|
+
{ "resource": "servers", "action": "context", "id": "123" }
|
|
242
|
+
|
|
243
|
+
// Site context (forge) — returns site + deployments (last 5), certificates,
|
|
244
|
+
// redirect_rules, security_rules
|
|
245
|
+
{ "resource": "sites", "action": "context", "server_id": "123", "id": "456" }
|
|
246
|
+
|
|
247
|
+
// Combine with auto-resolve: use names instead of IDs
|
|
248
|
+
{ "resource": "servers", "action": "context", "id": "prod" }
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Batch: Multiple Reads in a Single Call
|
|
252
|
+
|
|
253
|
+
`resource: "batch"` with `action: "run"` executes up to 10 read operations in parallel, reducing round-trips:
|
|
254
|
+
|
|
255
|
+
```json
|
|
256
|
+
// Fetch servers list + user info in one call (forge)
|
|
257
|
+
{
|
|
258
|
+
"resource": "batch",
|
|
259
|
+
"action": "run",
|
|
260
|
+
"operations": [
|
|
261
|
+
{ "resource": "servers", "action": "list" },
|
|
262
|
+
{ "resource": "user", "action": "get" }
|
|
263
|
+
]
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Fetch multiple site sub-resources in parallel (forge)
|
|
267
|
+
{
|
|
268
|
+
"resource": "batch",
|
|
269
|
+
"action": "run",
|
|
270
|
+
"operations": [
|
|
271
|
+
{ "resource": "env", "action": "get", "server_id": "123", "site_id": "456" },
|
|
272
|
+
{ "resource": "certificates", "action": "list", "server_id": "123", "site_id": "456" },
|
|
273
|
+
{ "resource": "deployments", "action": "list", "server_id": "123", "site_id": "456" }
|
|
274
|
+
]
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Result shape:
|
|
279
|
+
|
|
280
|
+
```json
|
|
281
|
+
{
|
|
282
|
+
"_batch": { "total": 2, "succeeded": 2, "failed": 0 },
|
|
283
|
+
"results": [
|
|
284
|
+
{ "index": 0, "resource": "servers", "action": "list", "data": [...] },
|
|
285
|
+
{ "index": 1, "resource": "user", "action": "get", "data": {...} }
|
|
286
|
+
]
|
|
287
|
+
}
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
Per-operation failures are isolated — a single error doesn't abort the rest.
|
|
291
|
+
|
|
292
|
+
> **Note**: Batch only supports read actions (`list`, `get`, `resolve`, `context`, `help`, `schema`). Write operations must use `forge_write` individually.
|
|
293
|
+
|
|
175
294
|
## Authentication
|
|
176
295
|
|
|
177
296
|
### Stdio Mode (Claude Desktop)
|
|
@@ -217,3 +336,7 @@ All `forge_write` operations are automatically logged to `~/.config/forge-tools/
|
|
|
217
336
|
4. **Chain operations**: List servers → list sites → deploy (follow the hierarchy)
|
|
218
337
|
5. **Scope matters**: Server-scoped resources need `server_id`, site-scoped need both `server_id` and `site_id`
|
|
219
338
|
6. **Read vs Write**: Use `forge` for queries, `forge_write` for mutations — MCP clients enforce this split
|
|
339
|
+
7. **Use names, not IDs**: Pass server names and site domains directly — they are auto-resolved to numeric IDs
|
|
340
|
+
8. **Use `context` for orientation**: One call fetches a server or site plus all its sub-resources in parallel
|
|
341
|
+
9. **Use `batch` to cut round-trips**: Bundle up to 10 read operations into a single `forge` call
|
|
342
|
+
10. **Use `resolve` to preview**: Search for resources by name before committing to an action
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"http-BMOiJdyw.js","names":[],"sources":["../src/sessions.ts","../src/http.ts"],"sourcesContent":["/**\n * Session manager for multi-tenant Streamable HTTP transport.\n *\n * Each MCP client session gets its own transport + server pair.\n * Sessions are identified by UUID and tracked in a Map.\n *\n * Supports automatic TTL-based cleanup of idle sessions to prevent\n * memory leaks from abandoned clients.\n */\n\nimport type { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport type { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\n\n/**\n * A managed session: transport + MCP server pair.\n */\nexport interface ManagedSession {\n transport: StreamableHTTPServerTransport;\n server: Server;\n createdAt: number;\n lastActiveAt: number;\n}\n\nexport interface SessionManagerOptions {\n /**\n * Maximum idle time in milliseconds before a session is reaped.\n * Default: 30 minutes. Set to 0 to disable automatic cleanup.\n */\n ttl?: number;\n\n /**\n * How often to check for expired sessions, in milliseconds.\n * Default: 60 seconds.\n */\n sweepInterval?: number;\n}\n\nconst DEFAULT_TTL = 30 * 60 * 1000; // 30 minutes\nconst DEFAULT_SWEEP_INTERVAL = 60 * 1000; // 60 seconds\n\nexport class SessionManager {\n private sessions = new Map<string, ManagedSession>();\n private sweepTimer: ReturnType<typeof setInterval> | undefined;\n private readonly ttl: number;\n\n constructor(options?: SessionManagerOptions) {\n this.ttl = options?.ttl ?? DEFAULT_TTL;\n\n if (this.ttl > 0) {\n const interval = options?.sweepInterval ?? DEFAULT_SWEEP_INTERVAL;\n this.sweepTimer = setInterval(() => {\n this.sweep();\n }, interval);\n // Don't keep the process alive just for the sweep timer\n this.sweepTimer.unref();\n }\n }\n\n /**\n * Register a session after its ID has been assigned by the transport.\n */\n register(transport: StreamableHTTPServerTransport, server: Server): void {\n const sessionId = transport.sessionId;\n if (sessionId) {\n const now = Date.now();\n this.sessions.set(sessionId, {\n transport,\n server,\n createdAt: now,\n lastActiveAt: now,\n });\n }\n }\n\n /**\n * Look up a session by its ID and refresh its activity timestamp.\n */\n get(sessionId: string): ManagedSession | undefined {\n const session = this.sessions.get(sessionId);\n if (session) {\n session.lastActiveAt = Date.now();\n }\n return session;\n }\n\n /**\n * Remove a session and close its transport + server.\n */\n async remove(sessionId: string): Promise<void> {\n const session = this.sessions.get(sessionId);\n if (session) {\n this.sessions.delete(sessionId);\n await session.transport.close();\n await session.server.close();\n }\n }\n\n /**\n * Get the number of active sessions.\n */\n get size(): number {\n return this.sessions.size;\n }\n\n /**\n * Sweep expired sessions. Called automatically by the sweep timer.\n * Returns the number of sessions reaped.\n */\n sweep(): number {\n if (this.ttl <= 0) return 0;\n\n const now = Date.now();\n const expired: string[] = [];\n\n for (const [id, session] of this.sessions) {\n if (now - session.lastActiveAt > this.ttl) {\n expired.push(id);\n }\n }\n\n for (const id of expired) {\n // Fire-and-forget cleanup — don't block the sweep\n /* v8 ignore start */\n this.remove(id).catch(() => {});\n /* v8 ignore stop */\n }\n\n return expired.length;\n }\n\n /**\n * Close all sessions, stop the sweep timer, and clean up.\n */\n async closeAll(): Promise<void> {\n if (this.sweepTimer) {\n clearInterval(this.sweepTimer);\n this.sweepTimer = undefined;\n }\n\n const promises: Promise<void>[] = [];\n for (const [, session] of this.sessions) {\n promises.push(session.transport.close());\n promises.push(session.server.close());\n }\n await Promise.all(promises);\n this.sessions.clear();\n }\n}\n","/**\n * Streamable HTTP transport for Forge MCP Server\n *\n * Implements the official MCP Streamable HTTP transport specification (2025-03-26)\n * using the SDK's StreamableHTTPServerTransport.\n *\n * Architecture:\n * - Stateful mode with per-session transport+server pairs (multi-tenant)\n * - Auth via Bearer token → authInfo.token → handler extra.authInfo\n * - Session manager (injected) maps session IDs to transport+server instances\n * - Health/status endpoints handled by h3, MCP endpoint by the SDK transport\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\n\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { CallToolRequestSchema, ListToolsRequestSchema } from \"@modelcontextprotocol/sdk/types.js\";\nimport { createApp, defineEventHandler, type H3 } from \"h3\";\n\nimport { parseAuthHeader } from \"./auth.ts\";\nimport { executeToolWithCredentials } from \"./handlers/index.ts\";\nimport { INSTRUCTIONS } from \"./instructions.ts\";\nimport { SessionManager } from \"./sessions.ts\";\nimport { getTools } from \"./tools.ts\";\nimport { VERSION } from \"./version.ts\";\n\nexport { SessionManager } from \"./sessions.ts\";\n\n/**\n * Options for the HTTP MCP server.\n */\nexport interface HttpServerOptions {\n /** When true, forge_write tool is not registered and write operations are rejected. */\n readOnly?: boolean;\n}\n\n/**\n * Create a configured MCP Server instance for HTTP transport.\n *\n * Unlike stdio, HTTP mode does NOT include forge_configure/forge_get_config\n * because credentials come from the Authorization header per-request.\n */\nexport function createMcpServer(options?: HttpServerOptions): Server {\n const readOnly = options?.readOnly ?? false;\n const tools = getTools({ readOnly });\n\n const server = new Server(\n {\n name: \"forge-mcp\",\n version: VERSION,\n },\n {\n capabilities: {\n tools: {},\n },\n instructions: INSTRUCTIONS,\n },\n );\n\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n return { tools };\n });\n\n server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {\n const { name, arguments: args } = request.params;\n const token = extra.authInfo?.token;\n\n /* v8 ignore start */\n if (!token) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: \"Error: Authentication required. No token found in request.\",\n },\n ],\n structuredContent: {\n success: false,\n error: \"Authentication required. No token found in request.\",\n },\n isError: true,\n };\n }\n /* v8 ignore stop */\n\n // Reject write operations in read-only mode\n if (readOnly && name === \"forge_write\") {\n return {\n content: [\n {\n type: \"text\" as const,\n text: \"Error: Server is running in read-only mode. Write operations are disabled.\",\n },\n ],\n structuredContent: {\n success: false,\n error: \"Server is running in read-only mode. Write operations are disabled.\",\n },\n isError: true,\n };\n }\n\n try {\n const result = await executeToolWithCredentials(\n name,\n /* v8 ignore next */ (args as Record<string, unknown>) ?? {},\n { apiToken: token },\n );\n return result as unknown as Record<string, unknown>;\n } catch (error) {\n /* v8 ignore start */\n const message = error instanceof Error ? error.message : String(error);\n /* v8 ignore stop */\n return {\n content: [{ type: \"text\" as const, text: `Error: ${message}` }],\n structuredContent: { success: false, error: message },\n isError: true,\n };\n }\n });\n\n return server;\n}\n\n/**\n * Handle an MCP request using the Streamable HTTP transport.\n *\n * Routes requests based on whether they have a session ID:\n * - No session ID + initialize request → create new session\n * - Has session ID → route to existing session's transport\n *\n * @param req - Node.js IncomingMessage\n * @param res - Node.js ServerResponse\n * @param sessions - Session manager instance (injected)\n * @param options - Server options (read-only mode, etc.)\n */\nexport async function handleMcpRequest(\n req: IncomingMessage,\n res: ServerResponse,\n sessions: SessionManager,\n options?: HttpServerOptions,\n): Promise<void> {\n // Extract and validate auth\n const authHeader = req.headers.authorization;\n const credentials = parseAuthHeader(authHeader);\n\n if (!credentials) {\n res.writeHead(401, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n jsonrpc: \"2.0\",\n error: {\n code: -32001,\n message: \"Authentication required. Provide a Bearer token with your Forge API token.\",\n },\n id: null,\n }),\n );\n return;\n }\n\n // Inject auth info for the SDK transport\n const authenticatedReq = req as IncomingMessage & {\n auth?: { token: string; clientId: string; scopes: string[] };\n };\n authenticatedReq.auth = {\n token: credentials.apiToken,\n clientId: \"forge-http-client\",\n scopes: [],\n };\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n\n if (sessionId) {\n // Existing session — route to its transport\n const session = sessions.get(sessionId);\n if (!session) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n jsonrpc: \"2.0\",\n error: {\n code: -32000,\n message: \"Session not found. The session may have expired or been terminated.\",\n },\n id: null,\n }),\n );\n return;\n }\n\n await session.transport.handleRequest(authenticatedReq, res);\n return;\n }\n\n // No session ID — this should be an initialize request.\n // Create a new transport + server pair.\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n });\n\n const server = createMcpServer(options);\n await server.connect(transport);\n\n // Set up cleanup on close\n transport.onclose = () => {\n const sid = transport.sessionId;\n /* v8 ignore start */\n if (sid) {\n sessions.remove(sid).catch(() => {\n // Ignore cleanup errors\n });\n }\n /* v8 ignore stop */\n };\n\n // Handle the request (this will set transport.sessionId during initialize)\n await transport.handleRequest(authenticatedReq, res);\n\n // After handling, register the session if the transport got a session ID\n /* v8 ignore start */\n if (transport.sessionId) {\n sessions.register(transport, server);\n } else {\n // No session was created (e.g., invalid request) — clean up\n await transport.close();\n await server.close();\n }\n /* v8 ignore stop */\n}\n\n/**\n * Create a request handler bound to a SessionManager instance.\n * Convenience factory for server.ts.\n */\nexport function createMcpRequestHandler(\n sessions: SessionManager,\n options?: HttpServerOptions,\n): (req: IncomingMessage, res: ServerResponse) => Promise<void> {\n /* v8 ignore start */\n return (req, res) => handleMcpRequest(req, res, sessions, options);\n /* v8 ignore stop */\n}\n\n/**\n * Create h3 app for health check and service info endpoints.\n * The MCP endpoint is handled separately by handleMcpRequest.\n */\nexport function createHealthApp(): H3 {\n const app = createApp();\n\n app.get(\n \"/\",\n defineEventHandler(() => {\n return { status: \"ok\", service: \"forge-mcp\", version: VERSION };\n }),\n );\n\n app.get(\n \"/health\",\n defineEventHandler(() => {\n return { status: \"ok\" };\n }),\n );\n\n return app;\n}\n"],"mappings":"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"}
|