@studiometa/forge-mcp 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/skills/SKILL.md CHANGED
@@ -12,40 +12,54 @@ keywords:
12
12
 
13
13
  # Laravel Forge MCP Server
14
14
 
15
- MCP (Model Context Protocol) server for [Laravel Forge](https://forge.laravel.com). Provides a single unified `forge` tool for all operations.
15
+ MCP (Model Context Protocol) server for [Laravel Forge](https://forge.laravel.com). Provides two tools: `forge` for read operations and `forge_write` for write operations.
16
16
 
17
17
  ## Quick Start
18
18
 
19
19
  Before your first interaction with any resource, call `action="help"` with that resource to discover required fields and examples.
20
20
 
21
- ## The `forge` Tool
21
+ ## Tools
22
22
 
23
- Single unified tool with this signature:
23
+ ### `forge` Read Operations
24
+
25
+ Safe, read-only queries. Use for listing, getting details, help, and schema.
26
+
27
+ **Actions**: `list`, `get`, `help`, `schema`
24
28
 
25
29
  ```
26
30
  forge(resource, action, [parameters...])
27
31
  ```
28
32
 
33
+ ### `forge_write` — Write Operations
34
+
35
+ Mutating operations that modify server state. Always requires confirmation.
36
+
37
+ **Actions**: `create`, `update`, `delete`, `deploy`, `reboot`, `restart`, `activate`, `run`
38
+
39
+ ```
40
+ forge_write(resource, action, [parameters...])
41
+ ```
42
+
29
43
  ### Resources & Actions
30
44
 
31
- | Resource | Actions | Scope |
32
- | ----------------- | --------------------------------------------- | ------ |
33
- | `servers` | `list`, `get`, `create`, `delete`, `reboot` | global |
34
- | `sites` | `list`, `get`, `create`, `delete` | server |
35
- | `deployments` | `list`, `deploy`, `get`, `update` | site |
36
- | `env` | `get`, `update` | site |
37
- | `nginx` | `get`, `update` | site |
38
- | `certificates` | `list`, `get`, `create`, `delete`, `activate` | site |
39
- | `databases` | `list`, `get`, `create`, `delete` | server |
40
- | `database-users` | `list`, `get`, `create`, `delete` | server |
41
- | `daemons` | `list`, `get`, `create`, `delete`, `restart` | server |
42
- | `firewall-rules` | `list`, `get`, `create`, `delete` | server |
43
- | `ssh-keys` | `list`, `get`, `create`, `delete` | server |
44
- | `security-rules` | `list`, `get`, `create`, `delete` | site |
45
- | `redirect-rules` | `list`, `get`, `create`, `delete` | site |
46
- | `monitors` | `list`, `get`, `create`, `delete` | server |
47
- | `nginx-templates` | `list`, `get`, `create`, `update`, `delete` | server |
48
- | `recipes` | `list`, `get`, `create`, `delete`, `run` | global |
45
+ | Resource | Read Actions (`forge`) | Write Actions (`forge_write`) | Scope |
46
+ | ----------------- | ---------------------- | ------------------------------ | ------ |
47
+ | `servers` | `list`, `get` | `create`, `delete`, `reboot` | global |
48
+ | `sites` | `list`, `get` | `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 |
49
63
 
50
64
  ### Scope Guide
51
65
 
@@ -85,61 +99,60 @@ Use `action: "schema"` for a compact machine-readable spec:
85
99
  ### Deploy a Site
86
100
 
87
101
  ```json
88
- // 1. Find the server
102
+ // 1. Find the server (forge tool — read)
89
103
  { "resource": "servers", "action": "list" }
90
104
 
91
- // 2. Find the site
105
+ // 2. Find the site (forge tool — read)
92
106
  { "resource": "sites", "action": "list", "server_id": "123" }
93
107
 
94
- // 3. Deploy
108
+ // 3. Deploy (forge_write tool — write, blocks until complete)
95
109
  { "resource": "deployments", "action": "deploy", "server_id": "123", "site_id": "456" }
96
-
97
- // 4. Check deployment status
98
- { "resource": "deployments", "action": "list", "server_id": "123", "site_id": "456" }
110
+ // Returns: deployment status, log output, and elapsed time
99
111
  ```
100
112
 
101
113
  ### Check Server Status
102
114
 
103
115
  ```json
104
- // Get server details
116
+ // Get server details (forge)
105
117
  { "resource": "servers", "action": "get", "id": "123" }
106
118
 
107
- // List sites on server
119
+ // List sites on server (forge)
108
120
  { "resource": "sites", "action": "list", "server_id": "123" }
109
121
 
110
- // List databases
122
+ // List databases (forge)
111
123
  { "resource": "databases", "action": "list", "server_id": "123" }
112
124
 
113
- // List background processes
125
+ // List background processes (forge)
114
126
  { "resource": "daemons", "action": "list", "server_id": "123" }
115
127
  ```
116
128
 
117
129
  ### Manage SSL Certificates
118
130
 
119
131
  ```json
120
- // List existing certs
132
+ // List existing certs (forge)
121
133
  { "resource": "certificates", "action": "list", "server_id": "123", "site_id": "456" }
122
134
 
123
- // Request a new Let's Encrypt cert
135
+ // Request a new Let's Encrypt cert (forge_write)
124
136
  { "resource": "certificates", "action": "create", "server_id": "123", "site_id": "456", "domain": "example.com", "type": "new" }
125
137
 
126
- // Activate it
138
+ // Activate it (forge_write)
127
139
  { "resource": "certificates", "action": "activate", "server_id": "123", "site_id": "456", "id": "789" }
128
140
  ```
129
141
 
130
142
  ### Update Environment Variables
131
143
 
132
144
  ```json
133
- // Get current env
145
+ // Get current env (forge)
134
146
  { "resource": "env", "action": "get", "server_id": "123", "site_id": "456" }
135
147
 
136
- // Update env
148
+ // Update env (forge_write)
137
149
  { "resource": "env", "action": "update", "server_id": "123", "site_id": "456", "content": "APP_ENV=production\nAPP_DEBUG=false" }
138
150
  ```
139
151
 
140
152
  ### Create a Queue Worker
141
153
 
142
154
  ```json
155
+ // forge_write
143
156
  {
144
157
  "resource": "daemons",
145
158
  "action": "create",
@@ -152,10 +165,10 @@ Use `action: "schema"` for a compact machine-readable spec:
152
165
  ### Run a Recipe on Multiple Servers
153
166
 
154
167
  ```json
155
- // Create recipe
168
+ // Create recipe (forge_write)
156
169
  { "resource": "recipes", "action": "create", "name": "Clear caches", "script": "php artisan cache:clear" }
157
170
 
158
- // Run it on servers
171
+ // Run it on servers (forge_write)
159
172
  { "resource": "recipes", "action": "run", "id": "456", "servers": [1, 2, 3] }
160
173
  ```
161
174
 
@@ -180,6 +193,22 @@ Set `FORGE_API_TOKEN` environment variable, or use the `forge_configure` tool:
180
193
  3. Create a new token
181
194
  4. Copy the token (shown only once)
182
195
 
196
+ ## Read-Only Mode
197
+
198
+ The server can run in read-only mode where the `forge_write` tool is not available at all:
199
+
200
+ ```bash
201
+ forge-mcp --read-only
202
+ # or
203
+ FORGE_READ_ONLY=true forge-mcp
204
+ ```
205
+
206
+ When enabled, only `forge` (read operations), `forge_configure`, and `forge_get_config` are registered.
207
+
208
+ ## Audit Logging
209
+
210
+ All `forge_write` operations are automatically logged to `~/.config/forge-tools/audit.log` (configurable via `FORGE_AUDIT_LOG` env var). Sensitive fields (tokens, passwords) are redacted. Logging never interrupts operations.
211
+
183
212
  ## Tips for Efficient Usage
184
213
 
185
214
  1. **Use help first**: Call `action="help"` for any resource before using it
@@ -187,3 +216,4 @@ Set `FORGE_API_TOKEN` environment variable, or use the `forge_configure` tool:
187
216
  3. **Start with list**: Use `action="list"` to discover existing resources
188
217
  4. **Chain operations**: List servers → list sites → deploy (follow the hierarchy)
189
218
  5. **Scope matters**: Server-scoped resources need `server_id`, site-scoped need both `server_id` and `site_id`
219
+ 6. **Read vs Write**: Use `forge` for queries, `forge_write` for mutations — MCP clients enforce this split
@@ -1 +0,0 @@
1
- {"version":3,"file":"http-BJUKoZdb.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 { TOOLS } from \"./tools.ts\";\nimport { VERSION } from \"./version.ts\";\n\nexport { SessionManager } from \"./sessions.ts\";\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(): Server {\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: 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 isError: true,\n };\n }\n /* v8 ignore stop */\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 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 */\nexport async function handleMcpRequest(\n req: IncomingMessage,\n res: ServerResponse,\n sessions: SessionManager,\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();\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): (req: IncomingMessage, res: ServerResponse) => Promise<void> {\n /* v8 ignore start */\n return (req, res) => handleMcpRequest(req, res, sessions);\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/index.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/stdio.ts","../src/index.ts"],"sourcesContent":["import { getToken, setToken } from \"@studiometa/forge-api\";\n\nimport type { ToolResult } from \"./handlers/types.ts\";\n\nimport { executeToolWithCredentials } from \"./handlers/index.ts\";\nimport { STDIO_ONLY_TOOLS, TOOLS } from \"./tools.ts\";\n\nexport type { ToolResult };\n\n/**\n * Get all available tools (including stdio-only configuration tools).\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function getAvailableTools(): any[] {\n return [...TOOLS, ...STDIO_ONLY_TOOLS];\n}\n\n/**\n * Handle the forge_configure tool.\n */\nexport function handleConfigureTool(args: { apiToken: string }): ToolResult {\n if (!args.apiToken || typeof args.apiToken !== \"string\" || args.apiToken.trim().length === 0) {\n return {\n content: [\n {\n type: \"text\",\n text: \"Error: apiToken is required and must be a non-empty string.\",\n },\n ],\n isError: true,\n };\n }\n\n setToken(args.apiToken);\n\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(\n {\n success: true,\n message: \"Laravel Forge API token configured successfully\",\n apiToken: `***${args.apiToken.slice(-4)}`,\n },\n null,\n 2,\n ),\n },\n ],\n };\n}\n\n/**\n * Handle the forge_get_config tool.\n */\nexport function handleGetConfigTool(): ToolResult {\n const token = getToken();\n\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(\n {\n apiToken: token ? `***${token.slice(-4)}` : \"not configured\",\n configured: !!token,\n },\n null,\n 2,\n ),\n },\n ],\n };\n}\n\n/**\n * Handle a tool call request.\n */\nexport async function handleToolCall(\n name: string,\n args: Record<string, unknown>,\n): Promise<ToolResult> {\n if (name === \"forge_configure\") {\n return handleConfigureTool(args as { apiToken: string });\n }\n\n if (name === \"forge_get_config\") {\n return handleGetConfigTool();\n }\n\n // Get API token\n const apiToken = getToken();\n if (!apiToken) {\n return {\n content: [\n {\n type: \"text\",\n text: 'Error: Forge API token not configured. Use \"forge_configure\" tool or set FORGE_API_TOKEN environment variable.',\n },\n ],\n isError: true,\n };\n }\n\n return executeToolWithCredentials(name, args, { apiToken });\n}\n","#!/usr/bin/env node\n\n/**\n * Forge MCP Server — Stdio Transport\n *\n * This is the local execution mode using stdio transport.\n * For remote HTTP deployment, use server.ts instead.\n *\n * Usage:\n * npx @studiometa/forge-mcp\n *\n * Or in Claude Desktop config:\n * {\n * \"mcpServers\": {\n * \"forge\": {\n * \"command\": \"forge-mcp\",\n * \"env\": { \"FORGE_API_TOKEN\": \"your-token\" }\n * }\n * }\n * }\n */\n\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { CallToolRequestSchema, ListToolsRequestSchema } from \"@modelcontextprotocol/sdk/types.js\";\n\nimport { INSTRUCTIONS } from \"./instructions.ts\";\nimport { getAvailableTools, handleToolCall } from \"./stdio.ts\";\nimport { VERSION } from \"./version.ts\";\n\n/**\n * Create and configure the MCP server.\n */\nexport function createStdioServer(): Server {\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: getAvailableTools() };\n });\n\n server.setRequestHandler(CallToolRequestSchema, async (request) => {\n const { name, arguments: args } = request.params;\n\n try {\n const result = await handleToolCall(name, (args as Record<string, unknown>) ?? {});\n return result as unknown as Record<string, unknown>;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n content: [{ type: \"text\" as const, text: `Error: ${message}` }],\n isError: true,\n };\n }\n });\n\n return server;\n}\n\n/**\n * Start the stdio server.\n */\nexport async function startStdioServer(): Promise<void> {\n const server = createStdioServer();\n const transport = new StdioServerTransport();\n await server.connect(transport);\n console.error(`Forge MCP server v${VERSION} running on stdio`);\n}\n\n// Start server when run directly\nconst isMainModule =\n import.meta.url === `file://${process.argv[1]}` ||\n process.argv[1]?.endsWith(\"/forge-mcp\") ||\n process.argv[1]?.endsWith(\"\\\\forge-mcp\");\n\nif (isMainModule) {\n startStdioServer().catch((error) => {\n console.error(\"Fatal error:\", error);\n process.exit(1);\n });\n}\n"],"mappings":";;;;;;;;;AAaA,SAAgB,oBAA2B;AACzC,QAAO,CAAC,GAAG,OAAO,GAAG,iBAAiB;;;;;AAMxC,SAAgB,oBAAoB,MAAwC;AAC1E,KAAI,CAAC,KAAK,YAAY,OAAO,KAAK,aAAa,YAAY,KAAK,SAAS,MAAM,CAAC,WAAW,EACzF,QAAO;EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM;GACP,CACF;EACD,SAAS;EACV;AAGH,UAAS,KAAK,SAAS;AAEvB,QAAO,EACL,SAAS,CACP;EACE,MAAM;EACN,MAAM,KAAK,UACT;GACE,SAAS;GACT,SAAS;GACT,UAAU,MAAM,KAAK,SAAS,MAAM,GAAG;GACxC,EACD,MACA,EACD;EACF,CACF,EACF;;;;;AAMH,SAAgB,sBAAkC;CAChD,MAAM,QAAQ,UAAU;AAExB,QAAO,EACL,SAAS,CACP;EACE,MAAM;EACN,MAAM,KAAK,UACT;GACE,UAAU,QAAQ,MAAM,MAAM,MAAM,GAAG,KAAK;GAC5C,YAAY,CAAC,CAAC;GACf,EACD,MACA,EACD;EACF,CACF,EACF;;;;;AAMH,eAAsB,eACpB,MACA,MACqB;AACrB,KAAI,SAAS,kBACX,QAAO,oBAAoB,KAA6B;AAG1D,KAAI,SAAS,mBACX,QAAO,qBAAqB;CAI9B,MAAM,WAAW,UAAU;AAC3B,KAAI,CAAC,SACH,QAAO;EACL,SAAS,CACP;GACE,MAAM;GACN,MAAM;GACP,CACF;EACD,SAAS;EACV;AAGH,QAAO,2BAA2B,MAAM,MAAM,EAAE,UAAU,CAAC;;;;;;;;;;;;;;;;;;;;;;;;ACxE7D,SAAgB,oBAA4B;CAC1C,MAAM,SAAS,IAAI,OACjB;EACE,MAAM;EACN,SAAS;EACV,EACD;EACE,cAAc,EACZ,OAAO,EAAE,EACV;EACD,cAAc;EACf,CACF;AAED,QAAO,kBAAkB,wBAAwB,YAAY;AAC3D,SAAO,EAAE,OAAO,mBAAmB,EAAE;GACrC;AAEF,QAAO,kBAAkB,uBAAuB,OAAO,YAAY;EACjE,MAAM,EAAE,MAAM,WAAW,SAAS,QAAQ;AAE1C,MAAI;AAEF,UADe,MAAM,eAAe,MAAO,QAAoC,EAAE,CAAC;WAE3E,OAAO;AAEd,UAAO;IACL,SAAS,CAAC;KAAE,MAAM;KAAiB,MAAM,UAF3B,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;KAEN,CAAC;IAC/D,SAAS;IACV;;GAEH;AAEF,QAAO;;;;;AAMT,eAAsB,mBAAkC;CACtD,MAAM,SAAS,mBAAmB;CAClC,MAAM,YAAY,IAAI,sBAAsB;AAC5C,OAAM,OAAO,QAAQ,UAAU;AAC/B,SAAQ,MAAM,qBAAqB,QAAQ,mBAAmB;;AAShE,IAJE,OAAO,KAAK,QAAQ,UAAU,QAAQ,KAAK,QAC3C,QAAQ,KAAK,IAAI,SAAS,aAAa,IACvC,QAAQ,KAAK,IAAI,SAAS,cAAc,CAGxC,mBAAkB,CAAC,OAAO,UAAU;AAClC,SAAQ,MAAM,gBAAgB,MAAM;AACpC,SAAQ,KAAK,EAAE;EACf"}