@studiometa/forge-mcp 0.1.0 → 0.2.1

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 CHANGED
@@ -7,10 +7,11 @@ MCP (Model Context Protocol) server for [Laravel Forge](https://forge.laravel.co
7
7
 
8
8
  ## Features
9
9
 
10
- - Single unified `forge` tool minimal token overhead
10
+ - **Two tools with clear safety split** — `forge` (read) and `forge_write` (write)
11
+ - MCP clients auto-approve `forge` reads, always prompt for `forge_write` writes
11
12
  - Resource/action routing from centralized constants
12
13
  - Built-in help system — `action=help` for any resource
13
- - Stdio transport for local use
14
+ - Stdio and Streamable HTTP transports
14
15
  - Configuration tools for interactive token setup
15
16
 
16
17
  ## Installation
@@ -42,37 +43,75 @@ Add to your Claude Desktop config:
42
43
 
43
44
  Alternatively, omit the `env` block and ask Claude to configure credentials using the `forge_configure` tool.
44
45
 
45
- ## The `forge` Tool
46
+ ### Read-Only Mode
46
47
 
47
- A single unified tool with `resource` + `action` routing:
48
+ To guarantee no write operations are possible at the server level:
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "forge": {
54
+ "command": "forge-mcp",
55
+ "args": ["--read-only"],
56
+ "env": {
57
+ "FORGE_API_TOKEN": "your-api-token"
58
+ }
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ Or via environment variable: `FORGE_READ_ONLY=true`.
65
+
66
+ When enabled, the `forge_write` tool is not registered at all — only `forge`, `forge_configure`, and `forge_get_config` are available.
67
+
68
+ ## Tools
69
+
70
+ ### `forge` — Read Operations
71
+
72
+ Safe, read-only queries. Annotated `readOnlyHint: true` so MCP clients can auto-approve.
73
+
74
+ **Actions**: `list`, `get`, `help`, `schema`
48
75
 
49
76
  ```json
50
77
  { "resource": "servers", "action": "list" }
51
78
  { "resource": "servers", "action": "get", "id": "123" }
52
79
  { "resource": "sites", "action": "list", "server_id": "123" }
53
- { "resource": "deployments", "action": "deploy", "server_id": "123", "site_id": "456" }
54
80
  { "resource": "servers", "action": "help" }
55
81
  ```
56
82
 
83
+ ### `forge_write` — Write Operations
84
+
85
+ Mutating operations. Annotated `destructiveHint: true` so MCP clients always prompt for confirmation.
86
+
87
+ **Actions**: `create`, `update`, `delete`, `deploy`, `reboot`, `restart`, `activate`, `run`
88
+
89
+ ```json
90
+ { "resource": "deployments", "action": "deploy", "server_id": "123", "site_id": "456" }
91
+ // deploy blocks until complete, returning: status, deployment log, elapsed time
92
+ { "resource": "servers", "action": "reboot", "id": "123" }
93
+ { "resource": "daemons", "action": "create", "server_id": "123", "command": "php artisan queue:work" }
94
+ ```
95
+
57
96
  ### Resources & Actions
58
97
 
59
- | Resource | Actions | Required Fields |
60
- | --------------- | ----------------------------------- | -------------------------- |
61
- | servers | list, get, create, delete, reboot | id (for get/delete/reboot) |
62
- | sites | list, get, create, delete | server_id |
63
- | deployments | list, deploy, get, update | server_id, site_id |
64
- | env | get, update | server_id, site_id |
65
- | nginx | get, update | server_id, site_id |
66
- | certificates | list, get, create, delete, activate | server_id, site_id |
67
- | databases | list, get, create, delete | server_id |
68
- | daemons | list, get, create, delete, restart | server_id |
69
- | firewall-rules | list, get, create, delete | server_id |
70
- | ssh-keys | list, get, create, delete | server_id |
71
- | security-rules | list, get, create, delete | server_id, site_id |
72
- | redirect-rules | list, get, create, delete | server_id, site_id |
73
- | monitors | list, get, create, delete | server_id |
74
- | nginx-templates | list, get, create, update, delete | server_id |
75
- | recipes | list, get, create, delete, run | id (for get/delete/run) |
98
+ | Resource | Read Actions | Write Actions | Required Fields |
99
+ | --------------- | ------------ | ------------------------ | -------------------------- |
100
+ | servers | list, get | create, delete, reboot | id (for get/delete/reboot) |
101
+ | sites | list, get | create, delete | server_id |
102
+ | deployments | list | deploy, update | server_id, site_id |
103
+ | env | get | update | server_id, site_id |
104
+ | nginx | get | update | server_id, site_id |
105
+ | certificates | list, get | create, delete, activate | server_id, site_id |
106
+ | databases | list, get | create, delete | server_id |
107
+ | daemons | list, get | create, delete, restart | server_id |
108
+ | firewall-rules | list, get | create, delete | server_id |
109
+ | ssh-keys | list, get | create, delete | server_id |
110
+ | security-rules | list, get | create, delete | server_id, site_id |
111
+ | redirect-rules | list, get | create, delete | server_id, site_id |
112
+ | monitors | list, get | create, delete | server_id |
113
+ | nginx-templates | list, get | create, update, delete | server_id |
114
+ | recipes | list, get | create, delete, run | id (for get/delete/run) |
76
115
 
77
116
  ### Discovery
78
117
 
@@ -90,6 +129,17 @@ Use `action: "help"` with any resource:
90
129
  | `forge_configure` | Save API token to local config |
91
130
  | `forge_get_config` | Show current config (token masked) |
92
131
 
132
+ ## Audit Logging
133
+
134
+ All write operations (`forge_write` tool calls) are automatically logged for traceability:
135
+
136
+ - **Default path**: `~/.config/forge-tools/audit.log`
137
+ - **Override**: Set `FORGE_AUDIT_LOG` environment variable
138
+ - **Format**: JSON lines (via pino) with timestamp, resource, action, sanitized args, and status
139
+ - **Safety**: Logging never interrupts operations — silent on failure
140
+
141
+ The CLI also logs write commands to the same audit log.
142
+
93
143
  ## Getting Your API Token
94
144
 
95
145
  1. Log into [Laravel Forge](https://forge.laravel.com)
@@ -0,0 +1,23 @@
1
+ /**
2
+ * CLI flag parsers shared between stdio and HTTP entry points.
3
+ *
4
+ * Extracted to a separate module so that both `index.ts` (stdio) and
5
+ * `server.ts` (HTTP) can import it without creating a shared dependency
6
+ * that triggers Vite code-splitting — which would break the `isMainModule`
7
+ * guard in `index.ts`.
8
+ *
9
+ * See: https://github.com/studiometa/forge-tools/issues/63
10
+ */
11
+ /**
12
+ * Parse read-only flag from process.argv and environment.
13
+ *
14
+ * Supports:
15
+ * - `--read-only` CLI flag
16
+ * - `FORGE_READ_ONLY=true` environment variable
17
+ */
18
+ function parseReadOnlyFlag() {
19
+ return process.argv.includes("--read-only") || process.env.FORGE_READ_ONLY === "true";
20
+ }
21
+ export { parseReadOnlyFlag as t };
22
+
23
+ //# sourceMappingURL=flags-LFbdErsZ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flags-LFbdErsZ.js","names":[],"sources":["../src/flags.ts"],"sourcesContent":["/**\n * CLI flag parsers shared between stdio and HTTP entry points.\n *\n * Extracted to a separate module so that both `index.ts` (stdio) and\n * `server.ts` (HTTP) can import it without creating a shared dependency\n * that triggers Vite code-splitting — which would break the `isMainModule`\n * guard in `index.ts`.\n *\n * See: https://github.com/studiometa/forge-tools/issues/63\n */\n\n/**\n * Parse read-only flag from process.argv and environment.\n *\n * Supports:\n * - `--read-only` CLI flag\n * - `FORGE_READ_ONLY=true` environment variable\n */\nexport function parseReadOnlyFlag(): boolean {\n return process.argv.includes(\"--read-only\") || process.env.FORGE_READ_ONLY === \"true\";\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AAkBA,SAAgB,oBAA6B;AAC3C,QAAO,QAAQ,KAAK,SAAS,cAAc,IAAI,QAAQ,IAAI,oBAAoB"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * CLI flag parsers shared between stdio and HTTP entry points.
3
+ *
4
+ * Extracted to a separate module so that both `index.ts` (stdio) and
5
+ * `server.ts` (HTTP) can import it without creating a shared dependency
6
+ * that triggers Vite code-splitting — which would break the `isMainModule`
7
+ * guard in `index.ts`.
8
+ *
9
+ * See: https://github.com/studiometa/forge-tools/issues/63
10
+ */
11
+ /**
12
+ * Parse read-only flag from process.argv and environment.
13
+ *
14
+ * Supports:
15
+ * - `--read-only` CLI flag
16
+ * - `FORGE_READ_ONLY=true` environment variable
17
+ */
18
+ export declare function parseReadOnlyFlag(): boolean;
19
+ //# sourceMappingURL=flags.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flags.d.ts","sourceRoot":"","sources":["../src/flags.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;;;GAMG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAE3C"}
@@ -45,9 +45,17 @@ export declare function formatDatabaseUser(user: ForgeDatabaseUser): string;
45
45
  */
46
46
  export declare function formatDeploymentList(deployments: ForgeDeployment[]): string;
47
47
  /**
48
- * Format a deployment action result (deploy/update-script).
49
- */
50
- export declare function formatDeployAction(siteId: string, serverId: string): string;
48
+ * Format a deployment action result.
49
+ *
50
+ * When a `DeployResult` is provided the output includes status, elapsed time,
51
+ * and the deployment log. When called with just IDs (legacy) it falls back to
52
+ * the simple confirmation message so existing tests keep passing.
53
+ */
54
+ export declare function formatDeployAction(siteId: string, serverId: string, result?: {
55
+ status: "success" | "failed";
56
+ log: string;
57
+ elapsed_ms: number;
58
+ }): string;
51
59
  /**
52
60
  * Format deployment script content.
53
61
  */
@@ -1 +1 @@
1
- {"version":3,"file":"formatters.d.ts","sourceRoot":"","sources":["../src/formatters.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"formatters.d.ts","sourceRoot":"","sources":["../src/formatters.ts"],"names":[],"mappings":"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"}
@@ -2,8 +2,9 @@
2
2
  * Tool execution handlers for Forge MCP server.
3
3
  * Shared between stdio and HTTP transports.
4
4
  *
5
- * Single consolidated tool for minimal token overhead:
6
- * - forge: resource + action based API
5
+ * Two tools with clear separation:
6
+ * - forge: read-only operations (list, get, help, schema)
7
+ * - forge_write: write operations (create, update, delete, deploy, reboot, etc.)
7
8
  */
8
9
  import type { ToolResult } from "./types.ts";
9
10
  export type { ToolResult } from "./types.ts";
@@ -11,8 +12,9 @@ export type { ToolResult } from "./types.ts";
11
12
  * Execute a tool call with provided credentials.
12
13
  *
13
14
  * This is the main entry point shared between stdio and HTTP transports.
15
+ * Validates that the action matches the tool (read vs write).
14
16
  */
15
- export declare function executeToolWithCredentials(_name: string, args: Record<string, unknown>, credentials: {
17
+ export declare function executeToolWithCredentials(name: string, args: Record<string, unknown>, credentials: {
16
18
  apiToken: string;
17
19
  }): Promise<ToolResult>;
18
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,OAAO,KAAK,EAA8B,UAAU,EAAE,MAAM,YAAY,CAAC;AA6BzE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAiE7C;;;;GAIG;AACH,wBAAsB,0BAA0B,CAC9C,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,WAAW,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAChC,OAAO,CAAC,UAAU,CAAC,CAyDrB"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/handlers/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,OAAO,KAAK,EAA8B,UAAU,EAAE,MAAM,YAAY,CAAC;AA8BzE,YAAY,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAwE7C;;;;;GAKG;AACH,wBAAsB,0BAA0B,CAC9C,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,WAAW,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAChC,OAAO,CAAC,UAAU,CAAC,CAyFrB"}
@@ -1,12 +1,17 @@
1
1
  import type { ExecutorContext } from "@studiometa/forge-core";
2
2
  /**
3
3
  * Result from MCP tool handlers.
4
+ *
5
+ * - `content` — human-readable text (always present)
6
+ * - `structuredContent` — machine-readable data matching the tool's `outputSchema`
7
+ * - `isError` — true when the result represents an error
4
8
  */
5
9
  export interface ToolResult {
6
10
  content: Array<{
7
11
  type: "text";
8
12
  text: string;
9
13
  }>;
14
+ structuredContent?: Record<string, unknown>;
10
15
  isError?: boolean;
11
16
  }
12
17
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/handlers/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,eAAe,EAAE,eAAe,CAAC;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,4EAA4E;IAC5E,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/handlers/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC5C,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,eAAe,EAAE,eAAe,CAAC;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,4EAA4E;IAC5E,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;GAEG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CACxB"}
@@ -1,8 +1,11 @@
1
1
  import type { ToolResult } from "./types.ts";
2
2
  import { UserInputError } from "../errors.ts";
3
3
  /**
4
- * Create a successful JSON result.
5
- * Accepts a string or an object (which will be JSON-serialized).
4
+ * Create a successful result with both human-readable text and structured content.
5
+ *
6
+ * - When `data` is a string, `structuredContent` wraps it as `{ result: data }`.
7
+ * - When `data` is an object/array, `structuredContent` is `{ result: data }` and
8
+ * the text representation is the JSON-serialized form.
6
9
  */
7
10
  export declare function jsonResult(data: string | Record<string, unknown> | unknown): ToolResult;
8
11
  /**
@@ -13,7 +16,7 @@ export declare function jsonResult(data: string | Record<string, unknown> | unkn
13
16
  */
14
17
  export declare function sanitizeId(value: string): boolean;
15
18
  /**
16
- * Create an error result.
19
+ * Create an error result with structured error content.
17
20
  */
18
21
  export declare function errorResult(message: string): ToolResult;
19
22
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/handlers/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;GAGG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,GAAG,UAAU,CAKvF;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAKvD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,UAAU,CAWhG"}
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/handlers/utils.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAE9C;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,GAAG,UAAU,CAMvF;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAEjD;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,CAMvD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,UAAU,CAehG"}
@@ -1,4 +1,4 @@
1
- import { a as INSTRUCTIONS, i as executeToolWithCredentials, r as TOOLS, t as VERSION } from "./version-Cw8OGt4r.js";
1
+ import { a as INSTRUCTIONS, i as getTools, n as executeToolWithCredentials, t as VERSION } from "./version-BmEJceWJ.js";
2
2
  import { parseAuthHeader } from "./auth.js";
3
3
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
@@ -110,7 +110,9 @@ var SessionManager = class {
110
110
  * Unlike stdio, HTTP mode does NOT include forge_configure/forge_get_config
111
111
  * because credentials come from the Authorization header per-request.
112
112
  */
113
- function createMcpServer() {
113
+ function createMcpServer(options) {
114
+ const readOnly = options?.readOnly ?? false;
115
+ const tools = getTools({ readOnly });
114
116
  const server = new Server({
115
117
  name: "forge-mcp",
116
118
  version: VERSION
@@ -119,7 +121,7 @@ function createMcpServer() {
119
121
  instructions: INSTRUCTIONS
120
122
  });
121
123
  server.setRequestHandler(ListToolsRequestSchema, async () => {
122
- return { tools: TOOLS };
124
+ return { tools };
123
125
  });
124
126
  server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
125
127
  const { name, arguments: args } = request.params;
@@ -130,9 +132,24 @@ function createMcpServer() {
130
132
  type: "text",
131
133
  text: "Error: Authentication required. No token found in request."
132
134
  }],
135
+ structuredContent: {
136
+ success: false,
137
+ error: "Authentication required. No token found in request."
138
+ },
133
139
  isError: true
134
140
  };
135
141
  /* v8 ignore stop */
142
+ if (readOnly && name === "forge_write") return {
143
+ content: [{
144
+ type: "text",
145
+ text: "Error: Server is running in read-only mode. Write operations are disabled."
146
+ }],
147
+ structuredContent: {
148
+ success: false,
149
+ error: "Server is running in read-only mode. Write operations are disabled."
150
+ },
151
+ isError: true
152
+ };
136
153
  try {
137
154
  return await executeToolWithCredentials(
138
155
  name,
@@ -141,12 +158,18 @@ function createMcpServer() {
141
158
  { apiToken: token }
142
159
  );
143
160
  } catch (error) {
161
+ /* v8 ignore start */
162
+ const message = error instanceof Error ? error.message : String(error);
144
163
  /* v8 ignore stop */
145
164
  return {
146
165
  content: [{
147
166
  type: "text",
148
- text: `Error: ${error instanceof Error ? error.message : String(error)}`
167
+ text: `Error: ${message}`
149
168
  }],
169
+ structuredContent: {
170
+ success: false,
171
+ error: message
172
+ },
150
173
  isError: true
151
174
  };
152
175
  }
@@ -163,8 +186,9 @@ function createMcpServer() {
163
186
  * @param req - Node.js IncomingMessage
164
187
  * @param res - Node.js ServerResponse
165
188
  * @param sessions - Session manager instance (injected)
189
+ * @param options - Server options (read-only mode, etc.)
166
190
  */
167
- async function handleMcpRequest(req, res, sessions) {
191
+ async function handleMcpRequest(req, res, sessions, options) {
168
192
  const authHeader = req.headers.authorization;
169
193
  const credentials = parseAuthHeader(authHeader);
170
194
  if (!credentials) {
@@ -204,7 +228,7 @@ async function handleMcpRequest(req, res, sessions) {
204
228
  return;
205
229
  }
206
230
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
207
- const server = createMcpServer();
231
+ const server = createMcpServer(options);
208
232
  await server.connect(transport);
209
233
  transport.onclose = () => {
210
234
  const sid = transport.sessionId;
@@ -225,9 +249,9 @@ async function handleMcpRequest(req, res, sessions) {
225
249
  * Create a request handler bound to a SessionManager instance.
226
250
  * Convenience factory for server.ts.
227
251
  */
228
- function createMcpRequestHandler(sessions) {
252
+ function createMcpRequestHandler(sessions, options) {
229
253
  /* v8 ignore start */
230
- return (req, res) => handleMcpRequest(req, res, sessions);
254
+ return (req, res) => handleMcpRequest(req, res, sessions, options);
231
255
  /* v8 ignore stop */
232
256
  }
233
257
  /**
@@ -250,4 +274,4 @@ function createHealthApp() {
250
274
  }
251
275
  export { SessionManager as a, handleMcpRequest as i, createMcpRequestHandler as n, createMcpServer as r, createHealthApp as t };
252
276
 
253
- //# sourceMappingURL=http-BJUKoZdb.js.map
277
+ //# sourceMappingURL=http-w0DliUHY.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-w0DliUHY.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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"}
package/dist/http.d.ts CHANGED
@@ -15,13 +15,20 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
15
15
  import { type H3 } from "h3";
16
16
  import { SessionManager } from "./sessions.ts";
17
17
  export { SessionManager } from "./sessions.ts";
18
+ /**
19
+ * Options for the HTTP MCP server.
20
+ */
21
+ export interface HttpServerOptions {
22
+ /** When true, forge_write tool is not registered and write operations are rejected. */
23
+ readOnly?: boolean;
24
+ }
18
25
  /**
19
26
  * Create a configured MCP Server instance for HTTP transport.
20
27
  *
21
28
  * Unlike stdio, HTTP mode does NOT include forge_configure/forge_get_config
22
29
  * because credentials come from the Authorization header per-request.
23
30
  */
24
- export declare function createMcpServer(): Server;
31
+ export declare function createMcpServer(options?: HttpServerOptions): Server;
25
32
  /**
26
33
  * Handle an MCP request using the Streamable HTTP transport.
27
34
  *
@@ -32,13 +39,14 @@ export declare function createMcpServer(): Server;
32
39
  * @param req - Node.js IncomingMessage
33
40
  * @param res - Node.js ServerResponse
34
41
  * @param sessions - Session manager instance (injected)
42
+ * @param options - Server options (read-only mode, etc.)
35
43
  */
36
- export declare function handleMcpRequest(req: IncomingMessage, res: ServerResponse, sessions: SessionManager): Promise<void>;
44
+ export declare function handleMcpRequest(req: IncomingMessage, res: ServerResponse, sessions: SessionManager, options?: HttpServerOptions): Promise<void>;
37
45
  /**
38
46
  * Create a request handler bound to a SessionManager instance.
39
47
  * Convenience factory for server.ts.
40
48
  */
41
- export declare function createMcpRequestHandler(sessions: SessionManager): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
49
+ export declare function createMcpRequestHandler(sessions: SessionManager, options?: HttpServerOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
42
50
  /**
43
51
  * Create h3 app for health check and service info endpoints.
44
52
  * The MCP endpoint is handled separately by handleMcpRequest.
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAGnE,OAAO,EAAiC,KAAK,EAAE,EAAE,MAAM,IAAI,CAAC;AAK5D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAI/C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,MAAM,CAuDxC;AAED;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,cAAc,GACvB,OAAO,CAAC,IAAI,CAAC,CAwFf;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,cAAc,GACvB,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAI9D;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,EAAE,CAkBpC"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAGnE,OAAO,EAAiC,KAAK,EAAE,EAAE,MAAM,IAAI,CAAC;AAK5D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAI/C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,uFAAuF;IACvF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,MAAM,CAgFnE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,cAAc,EACxB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAwFf;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,cAAc,EACxB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAI9D;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,EAAE,CAkBpC"}
package/dist/http.js CHANGED
@@ -1,3 +1,3 @@
1
- import "./version-Cw8OGt4r.js";
2
- import { a as SessionManager, i as handleMcpRequest, n as createMcpRequestHandler, r as createMcpServer, t as createHealthApp } from "./http-BJUKoZdb.js";
1
+ import "./version-BmEJceWJ.js";
2
+ import { a as SessionManager, i as handleMcpRequest, n as createMcpRequestHandler, r as createMcpServer, t as createHealthApp } from "./http-w0DliUHY.js";
3
3
  export { SessionManager, createHealthApp, createMcpRequestHandler, createMcpServer, handleMcpRequest };
package/dist/index.d.ts CHANGED
@@ -7,24 +7,35 @@
7
7
  *
8
8
  * Usage:
9
9
  * npx @studiometa/forge-mcp
10
+ * npx @studiometa/forge-mcp --read-only
11
+ * FORGE_READ_ONLY=true npx @studiometa/forge-mcp
10
12
  *
11
13
  * Or in Claude Desktop config:
12
14
  * {
13
15
  * "mcpServers": {
14
16
  * "forge": {
15
17
  * "command": "forge-mcp",
18
+ * "args": ["--read-only"],
16
19
  * "env": { "FORGE_API_TOKEN": "your-token" }
17
20
  * }
18
21
  * }
19
22
  * }
20
23
  */
21
24
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
25
+ export { parseReadOnlyFlag } from "./flags.ts";
26
+ /**
27
+ * Options for the stdio MCP server.
28
+ */
29
+ export interface StdioServerOptions {
30
+ /** When true, forge_write tool is not registered and write operations are rejected. */
31
+ readOnly?: boolean;
32
+ }
22
33
  /**
23
34
  * Create and configure the MCP server.
24
35
  */
25
- export declare function createStdioServer(): Server;
36
+ export declare function createStdioServer(options?: StdioServerOptions): Server;
26
37
  /**
27
38
  * Start the stdio server.
28
39
  */
29
- export declare function startStdioServer(): Promise<void>;
40
+ export declare function startStdioServer(options?: StdioServerOptions): Promise<void>;
30
41
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAQnE;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAkC1C;AAED;;GAEG;AACH,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC,CAKtD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAUnE,OAAO,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE/C;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,uFAAuF;IACvF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,MAAM,CAuCtE;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMlF"}