@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 +72 -22
- package/dist/flags-LFbdErsZ.js +23 -0
- package/dist/flags-LFbdErsZ.js.map +1 -0
- package/dist/flags.d.ts +19 -0
- package/dist/flags.d.ts.map +1 -0
- package/dist/formatters.d.ts +11 -3
- package/dist/formatters.d.ts.map +1 -1
- package/dist/handlers/index.d.ts +5 -3
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/types.d.ts +5 -0
- package/dist/handlers/types.d.ts.map +1 -1
- package/dist/handlers/utils.d.ts +6 -3
- package/dist/handlers/utils.d.ts.map +1 -1
- package/dist/{http-BJUKoZdb.js → http-w0DliUHY.js} +33 -9
- package/dist/http-w0DliUHY.js.map +1 -0
- package/dist/http.d.ts +11 -3
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +2 -2
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +90 -32
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +10 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +15 -6
- package/dist/server.js.map +1 -1
- package/dist/stdio.d.ts +17 -2
- package/dist/stdio.d.ts.map +1 -1
- package/dist/tools.d.ts +36 -11
- package/dist/tools.d.ts.map +1 -1
- package/dist/{version-Cw8OGt4r.js → version-BmEJceWJ.js} +350 -130
- package/dist/version-BmEJceWJ.js.map +1 -0
- package/package.json +1 -1
- package/skills/SKILL.md +68 -38
- package/dist/http-BJUKoZdb.js.map +0 -1
- package/dist/version-Cw8OGt4r.js.map +0 -1
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
|
|
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
|
-
##
|
|
21
|
+
## Tools
|
|
22
22
|
|
|
23
|
-
|
|
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
|
|
32
|
-
| ----------------- |
|
|
33
|
-
| `servers` | `list`, `get
|
|
34
|
-
| `sites` | `list`, `get
|
|
35
|
-
| `deployments` | `list
|
|
36
|
-
| `env` | `get
|
|
37
|
-
| `nginx` | `get
|
|
38
|
-
| `certificates` | `list`, `get
|
|
39
|
-
| `databases` | `list`, `get
|
|
40
|
-
| `database-users` | `list`, `get
|
|
41
|
-
| `daemons` | `list`, `get
|
|
42
|
-
| `firewall-rules` | `list`, `get
|
|
43
|
-
| `ssh-keys` | `list`, `get
|
|
44
|
-
| `security-rules` | `list`, `get
|
|
45
|
-
| `redirect-rules` | `list`, `get
|
|
46
|
-
| `monitors` | `list`, `get
|
|
47
|
-
| `nginx-templates` | `list`, `get
|
|
48
|
-
| `recipes` | `list`, `get
|
|
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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"}
|