@studiometa/forge-mcp 0.2.2 → 0.2.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 +12 -1
- package/dist/auth.d.ts.map +1 -1
- package/dist/auth.js +19 -1
- package/dist/auth.js.map +1 -1
- package/dist/crypto.d.ts +56 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +110 -0
- package/dist/crypto.js.map +1 -0
- package/dist/{http-BMOiJdyw.js → http-CK8WsamV.js} +16 -4
- package/dist/http-CK8WsamV.js.map +1 -0
- package/dist/http.d.ts +1 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +2 -2
- package/dist/index.js +1 -1
- package/dist/oauth.d.ts +118 -0
- package/dist/oauth.d.ts.map +1 -0
- package/dist/oauth.js +571 -0
- package/dist/oauth.js.map +1 -0
- package/dist/server.d.ts +24 -6
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +33 -9
- package/dist/server.js.map +1 -1
- package/dist/{version-D3OFS3DQ.js → version-BTMdX8xQ.js} +2 -2
- package/dist/{version-D3OFS3DQ.js.map → version-BTMdX8xQ.js.map} +1 -1
- package/package.json +9 -1
- package/dist/http-BMOiJdyw.js.map +0 -1
package/dist/auth.d.ts
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Authentication utilities for Forge MCP HTTP server
|
|
3
|
+
*
|
|
4
|
+
* Supports two token formats:
|
|
5
|
+
* 1. Raw Forge API token (backwards compatible)
|
|
6
|
+
* 2. Base64-encoded Forge API token (from OAuth flow)
|
|
7
|
+
*
|
|
8
|
+
* Detection heuristic: decode the token from base64, then re-encode.
|
|
9
|
+
* If the re-encoded string matches the original, it's a real base64 token
|
|
10
|
+
* and we use the decoded value. Otherwise, we treat it as a raw token.
|
|
3
11
|
*/
|
|
4
12
|
export interface ForgeCredentials {
|
|
5
13
|
apiToken: string;
|
|
6
14
|
}
|
|
7
15
|
/**
|
|
8
16
|
* Parse Bearer token containing Forge API credentials.
|
|
9
|
-
*
|
|
17
|
+
*
|
|
18
|
+
* Token formats:
|
|
19
|
+
* - Raw Forge API token (e.g., "Bearer my-api-token")
|
|
20
|
+
* - Base64-encoded token from OAuth flow (e.g., "Bearer base64(apiToken)")
|
|
10
21
|
*
|
|
11
22
|
* @param authHeader - Authorization header value (e.g., "Bearer <token>")
|
|
12
23
|
* @returns Parsed credentials or null if invalid
|
package/dist/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA
|
|
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"}
|
package/dist/auth.js
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
+
* Try to decode a base64-encoded token.
|
|
3
|
+
* Returns the decoded string if re-encoding produces the original, null otherwise.
|
|
4
|
+
*
|
|
5
|
+
* Buffer.from(str, 'base64') never throws — it silently ignores invalid chars.
|
|
6
|
+
* So this function cannot fail, no try/catch needed.
|
|
7
|
+
*/
|
|
8
|
+
function tryDecodeBase64(token) {
|
|
9
|
+
const decoded = Buffer.from(token, "base64").toString("utf-8");
|
|
10
|
+
if (!decoded || decoded === token) return null;
|
|
11
|
+
if (Buffer.from(decoded).toString("base64") === token) return decoded;
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
2
15
|
* Parse Bearer token containing Forge API credentials.
|
|
3
|
-
*
|
|
16
|
+
*
|
|
17
|
+
* Token formats:
|
|
18
|
+
* - Raw Forge API token (e.g., "Bearer my-api-token")
|
|
19
|
+
* - Base64-encoded token from OAuth flow (e.g., "Bearer base64(apiToken)")
|
|
4
20
|
*
|
|
5
21
|
* @param authHeader - Authorization header value (e.g., "Bearer <token>")
|
|
6
22
|
* @returns Parsed credentials or null if invalid
|
|
@@ -11,6 +27,8 @@ function parseAuthHeader(authHeader) {
|
|
|
11
27
|
if (!match) return null;
|
|
12
28
|
const token = match[1].trim();
|
|
13
29
|
if (!token) return null;
|
|
30
|
+
const decoded = tryDecodeBase64(token);
|
|
31
|
+
if (decoded) return { apiToken: decoded };
|
|
14
32
|
return { apiToken: token };
|
|
15
33
|
}
|
|
16
34
|
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\nexport interface ForgeCredentials {\n apiToken: string;\n}\n\n/**\n * Parse Bearer token containing Forge API credentials.\n * Token
|
|
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"}
|
package/dist/crypto.d.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cryptographic utilities for stateless OAuth tokens
|
|
3
|
+
*
|
|
4
|
+
* Uses AES-256-GCM for authenticated encryption.
|
|
5
|
+
* The authorization code contains encrypted credentials that can be
|
|
6
|
+
* decrypted without server-side storage.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Get the encryption secret from environment or generate a default.
|
|
10
|
+
* In production, OAUTH_SECRET should always be set.
|
|
11
|
+
*/
|
|
12
|
+
export declare function getSecret(): string;
|
|
13
|
+
/**
|
|
14
|
+
* Encrypt data using AES-256-GCM
|
|
15
|
+
*
|
|
16
|
+
* Output format: base64url(salt + iv + authTag + ciphertext)
|
|
17
|
+
*
|
|
18
|
+
* @param plaintext - Data to encrypt
|
|
19
|
+
* @param secret - Encryption secret (defaults to OAUTH_SECRET env var)
|
|
20
|
+
* @returns Base64url-encoded encrypted data
|
|
21
|
+
*/
|
|
22
|
+
export declare function encrypt(plaintext: string, secret?: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* Decrypt data encrypted with encrypt()
|
|
25
|
+
*
|
|
26
|
+
* @param ciphertext - Base64url-encoded encrypted data
|
|
27
|
+
* @param secret - Encryption secret (defaults to OAUTH_SECRET env var)
|
|
28
|
+
* @returns Decrypted plaintext
|
|
29
|
+
* @throws Error if decryption fails (invalid data or wrong secret)
|
|
30
|
+
*/
|
|
31
|
+
export declare function decrypt(ciphertext: string, secret?: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Authorization code payload structure
|
|
34
|
+
*/
|
|
35
|
+
export interface AuthCodePayload {
|
|
36
|
+
apiToken: string;
|
|
37
|
+
codeChallenge?: string;
|
|
38
|
+
codeChallengeMethod?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create an encrypted authorization code containing credentials and PKCE challenge
|
|
42
|
+
*
|
|
43
|
+
* @param credentials - Object with apiToken and optional PKCE params
|
|
44
|
+
* @param expiresInSeconds - Code expiration time (default: 5 minutes)
|
|
45
|
+
* @returns Encrypted authorization code
|
|
46
|
+
*/
|
|
47
|
+
export declare function createAuthCode(credentials: AuthCodePayload, expiresInSeconds?: number): string;
|
|
48
|
+
/**
|
|
49
|
+
* Decode and validate an authorization code
|
|
50
|
+
*
|
|
51
|
+
* @param code - Encrypted authorization code
|
|
52
|
+
* @returns Decoded payload with credentials and PKCE challenge
|
|
53
|
+
* @throws Error if code is invalid or expired
|
|
54
|
+
*/
|
|
55
|
+
export declare function decodeAuthCode(code: string): AuthCodePayload;
|
|
56
|
+
//# sourceMappingURL=crypto.d.ts.map
|
|
@@ -0,0 +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"}
|
package/dist/crypto.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from "node:crypto";
|
|
2
|
+
/**
|
|
3
|
+
* Cryptographic utilities for stateless OAuth tokens
|
|
4
|
+
*
|
|
5
|
+
* Uses AES-256-GCM for authenticated encryption.
|
|
6
|
+
* The authorization code contains encrypted credentials that can be
|
|
7
|
+
* decrypted without server-side storage.
|
|
8
|
+
*/
|
|
9
|
+
var ALGORITHM = "aes-256-gcm";
|
|
10
|
+
var IV_LENGTH = 12;
|
|
11
|
+
var AUTH_TAG_LENGTH = 16;
|
|
12
|
+
var SALT_LENGTH = 16;
|
|
13
|
+
/**
|
|
14
|
+
* Derive a 256-bit key from a password using scrypt
|
|
15
|
+
*/
|
|
16
|
+
function deriveKey(password, salt) {
|
|
17
|
+
return scryptSync(password, salt, 32);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Get the encryption secret from environment or generate a default.
|
|
21
|
+
* In production, OAUTH_SECRET should always be set.
|
|
22
|
+
*/
|
|
23
|
+
function getSecret() {
|
|
24
|
+
const secret = process.env.OAUTH_SECRET;
|
|
25
|
+
if (!secret) {
|
|
26
|
+
console.warn("WARNING: OAUTH_SECRET not set. Using default secret. Set OAUTH_SECRET in production!");
|
|
27
|
+
return "forge-mcp-default-secret-change-me";
|
|
28
|
+
}
|
|
29
|
+
return secret;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Encrypt data using AES-256-GCM
|
|
33
|
+
*
|
|
34
|
+
* Output format: base64url(salt + iv + authTag + ciphertext)
|
|
35
|
+
*
|
|
36
|
+
* @param plaintext - Data to encrypt
|
|
37
|
+
* @param secret - Encryption secret (defaults to OAUTH_SECRET env var)
|
|
38
|
+
* @returns Base64url-encoded encrypted data
|
|
39
|
+
*/
|
|
40
|
+
function encrypt(plaintext, secret = getSecret()) {
|
|
41
|
+
const salt = randomBytes(SALT_LENGTH);
|
|
42
|
+
const key = deriveKey(secret, salt);
|
|
43
|
+
const iv = randomBytes(IV_LENGTH);
|
|
44
|
+
const cipher = createCipheriv(ALGORITHM, key, iv);
|
|
45
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
|
|
46
|
+
const authTag = cipher.getAuthTag();
|
|
47
|
+
return Buffer.concat([
|
|
48
|
+
salt,
|
|
49
|
+
iv,
|
|
50
|
+
authTag,
|
|
51
|
+
encrypted
|
|
52
|
+
]).toString("base64url");
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Decrypt data encrypted with encrypt()
|
|
56
|
+
*
|
|
57
|
+
* @param ciphertext - Base64url-encoded encrypted data
|
|
58
|
+
* @param secret - Encryption secret (defaults to OAUTH_SECRET env var)
|
|
59
|
+
* @returns Decrypted plaintext
|
|
60
|
+
* @throws Error if decryption fails (invalid data or wrong secret)
|
|
61
|
+
*/
|
|
62
|
+
function decrypt(ciphertext, secret = getSecret()) {
|
|
63
|
+
try {
|
|
64
|
+
const combined = Buffer.from(ciphertext, "base64url");
|
|
65
|
+
const salt = combined.subarray(0, SALT_LENGTH);
|
|
66
|
+
const iv = combined.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);
|
|
67
|
+
const authTag = combined.subarray(SALT_LENGTH + IV_LENGTH, SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);
|
|
68
|
+
const encrypted = combined.subarray(SALT_LENGTH + IV_LENGTH + AUTH_TAG_LENGTH);
|
|
69
|
+
const decipher = createDecipheriv(ALGORITHM, deriveKey(secret, salt), iv, { authTagLength: AUTH_TAG_LENGTH });
|
|
70
|
+
decipher.setAuthTag(authTag);
|
|
71
|
+
return Buffer.concat([decipher.update(encrypted), decipher.final()]).toString("utf8");
|
|
72
|
+
} catch {
|
|
73
|
+
throw new Error("Decryption failed: invalid token or secret");
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Create an encrypted authorization code containing credentials and PKCE challenge
|
|
78
|
+
*
|
|
79
|
+
* @param credentials - Object with apiToken and optional PKCE params
|
|
80
|
+
* @param expiresInSeconds - Code expiration time (default: 5 minutes)
|
|
81
|
+
* @returns Encrypted authorization code
|
|
82
|
+
*/
|
|
83
|
+
function createAuthCode(credentials, expiresInSeconds = 300) {
|
|
84
|
+
const payload = {
|
|
85
|
+
...credentials,
|
|
86
|
+
exp: Date.now() + expiresInSeconds * 1e3
|
|
87
|
+
};
|
|
88
|
+
return encrypt(JSON.stringify(payload));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Decode and validate an authorization code
|
|
92
|
+
*
|
|
93
|
+
* @param code - Encrypted authorization code
|
|
94
|
+
* @returns Decoded payload with credentials and PKCE challenge
|
|
95
|
+
* @throws Error if code is invalid or expired
|
|
96
|
+
*/
|
|
97
|
+
function decodeAuthCode(code) {
|
|
98
|
+
const payload = JSON.parse(decrypt(code));
|
|
99
|
+
if (payload.exp && Date.now() > payload.exp) throw new Error("Authorization code expired");
|
|
100
|
+
const { apiToken, codeChallenge, codeChallengeMethod } = payload;
|
|
101
|
+
if (!apiToken) throw new Error("Invalid authorization code: missing credentials");
|
|
102
|
+
return {
|
|
103
|
+
apiToken,
|
|
104
|
+
codeChallenge,
|
|
105
|
+
codeChallengeMethod
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
export { createAuthCode, decodeAuthCode, decrypt, encrypt, getSecret };
|
|
109
|
+
|
|
110
|
+
//# sourceMappingURL=crypto.js.map
|
|
@@ -0,0 +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,5 +1,6 @@
|
|
|
1
|
-
import { a as INSTRUCTIONS, i as getTools, n as executeToolWithCredentials, t as VERSION } from "./version-
|
|
1
|
+
import { a as INSTRUCTIONS, i as getTools, n as executeToolWithCredentials, t as VERSION } from "./version-BTMdX8xQ.js";
|
|
2
2
|
import { parseAuthHeader } from "./auth.js";
|
|
3
|
+
import { authorizeGetHandler, authorizePostHandler, oauthMetadataHandler, protectedResourceHandler, registerHandler, tokenHandler } from "./oauth.js";
|
|
3
4
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
4
5
|
import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
5
6
|
import { randomUUID } from "node:crypto";
|
|
@@ -192,7 +193,12 @@ async function handleMcpRequest(req, res, sessions, options) {
|
|
|
192
193
|
const authHeader = req.headers.authorization;
|
|
193
194
|
const credentials = parseAuthHeader(authHeader);
|
|
194
195
|
if (!credentials) {
|
|
195
|
-
|
|
196
|
+
const host = req.headers.host || "localhost:3000";
|
|
197
|
+
const resourceMetadataUrl = `${req.headers["x-forwarded-proto"] || "http"}://${host}/.well-known/oauth-protected-resource`;
|
|
198
|
+
res.writeHead(401, {
|
|
199
|
+
"Content-Type": "application/json",
|
|
200
|
+
"WWW-Authenticate": `Bearer resource_metadata="${resourceMetadataUrl}"`
|
|
201
|
+
});
|
|
196
202
|
res.end(JSON.stringify({
|
|
197
203
|
jsonrpc: "2.0",
|
|
198
204
|
error: {
|
|
@@ -255,7 +261,7 @@ function createMcpRequestHandler(sessions, options) {
|
|
|
255
261
|
/* v8 ignore stop */
|
|
256
262
|
}
|
|
257
263
|
/**
|
|
258
|
-
* Create h3 app for health check
|
|
264
|
+
* Create h3 app for health check, service info, and OAuth endpoints.
|
|
259
265
|
* The MCP endpoint is handled separately by handleMcpRequest.
|
|
260
266
|
*/
|
|
261
267
|
function createHealthApp() {
|
|
@@ -270,8 +276,14 @@ function createHealthApp() {
|
|
|
270
276
|
app.get("/health", defineEventHandler(() => {
|
|
271
277
|
return { status: "ok" };
|
|
272
278
|
}));
|
|
279
|
+
app.get("/.well-known/oauth-authorization-server", oauthMetadataHandler);
|
|
280
|
+
app.get("/.well-known/oauth-protected-resource", protectedResourceHandler);
|
|
281
|
+
app.post("/register", registerHandler);
|
|
282
|
+
app.get("/authorize", authorizeGetHandler);
|
|
283
|
+
app.post("/authorize", authorizePostHandler);
|
|
284
|
+
app.post("/token", tokenHandler);
|
|
273
285
|
return app;
|
|
274
286
|
}
|
|
275
287
|
export { SessionManager as a, handleMcpRequest as i, createMcpRequestHandler as n, createMcpServer as r, createHealthApp as t };
|
|
276
288
|
|
|
277
|
-
//# sourceMappingURL=http-
|
|
289
|
+
//# sourceMappingURL=http-CK8WsamV.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http-CK8WsamV.js","names":[],"sources":["../src/sessions.ts","../src/http.ts"],"sourcesContent":["/**\n * Session manager for multi-tenant Streamable HTTP transport.\n *\n * Each MCP client session gets its own transport + server pair.\n * Sessions are identified by UUID and tracked in a Map.\n *\n * Supports automatic TTL-based cleanup of idle sessions to prevent\n * memory leaks from abandoned clients.\n */\n\nimport type { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport type { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\n\n/**\n * A managed session: transport + MCP server pair.\n */\nexport interface ManagedSession {\n transport: StreamableHTTPServerTransport;\n server: Server;\n createdAt: number;\n lastActiveAt: number;\n}\n\nexport interface SessionManagerOptions {\n /**\n * Maximum idle time in milliseconds before a session is reaped.\n * Default: 30 minutes. Set to 0 to disable automatic cleanup.\n */\n ttl?: number;\n\n /**\n * How often to check for expired sessions, in milliseconds.\n * Default: 60 seconds.\n */\n sweepInterval?: number;\n}\n\nconst DEFAULT_TTL = 30 * 60 * 1000; // 30 minutes\nconst DEFAULT_SWEEP_INTERVAL = 60 * 1000; // 60 seconds\n\nexport class SessionManager {\n private sessions = new Map<string, ManagedSession>();\n private sweepTimer: ReturnType<typeof setInterval> | undefined;\n private readonly ttl: number;\n\n constructor(options?: SessionManagerOptions) {\n this.ttl = options?.ttl ?? DEFAULT_TTL;\n\n if (this.ttl > 0) {\n const interval = options?.sweepInterval ?? DEFAULT_SWEEP_INTERVAL;\n this.sweepTimer = setInterval(() => {\n this.sweep();\n }, interval);\n // Don't keep the process alive just for the sweep timer\n this.sweepTimer.unref();\n }\n }\n\n /**\n * Register a session after its ID has been assigned by the transport.\n */\n register(transport: StreamableHTTPServerTransport, server: Server): void {\n const sessionId = transport.sessionId;\n if (sessionId) {\n const now = Date.now();\n this.sessions.set(sessionId, {\n transport,\n server,\n createdAt: now,\n lastActiveAt: now,\n });\n }\n }\n\n /**\n * Look up a session by its ID and refresh its activity timestamp.\n */\n get(sessionId: string): ManagedSession | undefined {\n const session = this.sessions.get(sessionId);\n if (session) {\n session.lastActiveAt = Date.now();\n }\n return session;\n }\n\n /**\n * Remove a session and close its transport + server.\n */\n async remove(sessionId: string): Promise<void> {\n const session = this.sessions.get(sessionId);\n if (session) {\n this.sessions.delete(sessionId);\n await session.transport.close();\n await session.server.close();\n }\n }\n\n /**\n * Get the number of active sessions.\n */\n get size(): number {\n return this.sessions.size;\n }\n\n /**\n * Sweep expired sessions. Called automatically by the sweep timer.\n * Returns the number of sessions reaped.\n */\n sweep(): number {\n if (this.ttl <= 0) return 0;\n\n const now = Date.now();\n const expired: string[] = [];\n\n for (const [id, session] of this.sessions) {\n if (now - session.lastActiveAt > this.ttl) {\n expired.push(id);\n }\n }\n\n for (const id of expired) {\n // Fire-and-forget cleanup — don't block the sweep\n /* v8 ignore start */\n this.remove(id).catch(() => {});\n /* v8 ignore stop */\n }\n\n return expired.length;\n }\n\n /**\n * Close all sessions, stop the sweep timer, and clean up.\n */\n async closeAll(): Promise<void> {\n if (this.sweepTimer) {\n clearInterval(this.sweepTimer);\n this.sweepTimer = undefined;\n }\n\n const promises: Promise<void>[] = [];\n for (const [, session] of this.sessions) {\n promises.push(session.transport.close());\n promises.push(session.server.close());\n }\n await Promise.all(promises);\n this.sessions.clear();\n }\n}\n","/**\n * Streamable HTTP transport for Forge MCP Server\n *\n * Implements the official MCP Streamable HTTP transport specification (2025-03-26)\n * using the SDK's StreamableHTTPServerTransport.\n *\n * Architecture:\n * - Stateful mode with per-session transport+server pairs (multi-tenant)\n * - Auth via Bearer token → authInfo.token → handler extra.authInfo\n * - Session manager (injected) maps session IDs to transport+server instances\n * - Health/status endpoints handled by h3, MCP endpoint by the SDK transport\n */\n\nimport { randomUUID } from \"node:crypto\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\n\nimport { Server } from \"@modelcontextprotocol/sdk/server/index.js\";\nimport { StreamableHTTPServerTransport } from \"@modelcontextprotocol/sdk/server/streamableHttp.js\";\nimport { CallToolRequestSchema, ListToolsRequestSchema } from \"@modelcontextprotocol/sdk/types.js\";\nimport { createApp, defineEventHandler, type H3 } from \"h3\";\n\nimport { parseAuthHeader } from \"./auth.ts\";\nimport { executeToolWithCredentials } from \"./handlers/index.ts\";\nimport { INSTRUCTIONS } from \"./instructions.ts\";\nimport {\n oauthMetadataHandler,\n protectedResourceHandler,\n registerHandler,\n authorizeGetHandler,\n authorizePostHandler,\n tokenHandler,\n} from \"./oauth.ts\";\nimport { SessionManager } from \"./sessions.ts\";\nimport { getTools } from \"./tools.ts\";\nimport { VERSION } from \"./version.ts\";\n\nexport { SessionManager } from \"./sessions.ts\";\n\n/**\n * Options for the HTTP MCP server.\n */\nexport interface HttpServerOptions {\n /** When true, forge_write tool is not registered and write operations are rejected. */\n readOnly?: boolean;\n}\n\n/**\n * Create a configured MCP Server instance for HTTP transport.\n *\n * Unlike stdio, HTTP mode does NOT include forge_configure/forge_get_config\n * because credentials come from the Authorization header per-request.\n */\nexport function createMcpServer(options?: HttpServerOptions): Server {\n const readOnly = options?.readOnly ?? false;\n const tools = getTools({ readOnly });\n\n const server = new Server(\n {\n name: \"forge-mcp\",\n version: VERSION,\n },\n {\n capabilities: {\n tools: {},\n },\n instructions: INSTRUCTIONS,\n },\n );\n\n server.setRequestHandler(ListToolsRequestSchema, async () => {\n return { tools };\n });\n\n server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {\n const { name, arguments: args } = request.params;\n const token = extra.authInfo?.token;\n\n /* v8 ignore start */\n if (!token) {\n return {\n content: [\n {\n type: \"text\" as const,\n text: \"Error: Authentication required. No token found in request.\",\n },\n ],\n structuredContent: {\n success: false,\n error: \"Authentication required. No token found in request.\",\n },\n isError: true,\n };\n }\n /* v8 ignore stop */\n\n // Reject write operations in read-only mode\n if (readOnly && name === \"forge_write\") {\n return {\n content: [\n {\n type: \"text\" as const,\n text: \"Error: Server is running in read-only mode. Write operations are disabled.\",\n },\n ],\n structuredContent: {\n success: false,\n error: \"Server is running in read-only mode. Write operations are disabled.\",\n },\n isError: true,\n };\n }\n\n try {\n const result = await executeToolWithCredentials(\n name,\n /* v8 ignore next */ (args as Record<string, unknown>) ?? {},\n { apiToken: token },\n );\n return result as unknown as Record<string, unknown>;\n } catch (error) {\n /* v8 ignore start */\n const message = error instanceof Error ? error.message : String(error);\n /* v8 ignore stop */\n return {\n content: [{ type: \"text\" as const, text: `Error: ${message}` }],\n 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 protocol = (req.headers[\"x-forwarded-proto\"] as string) || \"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\n const authenticatedReq = req as IncomingMessage & {\n auth?: { token: string; clientId: string; scopes: string[] };\n };\n authenticatedReq.auth = {\n token: credentials.apiToken,\n clientId: \"forge-http-client\",\n scopes: [],\n };\n\n const sessionId = req.headers[\"mcp-session-id\"] as string | undefined;\n\n if (sessionId) {\n // Existing session — route to its transport\n const session = sessions.get(sessionId);\n if (!session) {\n res.writeHead(404, { \"Content-Type\": \"application/json\" });\n res.end(\n JSON.stringify({\n jsonrpc: \"2.0\",\n error: {\n code: -32000,\n message: \"Session not found. The session may have expired or been terminated.\",\n },\n id: null,\n }),\n );\n return;\n }\n\n await session.transport.handleRequest(authenticatedReq, res);\n return;\n }\n\n // No session ID — this should be an initialize request.\n // Create a new transport + server pair.\n const transport = new StreamableHTTPServerTransport({\n sessionIdGenerator: () => randomUUID(),\n });\n\n const server = createMcpServer(options);\n await server.connect(transport);\n\n // Set up cleanup on close\n transport.onclose = () => {\n const sid = transport.sessionId;\n /* v8 ignore start */\n if (sid) {\n sessions.remove(sid).catch(() => {\n // Ignore cleanup errors\n });\n }\n /* v8 ignore stop */\n };\n\n // Handle the request (this will set transport.sessionId during initialize)\n await transport.handleRequest(authenticatedReq, res);\n\n // After handling, register the session if the transport got a session ID\n /* v8 ignore start */\n if (transport.sessionId) {\n sessions.register(transport, server);\n } else {\n // No session was created (e.g., invalid request) — clean up\n await transport.close();\n await server.close();\n }\n /* v8 ignore stop */\n}\n\n/**\n * Create a request handler bound to a SessionManager instance.\n * Convenience factory for server.ts.\n */\nexport function createMcpRequestHandler(\n sessions: SessionManager,\n 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 = createApp();\n\n // Service info & health\n app.get(\n \"/\",\n defineEventHandler(() => {\n return { status: \"ok\", service: \"forge-mcp\", version: VERSION };\n }),\n );\n\n app.get(\n \"/health\",\n defineEventHandler(() => {\n return { status: \"ok\" };\n }),\n );\n\n // OAuth 2.1 endpoints\n app.get(\"/.well-known/oauth-authorization-server\", oauthMetadataHandler);\n app.get(\"/.well-known/oauth-protected-resource\", protectedResourceHandler);\n app.post(\"/register\", registerHandler);\n app.get(\"/authorize\", authorizeGetHandler);\n app.post(\"/authorize\", authorizePostHandler);\n app.post(\"/token\", tokenHandler);\n\n return app;\n}\n"],"mappings":"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"}
|
package/dist/http.d.ts
CHANGED
|
@@ -48,7 +48,7 @@ export declare function handleMcpRequest(req: IncomingMessage, res: ServerRespon
|
|
|
48
48
|
*/
|
|
49
49
|
export declare function createMcpRequestHandler(sessions: SessionManager, options?: HttpServerOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
|
|
50
50
|
/**
|
|
51
|
-
* Create h3 app for health check
|
|
51
|
+
* Create h3 app for health check, service info, and OAuth endpoints.
|
|
52
52
|
* The MCP endpoint is handled separately by handleMcpRequest.
|
|
53
53
|
*/
|
|
54
54
|
export declare function createHealthApp(): H3;
|
package/dist/http.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAGnE,OAAO,EAAiC,KAAK,EAAE,EAAE,MAAM,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AAGnE,OAAO,EAAiC,KAAK,EAAE,EAAE,MAAM,IAAI,CAAC;AAa5D,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAI/C,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAE/C;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,uFAAuF;IACvF,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,MAAM,CAgFnE;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,cAAc,EACxB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,OAAO,CAAC,IAAI,CAAC,CAgGf;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,cAAc,EACxB,OAAO,CAAC,EAAE,iBAAiB,GAC1B,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,IAAI,CAAC,CAI9D;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI,EAAE,CA2BpC"}
|
package/dist/http.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import "./version-
|
|
2
|
-
import { a as SessionManager, i as handleMcpRequest, n as createMcpRequestHandler, r as createMcpServer, t as createHealthApp } from "./http-
|
|
1
|
+
import "./version-BTMdX8xQ.js";
|
|
2
|
+
import { a as SessionManager, i as handleMcpRequest, n as createMcpRequestHandler, r as createMcpServer, t as createHealthApp } from "./http-CK8WsamV.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-
|
|
3
|
+
import { a as INSTRUCTIONS, i as getTools, n as executeToolWithCredentials, r as STDIO_ONLY_TOOLS, t as VERSION } from "./version-BTMdX8xQ.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";
|
package/dist/oauth.d.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.1 endpoints for Claude Desktop integration
|
|
3
|
+
*
|
|
4
|
+
* Implements OAuth 2.1 with PKCE as specified in the MCP authorization spec.
|
|
5
|
+
* Uses stateless encrypted tokens — no server-side storage required.
|
|
6
|
+
*
|
|
7
|
+
* Spec: https://modelcontextprotocol.io/specification/2025-03-26/basic/authorization
|
|
8
|
+
*
|
|
9
|
+
* Flow:
|
|
10
|
+
* 1. Claude redirects user to /authorize with OAuth params (including PKCE)
|
|
11
|
+
* 2. User enters their Forge API token in a login form
|
|
12
|
+
* 3. Server encrypts the token + PKCE challenge into an authorization code
|
|
13
|
+
* 4. Redirects back to Claude with the code
|
|
14
|
+
* 5. Claude exchanges code for access token via /token (with code_verifier)
|
|
15
|
+
* 6. Server validates PKCE and returns base64-encoded access token
|
|
16
|
+
*/
|
|
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.
|
|
21
|
+
*/
|
|
22
|
+
export declare function createAccessToken(apiToken: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* OAuth metadata for discovery (RFC 8414)
|
|
25
|
+
* GET /.well-known/oauth-authorization-server
|
|
26
|
+
*
|
|
27
|
+
* MCP clients MUST check this endpoint first for server capabilities.
|
|
28
|
+
*/
|
|
29
|
+
export declare const oauthMetadataHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, {
|
|
30
|
+
issuer: string;
|
|
31
|
+
authorization_endpoint: string;
|
|
32
|
+
token_endpoint: string;
|
|
33
|
+
response_types_supported: string[];
|
|
34
|
+
grant_types_supported: string[];
|
|
35
|
+
code_challenge_methods_supported: string[];
|
|
36
|
+
token_endpoint_auth_methods_supported: string[];
|
|
37
|
+
registration_endpoint: string;
|
|
38
|
+
scopes_supported: string[];
|
|
39
|
+
service_documentation: string;
|
|
40
|
+
}>;
|
|
41
|
+
/**
|
|
42
|
+
* Protected resource metadata (RFC 9728)
|
|
43
|
+
* GET /.well-known/oauth-protected-resource
|
|
44
|
+
*
|
|
45
|
+
* Tells MCP clients where to find the OAuth authorization server.
|
|
46
|
+
*/
|
|
47
|
+
export declare const protectedResourceHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, {
|
|
48
|
+
resource: string;
|
|
49
|
+
authorization_servers: string[];
|
|
50
|
+
bearer_methods_supported: string[];
|
|
51
|
+
scopes_supported: string[];
|
|
52
|
+
}>;
|
|
53
|
+
/**
|
|
54
|
+
* Dynamic Client Registration endpoint (RFC 7591)
|
|
55
|
+
* POST /register
|
|
56
|
+
*
|
|
57
|
+
* MCP servers SHOULD support DCR to allow clients to register automatically.
|
|
58
|
+
* Since we use stateless tokens, we accept any registration and return
|
|
59
|
+
* a generated client_id.
|
|
60
|
+
*/
|
|
61
|
+
export declare const registerHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
62
|
+
error: string;
|
|
63
|
+
error_description: string;
|
|
64
|
+
client_id?: undefined;
|
|
65
|
+
client_name?: undefined;
|
|
66
|
+
redirect_uris?: undefined;
|
|
67
|
+
token_endpoint_auth_method?: undefined;
|
|
68
|
+
grant_types?: undefined;
|
|
69
|
+
response_types?: undefined;
|
|
70
|
+
} | {
|
|
71
|
+
client_id: string;
|
|
72
|
+
client_name: string;
|
|
73
|
+
redirect_uris: string[];
|
|
74
|
+
token_endpoint_auth_method: string;
|
|
75
|
+
grant_types: string[];
|
|
76
|
+
response_types: string[];
|
|
77
|
+
error?: undefined;
|
|
78
|
+
error_description?: undefined;
|
|
79
|
+
}>>;
|
|
80
|
+
/**
|
|
81
|
+
* Authorization endpoint — shows login form
|
|
82
|
+
* GET /authorize
|
|
83
|
+
*/
|
|
84
|
+
export declare const authorizeGetHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, string | import("h3").HTTPResponse>;
|
|
85
|
+
/**
|
|
86
|
+
* Authorization endpoint — process login
|
|
87
|
+
* POST /authorize
|
|
88
|
+
*/
|
|
89
|
+
export declare const authorizePostHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<string>>;
|
|
90
|
+
/**
|
|
91
|
+
* Token endpoint — exchange code for access token
|
|
92
|
+
* POST /token
|
|
93
|
+
*
|
|
94
|
+
* Supports:
|
|
95
|
+
* - authorization_code grant (with PKCE validation)
|
|
96
|
+
* - refresh_token grant
|
|
97
|
+
*/
|
|
98
|
+
export declare const tokenHandler: import("h3").EventHandlerWithFetch<import("h3").EventHandlerRequest, Promise<{
|
|
99
|
+
error: string;
|
|
100
|
+
error_description: string;
|
|
101
|
+
access_token?: undefined;
|
|
102
|
+
token_type?: undefined;
|
|
103
|
+
expires_in?: undefined;
|
|
104
|
+
refresh_token?: undefined;
|
|
105
|
+
} | {
|
|
106
|
+
access_token: string;
|
|
107
|
+
token_type: string;
|
|
108
|
+
expires_in: number;
|
|
109
|
+
refresh_token: string;
|
|
110
|
+
error?: undefined;
|
|
111
|
+
error_description?: undefined;
|
|
112
|
+
}>>;
|
|
113
|
+
/**
|
|
114
|
+
* Create S256 PKCE challenge from verifier
|
|
115
|
+
* SHA256(code_verifier) encoded as base64url
|
|
116
|
+
*/
|
|
117
|
+
export declare function createS256Challenge(codeVerifier: string): string;
|
|
118
|
+
//# sourceMappingURL=oauth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../src/oauth.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAgBH;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE1D;AAED;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB;;;;;;;;;;;EAyB/B,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB;;;;;EAcnC,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe;;;;;;;;;;;;;;;;;;GAoC1B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,mBAAmB,0GAyC9B,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,oBAAoB,uFAuD/B,CAAC;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,YAAY;;;;;;;;;;;;;;GA4EvB,CAAC;AA0CH;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEhE"}
|