@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.
- package/README.md +7 -6
- package/dist/formatters.d.ts +101 -36
- package/dist/formatters.d.ts.map +1 -1
- package/dist/handlers/backups.d.ts.map +1 -1
- package/dist/handlers/batch.d.ts.map +1 -1
- package/dist/handlers/certificates.d.ts.map +1 -1
- package/dist/handlers/commands.d.ts.map +1 -1
- package/dist/handlers/context.d.ts.map +1 -1
- package/dist/handlers/daemons.d.ts.map +1 -1
- package/dist/handlers/database-users.d.ts.map +1 -1
- package/dist/handlers/databases.d.ts.map +1 -1
- package/dist/handlers/deployments.d.ts.map +1 -1
- package/dist/handlers/env.d.ts.map +1 -1
- package/dist/handlers/factory.d.ts +7 -37
- package/dist/handlers/factory.d.ts.map +1 -1
- package/dist/handlers/firewall-rules.d.ts.map +1 -1
- package/dist/handlers/help.d.ts.map +1 -1
- package/dist/handlers/index.d.ts +1 -0
- package/dist/handlers/index.d.ts.map +1 -1
- package/dist/handlers/monitors.d.ts.map +1 -1
- package/dist/handlers/nginx-config.d.ts.map +1 -1
- package/dist/handlers/nginx-templates.d.ts.map +1 -1
- package/dist/handlers/recipes.d.ts.map +1 -1
- package/dist/handlers/redirect-rules.d.ts.map +1 -1
- package/dist/handlers/scheduled-jobs.d.ts.map +1 -1
- package/dist/handlers/security-rules.d.ts.map +1 -1
- package/dist/handlers/servers.d.ts.map +1 -1
- package/dist/handlers/sites.d.ts.map +1 -1
- package/dist/handlers/ssh-keys.d.ts.map +1 -1
- package/dist/handlers/user.d.ts.map +1 -1
- package/dist/handlers/utils.d.ts +1 -1
- package/dist/handlers/utils.d.ts.map +1 -1
- package/dist/hints.d.ts +1 -1
- package/dist/hints.d.ts.map +1 -1
- package/dist/{http-Cwp91mT-.js → http-BhU5Kdf3.js} +16 -12
- package/dist/http-BhU5Kdf3.js.map +1 -0
- package/dist/http.d.ts +1 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -10
- package/dist/index.js.map +1 -1
- package/dist/oauth.d.ts +1 -1
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +42 -41
- package/dist/oauth.js.map +1 -1
- package/dist/server.js +2 -2
- package/dist/sessions.d.ts.map +1 -1
- package/dist/stdio.d.ts +4 -2
- package/dist/stdio.d.ts.map +1 -1
- package/dist/tools.d.ts.map +1 -1
- package/dist/{version-CIiN0iJr.js → version-Bs3iU4Ei.js} +922 -364
- package/dist/version-Bs3iU4Ei.js.map +1 -0
- package/package.json +4 -4
- package/skills/SKILL.md +14 -12
- package/dist/http-Cwp91mT-.js.map +0 -1
- 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-
|
|
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 {
|
|
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
|
-
{
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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 =
|
|
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-
|
|
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 {
|
|
15
|
+
import { H3 } from "h3";
|
|
16
16
|
import { SessionManager } from "./sessions.ts";
|
|
17
17
|
export { SessionManager } from "./sessions.ts";
|
|
18
18
|
/**
|
package/dist/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"
|
|
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-
|
|
2
|
-
import { a as SessionManager, i as handleMcpRequest, n as createMcpRequestHandler, r as createMcpServer, t as createHealthApp } from "./http-
|
|
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 };
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";
|
|
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-
|
|
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
|
|
20
|
+
if (!args.apiToken && !args.organizationSlug) return {
|
|
21
21
|
content: [{
|
|
22
22
|
type: "text",
|
|
23
|
-
text: "Error:
|
|
23
|
+
text: "Error: at least one of apiToken or organizationSlug is required."
|
|
24
24
|
}],
|
|
25
25
|
structuredContent: {
|
|
26
26
|
success: false,
|
|
27
|
-
error: "
|
|
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
|
|
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(
|
|
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, {
|
|
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("
|
|
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
|
package/dist/oauth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;
|
|
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,
|
|
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 =
|
|
36
|
-
const baseUrl = `${
|
|
37
|
-
|
|
38
|
-
|
|
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 =
|
|
60
|
-
const baseUrl = `${
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
79
|
+
event.res.headers.set("Content-Type", "application/json");
|
|
80
80
|
let body;
|
|
81
81
|
try {
|
|
82
|
-
|
|
82
|
+
const raw = await readBody(event);
|
|
83
|
+
body = typeof raw === "object" && raw !== null ? raw : {};
|
|
83
84
|
} catch {
|
|
84
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
|
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
|
|
134
|
+
return Response.redirect(errorUrl.toString(), 302);
|
|
134
135
|
}
|
|
135
|
-
|
|
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
|
-
|
|
151
|
-
|
|
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
|
-
|
|
160
|
+
event.res.status = 400;
|
|
160
161
|
return renderErrorPage("redirect_uri must be HTTPS or localhost");
|
|
161
162
|
}
|
|
162
163
|
} catch {
|
|
163
|
-
|
|
164
|
+
event.res.status = 400;
|
|
164
165
|
return renderErrorPage("Invalid redirect_uri format");
|
|
165
166
|
}
|
|
166
167
|
if (!apiToken) {
|
|
167
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
284
|
+
return str.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll("\"", """).replaceAll("'", "'");
|
|
284
285
|
}
|
|
285
286
|
/**
|
|
286
287
|
* Render the login form HTML
|