@studiometa/forge-mcp 0.3.0 → 0.4.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.
Files changed (57) hide show
  1. package/README.md +7 -6
  2. package/dist/formatters.d.ts +101 -36
  3. package/dist/formatters.d.ts.map +1 -1
  4. package/dist/handlers/backups.d.ts.map +1 -1
  5. package/dist/handlers/batch.d.ts.map +1 -1
  6. package/dist/handlers/certificates.d.ts.map +1 -1
  7. package/dist/handlers/commands.d.ts.map +1 -1
  8. package/dist/handlers/context.d.ts.map +1 -1
  9. package/dist/handlers/daemons.d.ts.map +1 -1
  10. package/dist/handlers/database-users.d.ts.map +1 -1
  11. package/dist/handlers/databases.d.ts.map +1 -1
  12. package/dist/handlers/deployments.d.ts.map +1 -1
  13. package/dist/handlers/env.d.ts.map +1 -1
  14. package/dist/handlers/factory.d.ts +7 -37
  15. package/dist/handlers/factory.d.ts.map +1 -1
  16. package/dist/handlers/firewall-rules.d.ts.map +1 -1
  17. package/dist/handlers/help.d.ts.map +1 -1
  18. package/dist/handlers/index.d.ts +1 -0
  19. package/dist/handlers/index.d.ts.map +1 -1
  20. package/dist/handlers/monitors.d.ts.map +1 -1
  21. package/dist/handlers/nginx-config.d.ts.map +1 -1
  22. package/dist/handlers/nginx-templates.d.ts.map +1 -1
  23. package/dist/handlers/recipes.d.ts.map +1 -1
  24. package/dist/handlers/redirect-rules.d.ts.map +1 -1
  25. package/dist/handlers/scheduled-jobs.d.ts.map +1 -1
  26. package/dist/handlers/security-rules.d.ts.map +1 -1
  27. package/dist/handlers/servers.d.ts.map +1 -1
  28. package/dist/handlers/sites.d.ts.map +1 -1
  29. package/dist/handlers/ssh-keys.d.ts.map +1 -1
  30. package/dist/handlers/user.d.ts.map +1 -1
  31. package/dist/handlers/utils.d.ts +1 -1
  32. package/dist/handlers/utils.d.ts.map +1 -1
  33. package/dist/hints.d.ts +1 -1
  34. package/dist/hints.d.ts.map +1 -1
  35. package/dist/{http-Cwp91mT-.js → http-BhU5Kdf3.js} +16 -12
  36. package/dist/http-BhU5Kdf3.js.map +1 -0
  37. package/dist/http.d.ts +1 -1
  38. package/dist/http.d.ts.map +1 -1
  39. package/dist/http.js +2 -2
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +21 -10
  42. package/dist/index.js.map +1 -1
  43. package/dist/oauth.d.ts +1 -1
  44. package/dist/oauth.d.ts.map +1 -1
  45. package/dist/oauth.js +42 -41
  46. package/dist/oauth.js.map +1 -1
  47. package/dist/server.js +2 -2
  48. package/dist/sessions.d.ts.map +1 -1
  49. package/dist/stdio.d.ts +4 -2
  50. package/dist/stdio.d.ts.map +1 -1
  51. package/dist/tools.d.ts.map +1 -1
  52. package/dist/{version-CIiN0iJr.js → version-Bs3iU4Ei.js} +922 -364
  53. package/dist/version-Bs3iU4Ei.js.map +1 -0
  54. package/package.json +4 -4
  55. package/skills/SKILL.md +14 -12
  56. package/dist/http-Cwp91mT-.js.map +0 -1
  57. package/dist/version-CIiN0iJr.js.map +0 -1
@@ -1,11 +1,11 @@
1
- import { a as INSTRUCTIONS, i as getTools, n as executeToolWithCredentials, t as VERSION } from "./version-CIiN0iJr.js";
1
+ import { a as INSTRUCTIONS, i as getTools, n as executeToolWithCredentials, t as VERSION } from "./version-Bs3iU4Ei.js";
2
2
  import { parseAuthHeader } from "./auth.js";
3
3
  import { authorizeGetHandler, authorizePostHandler, oauthMetadataHandler, protectedResourceHandler, registerHandler, tokenHandler } from "./oauth.js";
4
4
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
5
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
6
6
  import { randomUUID } from "node:crypto";
7
7
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
8
- import { createApp, defineEventHandler } from "h3";
8
+ import { H3, defineEventHandler } from "h3";
9
9
  var DEFAULT_TTL = 1800 * 1e3;
10
10
  var DEFAULT_SWEEP_INTERVAL = 60 * 1e3;
11
11
  var SessionManager = class {
@@ -156,7 +156,10 @@ function createMcpServer(options) {
156
156
  name,
157
157
  /* v8 ignore next */
158
158
  args ?? {},
159
- { apiToken: token }
159
+ {
160
+ apiToken: token,
161
+ organizationSlug: typeof args?.organizationSlug === "string" ? args.organizationSlug : void 0
162
+ }
160
163
  );
161
164
  } catch (error) {
162
165
  /* v8 ignore start */
@@ -194,7 +197,8 @@ async function handleMcpRequest(req, res, sessions, options) {
194
197
  const credentials = parseAuthHeader(authHeader);
195
198
  if (!credentials) {
196
199
  const host = req.headers.host || "localhost:3000";
197
- const resourceMetadataUrl = `${req.headers["x-forwarded-proto"] || "http"}://${host}/.well-known/oauth-protected-resource`;
200
+ const proto = req.headers["x-forwarded-proto"];
201
+ const resourceMetadataUrl = `${(typeof proto === "string" ? proto : void 0) || "http"}://${host}/.well-known/oauth-protected-resource`;
198
202
  res.writeHead(401, {
199
203
  "Content-Type": "application/json",
200
204
  "WWW-Authenticate": `Bearer resource_metadata="${resourceMetadataUrl}"`
@@ -209,13 +213,13 @@ async function handleMcpRequest(req, res, sessions, options) {
209
213
  }));
210
214
  return;
211
215
  }
212
- const authenticatedReq = req;
213
- authenticatedReq.auth = {
216
+ Object.assign(req, { auth: {
214
217
  token: credentials.apiToken,
215
218
  clientId: "forge-http-client",
216
219
  scopes: []
217
- };
218
- const sessionId = req.headers["mcp-session-id"];
220
+ } });
221
+ const sessionHeader = req.headers["mcp-session-id"];
222
+ const sessionId = typeof sessionHeader === "string" ? sessionHeader : void 0;
219
223
  if (sessionId) {
220
224
  const session = sessions.get(sessionId);
221
225
  if (!session) {
@@ -230,7 +234,7 @@ async function handleMcpRequest(req, res, sessions, options) {
230
234
  }));
231
235
  return;
232
236
  }
233
- await session.transport.handleRequest(authenticatedReq, res);
237
+ await session.transport.handleRequest(req, res);
234
238
  return;
235
239
  }
236
240
  const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID() });
@@ -242,7 +246,7 @@ async function handleMcpRequest(req, res, sessions, options) {
242
246
  if (sid) sessions.remove(sid).catch(() => {});
243
247
  /* v8 ignore stop */
244
248
  };
245
- await transport.handleRequest(authenticatedReq, res);
249
+ await transport.handleRequest(req, res);
246
250
  /* v8 ignore start */
247
251
  if (transport.sessionId) sessions.register(transport, server);
248
252
  else {
@@ -265,7 +269,7 @@ function createMcpRequestHandler(sessions, options) {
265
269
  * The MCP endpoint is handled separately by handleMcpRequest.
266
270
  */
267
271
  function createHealthApp() {
268
- const app = createApp();
272
+ const app = new H3();
269
273
  app.get("/", defineEventHandler(() => {
270
274
  return {
271
275
  status: "ok",
@@ -286,4 +290,4 @@ function createHealthApp() {
286
290
  }
287
291
  export { SessionManager as a, handleMcpRequest as i, createMcpRequestHandler as n, createMcpServer as r, createHealthApp as t };
288
292
 
289
- //# sourceMappingURL=http-Cwp91mT-.js.map
293
+ //# sourceMappingURL=http-BhU5Kdf3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-BhU5Kdf3.js","names":[],"sources":["../src/sessions.ts","../src/http.ts"],"sourcesContent":["/* eslint-disable typescript-eslint/no-deprecated -- Using low-level Server type for session management */\n/**\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\n// Using low-level Server type for advanced transport handling\n// eslint-disable-next-line typescript-eslint/no-deprecated\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 // eslint-disable-next-line typescript-eslint/no-redundant-type-constituents -- NodeJS.Timeout resolves correctly at runtime\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","/* eslint-disable typescript-eslint/no-deprecated -- Using low-level Server for StreamableHTTPServerTransport */\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\n// Using low-level Server for advanced transport handling (StreamableHTTPServerTransport)\n// eslint-disable-next-line typescript-eslint/no-deprecated\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n type CallToolResult,\n} from \"@modelcontextprotocol/sdk/types.js\";\nimport { H3, defineEventHandler } from \"h3\";\n\nimport { parseAuthHeader } from \"./auth.ts\";\nimport { executeToolWithCredentials } from \"./handlers/index.ts\";\nimport { INSTRUCTIONS } from \"./instructions.ts\";\nimport {\n oauthMetadataHandler,\n protectedResourceHandler,\n registerHandler,\n authorizeGetHandler,\n authorizePostHandler,\n tokenHandler,\n} from \"./oauth.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(name, /* v8 ignore next */ args ?? {}, {\n apiToken: token,\n organizationSlug:\n typeof args?.organizationSlug === \"string\" ? args.organizationSlug : undefined,\n });\n return result as CallToolResult;\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 // Build resource_metadata URL for the WWW-Authenticate header (RFC 9728)\n const host = req.headers.host || \"localhost:3000\";\n const proto = req.headers[\"x-forwarded-proto\"];\n const protocol = (typeof proto === \"string\" ? proto : undefined) || \"http\";\n const resourceMetadataUrl = `${protocol}://${host}/.well-known/oauth-protected-resource`;\n\n res.writeHead(401, {\n \"Content-Type\": \"application/json\",\n \"WWW-Authenticate\": `Bearer resource_metadata=\"${resourceMetadataUrl}\"`,\n });\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 (MCP SDK expects auth on request)\n Object.assign(req, {\n auth: {\n token: credentials.apiToken,\n clientId: \"forge-http-client\",\n scopes: [],\n },\n });\n\n const sessionHeader = req.headers[\"mcp-session-id\"];\n const sessionId = typeof sessionHeader === \"string\" ? sessionHeader : 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(req, 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 // eslint-disable-next-line unicorn/prefer-add-event-listener -- MCP SDK uses property assignment\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(req, 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, service info, and OAuth endpoints.\n * The MCP endpoint is handled separately by handleMcpRequest.\n */\nexport function createHealthApp(): H3 {\n const app = new H3();\n\n // Service info & health\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 // OAuth 2.1 endpoints\n app.get(\"/.well-known/oauth-authorization-server\", oauthMetadataHandler);\n app.get(\"/.well-known/oauth-protected-resource\", protectedResourceHandler);\n app.post(\"/register\", registerHandler);\n app.get(\"/authorize\", authorizeGetHandler);\n app.post(\"/authorize\", authorizePostHandler);\n app.post(\"/token\", tokenHandler);\n\n return app;\n}\n"],"mappings":"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"}
package/dist/http.d.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  */
13
13
  import type { IncomingMessage, ServerResponse } from "node:http";
14
14
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
15
- import { type H3 } from "h3";
15
+ import { H3 } from "h3";
16
16
  import { SessionManager } from "./sessions.ts";
17
17
  export { SessionManager } from "./sessions.ts";
18
18
  /**
@@ -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;AAa5D,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,CAgGf;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,CA2BpC"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AACA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAIjE,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAOnE,OAAO,EAAE,EAAE,EAAsB,MAAM,IAAI,CAAC;AAa5C,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,CAkGf;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,CA2BpC"}
package/dist/http.js CHANGED
@@ -1,3 +1,3 @@
1
- import "./version-CIiN0iJr.js";
2
- import { a as SessionManager, i as handleMcpRequest, n as createMcpRequestHandler, r as createMcpServer, t as createHealthApp } from "./http-Cwp91mT-.js";
1
+ import "./version-Bs3iU4Ei.js";
2
+ import { a as SessionManager, i as handleMcpRequest, n as createMcpRequestHandler, r as createMcpServer, t as createHealthApp } from "./http-BhU5Kdf3.js";
3
3
  export { SessionManager, createHealthApp, createMcpRequestHandler, createMcpServer, handleMcpRequest };
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAGA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAIH,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAcnE,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"}
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as parseReadOnlyFlag } from "./flags-LFbdErsZ.js";
3
- import { a as INSTRUCTIONS, i as getTools, n as executeToolWithCredentials, r as STDIO_ONLY_TOOLS, t as VERSION } from "./version-CIiN0iJr.js";
3
+ import { a as INSTRUCTIONS, i as getTools, n as executeToolWithCredentials, r as STDIO_ONLY_TOOLS, t as VERSION } from "./version-Bs3iU4Ei.js";
4
4
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
7
- import { getToken, setToken } from "@studiometa/forge-api";
7
+ import { getOrganizationSlug, getToken, setOrganizationSlug, setToken } from "@studiometa/forge-api";
8
8
  /**
9
9
  * Get all available tools (including stdio-only configuration tools).
10
10
  *
@@ -17,23 +17,26 @@ function getAvailableTools(options) {
17
17
  * Handle the forge_configure tool.
18
18
  */
19
19
  function handleConfigureTool(args) {
20
- if (!args.apiToken || typeof args.apiToken !== "string" || args.apiToken.trim().length === 0) return {
20
+ if (!args.apiToken && !args.organizationSlug) return {
21
21
  content: [{
22
22
  type: "text",
23
- text: "Error: apiToken is required and must be a non-empty string."
23
+ text: "Error: at least one of apiToken or organizationSlug is required."
24
24
  }],
25
25
  structuredContent: {
26
26
  success: false,
27
- error: "apiToken is required and must be a non-empty string."
27
+ error: "at least one of apiToken or organizationSlug is required."
28
28
  },
29
29
  isError: true
30
30
  };
31
- setToken(args.apiToken);
31
+ if (args.apiToken) setToken(args.apiToken);
32
+ if (args.organizationSlug) setOrganizationSlug(args.organizationSlug);
33
+ const maskedToken = args.apiToken ? `***${args.apiToken.slice(-4)}` : void 0;
32
34
  const data = {
33
35
  success: true,
34
- message: "Laravel Forge API token configured successfully",
35
- apiToken: `***${args.apiToken.slice(-4)}`
36
+ message: "Laravel Forge configuration updated successfully"
36
37
  };
38
+ if (maskedToken) data.apiToken = maskedToken;
39
+ if (args.organizationSlug) data.organizationSlug = args.organizationSlug;
37
40
  return {
38
41
  content: [{
39
42
  type: "text",
@@ -47,8 +50,10 @@ function handleConfigureTool(args) {
47
50
  */
48
51
  function handleGetConfigTool() {
49
52
  const token = getToken();
53
+ const orgSlug = getOrganizationSlug();
50
54
  const data = {
51
55
  apiToken: token ? `***${token.slice(-4)}` : "not configured",
56
+ organizationSlug: orgSlug ?? "not configured",
52
57
  configured: !!token
53
58
  };
54
59
  return {
@@ -68,7 +73,10 @@ function handleGetConfigTool() {
68
73
  * - forge_write — write operations (create, update, delete, deploy, etc.)
69
74
  */
70
75
  async function handleToolCall(name, args, options) {
71
- if (name === "forge_configure") return handleConfigureTool(args);
76
+ if (name === "forge_configure") return handleConfigureTool({
77
+ apiToken: typeof args.apiToken === "string" ? args.apiToken : void 0,
78
+ organizationSlug: typeof args.organizationSlug === "string" ? args.organizationSlug : void 0
79
+ });
72
80
  if (name === "forge_get_config") return handleGetConfigTool();
73
81
  if (name === "forge_write" && options?.readOnly) return {
74
82
  content: [{
@@ -94,7 +102,10 @@ async function handleToolCall(name, args, options) {
94
102
  },
95
103
  isError: true
96
104
  };
97
- return executeToolWithCredentials(name, args, { apiToken });
105
+ return executeToolWithCredentials(name, args, {
106
+ apiToken,
107
+ organizationSlug: getOrganizationSlug() ?? void 0
108
+ });
98
109
  }
99
110
  return {
100
111
  content: [{
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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 { getTools, STDIO_ONLY_TOOLS } from \"./tools.ts\";\nimport type { GetToolsOptions } from \"./tools.ts\";\n\nexport type { ToolResult };\n\n/**\n * Get all available tools (including stdio-only configuration tools).\n *\n * @param options - Optional filtering. When `readOnly` is true, forge_write is excluded.\n */\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nexport function getAvailableTools(options?: GetToolsOptions): any[] {\n return [...getTools(options), ...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 structuredContent: {\n success: false,\n error: \"apiToken is required and must be a non-empty string.\",\n },\n isError: true,\n };\n }\n\n setToken(args.apiToken);\n\n const maskedToken = `***${args.apiToken.slice(-4)}`;\n const data = {\n success: true,\n message: \"Laravel Forge API token configured successfully\",\n apiToken: maskedToken,\n };\n\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(data, null, 2),\n },\n ],\n structuredContent: data,\n };\n}\n\n/**\n * Handle the forge_get_config tool.\n */\nexport function handleGetConfigTool(): ToolResult {\n const token = getToken();\n\n const data = {\n apiToken: token ? `***${token.slice(-4)}` : \"not configured\",\n configured: !!token,\n };\n\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(data, null, 2),\n },\n ],\n structuredContent: data,\n };\n}\n\n/**\n * Options for handleToolCall.\n */\nexport interface HandleToolCallOptions {\n /** When true, forge_write is rejected with an error. */\n readOnly?: boolean;\n}\n\n/**\n * Handle a tool call request.\n *\n * Routes to the appropriate handler based on tool name:\n * - forge_configure / forge_get_config — stdio-only config tools\n * - forge — read-only operations (list, get, help, schema)\n * - forge_write — write operations (create, update, delete, deploy, etc.)\n */\nexport async function handleToolCall(\n name: string,\n args: Record<string, unknown>,\n options?: HandleToolCallOptions,\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 // Reject forge_write in read-only mode\n if (name === \"forge_write\" && options?.readOnly) {\n return {\n content: [\n {\n type: \"text\",\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 // Both forge and forge_write require authentication\n if (name === \"forge\" || name === \"forge_write\") {\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 structuredContent: {\n success: false,\n error:\n 'Forge API token not configured. Use \"forge_configure\" tool or set FORGE_API_TOKEN environment variable.',\n },\n isError: true,\n };\n }\n\n return executeToolWithCredentials(name, args, { apiToken });\n }\n\n return {\n content: [\n {\n type: \"text\",\n text: `Error: Unknown tool \"${name}\".`,\n },\n ],\n structuredContent: { success: false, error: `Unknown tool \"${name}\".` },\n isError: true,\n };\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 * npx @studiometa/forge-mcp --read-only\n * FORGE_READ_ONLY=true npx @studiometa/forge-mcp\n *\n * Or in Claude Desktop config:\n * {\n * \"mcpServers\": {\n * \"forge\": {\n * \"command\": \"forge-mcp\",\n * \"args\": [\"--read-only\"],\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 { parseReadOnlyFlag } from \"./flags.ts\";\nimport { INSTRUCTIONS } from \"./instructions.ts\";\nimport { getAvailableTools, handleToolCall } from \"./stdio.ts\";\nimport { VERSION } from \"./version.ts\";\n\n// Re-export so consumers can still import from the main entry point\nexport { parseReadOnlyFlag } from \"./flags.ts\";\n\n/**\n * Options for the stdio MCP server.\n */\nexport interface StdioServerOptions {\n /** When true, forge_write tool is not registered and write operations are rejected. */\n readOnly?: boolean;\n}\n\n/**\n * Create and configure the MCP server.\n */\nexport function createStdioServer(options?: StdioServerOptions): Server {\n const readOnly = options?.readOnly ?? false;\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: getAvailableTools({ readOnly }) };\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 readOnly,\n });\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 structuredContent: { success: false, 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(options?: StdioServerOptions): Promise<void> {\n const server = createStdioServer(options);\n const transport = new StdioServerTransport();\n await server.connect(transport);\n const mode = options?.readOnly ? \" (read-only)\" : \"\";\n console.error(`Forge MCP server v${VERSION} running on stdio${mode}`);\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 const readOnly = parseReadOnlyFlag();\n startStdioServer({ readOnly }).catch((error) => {\n console.error(\"Fatal error:\", error);\n process.exit(1);\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/stdio.ts","../src/index.ts"],"sourcesContent":["import {\n getToken,\n getOrganizationSlug,\n setToken,\n setOrganizationSlug,\n} from \"@studiometa/forge-api\";\n\nimport type { ToolResult } from \"./handlers/types.ts\";\n\nimport { executeToolWithCredentials } from \"./handlers/index.ts\";\nimport type { Tool } from \"@modelcontextprotocol/sdk/types.js\";\nimport { getTools, STDIO_ONLY_TOOLS } from \"./tools.ts\";\nimport type { GetToolsOptions } from \"./tools.ts\";\n\nexport type { ToolResult };\n\n/**\n * Get all available tools (including stdio-only configuration tools).\n *\n * @param options - Optional filtering. When `readOnly` is true, forge_write is excluded.\n */\nexport function getAvailableTools(options?: GetToolsOptions): Tool[] {\n return [...getTools(options), ...STDIO_ONLY_TOOLS];\n}\n\n/**\n * Handle the forge_configure tool.\n */\nexport function handleConfigureTool(args: {\n apiToken?: string;\n organizationSlug?: string;\n}): ToolResult {\n if (!args.apiToken && !args.organizationSlug) {\n return {\n content: [\n {\n type: \"text\",\n text: \"Error: at least one of apiToken or organizationSlug is required.\",\n },\n ],\n structuredContent: {\n success: false,\n error: \"at least one of apiToken or organizationSlug is required.\",\n },\n isError: true,\n };\n }\n\n if (args.apiToken) {\n setToken(args.apiToken);\n }\n if (args.organizationSlug) {\n setOrganizationSlug(args.organizationSlug);\n }\n\n const maskedToken = args.apiToken ? `***${args.apiToken.slice(-4)}` : undefined;\n const data: Record<string, unknown> = {\n success: true,\n message: \"Laravel Forge configuration updated successfully\",\n };\n if (maskedToken) data.apiToken = maskedToken;\n if (args.organizationSlug) data.organizationSlug = args.organizationSlug;\n\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(data, null, 2),\n },\n ],\n structuredContent: data,\n };\n}\n\n/**\n * Handle the forge_get_config tool.\n */\nexport function handleGetConfigTool(): ToolResult {\n const token = getToken();\n const orgSlug = getOrganizationSlug();\n\n const data = {\n apiToken: token ? `***${token.slice(-4)}` : \"not configured\",\n organizationSlug: orgSlug ?? \"not configured\",\n configured: !!token,\n };\n\n return {\n content: [\n {\n type: \"text\",\n text: JSON.stringify(data, null, 2),\n },\n ],\n structuredContent: data,\n };\n}\n\n/**\n * Options for handleToolCall.\n */\nexport interface HandleToolCallOptions {\n /** When true, forge_write is rejected with an error. */\n readOnly?: boolean;\n}\n\n/**\n * Handle a tool call request.\n *\n * Routes to the appropriate handler based on tool name:\n * - forge_configure / forge_get_config — stdio-only config tools\n * - forge — read-only operations (list, get, help, schema)\n * - forge_write — write operations (create, update, delete, deploy, etc.)\n */\nexport async function handleToolCall(\n name: string,\n args: Record<string, unknown>,\n options?: HandleToolCallOptions,\n): Promise<ToolResult> {\n if (name === \"forge_configure\") {\n return handleConfigureTool({\n apiToken: typeof args.apiToken === \"string\" ? args.apiToken : undefined,\n organizationSlug:\n typeof args.organizationSlug === \"string\" ? args.organizationSlug : undefined,\n });\n }\n\n if (name === \"forge_get_config\") {\n return handleGetConfigTool();\n }\n\n // Reject forge_write in read-only mode\n if (name === \"forge_write\" && options?.readOnly) {\n return {\n content: [\n {\n type: \"text\",\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 // Both forge and forge_write require authentication\n if (name === \"forge\" || name === \"forge_write\") {\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 structuredContent: {\n success: false,\n error:\n 'Forge API token not configured. Use \"forge_configure\" tool or set FORGE_API_TOKEN environment variable.',\n },\n isError: true,\n };\n }\n\n const organizationSlug = getOrganizationSlug() ?? undefined;\n return executeToolWithCredentials(name, args, { apiToken, organizationSlug });\n }\n\n return {\n content: [\n {\n type: \"text\",\n text: `Error: Unknown tool \"${name}\".`,\n },\n ],\n structuredContent: { success: false, error: `Unknown tool \"${name}\".` },\n isError: true,\n };\n}\n","#!/usr/bin/env node\n/* eslint-disable typescript-eslint/no-deprecated -- Using low-level Server for StdioServerTransport */\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 * npx @studiometa/forge-mcp --read-only\n * FORGE_READ_ONLY=true npx @studiometa/forge-mcp\n *\n * Or in Claude Desktop config:\n * {\n * \"mcpServers\": {\n * \"forge\": {\n * \"command\": \"forge-mcp\",\n * \"args\": [\"--read-only\"],\n * \"env\": { \"FORGE_API_TOKEN\": \"your-token\" }\n * }\n * }\n * }\n */\n\n// Using low-level Server for advanced transport handling (StdioServerTransport)\n// eslint-disable-next-line typescript-eslint/no-deprecated\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport {\n CallToolRequestSchema,\n ListToolsRequestSchema,\n type CallToolResult,\n} from \"@modelcontextprotocol/sdk/types.js\";\n\nimport { parseReadOnlyFlag } from \"./flags.ts\";\nimport { INSTRUCTIONS } from \"./instructions.ts\";\nimport { getAvailableTools, handleToolCall } from \"./stdio.ts\";\nimport { VERSION } from \"./version.ts\";\n\n// Re-export so consumers can still import from the main entry point\nexport { parseReadOnlyFlag } from \"./flags.ts\";\n\n/**\n * Options for the stdio MCP server.\n */\nexport interface StdioServerOptions {\n /** When true, forge_write tool is not registered and write operations are rejected. */\n readOnly?: boolean;\n}\n\n/**\n * Create and configure the MCP server.\n */\nexport function createStdioServer(options?: StdioServerOptions): Server {\n const readOnly = options?.readOnly ?? false;\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: getAvailableTools({ readOnly }) };\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 ?? {}, {\n readOnly,\n });\n return result as CallToolResult;\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 structuredContent: { success: false, 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(options?: StdioServerOptions): Promise<void> {\n const server = createStdioServer(options);\n const transport = new StdioServerTransport();\n await server.connect(transport);\n const mode = options?.readOnly ? \" (read-only)\" : \"\";\n console.error(`Forge MCP server v${VERSION} running on stdio${mode}`);\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 const readOnly = parseReadOnlyFlag();\n startStdioServer({ readOnly }).catch((error) => {\n console.error(\"Fatal error:\", error);\n process.exit(1);\n });\n}\n"],"mappings":"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"}
package/dist/oauth.d.ts CHANGED
@@ -81,7 +81,7 @@ export declare const registerHandler: import("h3").EventHandlerWithFetch<import(
81
81
  * Authorization endpoint — shows login form
82
82
  * GET /authorize
83
83
  */
84
- export declare const authorizeGetHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, string | import("h3").HTTPResponse>;
84
+ export declare const authorizeGetHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, string | import("undici-types").Response>;
85
85
  /**
86
86
  * Authorization endpoint — process login
87
87
  * POST /authorize
@@ -1 +1 @@
1
- {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAgBH;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;EAyB/B,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB;;;;;EAcnC,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;GAoC1B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,0GAyC9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,uFAuD/B,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;GA4EvB,CAAC;AA0CH;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEhE"}
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAOH;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;EAyB/B,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB;;;;;EAcnC,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;GAqC1B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,gHAyC9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,uFAuD/B,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;GA4EvB,CAAC;AA0CH;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEhE"}
package/dist/oauth.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createAuthCode, decodeAuthCode } from "./crypto.js";
2
2
  import { createHash } from "node:crypto";
3
- import { defineEventHandler, getQuery, getRequestHeader, readBody, sendRedirect, setResponseHeader, setResponseStatus } from "h3";
3
+ import { defineEventHandler, getQuery, readBody } from "h3";
4
4
  /**
5
5
  * OAuth 2.1 endpoints for Claude Desktop integration
6
6
  *
@@ -32,10 +32,10 @@ function createAccessToken(apiToken) {
32
32
  * MCP clients MUST check this endpoint first for server capabilities.
33
33
  */
34
34
  const oauthMetadataHandler = defineEventHandler((event) => {
35
- const host = getRequestHeader(event, "host") || "localhost:3000";
36
- const baseUrl = `${getRequestHeader(event, "x-forwarded-proto") || "http"}://${host}`;
37
- setResponseHeader(event, "Content-Type", "application/json");
38
- setResponseHeader(event, "Cache-Control", "public, max-age=3600");
35
+ const host = event.req.headers.get("host") || "localhost:3000";
36
+ const baseUrl = `${event.req.headers.get("x-forwarded-proto") || "http"}://${host}`;
37
+ event.res.headers.set("Content-Type", "application/json");
38
+ event.res.headers.set("Cache-Control", "public, max-age=3600");
39
39
  return {
40
40
  issuer: baseUrl,
41
41
  authorization_endpoint: `${baseUrl}/authorize`,
@@ -56,10 +56,10 @@ const oauthMetadataHandler = defineEventHandler((event) => {
56
56
  * Tells MCP clients where to find the OAuth authorization server.
57
57
  */
58
58
  const protectedResourceHandler = defineEventHandler((event) => {
59
- const host = getRequestHeader(event, "host") || "localhost:3000";
60
- const baseUrl = `${getRequestHeader(event, "x-forwarded-proto") || "http"}://${host}`;
61
- setResponseHeader(event, "Content-Type", "application/json");
62
- setResponseHeader(event, "Cache-Control", "public, max-age=3600");
59
+ const host = event.req.headers.get("host") || "localhost:3000";
60
+ const baseUrl = `${event.req.headers.get("x-forwarded-proto") || "http"}://${host}`;
61
+ event.res.headers.set("Content-Type", "application/json");
62
+ event.res.headers.set("Cache-Control", "public, max-age=3600");
63
63
  return {
64
64
  resource: `${baseUrl}/mcp`,
65
65
  authorization_servers: [baseUrl],
@@ -76,24 +76,25 @@ const protectedResourceHandler = defineEventHandler((event) => {
76
76
  * a generated client_id.
77
77
  */
78
78
  const registerHandler = defineEventHandler(async (event) => {
79
- setResponseHeader(event, "Content-Type", "application/json");
79
+ event.res.headers.set("Content-Type", "application/json");
80
80
  let body;
81
81
  try {
82
- body = await readBody(event);
82
+ const raw = await readBody(event);
83
+ body = typeof raw === "object" && raw !== null ? raw : {};
83
84
  } catch {
84
- setResponseStatus(event, 400);
85
+ event.res.status = 400;
85
86
  return {
86
87
  error: "invalid_request",
87
88
  error_description: "Invalid JSON body"
88
89
  };
89
90
  }
90
- const clientName = body.client_name || "MCP Client";
91
- const redirectUris = body.redirect_uris || [];
91
+ const clientName = typeof body.client_name === "string" ? body.client_name : "MCP Client";
92
+ const redirectUris = Array.isArray(body.redirect_uris) ? body.redirect_uris : [];
92
93
  const clientId = Buffer.from(JSON.stringify({
93
94
  name: clientName,
94
95
  ts: Date.now()
95
96
  })).toString("base64url");
96
- setResponseStatus(event, 201);
97
+ event.res.status = 201;
97
98
  return {
98
99
  client_id: clientId,
99
100
  client_name: clientName,
@@ -109,13 +110,13 @@ const registerHandler = defineEventHandler(async (event) => {
109
110
  */
110
111
  const authorizeGetHandler = defineEventHandler((event) => {
111
112
  const query = getQuery(event);
112
- const redirectUri = query.redirect_uri;
113
- const state = query.state;
114
- const codeChallenge = query.code_challenge;
115
- const codeChallengeMethod = query.code_challenge_method;
113
+ const redirectUri = String(query.redirect_uri ?? "");
114
+ const state = String(query.state ?? "");
115
+ const codeChallenge = String(query.code_challenge ?? "");
116
+ const codeChallengeMethod = String(query.code_challenge_method ?? "");
116
117
  if (!redirectUri) {
117
- setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
118
- setResponseStatus(event, 400);
118
+ event.res.headers.set("Content-Type", "text/html; charset=utf-8");
119
+ event.res.status = 400;
119
120
  return renderErrorPage("Missing required parameter: redirect_uri");
120
121
  }
121
122
  if (!codeChallenge) {
@@ -123,16 +124,16 @@ const authorizeGetHandler = defineEventHandler((event) => {
123
124
  errorUrl.searchParams.set("error", "invalid_request");
124
125
  errorUrl.searchParams.set("error_description", "code_challenge is required");
125
126
  if (state) errorUrl.searchParams.set("state", state);
126
- return sendRedirect(event, errorUrl.toString(), 302);
127
+ return Response.redirect(errorUrl.toString(), 302);
127
128
  }
128
129
  if (codeChallengeMethod && codeChallengeMethod !== "S256") {
129
130
  const errorUrl = new URL(redirectUri);
130
131
  errorUrl.searchParams.set("error", "invalid_request");
131
132
  errorUrl.searchParams.set("error_description", "Only S256 code_challenge_method is supported");
132
133
  if (state) errorUrl.searchParams.set("state", state);
133
- return sendRedirect(event, errorUrl.toString(), 302);
134
+ return Response.redirect(errorUrl.toString(), 302);
134
135
  }
135
- setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
136
+ event.res.headers.set("Content-Type", "text/html; charset=utf-8");
136
137
  return renderLoginForm({
137
138
  redirectUri,
138
139
  state,
@@ -145,10 +146,10 @@ const authorizeGetHandler = defineEventHandler((event) => {
145
146
  * POST /authorize
146
147
  */
147
148
  const authorizePostHandler = defineEventHandler(async (event) => {
148
- const { apiToken, redirectUri, state, codeChallenge, codeChallengeMethod } = await readBody(event);
149
+ const { apiToken, redirectUri, state, codeChallenge, codeChallengeMethod } = await readBody(event) ?? {};
149
150
  if (!redirectUri) {
150
- setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
151
- setResponseStatus(event, 400);
151
+ event.res.headers.set("Content-Type", "text/html; charset=utf-8");
152
+ event.res.status = 400;
152
153
  return renderErrorPage("Missing redirect_uri parameter");
153
154
  }
154
155
  try {
@@ -156,15 +157,15 @@ const authorizePostHandler = defineEventHandler(async (event) => {
156
157
  const isLocalhost = uri.hostname === "localhost" || uri.hostname === "127.0.0.1";
157
158
  const isHttps = uri.protocol === "https:";
158
159
  if (!isLocalhost && !isHttps) {
159
- setResponseStatus(event, 400);
160
+ event.res.status = 400;
160
161
  return renderErrorPage("redirect_uri must be HTTPS or localhost");
161
162
  }
162
163
  } catch {
163
- setResponseStatus(event, 400);
164
+ event.res.status = 400;
164
165
  return renderErrorPage("Invalid redirect_uri format");
165
166
  }
166
167
  if (!apiToken) {
167
- setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
168
+ event.res.headers.set("Content-Type", "text/html; charset=utf-8");
168
169
  return renderLoginForm({
169
170
  redirectUri,
170
171
  state,
@@ -181,7 +182,7 @@ const authorizePostHandler = defineEventHandler(async (event) => {
181
182
  const redirectUrl = new URL(redirectUri);
182
183
  redirectUrl.searchParams.set("code", code);
183
184
  if (state) redirectUrl.searchParams.set("state", state);
184
- setResponseHeader(event, "Content-Type", "text/html; charset=utf-8");
185
+ event.res.headers.set("Content-Type", "text/html; charset=utf-8");
185
186
  return renderSuccessPage(redirectUrl.toString());
186
187
  });
187
188
  /**
@@ -193,25 +194,25 @@ const authorizePostHandler = defineEventHandler(async (event) => {
193
194
  * - refresh_token grant
194
195
  */
195
196
  const tokenHandler = defineEventHandler(async (event) => {
196
- setResponseHeader(event, "Content-Type", "application/json");
197
- const { grant_type, code, code_verifier, refresh_token } = await readBody(event);
197
+ event.res.headers.set("Content-Type", "application/json");
198
+ const { grant_type, code, code_verifier, refresh_token } = await readBody(event) ?? {};
198
199
  if (grant_type === "refresh_token") return handleRefreshToken(event, refresh_token);
199
200
  if (grant_type !== "authorization_code") {
200
- setResponseStatus(event, 400);
201
+ event.res.status = 400;
201
202
  return {
202
203
  error: "unsupported_grant_type",
203
204
  error_description: "Supported grant types: authorization_code, refresh_token"
204
205
  };
205
206
  }
206
207
  if (!code) {
207
- setResponseStatus(event, 400);
208
+ event.res.status = 400;
208
209
  return {
209
210
  error: "invalid_request",
210
211
  error_description: "Missing authorization code"
211
212
  };
212
213
  }
213
214
  if (!code_verifier) {
214
- setResponseStatus(event, 400);
215
+ event.res.status = 400;
215
216
  return {
216
217
  error: "invalid_request",
217
218
  error_description: "Missing code_verifier (PKCE required)"
@@ -221,7 +222,7 @@ const tokenHandler = defineEventHandler(async (event) => {
221
222
  const payload = decodeAuthCode(code);
222
223
  if (payload.codeChallenge) {
223
224
  if (createS256Challenge(code_verifier) !== payload.codeChallenge) {
224
- setResponseStatus(event, 400);
225
+ event.res.status = 400;
225
226
  return {
226
227
  error: "invalid_grant",
227
228
  error_description: "Invalid code_verifier"
@@ -235,7 +236,7 @@ const tokenHandler = defineEventHandler(async (event) => {
235
236
  refresh_token: createAuthCode({ apiToken: payload.apiToken }, 86400 * 30)
236
237
  };
237
238
  } catch (error) {
238
- setResponseStatus(event, 400);
239
+ event.res.status = 400;
239
240
  return {
240
241
  error: "invalid_grant",
241
242
  error_description: error instanceof Error ? error.message : "Invalid authorization code"
@@ -247,7 +248,7 @@ const tokenHandler = defineEventHandler(async (event) => {
247
248
  */
248
249
  function handleRefreshToken(event, refreshToken) {
249
250
  if (!refreshToken) {
250
- setResponseStatus(event, 400);
251
+ event.res.status = 400;
251
252
  return {
252
253
  error: "invalid_request",
253
254
  error_description: "Missing refresh_token"
@@ -262,7 +263,7 @@ function handleRefreshToken(event, refreshToken) {
262
263
  refresh_token: createAuthCode({ apiToken: payload.apiToken }, 86400 * 30)
263
264
  };
264
265
  } catch (error) {
265
- setResponseStatus(event, 400);
266
+ event.res.status = 400;
266
267
  return {
267
268
  error: "invalid_grant",
268
269
  error_description: error instanceof Error ? error.message : "Invalid refresh token"
@@ -280,7 +281,7 @@ function createS256Challenge(codeVerifier) {
280
281
  * Escape HTML special characters
281
282
  */
282
283
  function escapeHtml(str) {
283
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
284
+ return str.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("\"", "&quot;").replaceAll("'", "&#039;");
284
285
  }
285
286
  /**
286
287
  * Render the login form HTML