@cyanheads/git-mcp-server 2.2.2 → 2.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.
Files changed (35) hide show
  1. package/README.md +63 -124
  2. package/dist/config/index.js +248 -53
  3. package/dist/mcp-server/server.js +6 -6
  4. package/dist/mcp-server/tools/gitCherryPick/logic.js +39 -13
  5. package/dist/mcp-server/tools/gitCommit/logic.js +1 -1
  6. package/dist/mcp-server/tools/gitMerge/logic.js +38 -13
  7. package/dist/mcp-server/tools/gitTag/logic.js +38 -19
  8. package/dist/mcp-server/transports/auth/authFactory.js +41 -0
  9. package/dist/mcp-server/transports/auth/authMiddleware.js +57 -0
  10. package/dist/mcp-server/transports/auth/index.js +6 -4
  11. package/dist/mcp-server/transports/auth/lib/authTypes.js +8 -0
  12. package/dist/mcp-server/transports/auth/{core → lib}/authUtils.js +21 -14
  13. package/dist/mcp-server/transports/auth/strategies/authStrategy.js +1 -0
  14. package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js +113 -0
  15. package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js +102 -0
  16. package/dist/mcp-server/transports/core/baseTransportManager.js +19 -0
  17. package/dist/mcp-server/transports/core/honoNodeBridge.js +51 -0
  18. package/dist/mcp-server/transports/core/statefulTransportManager.js +234 -0
  19. package/dist/mcp-server/transports/core/statelessTransportManager.js +92 -0
  20. package/dist/mcp-server/transports/core/transportTypes.js +5 -0
  21. package/dist/mcp-server/transports/{httpErrorHandler.js → http/httpErrorHandler.js} +33 -8
  22. package/dist/mcp-server/transports/http/httpTransport.js +254 -0
  23. package/dist/mcp-server/transports/http/httpTypes.js +5 -0
  24. package/dist/mcp-server/transports/http/index.js +6 -0
  25. package/dist/mcp-server/transports/http/mcpTransportMiddleware.js +63 -0
  26. package/dist/mcp-server/transports/stdio/index.js +5 -0
  27. package/dist/mcp-server/transports/{stdioTransport.js → stdio/stdioTransport.js} +10 -5
  28. package/dist/types-global/errors.js +75 -19
  29. package/dist/utils/internal/errorHandler.js +11 -13
  30. package/package.json +18 -7
  31. package/dist/mcp-server/transports/auth/core/authTypes.js +0 -5
  32. package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.js +0 -149
  33. package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.js +0 -127
  34. package/dist/mcp-server/transports/httpTransport.js +0 -207
  35. /package/dist/mcp-server/transports/auth/{core → lib}/authContext.js +0 -0
@@ -7,6 +7,7 @@ import { promisify } from "util";
7
7
  import { z } from "zod";
8
8
  import { logger, sanitization } from "../../../utils/index.js";
9
9
  import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
10
+ import { config } from "../../../config/index.js";
10
11
  const execFileAsync = promisify(execFile);
11
12
  // 1. DEFINE the Zod input schema.
12
13
  export const GitCherryPickInputSchema = z.object({
@@ -36,21 +37,46 @@ export async function gitCherryPickLogic(params, context) {
36
37
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
37
38
  }
38
39
  const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
39
- const args = ["-C", targetPath, "cherry-pick"];
40
- if (params.mainline)
41
- args.push("-m", String(params.mainline));
42
- if (params.strategy)
43
- args.push(`-X${params.strategy}`);
44
- if (params.noCommit)
45
- args.push("--no-commit");
46
- if (params.signoff)
47
- args.push("--signoff");
48
- args.push(params.commitRef);
49
- logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
50
- const { stdout, stderr } = await execFileAsync("git", args);
40
+ const attemptCherryPick = async (withSigning) => {
41
+ const args = ["-C", targetPath, "cherry-pick"];
42
+ if (withSigning)
43
+ args.push("-S");
44
+ if (params.mainline)
45
+ args.push("-m", String(params.mainline));
46
+ if (params.strategy)
47
+ args.push(`-X${params.strategy}`);
48
+ if (params.noCommit)
49
+ args.push("--no-commit");
50
+ if (params.signoff)
51
+ args.push("--signoff");
52
+ args.push(params.commitRef);
53
+ logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
54
+ return await execFileAsync("git", args);
55
+ };
56
+ const createsCommit = !params.noCommit;
57
+ const shouldSign = !!config.gitSignCommits && createsCommit;
58
+ let stdout;
59
+ let stderr;
60
+ try {
61
+ const result = await attemptCherryPick(shouldSign);
62
+ stdout = result.stdout;
63
+ stderr = result.stderr;
64
+ }
65
+ catch (error) {
66
+ const isSigningError = (error.stderr || "").includes("gpg failed to sign");
67
+ if (shouldSign && isSigningError) {
68
+ logger.warning("Cherry-pick with signing failed. Retrying automatically without signature.", { ...context, operation });
69
+ const result = await attemptCherryPick(false);
70
+ stdout = result.stdout;
71
+ stderr = result.stderr;
72
+ }
73
+ else {
74
+ throw error;
75
+ }
76
+ }
51
77
  const output = stdout + stderr;
52
78
  const conflicts = /conflict/i.test(output);
53
- const commitCreated = !params.noCommit && !conflicts;
79
+ const commitCreated = createsCommit && !conflicts;
54
80
  const message = conflicts
55
81
  ? `Cherry-pick resulted in conflicts for commit(s) '${params.commitRef}'. Manual resolution required.`
56
82
  : `Successfully cherry-picked commit(s) '${params.commitRef}'.`;
@@ -70,7 +70,7 @@ export async function commitGitChanges(params, context) {
70
70
  let result;
71
71
  const shouldSign = config.gitSignCommits;
72
72
  try {
73
- result = await attemptCommit(shouldSign);
73
+ result = await attemptCommit(shouldSign || false);
74
74
  }
75
75
  catch (error) {
76
76
  const isSigningError = (error.stderr || "").includes("gpg failed to sign");
@@ -7,6 +7,7 @@ import { promisify } from "util";
7
7
  import { z } from "zod";
8
8
  import { logger, sanitization } from "../../../utils/index.js";
9
9
  import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
10
+ import { config } from "../../../config/index.js";
10
11
  const execFileAsync = promisify(execFile);
11
12
  // 1. DEFINE the Zod input schema.
12
13
  export const GitMergeInputSchema = z.object({
@@ -38,21 +39,45 @@ export async function gitMergeLogic(params, context) {
38
39
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
39
40
  }
40
41
  const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
41
- const args = ["-C", targetPath, "merge"];
42
- if (params.abort) {
43
- args.push("--abort");
42
+ const attemptMerge = async (withSigning) => {
43
+ const args = ["-C", targetPath, "merge"];
44
+ if (params.abort) {
45
+ args.push("--abort");
46
+ }
47
+ else {
48
+ if (withSigning)
49
+ args.push("-S");
50
+ if (params.noFf)
51
+ args.push("--no-ff");
52
+ if (params.squash)
53
+ args.push("--squash");
54
+ if (params.commitMessage && !params.squash)
55
+ args.push("-m", params.commitMessage);
56
+ args.push(params.branch);
57
+ }
58
+ logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
59
+ return await execFileAsync("git", args);
60
+ };
61
+ // A merge commit is only created if it's not a fast-forward (or --no-ff is used)
62
+ // and we are not squashing or aborting.
63
+ const createsMergeCommit = !params.squash && !params.abort;
64
+ const shouldSign = !!config.gitSignCommits && createsMergeCommit;
65
+ let stdout;
66
+ try {
67
+ const result = await attemptMerge(shouldSign);
68
+ stdout = result.stdout;
44
69
  }
45
- else {
46
- if (params.noFf)
47
- args.push("--no-ff");
48
- if (params.squash)
49
- args.push("--squash");
50
- if (params.commitMessage && !params.squash)
51
- args.push("-m", params.commitMessage);
52
- args.push(params.branch);
70
+ catch (error) {
71
+ const isSigningError = (error.stderr || "").includes("gpg failed to sign");
72
+ if (shouldSign && isSigningError) {
73
+ logger.warning("Merge with signing failed. Retrying automatically without signature.", { ...context, operation });
74
+ const result = await attemptMerge(false);
75
+ stdout = result.stdout;
76
+ }
77
+ else {
78
+ throw error;
79
+ }
53
80
  }
54
- logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
55
- const { stdout } = await execFileAsync("git", args);
56
81
  return {
57
82
  success: true,
58
83
  message: stdout.trim() || "Merge command executed successfully.",
@@ -5,8 +5,9 @@
5
5
  import { execFile } from "child_process";
6
6
  import { promisify } from "util";
7
7
  import { z } from "zod";
8
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
8
9
  import { logger, sanitization } from "../../../utils/index.js";
9
- import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
10
+ import { config } from "../../../config/index.js";
10
11
  const execFileAsync = promisify(execFile);
11
12
  // 1. DEFINE the Zod input schema.
12
13
  export const GitTagBaseSchema = z.object({
@@ -47,27 +48,45 @@ export async function gitTagLogic(params, context) {
47
48
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, "No session working directory set. Please specify a 'path' or use 'git_set_working_dir' first.");
48
49
  }
49
50
  const targetPath = sanitization.sanitizePath(params.path === "." ? workingDir : params.path, { allowAbsolute: true }).sanitizedPath;
50
- const args = ["-C", targetPath, "tag"];
51
- switch (params.mode) {
52
- case "list":
53
- args.push("--list");
54
- break;
55
- case "create":
56
- if (params.annotate)
57
- args.push("-a", "-m", params.message);
58
- args.push(params.tagName);
59
- if (params.commitRef)
60
- args.push(params.commitRef);
61
- break;
62
- case "delete":
63
- args.push("-d", params.tagName);
64
- break;
65
- }
66
- logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
67
- const { stdout } = await execFileAsync("git", args);
68
51
  if (params.mode === 'list') {
52
+ const { stdout } = await execFileAsync("git", ["-C", targetPath, "tag", "--list"]);
69
53
  const tags = stdout.trim().split("\n").filter(Boolean);
70
54
  return { success: true, mode: params.mode, tags };
71
55
  }
56
+ if (params.mode === 'delete') {
57
+ await execFileAsync("git", ["-C", targetPath, "tag", "-d", params.tagName]);
58
+ return { success: true, mode: params.mode, message: `Tag '${params.tagName}' deleted successfully.`, tagName: params.tagName };
59
+ }
60
+ // Handle create mode with signing logic
61
+ if (params.mode === 'create') {
62
+ const attemptTag = async (withSigning) => {
63
+ const args = ["-C", targetPath, "tag"];
64
+ if (params.annotate) {
65
+ // Use -s for signed annotated tag, -a for unsigned
66
+ args.push(withSigning ? "-s" : "-a");
67
+ args.push("-m", params.message);
68
+ }
69
+ args.push(params.tagName);
70
+ if (params.commitRef) {
71
+ args.push(params.commitRef);
72
+ }
73
+ logger.debug(`Executing command: git ${args.join(" ")}`, { ...context, operation });
74
+ return await execFileAsync("git", args);
75
+ };
76
+ const shouldSign = !!config.gitSignCommits && params.annotate;
77
+ try {
78
+ await attemptTag(shouldSign);
79
+ }
80
+ catch (error) {
81
+ const isSigningError = (error.stderr || "").includes("gpg failed to sign");
82
+ if (shouldSign && isSigningError) {
83
+ logger.warning("Tag with signing failed. Retrying automatically without signature.", { ...context, operation });
84
+ await attemptTag(false); // Fallback to unsigned annotated tag
85
+ }
86
+ else {
87
+ throw error;
88
+ }
89
+ }
90
+ }
72
91
  return { success: true, mode: params.mode, message: `Tag '${params.tagName}' ${params.mode}d successfully.`, tagName: params.tagName };
73
92
  }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @fileoverview Factory for creating an authentication strategy based on configuration.
3
+ * This module centralizes the logic for selecting and instantiating the correct
4
+ * authentication strategy, promoting loose coupling and easy extensibility.
5
+ * @module src/mcp-server/transports/auth/authFactory
6
+ */
7
+ import { config } from "../../../config/index.js";
8
+ import { logger, requestContextService } from "../../../utils/index.js";
9
+ import { JwtStrategy } from "./strategies/jwtStrategy.js";
10
+ import { OauthStrategy } from "./strategies/oauthStrategy.js";
11
+ /**
12
+ * Creates and returns an authentication strategy instance based on the
13
+ * application's configuration (`config.mcpAuthMode`).
14
+ *
15
+ * @returns An instance of a class that implements the `AuthStrategy` interface,
16
+ * or `null` if authentication is disabled (`none`).
17
+ * @throws {Error} If the auth mode is unknown or misconfigured.
18
+ */
19
+ export function createAuthStrategy() {
20
+ const context = requestContextService.createRequestContext({
21
+ operation: "createAuthStrategy",
22
+ authMode: config.mcpAuthMode,
23
+ });
24
+ logger.info("Creating authentication strategy...", context);
25
+ switch (config.mcpAuthMode) {
26
+ case "jwt":
27
+ logger.debug("Instantiating JWT authentication strategy.", context);
28
+ return new JwtStrategy();
29
+ case "oauth":
30
+ logger.debug("Instantiating OAuth authentication strategy.", context);
31
+ return new OauthStrategy();
32
+ case "none":
33
+ logger.info("Authentication is disabled ('none' mode).", context);
34
+ return null; // No authentication
35
+ default:
36
+ // This ensures that if a new auth mode is added to the config type
37
+ // but not to this factory, we get a compile-time or runtime error.
38
+ logger.error(`Unknown authentication mode: ${config.mcpAuthMode}`, context);
39
+ throw new Error(`Unknown authentication mode: ${config.mcpAuthMode}`);
40
+ }
41
+ }
@@ -0,0 +1,57 @@
1
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
2
+ import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
3
+ import { authContext } from "./lib/authContext.js";
4
+ /**
5
+ * Creates a Hono middleware function that enforces authentication using a given strategy.
6
+ *
7
+ * @param strategy - An instance of a class that implements the `AuthStrategy` interface.
8
+ * @returns A Hono middleware function.
9
+ */
10
+ export function createAuthMiddleware(strategy) {
11
+ return async function authMiddleware(c, next) {
12
+ const context = requestContextService.createRequestContext({
13
+ operation: "authMiddleware",
14
+ method: c.req.method,
15
+ path: c.req.path,
16
+ });
17
+ logger.debug("Initiating authentication check.", context);
18
+ const authHeader = c.req.header("Authorization");
19
+ if (!authHeader || !authHeader.startsWith("Bearer ")) {
20
+ logger.warning("Authorization header missing or invalid.", context);
21
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, "Missing or invalid Authorization header. Bearer scheme required.", context);
22
+ }
23
+ const token = authHeader.substring(7);
24
+ if (!token) {
25
+ logger.warning("Bearer token is missing from Authorization header.", context);
26
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, "Authentication token is missing.", context);
27
+ }
28
+ logger.debug("Extracted Bearer token, proceeding to verification.", context);
29
+ try {
30
+ const authInfo = await strategy.verify(token);
31
+ const authLogContext = {
32
+ ...context,
33
+ clientId: authInfo.clientId,
34
+ subject: authInfo.subject,
35
+ scopes: authInfo.scopes,
36
+ };
37
+ logger.info("Authentication successful. Auth context populated.", authLogContext);
38
+ // Run the next middleware in the chain within the populated auth context.
39
+ await authContext.run({ authInfo }, next);
40
+ }
41
+ catch (error) {
42
+ // The strategy is expected to throw an McpError.
43
+ // We re-throw it here to be caught by the global httpErrorHandler.
44
+ logger.warning("Authentication verification failed.", {
45
+ ...context,
46
+ error: error instanceof Error ? error.message : String(error),
47
+ });
48
+ // Ensure consistent error handling
49
+ throw ErrorHandler.handleError(error, {
50
+ operation: "authMiddlewareVerification",
51
+ context,
52
+ rethrow: true, // Rethrow to be caught by Hono's global error handler
53
+ errorCode: BaseErrorCode.UNAUTHORIZED, // Default to unauthorized if not more specific
54
+ });
55
+ }
56
+ };
57
+ }
@@ -3,7 +3,9 @@
3
3
  * Exports core utilities and middleware strategies for easier imports.
4
4
  * @module src/mcp-server/transports/auth/index
5
5
  */
6
- export { authContext } from "./core/authContext.js";
7
- export { withRequiredScopes } from "./core/authUtils.js";
8
- export { mcpAuthMiddleware as jwtAuthMiddleware } from "./strategies/jwt/jwtMiddleware.js";
9
- export { oauthMiddleware } from "./strategies/oauth/oauthMiddleware.js";
6
+ export { authContext } from "./lib/authContext.js";
7
+ export { withRequiredScopes } from "./lib/authUtils.js";
8
+ export { createAuthStrategy } from "./authFactory.js";
9
+ export { createAuthMiddleware } from "./authMiddleware.js";
10
+ export { JwtStrategy } from "./strategies/jwtStrategy.js";
11
+ export { OauthStrategy } from "./strategies/oauthStrategy.js";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * @fileoverview Shared types for authentication middleware.
3
+ * @module src/mcp-server/transports/auth/core/auth.types
4
+ */
5
+ export {};
6
+ // The declaration for `http.IncomingMessage` is no longer needed here,
7
+ // as the new architecture avoids direct mutation where possible and handles
8
+ // the attachment within the Hono context.
@@ -19,27 +19,34 @@ import { authContext } from "./authContext.js";
19
19
  * more required scopes are not present in the validated token.
20
20
  */
21
21
  export function withRequiredScopes(requiredScopes) {
22
+ const operationName = "withRequiredScopesCheck";
23
+ const initialContext = requestContextService.createRequestContext({
24
+ operation: operationName,
25
+ requiredScopes,
26
+ });
27
+ logger.debug("Performing scope authorization check.", initialContext);
22
28
  const store = authContext.getStore();
23
29
  if (!store || !store.authInfo) {
30
+ logger.crit("Authentication context is missing in withRequiredScopes. This is a server configuration error.", initialContext);
24
31
  // This is a server-side logic error; the auth middleware should always populate this.
25
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, "Authentication context is missing. This indicates a server configuration error.", requestContextService.createRequestContext({
26
- operation: "withRequiredScopesCheck",
32
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, "Authentication context is missing. This indicates a server configuration error.", {
33
+ ...initialContext,
27
34
  error: "AuthStore not found in AsyncLocalStorage.",
28
- }));
35
+ });
29
36
  }
30
- const { scopes: grantedScopes, clientId } = store.authInfo;
37
+ const { scopes: grantedScopes, clientId, subject } = store.authInfo;
31
38
  const grantedScopeSet = new Set(grantedScopes);
32
39
  const missingScopes = requiredScopes.filter((scope) => !grantedScopeSet.has(scope));
40
+ const finalContext = {
41
+ ...initialContext,
42
+ grantedScopes,
43
+ clientId,
44
+ subject,
45
+ };
33
46
  if (missingScopes.length > 0) {
34
- const context = requestContextService.createRequestContext({
35
- operation: "withRequiredScopesCheck",
36
- required: requiredScopes,
37
- granted: grantedScopes,
38
- missing: missingScopes,
39
- clientId: clientId,
40
- subject: store.authInfo.subject,
41
- });
42
- logger.warning("Authorization failed: Missing required scopes.", context);
43
- throw new McpError(BaseErrorCode.FORBIDDEN, `Insufficient permissions. Missing required scopes: ${missingScopes.join(", ")}`, { requiredScopes, missingScopes });
47
+ const errorContext = { ...finalContext, missingScopes };
48
+ logger.warning("Authorization failed: Missing required scopes.", errorContext);
49
+ throw new McpError(BaseErrorCode.FORBIDDEN, `Insufficient permissions. Missing required scopes: ${missingScopes.join(", ")}`, errorContext);
44
50
  }
51
+ logger.debug("Scope authorization successful.", finalContext);
45
52
  }
@@ -0,0 +1,113 @@
1
+ /**
2
+ * @fileoverview Implements the JWT authentication strategy.
3
+ * This module provides a concrete implementation of the AuthStrategy for validating
4
+ * JSON Web Tokens (JWTs). It encapsulates all logic related to JWT verification,
5
+ * including secret key management and payload validation.
6
+ * @module src/mcp-server/transports/auth/strategies/JwtStrategy
7
+ */
8
+ import { jwtVerify } from "jose";
9
+ import { config, environment } from "../../../../config/index.js";
10
+ import { BaseErrorCode, McpError } from "../../../../types-global/errors.js";
11
+ import { ErrorHandler, logger, requestContextService, } from "../../../../utils/index.js";
12
+ export class JwtStrategy {
13
+ secretKey;
14
+ constructor() {
15
+ const context = requestContextService.createRequestContext({
16
+ operation: "JwtStrategy.constructor",
17
+ });
18
+ logger.debug("Initializing JwtStrategy...", context);
19
+ if (config.mcpAuthMode === "jwt") {
20
+ if (environment === "production" && !config.mcpAuthSecretKey) {
21
+ logger.fatal("CRITICAL: MCP_AUTH_SECRET_KEY is not set in production for JWT auth.", context);
22
+ throw new McpError(BaseErrorCode.CONFIGURATION_ERROR, "MCP_AUTH_SECRET_KEY must be set for JWT auth in production.", context);
23
+ }
24
+ else if (!config.mcpAuthSecretKey) {
25
+ logger.warning("MCP_AUTH_SECRET_KEY is not set. JWT auth will be bypassed (DEV ONLY).", context);
26
+ this.secretKey = null;
27
+ }
28
+ else {
29
+ logger.info("JWT secret key loaded successfully.", context);
30
+ this.secretKey = new TextEncoder().encode(config.mcpAuthSecretKey);
31
+ }
32
+ }
33
+ else {
34
+ this.secretKey = null;
35
+ }
36
+ }
37
+ async verify(token) {
38
+ const context = requestContextService.createRequestContext({
39
+ operation: "JwtStrategy.verify",
40
+ });
41
+ logger.debug("Attempting to verify JWT.", context);
42
+ // Handle development mode bypass
43
+ if (!this.secretKey) {
44
+ if (environment !== "production") {
45
+ logger.warning("Bypassing JWT verification: No secret key (DEV ONLY).", context);
46
+ return {
47
+ token: "dev-mode-placeholder-token",
48
+ clientId: config.devMcpClientId || "dev-client-id",
49
+ scopes: config.devMcpScopes || ["dev-scope"],
50
+ };
51
+ }
52
+ // This path is defensive. The constructor should prevent this state in production.
53
+ logger.crit("Auth secret key is missing in production.", context);
54
+ throw new McpError(BaseErrorCode.CONFIGURATION_ERROR, "Auth secret key is missing in production. This indicates a server configuration error.", context);
55
+ }
56
+ try {
57
+ const { payload: decoded } = await jwtVerify(token, this.secretKey);
58
+ logger.debug("JWT signature verified successfully.", {
59
+ ...context,
60
+ claims: decoded,
61
+ });
62
+ const clientId = typeof decoded.cid === "string"
63
+ ? decoded.cid
64
+ : typeof decoded.client_id === "string"
65
+ ? decoded.client_id
66
+ : undefined;
67
+ if (!clientId) {
68
+ logger.warning("Invalid token: missing 'cid' or 'client_id' claim.", context);
69
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, "Invalid token: missing 'cid' or 'client_id' claim.", context);
70
+ }
71
+ let scopes = [];
72
+ if (Array.isArray(decoded.scp) &&
73
+ decoded.scp.every((s) => typeof s === "string")) {
74
+ scopes = decoded.scp;
75
+ }
76
+ else if (typeof decoded.scope === "string" && decoded.scope.trim()) {
77
+ scopes = decoded.scope.split(" ").filter(Boolean);
78
+ }
79
+ if (scopes.length === 0) {
80
+ logger.warning("Invalid token: missing or empty 'scp' or 'scope' claim.", context);
81
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, "Token must contain valid, non-empty scopes.", context);
82
+ }
83
+ const authInfo = {
84
+ token,
85
+ clientId,
86
+ scopes,
87
+ subject: decoded.sub,
88
+ };
89
+ logger.info("JWT verification successful.", {
90
+ ...context,
91
+ clientId,
92
+ scopes,
93
+ });
94
+ return authInfo;
95
+ }
96
+ catch (error) {
97
+ const message = error instanceof Error && error.name === "JWTExpired"
98
+ ? "Token has expired."
99
+ : "Token verification failed.";
100
+ logger.warning(`JWT verification failed: ${message}`, {
101
+ ...context,
102
+ errorName: error instanceof Error ? error.name : "Unknown",
103
+ });
104
+ throw ErrorHandler.handleError(error, {
105
+ operation: "JwtStrategy.verify",
106
+ context,
107
+ rethrow: true,
108
+ errorCode: BaseErrorCode.UNAUTHORIZED,
109
+ errorMapper: () => new McpError(BaseErrorCode.UNAUTHORIZED, message, context),
110
+ });
111
+ }
112
+ }
113
+ }
@@ -0,0 +1,102 @@
1
+ /**
2
+ * @fileoverview Implements the OAuth 2.1 authentication strategy.
3
+ * This module provides a concrete implementation of the AuthStrategy for validating
4
+ * JWTs against a remote JSON Web Key Set (JWKS), as is common in OAuth 2.1 flows.
5
+ * @module src/mcp-server/transports/auth/strategies/OauthStrategy
6
+ */
7
+ import { createRemoteJWKSet, jwtVerify } from "jose";
8
+ import { config } from "../../../../config/index.js";
9
+ import { BaseErrorCode, McpError } from "../../../../types-global/errors.js";
10
+ import { ErrorHandler, logger, requestContextService, } from "../../../../utils/index.js";
11
+ export class OauthStrategy {
12
+ jwks;
13
+ constructor() {
14
+ const context = requestContextService.createRequestContext({
15
+ operation: "OauthStrategy.constructor",
16
+ });
17
+ logger.debug("Initializing OauthStrategy...", context);
18
+ if (config.mcpAuthMode !== "oauth") {
19
+ // This check is for internal consistency, so a standard Error is acceptable here.
20
+ throw new Error("OauthStrategy instantiated for non-oauth auth mode.");
21
+ }
22
+ if (!config.oauthIssuerUrl || !config.oauthAudience) {
23
+ logger.fatal("CRITICAL: OAUTH_ISSUER_URL and OAUTH_AUDIENCE must be set for OAuth mode.", context);
24
+ // This is a user-facing configuration error, so McpError is appropriate.
25
+ throw new McpError(BaseErrorCode.CONFIGURATION_ERROR, "OAUTH_ISSUER_URL and OAUTH_AUDIENCE must be set for OAuth mode.", context);
26
+ }
27
+ try {
28
+ const jwksUrl = new URL(config.oauthJwksUri ||
29
+ `${config.oauthIssuerUrl.replace(/\/$/, "")}/.well-known/jwks.json`);
30
+ this.jwks = createRemoteJWKSet(jwksUrl, {
31
+ cooldownDuration: 300000, // 5 minutes
32
+ timeoutDuration: 5000, // 5 seconds
33
+ });
34
+ logger.info(`JWKS client initialized for URL: ${jwksUrl.href}`, context);
35
+ }
36
+ catch (error) {
37
+ logger.fatal("Failed to initialize JWKS client.", {
38
+ ...context,
39
+ error: error instanceof Error ? error.message : String(error),
40
+ });
41
+ // This is a critical startup failure, so a specific McpError is warranted.
42
+ throw new McpError(BaseErrorCode.SERVICE_UNAVAILABLE, "Could not initialize JWKS client for OAuth strategy.", {
43
+ ...context,
44
+ originalError: error instanceof Error ? error.message : "Unknown",
45
+ });
46
+ }
47
+ }
48
+ async verify(token) {
49
+ const context = requestContextService.createRequestContext({
50
+ operation: "OauthStrategy.verify",
51
+ });
52
+ logger.debug("Attempting to verify OAuth token via JWKS.", context);
53
+ try {
54
+ const { payload } = await jwtVerify(token, this.jwks, {
55
+ issuer: config.oauthIssuerUrl,
56
+ audience: config.oauthAudience,
57
+ });
58
+ logger.debug("OAuth token signature verified successfully.", {
59
+ ...context,
60
+ claims: payload,
61
+ });
62
+ const scopes = typeof payload.scope === "string" ? payload.scope.split(" ") : [];
63
+ if (scopes.length === 0) {
64
+ logger.warning("Invalid token: missing or empty 'scope' claim.", context);
65
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, "Token must contain valid, non-empty scopes.", context);
66
+ }
67
+ const clientId = typeof payload.client_id === "string" ? payload.client_id : undefined;
68
+ if (!clientId) {
69
+ logger.warning("Invalid token: missing 'client_id' claim.", context);
70
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, "Token must contain a 'client_id' claim.", context);
71
+ }
72
+ const authInfo = {
73
+ token,
74
+ clientId,
75
+ scopes,
76
+ subject: typeof payload.sub === "string" ? payload.sub : undefined,
77
+ };
78
+ logger.info("OAuth token verification successful.", {
79
+ ...context,
80
+ clientId,
81
+ scopes,
82
+ });
83
+ return authInfo;
84
+ }
85
+ catch (error) {
86
+ const message = error instanceof Error && error.name === "JWTExpired"
87
+ ? "Token has expired."
88
+ : "OAuth token verification failed.";
89
+ logger.warning(`OAuth token verification failed: ${message}`, {
90
+ ...context,
91
+ errorName: error instanceof Error ? error.name : "Unknown",
92
+ });
93
+ throw ErrorHandler.handleError(error, {
94
+ operation: "OauthStrategy.verify",
95
+ context,
96
+ rethrow: true,
97
+ errorCode: BaseErrorCode.UNAUTHORIZED,
98
+ errorMapper: () => new McpError(BaseErrorCode.UNAUTHORIZED, message, context),
99
+ });
100
+ }
101
+ }
102
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @fileoverview Abstract base class for transport managers.
3
+ * @module src/mcp-server/transports/core/baseTransportManager
4
+ */
5
+ import { logger, requestContextService, } from "../../../utils/index.js";
6
+ /**
7
+ * Abstract base class for transport managers, providing common functionality.
8
+ */
9
+ export class BaseTransportManager {
10
+ createServerInstanceFn;
11
+ constructor(createServerInstanceFn) {
12
+ const context = requestContextService.createRequestContext({
13
+ operation: "BaseTransportManager.constructor",
14
+ managerType: this.constructor.name,
15
+ });
16
+ logger.debug("Initializing transport manager.", context);
17
+ this.createServerInstanceFn = createServerInstanceFn;
18
+ }
19
+ }