@studiometa/forge-mcp 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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-K9wiprZX.js} +16 -12
  36. package/dist/http-K9wiprZX.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-CHrJu_54.js} +922 -364
  53. package/dist/version-CHrJu_54.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
package/dist/oauth.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"oauth.js","names":[],"sources":["../src/oauth.ts"],"sourcesContent":["/**\n * OAuth 2.1 endpoints for Claude Desktop integration\n *\n * Implements OAuth 2.1 with PKCE as specified in the MCP authorization spec.\n * Uses stateless encrypted tokens — no server-side storage required.\n *\n * Spec: https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization\n *\n * Flow:\n * 1. Claude redirects user to /authorize with OAuth params (including PKCE)\n * 2. User enters their Forge API token in a login form\n * 3. Server encrypts the token + PKCE challenge into an authorization code\n * 4. Redirects back to Claude with the code\n * 5. Claude exchanges code for access token via /token (with code_verifier)\n * 6. Server validates PKCE and returns base64-encoded access token\n */\n\nimport {\n defineEventHandler,\n getQuery,\n getRequestHeader,\n readBody,\n sendRedirect,\n setResponseHeader,\n setResponseStatus,\n type H3Event,\n} from \"h3\";\nimport { createHash } from \"node:crypto\";\n\nimport { createAuthCode, decodeAuthCode } from \"./crypto.ts\";\n\n/**\n * Create a base64-encoded access token from a Forge API token.\n * The access token is simply base64(apiToken) so that parseAuthHeader\n * can decode it on every request without any server-side lookup.\n */\nexport function createAccessToken(apiToken: string): string {\n return Buffer.from(apiToken).toString(\"base64\");\n}\n\n/**\n * OAuth metadata for discovery (RFC 8414)\n * GET /.well-known/oauth-authorization-server\n *\n * MCP clients MUST check this endpoint first for server capabilities.\n */\nexport const oauthMetadataHandler = defineEventHandler((event: H3Event) => {\n const host = getRequestHeader(event, \"host\") || \"localhost:3000\";\n const protocol = getRequestHeader(event, \"x-forwarded-proto\") || \"http\";\n const baseUrl = `${protocol}://${host}`;\n\n setResponseHeader(event, \"Content-Type\", \"application/json\");\n setResponseHeader(event, \"Cache-Control\", \"public, max-age=3600\");\n\n return {\n // Required fields per RFC 8414\n issuer: baseUrl,\n authorization_endpoint: `${baseUrl}/authorize`,\n token_endpoint: `${baseUrl}/token`,\n response_types_supported: [\"code\"],\n\n // OAuth 2.1 / MCP requirements\n grant_types_supported: [\"authorization_code\", \"refresh_token\"],\n code_challenge_methods_supported: [\"S256\"],\n token_endpoint_auth_methods_supported: [\"none\"], // Public client\n\n // Optional but useful\n registration_endpoint: `${baseUrl}/register`,\n scopes_supported: [\"forge\"],\n service_documentation: \"https://github.com/studiometa/forge-tools\",\n };\n});\n\n/**\n * Protected resource metadata (RFC 9728)\n * GET /.well-known/oauth-protected-resource\n *\n * Tells MCP clients where to find the OAuth authorization server.\n */\nexport const protectedResourceHandler = defineEventHandler((event: H3Event) => {\n const host = getRequestHeader(event, \"host\") || \"localhost:3000\";\n const protocol = getRequestHeader(event, \"x-forwarded-proto\") || \"http\";\n const baseUrl = `${protocol}://${host}`;\n\n setResponseHeader(event, \"Content-Type\", \"application/json\");\n setResponseHeader(event, \"Cache-Control\", \"public, max-age=3600\");\n\n return {\n resource: `${baseUrl}/mcp`,\n authorization_servers: [baseUrl],\n bearer_methods_supported: [\"header\"],\n scopes_supported: [\"forge\"],\n };\n});\n\n/**\n * Dynamic Client Registration endpoint (RFC 7591)\n * POST /register\n *\n * MCP servers SHOULD support DCR to allow clients to register automatically.\n * Since we use stateless tokens, we accept any registration and return\n * a generated client_id.\n */\nexport const registerHandler = defineEventHandler(async (event: H3Event) => {\n setResponseHeader(event, \"Content-Type\", \"application/json\");\n\n let body: Record<string, unknown>;\n try {\n body = (await readBody(event)) as Record<string, unknown>;\n } catch {\n setResponseStatus(event, 400);\n return {\n error: \"invalid_request\",\n error_description: \"Invalid JSON body\",\n };\n }\n\n // Extract client metadata\n const clientName = (body.client_name as string) || \"MCP Client\";\n const redirectUris = (body.redirect_uris as string[]) || [];\n\n // Generate a client_id based on the registration\n // Since we're stateless, we encode minimal info in the client_id\n const clientId = Buffer.from(\n JSON.stringify({\n name: clientName,\n ts: Date.now(),\n }),\n ).toString(\"base64url\");\n\n setResponseStatus(event, 201);\n return {\n client_id: clientId,\n client_name: clientName,\n redirect_uris: redirectUris,\n token_endpoint_auth_method: \"none\",\n grant_types: [\"authorization_code\", \"refresh_token\"],\n response_types: [\"code\"],\n };\n});\n\n/**\n * Authorization endpoint — shows login form\n * GET /authorize\n */\nexport const authorizeGetHandler = defineEventHandler((event: H3Event) => {\n const query = getQuery(event);\n\n // Extract OAuth parameters\n const redirectUri = query.redirect_uri as string;\n const state = query.state as string;\n const codeChallenge = query.code_challenge as string;\n const codeChallengeMethod = query.code_challenge_method as string;\n\n // Validate required parameters per OAuth 2.1\n if (!redirectUri) {\n setResponseHeader(event, \"Content-Type\", \"text/html; charset=utf-8\");\n setResponseStatus(event, 400);\n return renderErrorPage(\"Missing required parameter: redirect_uri\");\n }\n\n // PKCE is REQUIRED for public clients per MCP spec\n if (!codeChallenge) {\n const errorUrl = new URL(redirectUri);\n errorUrl.searchParams.set(\"error\", \"invalid_request\");\n errorUrl.searchParams.set(\"error_description\", \"code_challenge is required\");\n if (state) errorUrl.searchParams.set(\"state\", state);\n return sendRedirect(event, errorUrl.toString(), 302);\n }\n\n if (codeChallengeMethod && codeChallengeMethod !== \"S256\") {\n const errorUrl = new URL(redirectUri);\n errorUrl.searchParams.set(\"error\", \"invalid_request\");\n errorUrl.searchParams.set(\"error_description\", \"Only S256 code_challenge_method is supported\");\n if (state) errorUrl.searchParams.set(\"state\", state);\n return sendRedirect(event, errorUrl.toString(), 302);\n }\n\n setResponseHeader(event, \"Content-Type\", \"text/html; charset=utf-8\");\n\n return renderLoginForm({\n redirectUri,\n state,\n codeChallenge,\n codeChallengeMethod: codeChallengeMethod || \"S256\",\n });\n});\n\n/**\n * Authorization endpoint — process login\n * POST /authorize\n */\nexport const authorizePostHandler = defineEventHandler(async (event: H3Event) => {\n const body = (await readBody(event)) as Record<string, string>;\n\n const { apiToken, redirectUri, state, codeChallenge, codeChallengeMethod } = body;\n\n // Validate redirect URI first (security requirement)\n if (!redirectUri) {\n setResponseHeader(event, \"Content-Type\", \"text/html; charset=utf-8\");\n setResponseStatus(event, 400);\n return renderErrorPage(\"Missing redirect_uri parameter\");\n }\n\n // Validate redirect URI format (must be HTTPS or localhost)\n try {\n const uri = new URL(redirectUri);\n const isLocalhost = uri.hostname === \"localhost\" || uri.hostname === \"127.0.0.1\";\n const isHttps = uri.protocol === \"https:\";\n if (!isLocalhost && !isHttps) {\n setResponseStatus(event, 400);\n return renderErrorPage(\"redirect_uri must be HTTPS or localhost\");\n }\n } catch {\n setResponseStatus(event, 400);\n return renderErrorPage(\"Invalid redirect_uri format\");\n }\n\n // Validate required credentials\n if (!apiToken) {\n setResponseHeader(event, \"Content-Type\", \"text/html; charset=utf-8\");\n return renderLoginForm({\n redirectUri,\n state,\n codeChallenge,\n codeChallengeMethod,\n error: \"Forge API Token is required\",\n });\n }\n\n // Create encrypted authorization code with PKCE challenge\n const code = createAuthCode({\n apiToken,\n codeChallenge,\n codeChallengeMethod: codeChallengeMethod || \"S256\",\n });\n\n // Build redirect URL with authorization code\n const redirectUrl = new URL(redirectUri);\n redirectUrl.searchParams.set(\"code\", code);\n if (state) {\n redirectUrl.searchParams.set(\"state\", state);\n }\n\n // Show success page with auto-redirect\n setResponseHeader(event, \"Content-Type\", \"text/html; charset=utf-8\");\n return renderSuccessPage(redirectUrl.toString());\n});\n\n/**\n * Token endpoint — exchange code for access token\n * POST /token\n *\n * Supports:\n * - authorization_code grant (with PKCE validation)\n * - refresh_token grant\n */\nexport const tokenHandler = defineEventHandler(async (event: H3Event) => {\n setResponseHeader(event, \"Content-Type\", \"application/json\");\n\n // h3 auto-parses both JSON and URL-encoded bodies into objects\n const body = (await readBody(event)) as Record<string, string>;\n\n const { grant_type, code, code_verifier, refresh_token } = body;\n\n // Handle refresh token grant\n if (grant_type === \"refresh_token\") {\n return handleRefreshToken(event, refresh_token);\n }\n\n // Validate authorization code grant\n if (grant_type !== \"authorization_code\") {\n setResponseStatus(event, 400);\n return {\n error: \"unsupported_grant_type\",\n error_description: \"Supported grant types: authorization_code, refresh_token\",\n };\n }\n\n if (!code) {\n setResponseStatus(event, 400);\n return {\n error: \"invalid_request\",\n error_description: \"Missing authorization code\",\n };\n }\n\n if (!code_verifier) {\n setResponseStatus(event, 400);\n return {\n error: \"invalid_request\",\n error_description: \"Missing code_verifier (PKCE required)\",\n };\n }\n\n try {\n // Decode the authorization code\n const payload = decodeAuthCode(code);\n\n // Validate PKCE: SHA256(code_verifier) must equal code_challenge\n if (payload.codeChallenge) {\n const expectedChallenge = createS256Challenge(code_verifier);\n if (expectedChallenge !== payload.codeChallenge) {\n setResponseStatus(event, 400);\n return {\n error: \"invalid_grant\",\n error_description: \"Invalid code_verifier\",\n };\n }\n }\n\n // Create access token: base64(apiToken) — decodable on every request\n const accessToken = createAccessToken(payload.apiToken);\n\n // Create refresh token (encrypted credentials, longer expiry)\n const refreshToken = createAuthCode(\n { apiToken: payload.apiToken },\n 86400 * 30, // 30 days\n );\n\n return {\n access_token: accessToken,\n token_type: \"Bearer\",\n expires_in: 3600, // 1 hour\n refresh_token: refreshToken,\n };\n } catch (error) {\n setResponseStatus(event, 400);\n return {\n error: \"invalid_grant\",\n error_description: error instanceof Error ? error.message : \"Invalid authorization code\",\n };\n }\n});\n\n/**\n * Handle refresh token grant\n */\nfunction handleRefreshToken(event: H3Event, refreshToken: string | undefined) {\n if (!refreshToken) {\n setResponseStatus(event, 400);\n return {\n error: \"invalid_request\",\n error_description: \"Missing refresh_token\",\n };\n }\n\n try {\n // Decode refresh token (it's just an encrypted auth code with longer expiry)\n const payload = decodeAuthCode(refreshToken);\n\n // Create new access token\n const accessToken = createAccessToken(payload.apiToken);\n\n // Create new refresh token (rotate for security)\n const newRefreshToken = createAuthCode(\n { apiToken: payload.apiToken },\n 86400 * 30, // 30 days\n );\n\n return {\n access_token: accessToken,\n token_type: \"Bearer\",\n expires_in: 3600,\n refresh_token: newRefreshToken,\n };\n } catch (error) {\n setResponseStatus(event, 400);\n return {\n error: \"invalid_grant\",\n error_description: error instanceof Error ? error.message : \"Invalid refresh token\",\n };\n }\n}\n\n/**\n * Create S256 PKCE challenge from verifier\n * SHA256(code_verifier) encoded as base64url\n */\nexport function createS256Challenge(codeVerifier: string): string {\n return createHash(\"sha256\").update(codeVerifier).digest(\"base64url\");\n}\n\n/**\n * Escape HTML special characters\n */\nfunction escapeHtml(str: string): string {\n return str\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\")\n .replace(/'/g, \"&#039;\");\n}\n\n/**\n * Render the login form HTML\n */\nfunction renderLoginForm(params: {\n redirectUri?: string;\n state?: string;\n codeChallenge?: string;\n codeChallengeMethod?: string;\n error?: string;\n}): string {\n const { redirectUri, state, codeChallenge, codeChallengeMethod, error } = params;\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Connect to Laravel Forge</title>\n <style>\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: linear-gradient(135deg, #18b69b 0%, #0e7460 100%);\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n }\n .container {\n background: white;\n border-radius: 16px;\n box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);\n padding: 40px;\n width: 100%;\n max-width: 420px;\n }\n .logo {\n text-align: center;\n margin-bottom: 24px;\n }\n .logo svg { width: 48px; height: 48px; }\n h1 {\n text-align: center;\n color: #1a1a2e;\n font-size: 24px;\n margin-bottom: 8px;\n }\n .subtitle {\n text-align: center;\n color: #666;\n font-size: 14px;\n margin-bottom: 32px;\n }\n .error {\n background: #fee2e2;\n border: 1px solid #fecaca;\n color: #dc2626;\n padding: 12px 16px;\n border-radius: 8px;\n margin-bottom: 24px;\n font-size: 14px;\n }\n .notice {\n background: #f0fdf4;\n border: 1px solid #bbf7d0;\n color: #166534;\n padding: 12px 16px;\n border-radius: 8px;\n margin-bottom: 24px;\n font-size: 13px;\n line-height: 1.4;\n }\n .form-group { margin-bottom: 20px; }\n label {\n display: block;\n font-size: 14px;\n font-weight: 500;\n color: #374151;\n margin-bottom: 6px;\n }\n input {\n width: 100%;\n padding: 12px 16px;\n border: 1px solid #d1d5db;\n border-radius: 8px;\n font-size: 16px;\n transition: border-color 0.2s, box-shadow 0.2s;\n }\n input:focus {\n outline: none;\n border-color: #18b69b;\n box-shadow: 0 0 0 3px rgba(24, 182, 155, 0.2);\n }\n input::placeholder { color: #9ca3af; }\n .help-text {\n font-size: 12px;\n color: #6b7280;\n margin-top: 4px;\n }\n .help-text a { color: #18b69b; text-decoration: none; }\n .help-text a:hover { text-decoration: underline; }\n button {\n width: 100%;\n padding: 14px 24px;\n background: linear-gradient(135deg, #18b69b 0%, #0e7460 100%);\n color: white;\n border: none;\n border-radius: 8px;\n font-size: 16px;\n font-weight: 600;\n cursor: pointer;\n transition: transform 0.2s, box-shadow 0.2s;\n }\n button:hover {\n transform: translateY(-1px);\n box-shadow: 0 4px 12px rgba(24, 182, 155, 0.4);\n }\n button:active { transform: translateY(0); }\n .footer {\n text-align: center;\n margin-top: 24px;\n font-size: 12px;\n color: #9ca3af;\n }\n .footer a { color: #18b69b; text-decoration: none; }\n .footer a:hover { text-decoration: underline; }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"logo\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M12 2L2 7L12 12L22 7L12 2Z\" stroke=\"#18b69b\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n <path d=\"M2 17L12 22L22 17\" stroke=\"#0e7460\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n <path d=\"M2 12L12 17L22 12\" stroke=\"#18b69b\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n <h1>Connect to Laravel Forge</h1>\n <p class=\"subtitle\">Enter your Forge API token to connect with Claude</p>\n\n ${error ? `<div class=\"error\">${escapeHtml(error)}</div>` : \"\"}\n\n <div class=\"notice\">\n <strong>Your token is not stored on this server.</strong> It is encrypted into the authorization code and sent back to Claude Desktop, which holds it in its own secure storage.\n </div>\n\n <form method=\"POST\" action=\"/authorize\">\n <input type=\"hidden\" name=\"redirectUri\" value=\"${escapeHtml(redirectUri || \"\")}\">\n <input type=\"hidden\" name=\"state\" value=\"${escapeHtml(state || \"\")}\">\n <input type=\"hidden\" name=\"codeChallenge\" value=\"${escapeHtml(codeChallenge || \"\")}\">\n <input type=\"hidden\" name=\"codeChallengeMethod\" value=\"${escapeHtml(codeChallengeMethod || \"S256\")}\">\n\n <div class=\"form-group\">\n <label for=\"apiToken\">Forge API Token *</label>\n <input type=\"password\" id=\"apiToken\" name=\"apiToken\" required placeholder=\"Enter your Forge API token\">\n <p class=\"help-text\">\n Generate at <a href=\"https://forge.laravel.com/user-profile/api\" target=\"_blank\" rel=\"noopener\">forge.laravel.com → API Tokens</a>\n </p>\n </div>\n\n <button type=\"submit\">Connect to Forge</button>\n </form>\n\n <p class=\"footer\">\n Powered by <a href=\"https://github.com/studiometa/forge-tools\" target=\"_blank\" rel=\"noopener\">forge-tools</a>\n </p>\n </div>\n</body>\n</html>`;\n}\n\n/**\n * Render success page with auto-redirect\n */\nfunction renderSuccessPage(redirectUrl: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <meta http-equiv=\"refresh\" content=\"2;url=${escapeHtml(redirectUrl)}\">\n <title>Connected — Forge MCP</title>\n <style>\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: linear-gradient(135deg, #18b69b 0%, #0e7460 100%);\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n }\n .container {\n background: white;\n border-radius: 16px;\n box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);\n padding: 40px;\n width: 100%;\n max-width: 420px;\n text-align: center;\n }\n .success-icon {\n width: 64px;\n height: 64px;\n margin: 0 auto 24px;\n background: linear-gradient(135deg, #10b981 0%, #059669 100%);\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n .success-icon svg { width: 32px; height: 32px; stroke: white; }\n h1 { color: #1a1a2e; font-size: 24px; margin-bottom: 8px; }\n .message { color: #666; font-size: 14px; margin-bottom: 24px; }\n .spinner {\n width: 24px;\n height: 24px;\n border: 3px solid #e5e7eb;\n border-top-color: #18b69b;\n border-radius: 50%;\n animation: spin 1s linear infinite;\n margin: 0 auto 16px;\n }\n @keyframes spin { to { transform: rotate(360deg); } }\n .redirect-text { color: #9ca3af; font-size: 13px; margin-bottom: 16px; }\n .manual-link { color: #18b69b; text-decoration: none; font-size: 14px; }\n .manual-link:hover { text-decoration: underline; }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"success-icon\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M20 6L9 17L4 12\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n <h1>Successfully Connected!</h1>\n <p class=\"message\">Your Forge API token has been encrypted and transmitted securely.</p>\n <div class=\"spinner\"></div>\n <p class=\"redirect-text\">Redirecting to Claude Desktop...</p>\n <a href=\"${escapeHtml(redirectUrl)}\" class=\"manual-link\">Click here if not redirected automatically</a>\n </div>\n <script>\n setTimeout(function() {\n window.location.href = ${JSON.stringify(redirectUrl)};\n }, 2000);\n </script>\n</body>\n</html>`;\n}\n\n/**\n * Render error page\n */\nfunction renderErrorPage(message: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Error — Forge MCP</title>\n <style>\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #f3f4f6;\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n }\n .container {\n background: white;\n border-radius: 16px;\n box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n padding: 40px;\n text-align: center;\n max-width: 400px;\n }\n h1 { color: #dc2626; margin-bottom: 16px; }\n p { color: #6b7280; }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <h1>Error</h1>\n <p>${escapeHtml(message)}</p>\n </div>\n</body>\n</html>`;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"oauth.js","names":[],"sources":["../src/oauth.ts"],"sourcesContent":["/**\n * OAuth 2.1 endpoints for Claude Desktop integration\n *\n * Implements OAuth 2.1 with PKCE as specified in the MCP authorization spec.\n * Uses stateless encrypted tokens — no server-side storage required.\n *\n * Spec: https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization\n *\n * Flow:\n * 1. Claude redirects user to /authorize with OAuth params (including PKCE)\n * 2. User enters their Forge API token in a login form\n * 3. Server encrypts the token + PKCE challenge into an authorization code\n * 4. Redirects back to Claude with the code\n * 5. Claude exchanges code for access token via /token (with code_verifier)\n * 6. Server validates PKCE and returns base64-encoded access token\n */\n\nimport { defineEventHandler, getQuery, readBody, type H3Event } from \"h3\";\nimport { createHash } from \"node:crypto\";\n\nimport { createAuthCode, decodeAuthCode } from \"./crypto.ts\";\n\n/**\n * Create a base64-encoded access token from a Forge API token.\n * The access token is simply base64(apiToken) so that parseAuthHeader\n * can decode it on every request without any server-side lookup.\n */\nexport function createAccessToken(apiToken: string): string {\n return Buffer.from(apiToken).toString(\"base64\");\n}\n\n/**\n * OAuth metadata for discovery (RFC 8414)\n * GET /.well-known/oauth-authorization-server\n *\n * MCP clients MUST check this endpoint first for server capabilities.\n */\nexport const oauthMetadataHandler = defineEventHandler((event: H3Event) => {\n const host = event.req.headers.get(\"host\") || \"localhost:3000\";\n const protocol = event.req.headers.get(\"x-forwarded-proto\") || \"http\";\n const baseUrl = `${protocol}://${host}`;\n\n event.res.headers.set(\"Content-Type\", \"application/json\");\n event.res.headers.set(\"Cache-Control\", \"public, max-age=3600\");\n\n return {\n // Required fields per RFC 8414\n issuer: baseUrl,\n authorization_endpoint: `${baseUrl}/authorize`,\n token_endpoint: `${baseUrl}/token`,\n response_types_supported: [\"code\"],\n\n // OAuth 2.1 / MCP requirements\n grant_types_supported: [\"authorization_code\", \"refresh_token\"],\n code_challenge_methods_supported: [\"S256\"],\n token_endpoint_auth_methods_supported: [\"none\"], // Public client\n\n // Optional but useful\n registration_endpoint: `${baseUrl}/register`,\n scopes_supported: [\"forge\"],\n service_documentation: \"https://github.com/studiometa/forge-tools\",\n };\n});\n\n/**\n * Protected resource metadata (RFC 9728)\n * GET /.well-known/oauth-protected-resource\n *\n * Tells MCP clients where to find the OAuth authorization server.\n */\nexport const protectedResourceHandler = defineEventHandler((event: H3Event) => {\n const host = event.req.headers.get(\"host\") || \"localhost:3000\";\n const protocol = event.req.headers.get(\"x-forwarded-proto\") || \"http\";\n const baseUrl = `${protocol}://${host}`;\n\n event.res.headers.set(\"Content-Type\", \"application/json\");\n event.res.headers.set(\"Cache-Control\", \"public, max-age=3600\");\n\n return {\n resource: `${baseUrl}/mcp`,\n authorization_servers: [baseUrl],\n bearer_methods_supported: [\"header\"],\n scopes_supported: [\"forge\"],\n };\n});\n\n/**\n * Dynamic Client Registration endpoint (RFC 7591)\n * POST /register\n *\n * MCP servers SHOULD support DCR to allow clients to register automatically.\n * Since we use stateless tokens, we accept any registration and return\n * a generated client_id.\n */\nexport const registerHandler = defineEventHandler(async (event: H3Event) => {\n event.res.headers.set(\"Content-Type\", \"application/json\");\n\n let body: Record<string, unknown>;\n try {\n const raw: unknown = await readBody(event);\n body = typeof raw === \"object\" && raw !== null ? (raw as Record<string, unknown>) : {};\n } catch {\n event.res.status = 400;\n return {\n error: \"invalid_request\",\n error_description: \"Invalid JSON body\",\n };\n }\n\n // Extract client metadata\n const clientName = typeof body.client_name === \"string\" ? body.client_name : \"MCP Client\";\n const redirectUris = Array.isArray(body.redirect_uris) ? (body.redirect_uris as string[]) : [];\n\n // Generate a client_id based on the registration\n // Since we're stateless, we encode minimal info in the client_id\n const clientId = Buffer.from(\n JSON.stringify({\n name: clientName,\n ts: Date.now(),\n }),\n ).toString(\"base64url\");\n\n event.res.status = 201;\n return {\n client_id: clientId,\n client_name: clientName,\n redirect_uris: redirectUris,\n token_endpoint_auth_method: \"none\",\n grant_types: [\"authorization_code\", \"refresh_token\"],\n response_types: [\"code\"],\n };\n});\n\n/**\n * Authorization endpoint — shows login form\n * GET /authorize\n */\nexport const authorizeGetHandler = defineEventHandler((event: H3Event) => {\n const query = getQuery(event);\n\n // Extract OAuth parameters\n const redirectUri = String(query.redirect_uri ?? \"\");\n const state = String(query.state ?? \"\");\n const codeChallenge = String(query.code_challenge ?? \"\");\n const codeChallengeMethod = String(query.code_challenge_method ?? \"\");\n\n // Validate required parameters per OAuth 2.1\n if (!redirectUri) {\n event.res.headers.set(\"Content-Type\", \"text/html; charset=utf-8\");\n event.res.status = 400;\n return renderErrorPage(\"Missing required parameter: redirect_uri\");\n }\n\n // PKCE is REQUIRED for public clients per MCP spec\n if (!codeChallenge) {\n const errorUrl = new URL(redirectUri);\n errorUrl.searchParams.set(\"error\", \"invalid_request\");\n errorUrl.searchParams.set(\"error_description\", \"code_challenge is required\");\n if (state) errorUrl.searchParams.set(\"state\", state);\n return Response.redirect(errorUrl.toString(), 302);\n }\n\n if (codeChallengeMethod && codeChallengeMethod !== \"S256\") {\n const errorUrl = new URL(redirectUri);\n errorUrl.searchParams.set(\"error\", \"invalid_request\");\n errorUrl.searchParams.set(\"error_description\", \"Only S256 code_challenge_method is supported\");\n if (state) errorUrl.searchParams.set(\"state\", state);\n return Response.redirect(errorUrl.toString(), 302);\n }\n\n event.res.headers.set(\"Content-Type\", \"text/html; charset=utf-8\");\n\n return renderLoginForm({\n redirectUri,\n state,\n codeChallenge,\n codeChallengeMethod: codeChallengeMethod || \"S256\",\n });\n});\n\n/**\n * Authorization endpoint — process login\n * POST /authorize\n */\nexport const authorizePostHandler = defineEventHandler(async (event: H3Event) => {\n const body = (await readBody<Record<string, string>>(event)) ?? {};\n\n const { apiToken, redirectUri, state, codeChallenge, codeChallengeMethod } = body;\n\n // Validate redirect URI first (security requirement)\n if (!redirectUri) {\n event.res.headers.set(\"Content-Type\", \"text/html; charset=utf-8\");\n event.res.status = 400;\n return renderErrorPage(\"Missing redirect_uri parameter\");\n }\n\n // Validate redirect URI format (must be HTTPS or localhost)\n try {\n const uri = new URL(redirectUri);\n const isLocalhost = uri.hostname === \"localhost\" || uri.hostname === \"127.0.0.1\";\n const isHttps = uri.protocol === \"https:\";\n if (!isLocalhost && !isHttps) {\n event.res.status = 400;\n return renderErrorPage(\"redirect_uri must be HTTPS or localhost\");\n }\n } catch {\n event.res.status = 400;\n return renderErrorPage(\"Invalid redirect_uri format\");\n }\n\n // Validate required credentials\n if (!apiToken) {\n event.res.headers.set(\"Content-Type\", \"text/html; charset=utf-8\");\n return renderLoginForm({\n redirectUri,\n state,\n codeChallenge,\n codeChallengeMethod,\n error: \"Forge API Token is required\",\n });\n }\n\n // Create encrypted authorization code with PKCE challenge\n const code = createAuthCode({\n apiToken,\n codeChallenge,\n codeChallengeMethod: codeChallengeMethod || \"S256\",\n });\n\n // Build redirect URL with authorization code\n const redirectUrl = new URL(redirectUri);\n redirectUrl.searchParams.set(\"code\", code);\n if (state) {\n redirectUrl.searchParams.set(\"state\", state);\n }\n\n // Show success page with auto-redirect\n event.res.headers.set(\"Content-Type\", \"text/html; charset=utf-8\");\n return renderSuccessPage(redirectUrl.toString());\n});\n\n/**\n * Token endpoint — exchange code for access token\n * POST /token\n *\n * Supports:\n * - authorization_code grant (with PKCE validation)\n * - refresh_token grant\n */\nexport const tokenHandler = defineEventHandler(async (event: H3Event) => {\n event.res.headers.set(\"Content-Type\", \"application/json\");\n\n // h3 auto-parses both JSON and URL-encoded bodies into objects\n const body = (await readBody<Record<string, string>>(event)) ?? {};\n\n const { grant_type, code, code_verifier, refresh_token } = body;\n\n // Handle refresh token grant\n if (grant_type === \"refresh_token\") {\n return handleRefreshToken(event, refresh_token);\n }\n\n // Validate authorization code grant\n if (grant_type !== \"authorization_code\") {\n event.res.status = 400;\n return {\n error: \"unsupported_grant_type\",\n error_description: \"Supported grant types: authorization_code, refresh_token\",\n };\n }\n\n if (!code) {\n event.res.status = 400;\n return {\n error: \"invalid_request\",\n error_description: \"Missing authorization code\",\n };\n }\n\n if (!code_verifier) {\n event.res.status = 400;\n return {\n error: \"invalid_request\",\n error_description: \"Missing code_verifier (PKCE required)\",\n };\n }\n\n try {\n // Decode the authorization code\n const payload = decodeAuthCode(code);\n\n // Validate PKCE: SHA256(code_verifier) must equal code_challenge\n if (payload.codeChallenge) {\n const expectedChallenge = createS256Challenge(code_verifier);\n if (expectedChallenge !== payload.codeChallenge) {\n event.res.status = 400;\n return {\n error: \"invalid_grant\",\n error_description: \"Invalid code_verifier\",\n };\n }\n }\n\n // Create access token: base64(apiToken) — decodable on every request\n const accessToken = createAccessToken(payload.apiToken);\n\n // Create refresh token (encrypted credentials, longer expiry)\n const refreshToken = createAuthCode(\n { apiToken: payload.apiToken },\n 86400 * 30, // 30 days\n );\n\n return {\n access_token: accessToken,\n token_type: \"Bearer\",\n expires_in: 3600, // 1 hour\n refresh_token: refreshToken,\n };\n } catch (error) {\n event.res.status = 400;\n return {\n error: \"invalid_grant\",\n error_description: error instanceof Error ? error.message : \"Invalid authorization code\",\n };\n }\n});\n\n/**\n * Handle refresh token grant\n */\nfunction handleRefreshToken(event: H3Event, refreshToken: string | undefined) {\n if (!refreshToken) {\n event.res.status = 400;\n return {\n error: \"invalid_request\",\n error_description: \"Missing refresh_token\",\n };\n }\n\n try {\n // Decode refresh token (it's just an encrypted auth code with longer expiry)\n const payload = decodeAuthCode(refreshToken);\n\n // Create new access token\n const accessToken = createAccessToken(payload.apiToken);\n\n // Create new refresh token (rotate for security)\n const newRefreshToken = createAuthCode(\n { apiToken: payload.apiToken },\n 86400 * 30, // 30 days\n );\n\n return {\n access_token: accessToken,\n token_type: \"Bearer\",\n expires_in: 3600,\n refresh_token: newRefreshToken,\n };\n } catch (error) {\n event.res.status = 400;\n return {\n error: \"invalid_grant\",\n error_description: error instanceof Error ? error.message : \"Invalid refresh token\",\n };\n }\n}\n\n/**\n * Create S256 PKCE challenge from verifier\n * SHA256(code_verifier) encoded as base64url\n */\nexport function createS256Challenge(codeVerifier: string): string {\n return createHash(\"sha256\").update(codeVerifier).digest(\"base64url\");\n}\n\n/**\n * Escape HTML special characters\n */\nfunction escapeHtml(str: string): string {\n return str\n .replaceAll(\"&\", \"&amp;\")\n .replaceAll(\"<\", \"&lt;\")\n .replaceAll(\">\", \"&gt;\")\n .replaceAll('\"', \"&quot;\")\n .replaceAll(\"'\", \"&#039;\");\n}\n\n/**\n * Render the login form HTML\n */\nfunction renderLoginForm(params: {\n redirectUri?: string;\n state?: string;\n codeChallenge?: string;\n codeChallengeMethod?: string;\n error?: string;\n}): string {\n const { redirectUri, state, codeChallenge, codeChallengeMethod, error } = params;\n\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Connect to Laravel Forge</title>\n <style>\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: linear-gradient(135deg, #18b69b 0%, #0e7460 100%);\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n }\n .container {\n background: white;\n border-radius: 16px;\n box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);\n padding: 40px;\n width: 100%;\n max-width: 420px;\n }\n .logo {\n text-align: center;\n margin-bottom: 24px;\n }\n .logo svg { width: 48px; height: 48px; }\n h1 {\n text-align: center;\n color: #1a1a2e;\n font-size: 24px;\n margin-bottom: 8px;\n }\n .subtitle {\n text-align: center;\n color: #666;\n font-size: 14px;\n margin-bottom: 32px;\n }\n .error {\n background: #fee2e2;\n border: 1px solid #fecaca;\n color: #dc2626;\n padding: 12px 16px;\n border-radius: 8px;\n margin-bottom: 24px;\n font-size: 14px;\n }\n .notice {\n background: #f0fdf4;\n border: 1px solid #bbf7d0;\n color: #166534;\n padding: 12px 16px;\n border-radius: 8px;\n margin-bottom: 24px;\n font-size: 13px;\n line-height: 1.4;\n }\n .form-group { margin-bottom: 20px; }\n label {\n display: block;\n font-size: 14px;\n font-weight: 500;\n color: #374151;\n margin-bottom: 6px;\n }\n input {\n width: 100%;\n padding: 12px 16px;\n border: 1px solid #d1d5db;\n border-radius: 8px;\n font-size: 16px;\n transition: border-color 0.2s, box-shadow 0.2s;\n }\n input:focus {\n outline: none;\n border-color: #18b69b;\n box-shadow: 0 0 0 3px rgba(24, 182, 155, 0.2);\n }\n input::placeholder { color: #9ca3af; }\n .help-text {\n font-size: 12px;\n color: #6b7280;\n margin-top: 4px;\n }\n .help-text a { color: #18b69b; text-decoration: none; }\n .help-text a:hover { text-decoration: underline; }\n button {\n width: 100%;\n padding: 14px 24px;\n background: linear-gradient(135deg, #18b69b 0%, #0e7460 100%);\n color: white;\n border: none;\n border-radius: 8px;\n font-size: 16px;\n font-weight: 600;\n cursor: pointer;\n transition: transform 0.2s, box-shadow 0.2s;\n }\n button:hover {\n transform: translateY(-1px);\n box-shadow: 0 4px 12px rgba(24, 182, 155, 0.4);\n }\n button:active { transform: translateY(0); }\n .footer {\n text-align: center;\n margin-top: 24px;\n font-size: 12px;\n color: #9ca3af;\n }\n .footer a { color: #18b69b; text-decoration: none; }\n .footer a:hover { text-decoration: underline; }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"logo\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M12 2L2 7L12 12L22 7L12 2Z\" stroke=\"#18b69b\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n <path d=\"M2 17L12 22L22 17\" stroke=\"#0e7460\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n <path d=\"M2 12L12 17L22 12\" stroke=\"#18b69b\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n <h1>Connect to Laravel Forge</h1>\n <p class=\"subtitle\">Enter your Forge API token to connect with Claude</p>\n\n ${error ? `<div class=\"error\">${escapeHtml(error)}</div>` : \"\"}\n\n <div class=\"notice\">\n <strong>Your token is not stored on this server.</strong> It is encrypted into the authorization code and sent back to Claude Desktop, which holds it in its own secure storage.\n </div>\n\n <form method=\"POST\" action=\"/authorize\">\n <input type=\"hidden\" name=\"redirectUri\" value=\"${escapeHtml(redirectUri || \"\")}\">\n <input type=\"hidden\" name=\"state\" value=\"${escapeHtml(state || \"\")}\">\n <input type=\"hidden\" name=\"codeChallenge\" value=\"${escapeHtml(codeChallenge || \"\")}\">\n <input type=\"hidden\" name=\"codeChallengeMethod\" value=\"${escapeHtml(codeChallengeMethod || \"S256\")}\">\n\n <div class=\"form-group\">\n <label for=\"apiToken\">Forge API Token *</label>\n <input type=\"password\" id=\"apiToken\" name=\"apiToken\" required placeholder=\"Enter your Forge API token\">\n <p class=\"help-text\">\n Generate at <a href=\"https://forge.laravel.com/user-profile/api\" target=\"_blank\" rel=\"noopener\">forge.laravel.com → API Tokens</a>\n </p>\n </div>\n\n <button type=\"submit\">Connect to Forge</button>\n </form>\n\n <p class=\"footer\">\n Powered by <a href=\"https://github.com/studiometa/forge-tools\" target=\"_blank\" rel=\"noopener\">forge-tools</a>\n </p>\n </div>\n</body>\n</html>`;\n}\n\n/**\n * Render success page with auto-redirect\n */\nfunction renderSuccessPage(redirectUrl: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <meta http-equiv=\"refresh\" content=\"2;url=${escapeHtml(redirectUrl)}\">\n <title>Connected — Forge MCP</title>\n <style>\n * { box-sizing: border-box; margin: 0; padding: 0; }\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: linear-gradient(135deg, #18b69b 0%, #0e7460 100%);\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n }\n .container {\n background: white;\n border-radius: 16px;\n box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);\n padding: 40px;\n width: 100%;\n max-width: 420px;\n text-align: center;\n }\n .success-icon {\n width: 64px;\n height: 64px;\n margin: 0 auto 24px;\n background: linear-gradient(135deg, #10b981 0%, #059669 100%);\n border-radius: 50%;\n display: flex;\n align-items: center;\n justify-content: center;\n }\n .success-icon svg { width: 32px; height: 32px; stroke: white; }\n h1 { color: #1a1a2e; font-size: 24px; margin-bottom: 8px; }\n .message { color: #666; font-size: 14px; margin-bottom: 24px; }\n .spinner {\n width: 24px;\n height: 24px;\n border: 3px solid #e5e7eb;\n border-top-color: #18b69b;\n border-radius: 50%;\n animation: spin 1s linear infinite;\n margin: 0 auto 16px;\n }\n @keyframes spin { to { transform: rotate(360deg); } }\n .redirect-text { color: #9ca3af; font-size: 13px; margin-bottom: 16px; }\n .manual-link { color: #18b69b; text-decoration: none; font-size: 14px; }\n .manual-link:hover { text-decoration: underline; }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"success-icon\">\n <svg viewBox=\"0 0 24 24\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <path d=\"M20 6L9 17L4 12\" stroke-width=\"2.5\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n </div>\n <h1>Successfully Connected!</h1>\n <p class=\"message\">Your Forge API token has been encrypted and transmitted securely.</p>\n <div class=\"spinner\"></div>\n <p class=\"redirect-text\">Redirecting to Claude Desktop...</p>\n <a href=\"${escapeHtml(redirectUrl)}\" class=\"manual-link\">Click here if not redirected automatically</a>\n </div>\n <script>\n setTimeout(function() {\n window.location.href = ${JSON.stringify(redirectUrl)};\n }, 2000);\n </script>\n</body>\n</html>`;\n}\n\n/**\n * Render error page\n */\nfunction renderErrorPage(message: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>Error — Forge MCP</title>\n <style>\n body {\n font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;\n background: #f3f4f6;\n min-height: 100vh;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 20px;\n }\n .container {\n background: white;\n border-radius: 16px;\n box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);\n padding: 40px;\n text-align: center;\n max-width: 400px;\n }\n h1 { color: #dc2626; margin-bottom: 16px; }\n p { color: #6b7280; }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <h1>Error</h1>\n <p>${escapeHtml(message)}</p>\n </div>\n</body>\n</html>`;\n}\n"],"mappings":"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"}
package/dist/server.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { t as parseReadOnlyFlag } from "./flags-LFbdErsZ.js";
3
- import { t as VERSION } from "./version-CIiN0iJr.js";
4
- import { a as SessionManager, n as createMcpRequestHandler, t as createHealthApp } from "./http-Cwp91mT-.js";
3
+ import { t as VERSION } from "./version-CHrJu_54.js";
4
+ import { a as SessionManager, n as createMcpRequestHandler, t as createHealthApp } from "./http-K9wiprZX.js";
5
5
  import { toNodeHandler } from "h3/node";
6
6
  import { createServer } from "node:http";
7
7
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACxE,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AAExG;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,6BAA6B,CAAC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAKD,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAqC;IACrD,OAAO,CAAC,UAAU,CAA6C;IAC/D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEjB,OAAO,CAAC,EAAE,qBAAqB;IAa3C;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE,6BAA6B,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAaxE;;OAEG;IACH,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAQlD;;OAEG;IACG,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS9C;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;OAGG;IACH,KAAK,IAAI,MAAM;IAsBf;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAchC"}
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AACA;;;;;;;;GAQG;AAIH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACxE,OAAO,KAAK,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AAExG;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,6BAA6B,CAAC;IACzC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAKD,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAqC;IAErD,OAAO,CAAC,UAAU,CAA6C;IAC/D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;gBAEjB,OAAO,CAAC,EAAE,qBAAqB;IAa3C;;OAEG;IACH,QAAQ,CAAC,SAAS,EAAE,6BAA6B,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAaxE;;OAEG;IACH,GAAG,CAAC,SAAS,EAAE,MAAM,GAAG,cAAc,GAAG,SAAS;IAQlD;;OAEG;IACG,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS9C;;OAEG;IACH,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED;;;OAGG;IACH,KAAK,IAAI,MAAM;IAsBf;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAchC"}
package/dist/stdio.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { ToolResult } from "./handlers/types.ts";
2
+ import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
3
  import type { GetToolsOptions } from "./tools.ts";
3
4
  export type { ToolResult };
4
5
  /**
@@ -6,12 +7,13 @@ export type { ToolResult };
6
7
  *
7
8
  * @param options - Optional filtering. When `readOnly` is true, forge_write is excluded.
8
9
  */
9
- export declare function getAvailableTools(options?: GetToolsOptions): any[];
10
+ export declare function getAvailableTools(options?: GetToolsOptions): Tool[];
10
11
  /**
11
12
  * Handle the forge_configure tool.
12
13
  */
13
14
  export declare function handleConfigureTool(args: {
14
- apiToken: string;
15
+ apiToken?: string;
16
+ organizationSlug?: string;
15
17
  }): ToolResult;
16
18
  /**
17
19
  * Handle the forge_get_config tool.
@@ -1 +1 @@
1
- {"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../src/stdio.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAItD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,YAAY,EAAE,UAAU,EAAE,CAAC;AAE3B;;;;GAIG;AAEH,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,GAAG,EAAE,CAElE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,GAAG,UAAU,CAmC1E;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,UAAU,CAiBhD;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,wDAAwD;IACxD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,UAAU,CAAC,CA2DrB"}
1
+ {"version":3,"file":"stdio.d.ts","sourceRoot":"","sources":["../src/stdio.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAGtD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAE/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAElD,YAAY,EAAE,UAAU,EAAE,CAAC;AAE3B;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,EAAE,CAEnE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,GAAG,UAAU,CAyCb;AAED;;GAEG;AACH,wBAAgB,mBAAmB,IAAI,UAAU,CAmBhD;AAED;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,wDAAwD;IACxD,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAsB,cAAc,CAClC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,UAAU,CAAC,CAgErB"}
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAI/D;;GAEG;AACH,eAAO,MAAM,YAAY,kEAAmE,CAAC;AAE7F;;;GAGG;AACH,eAAO,MAAM,aAAa,2FAShB,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AACvD,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,WAAW,CAEnE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,UAAU,CAEjE;AAkQD;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,EAAE,IAAI,EAAwC,CAAC;AAEjE;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,EAAE,CAK1D;AAED;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,IAAI,EAsDlC,CAAC"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,oCAAoC,CAAC;AAI/D;;GAEG;AACH,eAAO,MAAM,YAAY,kEAAmE,CAAC;AAE7F;;;GAGG;AACH,eAAO,MAAM,aAAa,2FAShB,CAAC;AAEX,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,YAAY,CAAC,CAAC,MAAM,CAAC,CAAC;AACvD,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAEzD;;GAEG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,WAAW,CAEnE;AAED;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,IAAI,UAAU,CAEjE;AAiQD;;;;;;GAMG;AACH,eAAO,MAAM,KAAK,EAAE,IAAI,EAAwC,CAAC;AAEjE;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,IAAI,EAAE,CAK1D;AAED;;GAEG;AACH,eAAO,MAAM,gBAAgB,EAAE,IAAI,EA2DlC,CAAC"}