@cyanheads/git-mcp-server 2.2.2 → 2.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -122
- package/dist/config/index.js +248 -53
- package/dist/mcp-server/resources/gitWorkingDir/index.js +5 -0
- package/dist/mcp-server/resources/gitWorkingDir/logic.js +17 -0
- package/dist/mcp-server/resources/gitWorkingDir/registration.js +64 -0
- package/dist/mcp-server/server.js +11 -6
- package/dist/mcp-server/tools/gitCherryPick/logic.js +39 -13
- package/dist/mcp-server/tools/gitCommit/logic.js +1 -1
- package/dist/mcp-server/tools/gitMerge/logic.js +38 -13
- package/dist/mcp-server/tools/gitTag/logic.js +38 -19
- package/dist/mcp-server/transports/auth/authFactory.js +41 -0
- package/dist/mcp-server/transports/auth/authMiddleware.js +57 -0
- package/dist/mcp-server/transports/auth/index.js +6 -4
- package/dist/mcp-server/transports/auth/lib/authTypes.js +8 -0
- package/dist/mcp-server/transports/auth/{core → lib}/authUtils.js +21 -14
- package/dist/mcp-server/transports/auth/strategies/authStrategy.js +1 -0
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js +113 -0
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js +102 -0
- package/dist/mcp-server/transports/core/baseTransportManager.js +19 -0
- package/dist/mcp-server/transports/core/honoNodeBridge.js +51 -0
- package/dist/mcp-server/transports/core/statefulTransportManager.js +234 -0
- package/dist/mcp-server/transports/core/statelessTransportManager.js +92 -0
- package/dist/mcp-server/transports/core/transportTypes.js +5 -0
- package/dist/mcp-server/transports/{httpErrorHandler.js → http/httpErrorHandler.js} +33 -8
- package/dist/mcp-server/transports/http/httpTransport.js +254 -0
- package/dist/mcp-server/transports/http/httpTypes.js +5 -0
- package/dist/mcp-server/transports/http/index.js +6 -0
- package/dist/mcp-server/transports/http/mcpTransportMiddleware.js +63 -0
- package/dist/mcp-server/transports/stdio/index.js +5 -0
- package/dist/mcp-server/transports/{stdioTransport.js → stdio/stdioTransport.js} +10 -5
- package/dist/types-global/errors.js +75 -19
- package/dist/utils/internal/errorHandler.js +11 -13
- package/package.json +18 -7
- package/dist/mcp-server/transports/auth/core/authTypes.js +0 -5
- package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.js +0 -149
- package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.js +0 -127
- package/dist/mcp-server/transports/httpTransport.js +0 -207
- /package/dist/mcp-server/transports/auth/{core → lib}/authContext.js +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Handles registration for the git working directory resource.
|
|
3
|
+
* @module src/mcp-server/resources/gitWorkingDir/registration
|
|
4
|
+
*/
|
|
5
|
+
import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
6
|
+
import { logger, requestContextService } from "../../../utils/index.js";
|
|
7
|
+
import { getGitWorkingDirLogic } from "./logic.js";
|
|
8
|
+
import { ErrorHandler } from "../../../utils/index.js";
|
|
9
|
+
import { BaseErrorCode } from "../../../types-global/errors.js";
|
|
10
|
+
const RESOURCE_URI = "git://working-directory";
|
|
11
|
+
const RESOURCE_NAME = "git_working_directory";
|
|
12
|
+
const RESOURCE_DESCRIPTION = "A resource that returns the currently configured working directory for the Git session as a JSON object. Returns 'NOT_SET' if no directory is configured.";
|
|
13
|
+
/**
|
|
14
|
+
* Registers the Git Working Directory resource with the MCP server instance.
|
|
15
|
+
* @param server The MCP server instance.
|
|
16
|
+
* @param getWorkingDirectory Function to get the session's working directory.
|
|
17
|
+
* @param getSessionId Function to get the session ID from context.
|
|
18
|
+
*/
|
|
19
|
+
export const registerGitWorkingDirResource = async (server, getWorkingDirectory, getSessionId) => {
|
|
20
|
+
const operation = "registerGitWorkingDirResource";
|
|
21
|
+
const context = requestContextService.createRequestContext({ operation });
|
|
22
|
+
await ErrorHandler.tryCatch(async () => {
|
|
23
|
+
const template = new ResourceTemplate(RESOURCE_URI, {
|
|
24
|
+
list: async () => ({ resources: [] }),
|
|
25
|
+
});
|
|
26
|
+
server.resource(RESOURCE_NAME, template, {
|
|
27
|
+
name: "Current Git Working Directory",
|
|
28
|
+
description: RESOURCE_DESCRIPTION,
|
|
29
|
+
mimeType: "application/json",
|
|
30
|
+
}, async (uri, params, callContext) => {
|
|
31
|
+
const handlerContext = requestContextService.createRequestContext({
|
|
32
|
+
parentRequestId: context.requestId,
|
|
33
|
+
operation: "HandleResourceRead",
|
|
34
|
+
resourceUri: uri.href,
|
|
35
|
+
inputParams: params,
|
|
36
|
+
});
|
|
37
|
+
try {
|
|
38
|
+
const sessionId = getSessionId(handlerContext);
|
|
39
|
+
const workingDir = getGitWorkingDirLogic(handlerContext, getWorkingDirectory, sessionId);
|
|
40
|
+
const responseData = { workingDirectory: workingDir };
|
|
41
|
+
return {
|
|
42
|
+
contents: [{
|
|
43
|
+
uri: uri.href,
|
|
44
|
+
text: JSON.stringify(responseData),
|
|
45
|
+
mimeType: "application/json",
|
|
46
|
+
}],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw ErrorHandler.handleError(error, {
|
|
51
|
+
operation: "gitWorkingDirReadHandler",
|
|
52
|
+
context: handlerContext,
|
|
53
|
+
input: { uri: uri.href, params },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
logger.info(`Resource '${RESOURCE_NAME}' registered successfully.`, context);
|
|
58
|
+
}, {
|
|
59
|
+
operation: `RegisteringResource_${RESOURCE_NAME}`,
|
|
60
|
+
context: context,
|
|
61
|
+
errorCode: BaseErrorCode.INITIALIZATION_FAILED,
|
|
62
|
+
critical: true,
|
|
63
|
+
});
|
|
64
|
+
};
|
|
@@ -36,9 +36,11 @@ import { registerGitStatusTool } from "./tools/gitStatus/index.js";
|
|
|
36
36
|
import { registerGitTagTool } from "./tools/gitTag/index.js";
|
|
37
37
|
import { registerGitWorktreeTool } from "./tools/gitWorktree/index.js";
|
|
38
38
|
import { registerGitWrapupInstructionsTool } from "./tools/gitWrapupInstructions/index.js";
|
|
39
|
+
// Import registration functions for ALL resources
|
|
40
|
+
import { registerGitWorkingDirResource } from "./resources/gitWorkingDir/index.js";
|
|
39
41
|
// Import transport setup functions
|
|
40
|
-
import { startHttpTransport } from "./transports/
|
|
41
|
-
import {
|
|
42
|
+
import { startHttpTransport } from "./transports/http/index.js";
|
|
43
|
+
import { startStdioTransport } from "./transports/stdio/index.js";
|
|
42
44
|
async function createMcpServerInstance() {
|
|
43
45
|
const context = requestContextService.createRequestContext({
|
|
44
46
|
operation: "createMcpServerInstance",
|
|
@@ -103,6 +105,9 @@ async function createMcpServerInstance() {
|
|
|
103
105
|
await registerGitWorktreeTool(server, getWorkingDirectory, getSessionIdFromContext);
|
|
104
106
|
await registerGitWrapupInstructionsTool(server, getWorkingDirectory, getSessionIdFromContext);
|
|
105
107
|
logger.info("Git tools registered successfully", context);
|
|
108
|
+
logger.debug("Registering Git resources...", context);
|
|
109
|
+
await registerGitWorkingDirResource(server, getWorkingDirectory, getSessionIdFromContext);
|
|
110
|
+
logger.info("Git resources registered successfully", context);
|
|
106
111
|
}
|
|
107
112
|
catch (err) {
|
|
108
113
|
logger.error("Failed to register resources/tools", {
|
|
@@ -121,13 +126,13 @@ async function startTransport() {
|
|
|
121
126
|
transport: transportType,
|
|
122
127
|
});
|
|
123
128
|
logger.info(`Starting transport: ${transportType}`, context);
|
|
124
|
-
const server = await createMcpServerInstance();
|
|
125
129
|
if (transportType === "http") {
|
|
126
|
-
await startHttpTransport(
|
|
127
|
-
return;
|
|
130
|
+
const { server } = await startHttpTransport(createMcpServerInstance, context);
|
|
131
|
+
return server;
|
|
128
132
|
}
|
|
129
133
|
if (transportType === "stdio") {
|
|
130
|
-
await
|
|
134
|
+
const server = await createMcpServerInstance();
|
|
135
|
+
await startStdioTransport(server, context);
|
|
131
136
|
return server;
|
|
132
137
|
}
|
|
133
138
|
logger.fatal(`Unsupported transport type configured: ${transportType}`, context);
|
|
@@ -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
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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 =
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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 {
|
|
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 "./
|
|
7
|
-
export { withRequiredScopes } from "./
|
|
8
|
-
export {
|
|
9
|
-
export {
|
|
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.",
|
|
26
|
-
|
|
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
|
|
35
|
-
|
|
36
|
-
|
|
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 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
}
|