@studiometa/forge-mcp 0.4.1 → 0.4.3

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/dist/auth.d.ts CHANGED
@@ -11,6 +11,7 @@
11
11
  */
12
12
  export interface ForgeCredentials {
13
13
  apiToken: string;
14
+ organizationSlug?: string;
14
15
  }
15
16
  /**
16
17
  * Parse Bearer token containing Forge API credentials.
@@ -18,6 +19,7 @@ export interface ForgeCredentials {
18
19
  * Token formats:
19
20
  * - Raw Forge API token (e.g., "Bearer my-api-token")
20
21
  * - Base64-encoded token from OAuth flow (e.g., "Bearer base64(apiToken)")
22
+ * - Base64-encoded JSON from OAuth flow (e.g., "Bearer base64({apiToken, organizationSlug})")
21
23
  *
22
24
  * @param authHeader - Authorization header value (e.g., "Bearer <token>")
23
25
  * @returns Parsed credentials or null if invalid
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;CAClB;AA0BD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,gBAAgB,GAAG,IAAI,CAwB9F"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAgDD;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,gBAAgB,GAAG,IAAI,CA6B9F"}
package/dist/auth.js CHANGED
@@ -11,12 +11,26 @@ function tryDecodeBase64(token) {
11
11
  if (Buffer.from(decoded).toString("base64") === token) return decoded;
12
12
  return null;
13
13
  }
14
+ function tryParseCredentials(decoded) {
15
+ try {
16
+ const parsed = JSON.parse(decoded);
17
+ if (typeof parsed === "object" && parsed !== null && "apiToken" in parsed && typeof parsed.apiToken === "string") {
18
+ const obj = parsed;
19
+ return {
20
+ apiToken: obj.apiToken,
21
+ organizationSlug: typeof obj.organizationSlug === "string" ? obj.organizationSlug : void 0
22
+ };
23
+ }
24
+ } catch {}
25
+ return null;
26
+ }
14
27
  /**
15
28
  * Parse Bearer token containing Forge API credentials.
16
29
  *
17
30
  * Token formats:
18
31
  * - Raw Forge API token (e.g., "Bearer my-api-token")
19
32
  * - Base64-encoded token from OAuth flow (e.g., "Bearer base64(apiToken)")
33
+ * - Base64-encoded JSON from OAuth flow (e.g., "Bearer base64({apiToken, organizationSlug})")
20
34
  *
21
35
  * @param authHeader - Authorization header value (e.g., "Bearer <token>")
22
36
  * @returns Parsed credentials or null if invalid
@@ -28,7 +42,11 @@ function parseAuthHeader(authHeader) {
28
42
  const token = match[1].trim();
29
43
  if (!token) return null;
30
44
  const decoded = tryDecodeBase64(token);
31
- if (decoded) return { apiToken: decoded };
45
+ if (decoded) {
46
+ const credentials = tryParseCredentials(decoded);
47
+ if (credentials) return credentials;
48
+ return { apiToken: decoded };
49
+ }
32
50
  return { apiToken: token };
33
51
  }
34
52
  export { parseAuthHeader };
package/dist/auth.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"auth.js","names":[],"sources":["../src/auth.ts"],"sourcesContent":["/**\n * Authentication utilities for Forge MCP HTTP server\n *\n * Supports two token formats:\n * 1. Raw Forge API token (backwards compatible)\n * 2. Base64-encoded Forge API token (from OAuth flow)\n *\n * Detection heuristic: decode the token from base64, then re-encode.\n * If the re-encoded string matches the original, it's a real base64 token\n * and we use the decoded value. Otherwise, we treat it as a raw token.\n */\n\nexport interface ForgeCredentials {\n apiToken: string;\n}\n\n/**\n * Try to decode a base64-encoded token.\n * Returns the decoded string if re-encoding produces the original, null otherwise.\n *\n * Buffer.from(str, 'base64') never throws — it silently ignores invalid chars.\n * So this function cannot fail, no try/catch needed.\n */\nfunction tryDecodeBase64(token: string): string | null {\n const decoded = Buffer.from(token, \"base64\").toString(\"utf-8\");\n\n // decoded must be non-empty and different from input\n if (!decoded || decoded === token) {\n return null;\n }\n\n // Re-encode and compare — if it roundtrips, it was genuinely base64\n const reEncoded = Buffer.from(decoded).toString(\"base64\");\n if (reEncoded === token) {\n return decoded;\n }\n\n return null;\n}\n\n/**\n * Parse Bearer token containing Forge API credentials.\n *\n * Token formats:\n * - Raw Forge API token (e.g., \"Bearer my-api-token\")\n * - Base64-encoded token from OAuth flow (e.g., \"Bearer base64(apiToken)\")\n *\n * @param authHeader - Authorization header value (e.g., \"Bearer <token>\")\n * @returns Parsed credentials or null if invalid\n */\nexport function parseAuthHeader(authHeader: string | undefined | null): ForgeCredentials | null {\n if (!authHeader) {\n return null;\n }\n\n const match = authHeader.match(/^Bearer\\s+(.+)$/i);\n if (!match) {\n return null;\n }\n\n const token = match[1].trim();\n\n if (!token) {\n return null;\n }\n\n // Try to decode as base64 (OAuth access token)\n const decoded = tryDecodeBase64(token);\n if (decoded) {\n return { apiToken: decoded };\n }\n\n // Treat as raw Forge API token (backwards compatible)\n return { apiToken: token };\n}\n"],"mappings":";;;;;;;AAuBA,SAAS,gBAAgB,OAA8B;CACrD,MAAM,UAAU,OAAO,KAAK,OAAO,SAAS,CAAC,SAAS,QAAQ;AAG9D,KAAI,CAAC,WAAW,YAAY,MAC1B,QAAO;AAKT,KADkB,OAAO,KAAK,QAAQ,CAAC,SAAS,SAAS,KACvC,MAChB,QAAO;AAGT,QAAO;;;;;;;;;;;;AAaT,SAAgB,gBAAgB,YAAgE;AAC9F,KAAI,CAAC,WACH,QAAO;CAGT,MAAM,QAAQ,WAAW,MAAM,mBAAmB;AAClD,KAAI,CAAC,MACH,QAAO;CAGT,MAAM,QAAQ,MAAM,GAAG,MAAM;AAE7B,KAAI,CAAC,MACH,QAAO;CAIT,MAAM,UAAU,gBAAgB,MAAM;AACtC,KAAI,QACF,QAAO,EAAE,UAAU,SAAS;AAI9B,QAAO,EAAE,UAAU,OAAO"}
1
+ {"version":3,"file":"auth.js","names":[],"sources":["../src/auth.ts"],"sourcesContent":["/**\n * Authentication utilities for Forge MCP HTTP server\n *\n * Supports two token formats:\n * 1. Raw Forge API token (backwards compatible)\n * 2. Base64-encoded Forge API token (from OAuth flow)\n *\n * Detection heuristic: decode the token from base64, then re-encode.\n * If the re-encoded string matches the original, it's a real base64 token\n * and we use the decoded value. Otherwise, we treat it as a raw token.\n */\n\nexport interface ForgeCredentials {\n apiToken: string;\n organizationSlug?: string;\n}\n\n/**\n * Try to decode a base64-encoded token.\n * Returns the decoded string if re-encoding produces the original, null otherwise.\n *\n * Buffer.from(str, 'base64') never throws — it silently ignores invalid chars.\n * So this function cannot fail, no try/catch needed.\n */\nfunction tryDecodeBase64(token: string): string | null {\n const decoded = Buffer.from(token, \"base64\").toString(\"utf-8\");\n\n // decoded must be non-empty and different from input\n if (!decoded || decoded === token) {\n return null;\n }\n\n // Re-encode and compare — if it roundtrips, it was genuinely base64\n const reEncoded = Buffer.from(decoded).toString(\"base64\");\n if (reEncoded === token) {\n return decoded;\n }\n\n return null;\n}\n\nfunction tryParseCredentials(decoded: string): ForgeCredentials | null {\n try {\n const parsed: unknown = JSON.parse(decoded);\n if (\n typeof parsed === \"object\" &&\n parsed !== null &&\n \"apiToken\" in parsed &&\n typeof (parsed as Record<string, unknown>).apiToken === \"string\"\n ) {\n const obj = parsed as Record<string, unknown>;\n return {\n apiToken: obj.apiToken as string,\n organizationSlug:\n typeof obj.organizationSlug === \"string\" ? obj.organizationSlug : undefined,\n };\n }\n } catch {\n // Not valid JSON, fall through\n }\n return null;\n}\n\n/**\n * Parse Bearer token containing Forge API credentials.\n *\n * Token formats:\n * - Raw Forge API token (e.g., \"Bearer my-api-token\")\n * - Base64-encoded token from OAuth flow (e.g., \"Bearer base64(apiToken)\")\n * - Base64-encoded JSON from OAuth flow (e.g., \"Bearer base64({apiToken, organizationSlug})\")\n *\n * @param authHeader - Authorization header value (e.g., \"Bearer <token>\")\n * @returns Parsed credentials or null if invalid\n */\nexport function parseAuthHeader(authHeader: string | undefined | null): ForgeCredentials | null {\n if (!authHeader) {\n return null;\n }\n\n const match = authHeader.match(/^Bearer\\s+(.+)$/i);\n if (!match) {\n return null;\n }\n\n const token = match[1].trim();\n\n if (!token) {\n return null;\n }\n\n // Try to decode as base64 (OAuth access token)\n const decoded = tryDecodeBase64(token);\n if (decoded) {\n const credentials = tryParseCredentials(decoded);\n if (credentials) {\n return credentials;\n }\n\n return { apiToken: decoded };\n }\n\n // Treat as raw Forge API token (backwards compatible)\n return { apiToken: token };\n}\n"],"mappings":";;;;;;;AAwBA,SAAS,gBAAgB,OAA8B;CACrD,MAAM,UAAU,OAAO,KAAK,OAAO,SAAS,CAAC,SAAS,QAAQ;AAG9D,KAAI,CAAC,WAAW,YAAY,MAC1B,QAAO;AAKT,KADkB,OAAO,KAAK,QAAQ,CAAC,SAAS,SAAS,KACvC,MAChB,QAAO;AAGT,QAAO;;AAGT,SAAS,oBAAoB,SAA0C;AACrE,KAAI;EACF,MAAM,SAAkB,KAAK,MAAM,QAAQ;AAC3C,MACE,OAAO,WAAW,YAClB,WAAW,QACX,cAAc,UACd,OAAQ,OAAmC,aAAa,UACxD;GACA,MAAM,MAAM;AACZ,UAAO;IACL,UAAU,IAAI;IACd,kBACE,OAAO,IAAI,qBAAqB,WAAW,IAAI,mBAAmB,KAAA;IACrE;;SAEG;AAGR,QAAO;;;;;;;;;;;;;AAcT,SAAgB,gBAAgB,YAAgE;AAC9F,KAAI,CAAC,WACH,QAAO;CAGT,MAAM,QAAQ,WAAW,MAAM,mBAAmB;AAClD,KAAI,CAAC,MACH,QAAO;CAGT,MAAM,QAAQ,MAAM,GAAG,MAAM;AAE7B,KAAI,CAAC,MACH,QAAO;CAIT,MAAM,UAAU,gBAAgB,MAAM;AACtC,KAAI,SAAS;EACX,MAAM,cAAc,oBAAoB,QAAQ;AAChD,MAAI,YACF,QAAO;AAGT,SAAO,EAAE,UAAU,SAAS;;AAI9B,QAAO,EAAE,UAAU,OAAO"}
package/dist/crypto.d.ts CHANGED
@@ -34,6 +34,7 @@ export declare function decrypt(ciphertext: string, secret?: string): string;
34
34
  */
35
35
  export interface AuthCodePayload {
36
36
  apiToken: string;
37
+ organizationSlug?: string;
37
38
  codeChallenge?: string;
38
39
  codeChallengeMethod?: string;
39
40
  }
@@ -1 +1 @@
1
- {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAgBH;;;GAGG;AACH,wBAAgB,SAAS,IAAI,MAAM,CASlC;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,GAAE,MAAoB,GAAG,MAAM,CAa/E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,GAAE,MAAoB,GAAG,MAAM,CAwBhF;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,eAAe,EAC5B,gBAAgB,GAAE,MAAY,GAC7B,MAAM,CAMR;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAc5D"}
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAgBH;;;GAGG;AACH,wBAAgB,SAAS,IAAI,MAAM,CASlC;AAED;;;;;;;;GAQG;AACH,wBAAgB,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,GAAE,MAAoB,GAAG,MAAM,CAa/E;AAED;;;;;;;GAOG;AACH,wBAAgB,OAAO,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,GAAE,MAAoB,GAAG,MAAM,CAwBhF;AAED;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,eAAe,EAC5B,gBAAgB,GAAE,MAAY,GAC7B,MAAM,CAMR;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,eAAe,CAc5D"}
package/dist/crypto.js CHANGED
@@ -97,10 +97,11 @@ function createAuthCode(credentials, expiresInSeconds = 300) {
97
97
  function decodeAuthCode(code) {
98
98
  const payload = JSON.parse(decrypt(code));
99
99
  if (payload.exp && Date.now() > payload.exp) throw new Error("Authorization code expired");
100
- const { apiToken, codeChallenge, codeChallengeMethod } = payload;
100
+ const { apiToken, organizationSlug, codeChallenge, codeChallengeMethod } = payload;
101
101
  if (!apiToken) throw new Error("Invalid authorization code: missing credentials");
102
102
  return {
103
103
  apiToken,
104
+ organizationSlug,
104
105
  codeChallenge,
105
106
  codeChallengeMethod
106
107
  };
@@ -1 +1 @@
1
- {"version":3,"file":"crypto.js","names":[],"sources":["../src/crypto.ts"],"sourcesContent":["/**\n * Cryptographic utilities for stateless OAuth tokens\n *\n * Uses AES-256-GCM for authenticated encryption.\n * The authorization code contains encrypted credentials that can be\n * decrypted without server-side storage.\n */\n\nimport { createCipheriv, createDecipheriv, randomBytes, scryptSync } from \"node:crypto\";\n\nconst ALGORITHM = \"aes-256-gcm\";\nconst IV_LENGTH = 12; // GCM recommended IV length\nconst AUTH_TAG_LENGTH = 16;\nconst SALT_LENGTH = 16;\n\n/**\n * Derive a 256-bit key from a password using scrypt\n */\nfunction deriveKey(password: string, salt: Buffer): Buffer {\n return scryptSync(password, salt, 32);\n}\n\n/**\n * Get the encryption secret from environment or generate a default.\n * In production, OAUTH_SECRET should always be set.\n */\nexport function getSecret(): string {\n const secret = process.env.OAUTH_SECRET;\n if (!secret) {\n console.warn(\n \"WARNING: OAUTH_SECRET not set. Using default secret. Set OAUTH_SECRET in production!\",\n );\n return \"forge-mcp-default-secret-change-me\";\n }\n return secret;\n}\n\n/**\n * Encrypt data using AES-256-GCM\n *\n * Output format: base64url(salt + iv + authTag + ciphertext)\n *\n * @param plaintext - Data to encrypt\n * @param secret - Encryption secret (defaults to OAUTH_SECRET env var)\n * @returns Base64url-encoded encrypted data\n */\nexport function encrypt(plaintext: string, secret: string = getSecret()): string {\n const salt = randomBytes(SALT_LENGTH);\n const key = deriveKey(secret, salt);\n const iv = randomBytes(IV_LENGTH);\n\n const cipher = createCipheriv(ALGORITHM, key, iv);\n const encrypted = Buffer.concat([cipher.update(plaintext, \"utf8\"), cipher.final()]);\n const authTag = cipher.getAuthTag();\n\n // Combine: salt + iv + authTag + ciphertext\n const combined = Buffer.concat([salt, iv, authTag, encrypted]);\n\n return combined.toString(\"base64url\");\n}\n\n/**\n * Decrypt data encrypted with encrypt()\n *\n * @param ciphertext - Base64url-encoded encrypted data\n * @param secret - Encryption secret (defaults to OAUTH_SECRET env var)\n * @returns Decrypted plaintext\n * @throws Error if decryption fails (invalid data or wrong secret)\n */\nexport function decrypt(ciphertext: string, secret: string = getSecret()): string {\n try {\n const combined = Buffer.from(ciphertext, \"base64url\");\n\n // Extract components\n const salt = combined.subarray(0, SALT_LENGTH);\n const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);\n const authTag = combined.subarray(\n SALT_LENGTH + IV_LENGTH,\n SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH,\n );\n const encrypted = combined.subarray(SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);\n\n const key = deriveKey(secret, salt);\n\n const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });\n decipher.setAuthTag(authTag);\n\n const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);\n\n return decrypted.toString(\"utf8\");\n } catch {\n throw new Error(\"Decryption failed: invalid token or secret\");\n }\n}\n\n/**\n * Authorization code payload structure\n */\nexport interface AuthCodePayload {\n apiToken: string;\n codeChallenge?: string;\n codeChallengeMethod?: string;\n}\n\n/**\n * Create an encrypted authorization code containing credentials and PKCE challenge\n *\n * @param credentials - Object with apiToken and optional PKCE params\n * @param expiresInSeconds - Code expiration time (default: 5 minutes)\n * @returns Encrypted authorization code\n */\nexport function createAuthCode(\n credentials: AuthCodePayload,\n expiresInSeconds: number = 300,\n): string {\n const payload = {\n ...credentials,\n exp: Date.now() + expiresInSeconds * 1000,\n };\n return encrypt(JSON.stringify(payload));\n}\n\n/**\n * Decode and validate an authorization code\n *\n * @param code - Encrypted authorization code\n * @returns Decoded payload with credentials and PKCE challenge\n * @throws Error if code is invalid or expired\n */\nexport function decodeAuthCode(code: string): AuthCodePayload {\n const payload = JSON.parse(decrypt(code));\n\n if (payload.exp && Date.now() > payload.exp) {\n throw new Error(\"Authorization code expired\");\n }\n\n const { apiToken, codeChallenge, codeChallengeMethod } = payload;\n\n if (!apiToken) {\n throw new Error(\"Invalid authorization code: missing credentials\");\n }\n\n return { apiToken, codeChallenge, codeChallengeMethod };\n}\n"],"mappings":";;;;;;;;AAUA,IAAM,YAAY;AAClB,IAAM,YAAY;AAClB,IAAM,kBAAkB;AACxB,IAAM,cAAc;;;;AAKpB,SAAS,UAAU,UAAkB,MAAsB;AACzD,QAAO,WAAW,UAAU,MAAM,GAAG;;;;;;AAOvC,SAAgB,YAAoB;CAClC,MAAM,SAAS,QAAQ,IAAI;AAC3B,KAAI,CAAC,QAAQ;AACX,UAAQ,KACN,uFACD;AACD,SAAO;;AAET,QAAO;;;;;;;;;;;AAYT,SAAgB,QAAQ,WAAmB,SAAiB,WAAW,EAAU;CAC/E,MAAM,OAAO,YAAY,YAAY;CACrC,MAAM,MAAM,UAAU,QAAQ,KAAK;CACnC,MAAM,KAAK,YAAY,UAAU;CAEjC,MAAM,SAAS,eAAe,WAAW,KAAK,GAAG;CACjD,MAAM,YAAY,OAAO,OAAO,CAAC,OAAO,OAAO,WAAW,OAAO,EAAE,OAAO,OAAO,CAAC,CAAC;CACnF,MAAM,UAAU,OAAO,YAAY;AAKnC,QAFiB,OAAO,OAAO;EAAC;EAAM;EAAI;EAAS;EAAU,CAAC,CAE9C,SAAS,YAAY;;;;;;;;;;AAWvC,SAAgB,QAAQ,YAAoB,SAAiB,WAAW,EAAU;AAChF,KAAI;EACF,MAAM,WAAW,OAAO,KAAK,YAAY,YAAY;EAGrD,MAAM,OAAO,SAAS,SAAS,GAAG,YAAY;EAC9C,MAAM,KAAK,SAAS,SAAS,aAAa,cAAc,UAAU;EAClE,MAAM,UAAU,SAAS,SACvB,cAAc,WACd,cAAc,YAAY,gBAC3B;EACD,MAAM,YAAY,SAAS,SAAS,cAAc,YAAY,gBAAgB;EAI9E,MAAM,WAAW,iBAAiB,WAFtB,UAAU,QAAQ,KAAK,EAEe,IAAI,EAAE,eAAe,iBAAiB,CAAC;AACzF,WAAS,WAAW,QAAQ;AAI5B,SAFkB,OAAO,OAAO,CAAC,SAAS,OAAO,UAAU,EAAE,SAAS,OAAO,CAAC,CAAC,CAE9D,SAAS,OAAO;SAC3B;AACN,QAAM,IAAI,MAAM,6CAA6C;;;;;;;;;;AAoBjE,SAAgB,eACd,aACA,mBAA2B,KACnB;CACR,MAAM,UAAU;EACd,GAAG;EACH,KAAK,KAAK,KAAK,GAAG,mBAAmB;EACtC;AACD,QAAO,QAAQ,KAAK,UAAU,QAAQ,CAAC;;;;;;;;;AAUzC,SAAgB,eAAe,MAA+B;CAC5D,MAAM,UAAU,KAAK,MAAM,QAAQ,KAAK,CAAC;AAEzC,KAAI,QAAQ,OAAO,KAAK,KAAK,GAAG,QAAQ,IACtC,OAAM,IAAI,MAAM,6BAA6B;CAG/C,MAAM,EAAE,UAAU,eAAe,wBAAwB;AAEzD,KAAI,CAAC,SACH,OAAM,IAAI,MAAM,kDAAkD;AAGpE,QAAO;EAAE;EAAU;EAAe;EAAqB"}
1
+ {"version":3,"file":"crypto.js","names":[],"sources":["../src/crypto.ts"],"sourcesContent":["/**\n * Cryptographic utilities for stateless OAuth tokens\n *\n * Uses AES-256-GCM for authenticated encryption.\n * The authorization code contains encrypted credentials that can be\n * decrypted without server-side storage.\n */\n\nimport { createCipheriv, createDecipheriv, randomBytes, scryptSync } from \"node:crypto\";\n\nconst ALGORITHM = \"aes-256-gcm\";\nconst IV_LENGTH = 12; // GCM recommended IV length\nconst AUTH_TAG_LENGTH = 16;\nconst SALT_LENGTH = 16;\n\n/**\n * Derive a 256-bit key from a password using scrypt\n */\nfunction deriveKey(password: string, salt: Buffer): Buffer {\n return scryptSync(password, salt, 32);\n}\n\n/**\n * Get the encryption secret from environment or generate a default.\n * In production, OAUTH_SECRET should always be set.\n */\nexport function getSecret(): string {\n const secret = process.env.OAUTH_SECRET;\n if (!secret) {\n console.warn(\n \"WARNING: OAUTH_SECRET not set. Using default secret. Set OAUTH_SECRET in production!\",\n );\n return \"forge-mcp-default-secret-change-me\";\n }\n return secret;\n}\n\n/**\n * Encrypt data using AES-256-GCM\n *\n * Output format: base64url(salt + iv + authTag + ciphertext)\n *\n * @param plaintext - Data to encrypt\n * @param secret - Encryption secret (defaults to OAUTH_SECRET env var)\n * @returns Base64url-encoded encrypted data\n */\nexport function encrypt(plaintext: string, secret: string = getSecret()): string {\n const salt = randomBytes(SALT_LENGTH);\n const key = deriveKey(secret, salt);\n const iv = randomBytes(IV_LENGTH);\n\n const cipher = createCipheriv(ALGORITHM, key, iv);\n const encrypted = Buffer.concat([cipher.update(plaintext, \"utf8\"), cipher.final()]);\n const authTag = cipher.getAuthTag();\n\n // Combine: salt + iv + authTag + ciphertext\n const combined = Buffer.concat([salt, iv, authTag, encrypted]);\n\n return combined.toString(\"base64url\");\n}\n\n/**\n * Decrypt data encrypted with encrypt()\n *\n * @param ciphertext - Base64url-encoded encrypted data\n * @param secret - Encryption secret (defaults to OAUTH_SECRET env var)\n * @returns Decrypted plaintext\n * @throws Error if decryption fails (invalid data or wrong secret)\n */\nexport function decrypt(ciphertext: string, secret: string = getSecret()): string {\n try {\n const combined = Buffer.from(ciphertext, \"base64url\");\n\n // Extract components\n const salt = combined.subarray(0, SALT_LENGTH);\n const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);\n const authTag = combined.subarray(\n SALT_LENGTH + IV_LENGTH,\n SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH,\n );\n const encrypted = combined.subarray(SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);\n\n const key = deriveKey(secret, salt);\n\n const decipher = createDecipheriv(ALGORITHM, key, iv, { authTagLength: AUTH_TAG_LENGTH });\n decipher.setAuthTag(authTag);\n\n const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);\n\n return decrypted.toString(\"utf8\");\n } catch {\n throw new Error(\"Decryption failed: invalid token or secret\");\n }\n}\n\n/**\n * Authorization code payload structure\n */\nexport interface AuthCodePayload {\n apiToken: string;\n organizationSlug?: string;\n codeChallenge?: string;\n codeChallengeMethod?: string;\n}\n\n/**\n * Create an encrypted authorization code containing credentials and PKCE challenge\n *\n * @param credentials - Object with apiToken and optional PKCE params\n * @param expiresInSeconds - Code expiration time (default: 5 minutes)\n * @returns Encrypted authorization code\n */\nexport function createAuthCode(\n credentials: AuthCodePayload,\n expiresInSeconds: number = 300,\n): string {\n const payload = {\n ...credentials,\n exp: Date.now() + expiresInSeconds * 1000,\n };\n return encrypt(JSON.stringify(payload));\n}\n\n/**\n * Decode and validate an authorization code\n *\n * @param code - Encrypted authorization code\n * @returns Decoded payload with credentials and PKCE challenge\n * @throws Error if code is invalid or expired\n */\nexport function decodeAuthCode(code: string): AuthCodePayload {\n const payload = JSON.parse(decrypt(code));\n\n if (payload.exp && Date.now() > payload.exp) {\n throw new Error(\"Authorization code expired\");\n }\n\n const { apiToken, organizationSlug, codeChallenge, codeChallengeMethod } = payload;\n\n if (!apiToken) {\n throw new Error(\"Invalid authorization code: missing credentials\");\n }\n\n return { apiToken, organizationSlug, codeChallenge, codeChallengeMethod };\n}\n"],"mappings":";;;;;;;;AAUA,IAAM,YAAY;AAClB,IAAM,YAAY;AAClB,IAAM,kBAAkB;AACxB,IAAM,cAAc;;;;AAKpB,SAAS,UAAU,UAAkB,MAAsB;AACzD,QAAO,WAAW,UAAU,MAAM,GAAG;;;;;;AAOvC,SAAgB,YAAoB;CAClC,MAAM,SAAS,QAAQ,IAAI;AAC3B,KAAI,CAAC,QAAQ;AACX,UAAQ,KACN,uFACD;AACD,SAAO;;AAET,QAAO;;;;;;;;;;;AAYT,SAAgB,QAAQ,WAAmB,SAAiB,WAAW,EAAU;CAC/E,MAAM,OAAO,YAAY,YAAY;CACrC,MAAM,MAAM,UAAU,QAAQ,KAAK;CACnC,MAAM,KAAK,YAAY,UAAU;CAEjC,MAAM,SAAS,eAAe,WAAW,KAAK,GAAG;CACjD,MAAM,YAAY,OAAO,OAAO,CAAC,OAAO,OAAO,WAAW,OAAO,EAAE,OAAO,OAAO,CAAC,CAAC;CACnF,MAAM,UAAU,OAAO,YAAY;AAKnC,QAFiB,OAAO,OAAO;EAAC;EAAM;EAAI;EAAS;EAAU,CAAC,CAE9C,SAAS,YAAY;;;;;;;;;;AAWvC,SAAgB,QAAQ,YAAoB,SAAiB,WAAW,EAAU;AAChF,KAAI;EACF,MAAM,WAAW,OAAO,KAAK,YAAY,YAAY;EAGrD,MAAM,OAAO,SAAS,SAAS,GAAG,YAAY;EAC9C,MAAM,KAAK,SAAS,SAAS,aAAa,cAAc,UAAU;EAClE,MAAM,UAAU,SAAS,SACvB,cAAc,WACd,cAAc,YAAY,gBAC3B;EACD,MAAM,YAAY,SAAS,SAAS,cAAc,YAAY,gBAAgB;EAI9E,MAAM,WAAW,iBAAiB,WAFtB,UAAU,QAAQ,KAAK,EAEe,IAAI,EAAE,eAAe,iBAAiB,CAAC;AACzF,WAAS,WAAW,QAAQ;AAI5B,SAFkB,OAAO,OAAO,CAAC,SAAS,OAAO,UAAU,EAAE,SAAS,OAAO,CAAC,CAAC,CAE9D,SAAS,OAAO;SAC3B;AACN,QAAM,IAAI,MAAM,6CAA6C;;;;;;;;;;AAqBjE,SAAgB,eACd,aACA,mBAA2B,KACnB;CACR,MAAM,UAAU;EACd,GAAG;EACH,KAAK,KAAK,KAAK,GAAG,mBAAmB;EACtC;AACD,QAAO,QAAQ,KAAK,UAAU,QAAQ,CAAC;;;;;;;;;AAUzC,SAAgB,eAAe,MAA+B;CAC5D,MAAM,UAAU,KAAK,MAAM,QAAQ,KAAK,CAAC;AAEzC,KAAI,QAAQ,OAAO,KAAK,KAAK,GAAG,QAAQ,IACtC,OAAM,IAAI,MAAM,6BAA6B;CAG/C,MAAM,EAAE,UAAU,kBAAkB,eAAe,wBAAwB;AAE3E,KAAI,CAAC,SACH,OAAM,IAAI,MAAM,kDAAkD;AAGpE,QAAO;EAAE;EAAU;EAAkB;EAAe;EAAqB"}
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;GAKG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,SAAgB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;gBAErB,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE;IAM7C;;OAEG;IACH,kBAAkB,IAAI,MAAM;CAO7B;AAED;;GAEG;AACH,eAAO,MAAM,aAAa;iCAEJ,MAAM;+CAMQ,MAAM,UAAU,MAAM,EAAE;qCAUlC,MAAM,YAAY,MAAM,gBAAgB,MAAM,EAAE;yCAO5C,MAAM,kBAAkB,MAAM,EAAE;sDAOnB,MAAM,EAAE;oCAO1B,MAAM,WAAW,MAAM;CAsBtC,CAAC;AAEX;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAExE"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;;;;;GAKG;AACH,qBAAa,cAAe,SAAQ,KAAK;IACvC,SAAgB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IAEjC,YAAY,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,EAI5C;IAED;;OAEG;IACH,kBAAkB,IAAI,MAAM,CAM3B;CACF;AAED;;GAEG;AACH,eAAO,MAAM,aAAa;aAExB,SAAS,WAAW,MAAM;aAM1B,qBAAqB,aAAa,MAAM,UAAU,MAAM,EAAE;aAU1D,aAAa,WAAW,MAAM,YAAY,MAAM,gBAAgB,MAAM,EAAE;aAOxE,eAAe,aAAa,MAAM,kBAAkB,MAAM,EAAE;aAO5D,uBAAuB,kBAAkB,MAAM,EAAE;aAOjD,QAAQ,eAAe,MAAM,WAAW,MAAM;CAsBtC,CAAC;AAEX;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,cAAc,CAExE"}
@@ -1,8 +1,9 @@
1
- import { a as INSTRUCTIONS, i as getTools, n as executeToolWithCredentials, t as VERSION } from "./version-Bs3iU4Ei.js";
1
+ import { a as INSTRUCTIONS, i as getTools, n as executeToolWithCredentials, t as VERSION } from "./version-brWM16Jb.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
+ import { getOrganizationSlug } from "@studiometa/forge-api";
6
7
  import { randomUUID } from "node:crypto";
7
8
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
8
9
  import { H3, defineEventHandler } from "h3";
@@ -109,7 +110,8 @@ var SessionManager = class {
109
110
  * Create a configured MCP Server instance for HTTP transport.
110
111
  *
111
112
  * Unlike stdio, HTTP mode does NOT include forge_configure/forge_get_config
112
- * because credentials come from the Authorization header per-request.
113
+ * because API tokens come from the Authorization header per-request.
114
+ * Organization slug still falls back to FORGE_ORG/config when omitted.
113
115
  */
114
116
  function createMcpServer(options) {
115
117
  const readOnly = options?.readOnly ?? false;
@@ -126,7 +128,9 @@ function createMcpServer(options) {
126
128
  });
127
129
  server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
128
130
  const { name, arguments: args } = request.params;
129
- const token = extra.authInfo?.token;
131
+ const authInfo = extra.authInfo;
132
+ const token = authInfo?.token;
133
+ const organizationSlugFromAuth = typeof authInfo?.organizationSlug === "string" ? authInfo.organizationSlug : void 0;
130
134
  /* v8 ignore start */
131
135
  if (!token) return {
132
136
  content: [{
@@ -152,13 +156,15 @@ function createMcpServer(options) {
152
156
  isError: true
153
157
  };
154
158
  try {
159
+ /* v8 ignore next 2 -- defensive type guard */
160
+ const orgSlugFromArgs = typeof args?.organizationSlug === "string" ? args.organizationSlug : void 0;
155
161
  return await executeToolWithCredentials(
156
162
  name,
157
163
  /* v8 ignore next */
158
164
  args ?? {},
159
165
  {
160
166
  apiToken: token,
161
- organizationSlug: typeof args?.organizationSlug === "string" ? args.organizationSlug : void 0
167
+ organizationSlug: orgSlugFromArgs ?? organizationSlugFromAuth ?? getOrganizationSlug() ?? void 0
162
168
  }
163
169
  );
164
170
  } catch (error) {
@@ -215,8 +221,10 @@ async function handleMcpRequest(req, res, sessions, options) {
215
221
  }
216
222
  Object.assign(req, { auth: {
217
223
  token: credentials.apiToken,
224
+ organizationSlug: credentials.organizationSlug,
218
225
  clientId: "forge-http-client",
219
- scopes: []
226
+ scopes: [],
227
+ extra: { organizationSlug: credentials.organizationSlug }
220
228
  } });
221
229
  const sessionHeader = req.headers["mcp-session-id"];
222
230
  const sessionId = typeof sessionHeader === "string" ? sessionHeader : void 0;
@@ -290,4 +298,4 @@ function createHealthApp() {
290
298
  }
291
299
  export { SessionManager as a, handleMcpRequest as i, createMcpRequestHandler as n, createMcpServer as r, createHealthApp as t };
292
300
 
293
- //# sourceMappingURL=http-BhU5Kdf3.js.map
301
+ //# sourceMappingURL=http-DyGKZqn4.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-DyGKZqn4.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 { getOrganizationSlug } from \"@studiometa/forge-api\";\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 API tokens come from the Authorization header per-request.\n * Organization slug still falls back to FORGE_ORG/config when omitted.\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 authInfo = extra.authInfo as { token?: string; organizationSlug?: string } | undefined;\n const token = authInfo?.token;\n const organizationSlugFromAuth =\n typeof authInfo?.organizationSlug === \"string\" ? authInfo.organizationSlug : undefined;\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 // Organization slug can come from:\n // 1. Tool args (per-request override, highest priority)\n // 2. Auth token (configured during OAuth flow)\n // 3. FORGE_ORG / config fallback\n /* v8 ignore next 2 -- defensive type guard */\n const orgSlugFromArgs =\n typeof args?.organizationSlug === \"string\" ? args.organizationSlug : undefined;\n const result = await executeToolWithCredentials(name, /* v8 ignore next */ args ?? {}, {\n apiToken: token,\n organizationSlug:\n orgSlugFromArgs ?? organizationSlugFromAuth ?? getOrganizationSlug() ?? 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 // Include organizationSlug in extra for downstream handlers\n Object.assign(req, {\n auth: {\n token: credentials.apiToken,\n organizationSlug: credentials.organizationSlug,\n clientId: \"forge-http-client\",\n scopes: [],\n extra: {\n organizationSlug: credentials.organizationSlug,\n },\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":";;;;;;;;;AAwCA,IAAM,cAAc,OAAU;AAC9B,IAAM,yBAAyB,KAAK;AAEpC,IAAa,iBAAb,MAA4B;CAC1B,2BAAmB,IAAI,KAA6B;CAEpD;CACA;CAEA,YAAY,SAAiC;AAC3C,OAAK,MAAM,SAAS,OAAO;AAE3B,MAAI,KAAK,MAAM,GAAG;GAChB,MAAM,WAAW,SAAS,iBAAiB;AAC3C,QAAK,aAAa,kBAAkB;AAClC,SAAK,OAAO;MACX,SAAS;AAEZ,QAAK,WAAW,OAAO;;;;;;CAO3B,SAAS,WAA0C,QAAsB;EACvE,MAAM,YAAY,UAAU;AAC5B,MAAI,WAAW;GACb,MAAM,MAAM,KAAK,KAAK;AACtB,QAAK,SAAS,IAAI,WAAW;IAC3B;IACA;IACA,WAAW;IACX,cAAc;IACf,CAAC;;;;;;CAON,IAAI,WAA+C;EACjD,MAAM,UAAU,KAAK,SAAS,IAAI,UAAU;AAC5C,MAAI,QACF,SAAQ,eAAe,KAAK,KAAK;AAEnC,SAAO;;;;;CAMT,MAAM,OAAO,WAAkC;EAC7C,MAAM,UAAU,KAAK,SAAS,IAAI,UAAU;AAC5C,MAAI,SAAS;AACX,QAAK,SAAS,OAAO,UAAU;AAC/B,SAAM,QAAQ,UAAU,OAAO;AAC/B,SAAM,QAAQ,OAAO,OAAO;;;;;;CAOhC,IAAI,OAAe;AACjB,SAAO,KAAK,SAAS;;;;;;CAOvB,QAAgB;AACd,MAAI,KAAK,OAAO,EAAG,QAAO;EAE1B,MAAM,MAAM,KAAK,KAAK;EACtB,MAAM,UAAoB,EAAE;AAE5B,OAAK,MAAM,CAAC,IAAI,YAAY,KAAK,SAC/B,KAAI,MAAM,QAAQ,eAAe,KAAK,IACpC,SAAQ,KAAK,GAAG;AAIpB,OAAK,MAAM,MAAM;;AAGf,OAAK,OAAO,GAAG,CAAC,YAAY,GAAG;AAIjC,SAAO,QAAQ;;;;;CAMjB,MAAM,WAA0B;AAC9B,MAAI,KAAK,YAAY;AACnB,iBAAc,KAAK,WAAW;AAC9B,QAAK,aAAa,KAAA;;EAGpB,MAAM,WAA4B,EAAE;AACpC,OAAK,MAAM,GAAG,YAAY,KAAK,UAAU;AACvC,YAAS,KAAK,QAAQ,UAAU,OAAO,CAAC;AACxC,YAAS,KAAK,QAAQ,OAAO,OAAO,CAAC;;AAEvC,QAAM,QAAQ,IAAI,SAAS;AAC3B,OAAK,SAAS,OAAO;;;;;;;;;;;;;;;;;;;;;;ACxFzB,SAAgB,gBAAgB,SAAqC;CACnE,MAAM,WAAW,SAAS,YAAY;CACtC,MAAM,QAAQ,SAAS,EAAE,UAAU,CAAC;CAEpC,MAAM,SAAS,IAAI,OACjB;EACE,MAAM;EACN,SAAS;EACV,EACD;EACE,cAAc,EACZ,OAAO,EAAE,EACV;EACD,cAAc;EACf,CACF;AAED,QAAO,kBAAkB,wBAAwB,YAAY;AAC3D,SAAO,EAAE,OAAO;GAChB;AAEF,QAAO,kBAAkB,uBAAuB,OAAO,SAAS,UAAU;EACxE,MAAM,EAAE,MAAM,WAAW,SAAS,QAAQ;EAC1C,MAAM,WAAW,MAAM;EACvB,MAAM,QAAQ,UAAU;EACxB,MAAM,2BACJ,OAAO,UAAU,qBAAqB,WAAW,SAAS,mBAAmB,KAAA;;AAG/E,MAAI,CAAC,MACH,QAAO;GACL,SAAS,CACP;IACE,MAAM;IACN,MAAM;IACP,CACF;GACD,mBAAmB;IACjB,SAAS;IACT,OAAO;IACR;GACD,SAAS;GACV;;AAKH,MAAI,YAAY,SAAS,cACvB,QAAO;GACL,SAAS,CACP;IACE,MAAM;IACN,MAAM;IACP,CACF;GACD,mBAAmB;IACjB,SAAS;IACT,OAAO;IACR;GACD,SAAS;GACV;AAGH,MAAI;;GAMF,MAAM,kBACJ,OAAO,MAAM,qBAAqB,WAAW,KAAK,mBAAmB,KAAA;AAMvE,UALe,MAAM;IAA2B;;IAA2B,QAAQ,EAAE;IAAE;KACrF,UAAU;KACV,kBACE,mBAAmB,4BAA4B,qBAAqB,IAAI,KAAA;KAC3E;IAAC;WAEK,OAAO;;GAEd,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;;AAEtE,UAAO;IACL,SAAS,CAAC;KAAE,MAAM;KAAiB,MAAM,UAAU;KAAW,CAAC;IAC/D,mBAAmB;KAAE,SAAS;KAAO,OAAO;KAAS;IACrD,SAAS;IACV;;GAEH;AAEF,QAAO;;;;;;;;;;;;;;AAeT,eAAsB,iBACpB,KACA,KACA,UACA,SACe;CAEf,MAAM,aAAa,IAAI,QAAQ;CAC/B,MAAM,cAAc,gBAAgB,WAAW;AAE/C,KAAI,CAAC,aAAa;EAEhB,MAAM,OAAO,IAAI,QAAQ,QAAQ;EACjC,MAAM,QAAQ,IAAI,QAAQ;EAE1B,MAAM,sBAAsB,IADV,OAAO,UAAU,WAAW,QAAQ,KAAA,MAAc,OAC5B,KAAK,KAAK;AAElD,MAAI,UAAU,KAAK;GACjB,gBAAgB;GAChB,oBAAoB,6BAA6B,oBAAoB;GACtE,CAAC;AACF,MAAI,IACF,KAAK,UAAU;GACb,SAAS;GACT,OAAO;IACL,MAAM;IACN,SAAS;IACV;GACD,IAAI;GACL,CAAC,CACH;AACD;;AAKF,QAAO,OAAO,KAAK,EACjB,MAAM;EACJ,OAAO,YAAY;EACnB,kBAAkB,YAAY;EAC9B,UAAU;EACV,QAAQ,EAAE;EACV,OAAO,EACL,kBAAkB,YAAY,kBAC/B;EACF,EACF,CAAC;CAEF,MAAM,gBAAgB,IAAI,QAAQ;CAClC,MAAM,YAAY,OAAO,kBAAkB,WAAW,gBAAgB,KAAA;AAEtE,KAAI,WAAW;EAEb,MAAM,UAAU,SAAS,IAAI,UAAU;AACvC,MAAI,CAAC,SAAS;AACZ,OAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,OAAI,IACF,KAAK,UAAU;IACb,SAAS;IACT,OAAO;KACL,MAAM;KACN,SAAS;KACV;IACD,IAAI;IACL,CAAC,CACH;AACD;;AAGF,QAAM,QAAQ,UAAU,cAAc,KAAK,IAAI;AAC/C;;CAKF,MAAM,YAAY,IAAI,8BAA8B,EAClD,0BAA0B,YAAY,EACvC,CAAC;CAEF,MAAM,SAAS,gBAAgB,QAAQ;AACvC,OAAM,OAAO,QAAQ,UAAU;AAI/B,WAAU,gBAAgB;EACxB,MAAM,MAAM,UAAU;;AAEtB,MAAI,IACF,UAAS,OAAO,IAAI,CAAC,YAAY,GAE/B;;;AAMN,OAAM,UAAU,cAAc,KAAK,IAAI;;AAIvC,KAAI,UAAU,UACZ,UAAS,SAAS,WAAW,OAAO;MAC/B;AAEL,QAAM,UAAU,OAAO;AACvB,QAAM,OAAO,OAAO;;;;;;;;AASxB,SAAgB,wBACd,UACA,SAC8D;;AAE9D,SAAQ,KAAK,QAAQ,iBAAiB,KAAK,KAAK,UAAU,QAAQ;;;;;;;AAQpE,SAAgB,kBAAsB;CACpC,MAAM,MAAM,IAAI,IAAI;AAGpB,KAAI,IACF,KACA,yBAAyB;AACvB,SAAO;GAAE,QAAQ;GAAM,SAAS;GAAa,SAAS;GAAS;GAC/D,CACH;AAED,KAAI,IACF,WACA,yBAAyB;AACvB,SAAO,EAAE,QAAQ,MAAM;GACvB,CACH;AAGD,KAAI,IAAI,2CAA2C,qBAAqB;AACxE,KAAI,IAAI,yCAAyC,yBAAyB;AAC1E,KAAI,KAAK,aAAa,gBAAgB;AACtC,KAAI,IAAI,cAAc,oBAAoB;AAC1C,KAAI,KAAK,cAAc,qBAAqB;AAC5C,KAAI,KAAK,UAAU,aAAa;AAEhC,QAAO"}
package/dist/http.d.ts CHANGED
@@ -26,7 +26,8 @@ export interface HttpServerOptions {
26
26
  * Create a configured MCP Server instance for HTTP transport.
27
27
  *
28
28
  * Unlike stdio, HTTP mode does NOT include forge_configure/forge_get_config
29
- * because credentials come from the Authorization header per-request.
29
+ * because API tokens come from the Authorization header per-request.
30
+ * Organization slug still falls back to FORGE_ORG/config when omitted.
30
31
  */
31
32
  export declare function createMcpServer(options?: HttpServerOptions): Server;
32
33
  /**
@@ -1 +1 @@
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"}
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;AAQnE,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;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,MAAM,CA0FnE;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,CAuGf;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-Bs3iU4Ei.js";
2
- import { a as SessionManager, i as handleMcpRequest, n as createMcpRequestHandler, r as createMcpServer, t as createHealthApp } from "./http-BhU5Kdf3.js";
1
+ import "./version-brWM16Jb.js";
2
+ import { a as SessionManager, i as handleMcpRequest, n as createMcpRequestHandler, r as createMcpServer, t as createHealthApp } from "./http-DyGKZqn4.js";
3
3
  export { SessionManager, createHealthApp, createMcpRequestHandler, createMcpServer, handleMcpRequest };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
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-Bs3iU4Ei.js";
3
+ import { a as INSTRUCTIONS, i as getTools, n as executeToolWithCredentials, r as STDIO_ONLY_TOOLS, t as VERSION } from "./version-brWM16Jb.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";
@@ -102,9 +102,11 @@ async function handleToolCall(name, args, options) {
102
102
  },
103
103
  isError: true
104
104
  };
105
+ const orgSlugFromArgs = typeof args.organizationSlug === "string" ? args.organizationSlug : void 0;
106
+ const orgSlugFromConfig = getOrganizationSlug() ?? void 0;
105
107
  return executeToolWithCredentials(name, args, {
106
108
  apiToken,
107
- organizationSlug: getOrganizationSlug() ?? void 0
109
+ organizationSlug: orgSlugFromArgs ?? orgSlugFromConfig
108
110
  });
109
111
  }
110
112
  return {
package/dist/index.js.map CHANGED
@@ -1 +1 @@
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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"}
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 // Organization slug can come from:\n // 1. Tool args (per-request override, highest priority)\n // 2. Config/env (via getOrganizationSlug)\n const orgSlugFromArgs =\n typeof args.organizationSlug === \"string\" ? args.organizationSlug : undefined;\n const orgSlugFromConfig = getOrganizationSlug() ?? undefined;\n\n return executeToolWithCredentials(name, args, {\n apiToken,\n organizationSlug: orgSlugFromArgs ?? orgSlugFromConfig,\n });\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
@@ -8,18 +8,20 @@
8
8
  *
9
9
  * Flow:
10
10
  * 1. Claude redirects user to /authorize with OAuth params (including PKCE)
11
- * 2. User enters their Forge API token in a login form
11
+ * 2. User enters their Forge API token and optional organization slug in a login form
12
12
  * 3. Server encrypts the token + PKCE challenge into an authorization code
13
13
  * 4. Redirects back to Claude with the code
14
14
  * 5. Claude exchanges code for access token via /token (with code_verifier)
15
15
  * 6. Server validates PKCE and returns base64-encoded access token
16
16
  */
17
17
  /**
18
- * Create a base64-encoded access token from a Forge API token.
19
- * The access token is simply base64(apiToken) so that parseAuthHeader
20
- * can decode it on every request without any server-side lookup.
18
+ * Create a base64-encoded access token from Forge credentials.
19
+ *
20
+ * Backward compatibility:
21
+ * - without organizationSlug: base64(apiToken)
22
+ * - with organizationSlug: base64(JSON.stringify({ apiToken, organizationSlug }))
21
23
  */
22
- export declare function createAccessToken(apiToken: string): string;
24
+ export declare function createAccessToken(apiToken: string, organizationSlug?: string): string;
23
25
  /**
24
26
  * OAuth metadata for discovery (RFC 8414)
25
27
  * GET /.well-known/oauth-authorization-server
@@ -68,20 +70,20 @@ export declare const registerHandler: import("h3").EventHandlerWithFetch<import(
68
70
  grant_types?: undefined;
69
71
  response_types?: undefined;
70
72
  } | {
73
+ error?: undefined;
74
+ error_description?: undefined;
71
75
  client_id: string;
72
76
  client_name: string;
73
77
  redirect_uris: string[];
74
78
  token_endpoint_auth_method: string;
75
79
  grant_types: string[];
76
80
  response_types: string[];
77
- error?: undefined;
78
- error_description?: undefined;
79
81
  }>>;
80
82
  /**
81
83
  * Authorization endpoint — shows login form
82
84
  * GET /authorize
83
85
  */
84
- export declare const authorizeGetHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, string | import("undici-types").Response>;
86
+ export declare const authorizeGetHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, string | Response>;
85
87
  /**
86
88
  * Authorization endpoint — process login
87
89
  * POST /authorize
@@ -103,12 +105,12 @@ export declare const tokenHandler: import("h3").EventHandlerWithFetch<import("h3
103
105
  expires_in?: undefined;
104
106
  refresh_token?: undefined;
105
107
  } | {
108
+ error?: undefined;
109
+ error_description?: undefined;
106
110
  access_token: string;
107
111
  token_type: string;
108
112
  expires_in: number;
109
113
  refresh_token: string;
110
- error?: undefined;
111
- error_description?: undefined;
112
114
  }>>;
113
115
  /**
114
116
  * Create S256 PKCE challenge from verifier
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAOH;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,MAAM,CAGrF;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,yFAyC9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,uFA0D/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
@@ -11,19 +11,25 @@ import { defineEventHandler, getQuery, readBody } from "h3";
11
11
  *
12
12
  * Flow:
13
13
  * 1. Claude redirects user to /authorize with OAuth params (including PKCE)
14
- * 2. User enters their Forge API token in a login form
14
+ * 2. User enters their Forge API token and optional organization slug in a login form
15
15
  * 3. Server encrypts the token + PKCE challenge into an authorization code
16
16
  * 4. Redirects back to Claude with the code
17
17
  * 5. Claude exchanges code for access token via /token (with code_verifier)
18
18
  * 6. Server validates PKCE and returns base64-encoded access token
19
19
  */
20
20
  /**
21
- * Create a base64-encoded access token from a Forge API token.
22
- * The access token is simply base64(apiToken) so that parseAuthHeader
23
- * can decode it on every request without any server-side lookup.
21
+ * Create a base64-encoded access token from Forge credentials.
22
+ *
23
+ * Backward compatibility:
24
+ * - without organizationSlug: base64(apiToken)
25
+ * - with organizationSlug: base64(JSON.stringify({ apiToken, organizationSlug }))
24
26
  */
25
- function createAccessToken(apiToken) {
26
- return Buffer.from(apiToken).toString("base64");
27
+ function createAccessToken(apiToken, organizationSlug) {
28
+ const payload = organizationSlug ? JSON.stringify({
29
+ apiToken,
30
+ organizationSlug
31
+ }) : apiToken;
32
+ return Buffer.from(payload).toString("base64");
27
33
  }
28
34
  /**
29
35
  * OAuth metadata for discovery (RFC 8414)
@@ -146,7 +152,7 @@ const authorizeGetHandler = defineEventHandler((event) => {
146
152
  * POST /authorize
147
153
  */
148
154
  const authorizePostHandler = defineEventHandler(async (event) => {
149
- const { apiToken, redirectUri, state, codeChallenge, codeChallengeMethod } = await readBody(event) ?? {};
155
+ const { apiToken, organizationSlug, redirectUri, state, codeChallenge, codeChallengeMethod } = await readBody(event) ?? {};
150
156
  if (!redirectUri) {
151
157
  event.res.headers.set("Content-Type", "text/html; charset=utf-8");
152
158
  event.res.status = 400;
@@ -171,11 +177,13 @@ const authorizePostHandler = defineEventHandler(async (event) => {
171
177
  state,
172
178
  codeChallenge,
173
179
  codeChallengeMethod,
180
+ organizationSlug,
174
181
  error: "Forge API Token is required"
175
182
  });
176
183
  }
177
184
  const code = createAuthCode({
178
185
  apiToken,
186
+ organizationSlug: organizationSlug || void 0,
179
187
  codeChallenge,
180
188
  codeChallengeMethod: codeChallengeMethod || "S256"
181
189
  });
@@ -230,10 +238,13 @@ const tokenHandler = defineEventHandler(async (event) => {
230
238
  }
231
239
  }
232
240
  return {
233
- access_token: createAccessToken(payload.apiToken),
241
+ access_token: createAccessToken(payload.apiToken, payload.organizationSlug),
234
242
  token_type: "Bearer",
235
243
  expires_in: 3600,
236
- refresh_token: createAuthCode({ apiToken: payload.apiToken }, 86400 * 30)
244
+ refresh_token: createAuthCode({
245
+ apiToken: payload.apiToken,
246
+ organizationSlug: payload.organizationSlug
247
+ }, 86400 * 30)
237
248
  };
238
249
  } catch (error) {
239
250
  event.res.status = 400;
@@ -257,10 +268,13 @@ function handleRefreshToken(event, refreshToken) {
257
268
  try {
258
269
  const payload = decodeAuthCode(refreshToken);
259
270
  return {
260
- access_token: createAccessToken(payload.apiToken),
271
+ access_token: createAccessToken(payload.apiToken, payload.organizationSlug),
261
272
  token_type: "Bearer",
262
273
  expires_in: 3600,
263
- refresh_token: createAuthCode({ apiToken: payload.apiToken }, 86400 * 30)
274
+ refresh_token: createAuthCode({
275
+ apiToken: payload.apiToken,
276
+ organizationSlug: payload.organizationSlug
277
+ }, 86400 * 30)
264
278
  };
265
279
  } catch (error) {
266
280
  event.res.status = 400;
@@ -287,7 +301,7 @@ function escapeHtml(str) {
287
301
  * Render the login form HTML
288
302
  */
289
303
  function renderLoginForm(params) {
290
- const { redirectUri, state, codeChallenge, codeChallengeMethod, error } = params;
304
+ const { redirectUri, state, codeChallenge, codeChallengeMethod, organizationSlug, error } = params;
291
305
  return `<!DOCTYPE html>
292
306
  <html lang="en">
293
307
  <head>
@@ -437,6 +451,12 @@ function renderLoginForm(params) {
437
451
  </p>
438
452
  </div>
439
453
 
454
+ <div class="form-group">
455
+ <label for="organizationSlug">Organization Slug</label>
456
+ <input type="text" id="organizationSlug" name="organizationSlug" value="${escapeHtml(organizationSlug || "")}" placeholder="e.g. studio-meta" autocapitalize="off" autocorrect="off" spellcheck="false">
457
+ <p class="help-text">Optional, but recommended as the default org for Claude MCP requests.</p>
458
+ </div>
459
+
440
460
  <button type="submit">Connect to Forge</button>
441
461
  </form>
442
462