@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
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @fileoverview Provides a bridge between the MCP SDK's Node.js-style
3
+ * streamable HTTP transport and Hono's Web Standards-based streaming response.
4
+ * @module src/mcp-server/transports/core/honoNodeBridge
5
+ */
6
+ import { PassThrough } from "stream";
7
+ /**
8
+ * A mock ServerResponse that pipes writes to a PassThrough stream.
9
+ * This is the bridge between Model Context Protocol's SDK's Node.js-style response handling
10
+ * and Hono's stream-based body. It captures status and headers.
11
+ */
12
+ export class HonoStreamResponse extends PassThrough {
13
+ statusCode = 200;
14
+ headers = {};
15
+ constructor() {
16
+ super();
17
+ }
18
+ writeHead(statusCode, headers) {
19
+ this.statusCode = statusCode;
20
+ if (headers) {
21
+ this.headers = { ...this.headers, ...headers };
22
+ }
23
+ return this;
24
+ }
25
+ setHeader(name, value) {
26
+ this.headers[name.toLowerCase()] = value;
27
+ return this;
28
+ }
29
+ getHeader(name) {
30
+ return this.headers[name.toLowerCase()];
31
+ }
32
+ getHeaders() {
33
+ return this.headers;
34
+ }
35
+ removeHeader(name) {
36
+ delete this.headers[name.toLowerCase()];
37
+ }
38
+ write(chunk, encodingOrCallback, callback) {
39
+ const encoding = typeof encodingOrCallback === "string" ? encodingOrCallback : undefined;
40
+ const cb = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
41
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
+ return super.write(chunk, encoding, cb);
43
+ }
44
+ end(chunk, encodingOrCallback, callback) {
45
+ const encoding = typeof encodingOrCallback === "string" ? encodingOrCallback : undefined;
46
+ const cb = typeof encodingOrCallback === "function" ? encodingOrCallback : callback;
47
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
+ super.end(chunk, encoding, cb);
49
+ return this;
50
+ }
51
+ }
@@ -0,0 +1,234 @@
1
+ /**
2
+ * @fileoverview Stateful Transport Manager implementation for MCP SDK.
3
+ * This manager handles multiple, persistent sessions, creating a dedicated
4
+ * McpServer and StreamableHTTPServerTransport instance for each one.
5
+ * This version is adapted for Hono by bridging the SDK's Node.js-style
6
+ * request handling with Hono's stream-based response model.
7
+ * @module src/mcp-server/transports/core/statefulTransportManager
8
+ */
9
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
10
+ import { randomUUID } from "node:crypto";
11
+ import { Readable } from "stream";
12
+ import { config } from "../../../config/index.js";
13
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
14
+ import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
15
+ import { BaseTransportManager } from "./baseTransportManager.js";
16
+ import { HonoStreamResponse } from "./honoNodeBridge.js";
17
+ /**
18
+ * Stateful Transport Manager that handles MCP SDK integration and session management
19
+ * for a Hono-based HTTP server.
20
+ */
21
+ export class StatefulTransportManager extends BaseTransportManager {
22
+ transports = new Map();
23
+ servers = new Map();
24
+ sessions = new Map();
25
+ garbageCollector;
26
+ constructor(createServerInstanceFn) {
27
+ super(createServerInstanceFn);
28
+ const context = requestContextService.createRequestContext({
29
+ operation: "StatefulTransportManager.constructor",
30
+ });
31
+ logger.info("Starting session garbage collector.", context);
32
+ this.garbageCollector = setInterval(() => this.cleanupStaleSessions(), config.mcpStatefulSessionStaleTimeoutMs);
33
+ }
34
+ async initializeAndHandle(headers, body, context) {
35
+ const operationName = "StatefulTransportManager.initializeAndHandle";
36
+ const opContext = { ...context, operation: operationName };
37
+ logger.debug("Initializing new stateful session.", opContext);
38
+ const server = await this.createServerInstanceFn();
39
+ const mockRes = new HonoStreamResponse();
40
+ const transport = new StreamableHTTPServerTransport({
41
+ sessionIdGenerator: () => randomUUID(),
42
+ onsessioninitialized: (sessionId) => {
43
+ const sessionContext = { ...opContext, sessionId };
44
+ this.transports.set(sessionId, transport);
45
+ this.servers.set(sessionId, server);
46
+ this.sessions.set(sessionId, {
47
+ id: sessionId,
48
+ createdAt: new Date(),
49
+ lastAccessedAt: new Date(),
50
+ });
51
+ logger.info(`MCP Session created: ${sessionId}`, sessionContext);
52
+ },
53
+ });
54
+ transport.onclose = () => {
55
+ const sessionId = transport.sessionId;
56
+ if (sessionId) {
57
+ const closeContext = requestContextService.createRequestContext({
58
+ operation: "StatefulTransportManager.transport.onclose",
59
+ sessionId,
60
+ });
61
+ this.closeSession(sessionId, closeContext).catch((err) => logger.error(`Error during transport.onclose cleanup for session ${sessionId}`, err, closeContext));
62
+ }
63
+ };
64
+ await server.connect(transport);
65
+ logger.debug("Server connected to transport, handling initial request.", opContext);
66
+ const mockReq = {
67
+ headers,
68
+ method: "POST",
69
+ url: config.mcpHttpEndpointPath,
70
+ };
71
+ await transport.handleRequest(mockReq, mockRes, body);
72
+ const responseHeaders = new Headers();
73
+ for (const [key, value] of Object.entries(mockRes.getHeaders())) {
74
+ responseHeaders.set(key, Array.isArray(value) ? value.join(", ") : String(value));
75
+ }
76
+ if (transport.sessionId) {
77
+ responseHeaders.set("Mcp-Session-Id", transport.sessionId);
78
+ }
79
+ const webStream = Readable.toWeb(mockRes);
80
+ return {
81
+ headers: responseHeaders,
82
+ statusCode: mockRes.statusCode,
83
+ stream: webStream,
84
+ sessionId: transport.sessionId,
85
+ };
86
+ }
87
+ async handleRequest(headers, body, context, sessionId) {
88
+ if (!sessionId) {
89
+ throw new McpError(BaseErrorCode.INVALID_INPUT, "Session ID is required for stateful requests.", context);
90
+ }
91
+ const sessionContext = {
92
+ ...context,
93
+ sessionId,
94
+ operation: "StatefulTransportManager.handleRequest",
95
+ };
96
+ logger.debug(`Handling request for session: ${sessionId}`, {
97
+ ...sessionContext,
98
+ method: headers["x-forwarded-proto"] || "http",
99
+ });
100
+ const transport = this.transports.get(sessionId);
101
+ if (!transport) {
102
+ logger.warning(`Request for non-existent session: ${sessionId}`, sessionContext);
103
+ return {
104
+ headers: new Headers({ "Content-Type": "application/json" }),
105
+ statusCode: 404,
106
+ body: {
107
+ jsonrpc: "2.0",
108
+ error: { code: -32601, message: "Session not found" },
109
+ },
110
+ };
111
+ }
112
+ const session = this.sessions.get(sessionId);
113
+ if (session) {
114
+ session.lastAccessedAt = new Date();
115
+ logger.debug(`Updated lastAccessedAt for session ${sessionId}.`, sessionContext);
116
+ }
117
+ const mockReq = {
118
+ headers,
119
+ method: "POST",
120
+ };
121
+ const mockRes = new HonoStreamResponse();
122
+ await transport.handleRequest(mockReq, mockRes, body);
123
+ const responseHeaders = new Headers();
124
+ for (const [key, value] of Object.entries(mockRes.getHeaders())) {
125
+ responseHeaders.set(key, Array.isArray(value) ? value.join(", ") : String(value));
126
+ }
127
+ const webStream = Readable.toWeb(mockRes);
128
+ return {
129
+ headers: responseHeaders,
130
+ statusCode: mockRes.statusCode,
131
+ stream: webStream,
132
+ sessionId: transport.sessionId,
133
+ };
134
+ }
135
+ async handleDeleteRequest(sessionId, context) {
136
+ const sessionContext = {
137
+ ...context,
138
+ sessionId,
139
+ operation: "StatefulTransportManager.handleDeleteRequest",
140
+ };
141
+ logger.info(`Attempting to delete session: ${sessionId}`, sessionContext);
142
+ const transport = this.transports.get(sessionId);
143
+ if (!transport) {
144
+ logger.warning(`Attempted to delete non-existent session: ${sessionId}`, sessionContext);
145
+ throw new McpError(BaseErrorCode.NOT_FOUND, "Session not found or expired.", sessionContext);
146
+ }
147
+ await this.closeSession(sessionId, sessionContext);
148
+ const headers = new Headers();
149
+ headers.set("Content-Type", "application/json");
150
+ return {
151
+ headers,
152
+ statusCode: 200,
153
+ body: { status: "session_closed", sessionId },
154
+ };
155
+ }
156
+ getSession(sessionId) {
157
+ const context = requestContextService.createRequestContext({
158
+ operation: "StatefulTransportManager.getSession",
159
+ sessionId,
160
+ });
161
+ logger.debug(`Retrieving session: ${sessionId}`, context);
162
+ return this.sessions.get(sessionId);
163
+ }
164
+ async shutdown() {
165
+ const context = requestContextService.createRequestContext({
166
+ operation: "StatefulTransportManager.shutdown",
167
+ });
168
+ logger.info("Shutting down stateful transport manager...", context);
169
+ clearInterval(this.garbageCollector);
170
+ logger.debug("Garbage collector stopped.", context);
171
+ const sessionIds = Array.from(this.transports.keys());
172
+ logger.info(`Closing ${sessionIds.length} active sessions.`, context);
173
+ const closePromises = sessionIds.map((sessionId) => this.closeSession(sessionId, context));
174
+ await Promise.all(closePromises);
175
+ this.transports.clear();
176
+ this.sessions.clear();
177
+ this.servers.clear();
178
+ logger.info("All active sessions closed and manager shut down.", context);
179
+ }
180
+ async closeSession(sessionId, context) {
181
+ const sessionContext = {
182
+ ...context,
183
+ sessionId,
184
+ operation: "StatefulTransportManager.closeSession",
185
+ };
186
+ logger.debug(`Closing session: ${sessionId}`, sessionContext);
187
+ const transport = this.transports.get(sessionId);
188
+ const server = this.servers.get(sessionId);
189
+ await ErrorHandler.tryCatch(async () => {
190
+ if (transport) {
191
+ await transport.close();
192
+ logger.debug(`Transport closed for session ${sessionId}.`, sessionContext);
193
+ }
194
+ if (server) {
195
+ await server.close();
196
+ logger.debug(`Server instance closed for session ${sessionId}.`, sessionContext);
197
+ }
198
+ }, {
199
+ operation: "closeSession.cleanup",
200
+ context: sessionContext,
201
+ });
202
+ this.transports.delete(sessionId);
203
+ this.servers.delete(sessionId);
204
+ this.sessions.delete(sessionId);
205
+ logger.info(`MCP Session closed and resources released: ${sessionId}`, sessionContext);
206
+ }
207
+ async cleanupStaleSessions() {
208
+ const context = requestContextService.createRequestContext({
209
+ operation: "StatefulTransportManager.cleanupStaleSessions",
210
+ });
211
+ logger.debug("Running stale session cleanup...", context);
212
+ const now = Date.now();
213
+ const STALE_TIMEOUT_MS = config.mcpStatefulSessionStaleTimeoutMs;
214
+ let staleCount = 0;
215
+ for (const [sessionId, session] of this.sessions.entries()) {
216
+ if (now - session.lastAccessedAt.getTime() > STALE_TIMEOUT_MS) {
217
+ staleCount++;
218
+ const sessionContext = {
219
+ ...context,
220
+ sessionId,
221
+ lastAccessed: session.lastAccessedAt.toISOString(),
222
+ };
223
+ logger.info(`Found stale session, closing: ${sessionId}`, sessionContext);
224
+ await this.closeSession(sessionId, sessionContext);
225
+ }
226
+ }
227
+ if (staleCount > 0) {
228
+ logger.info(`Stale session cleanup complete. Closed ${staleCount} sessions.`, context);
229
+ }
230
+ else {
231
+ logger.debug("No stale sessions found.", context);
232
+ }
233
+ }
234
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * @fileoverview Stateless Transport Manager implementation for MCP SDK.
3
+ * This manager handles single-request operations without maintaining sessions.
4
+ * Each request creates a temporary server instance that is cleaned up immediately.
5
+ * This version is adapted for Hono by bridging the SDK's Node.js-style
6
+ * request handling with Hono's stream-based response model.
7
+ * @module src/mcp-server/transports/core/statelessTransportManager
8
+ */
9
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
10
+ import { Readable } from "stream";
11
+ import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
12
+ import { BaseTransportManager } from "./baseTransportManager.js";
13
+ import { HonoStreamResponse } from "./honoNodeBridge.js";
14
+ /**
15
+ * Stateless Transport Manager that handles ephemeral MCP operations.
16
+ */
17
+ export class StatelessTransportManager extends BaseTransportManager {
18
+ async handleRequest(headers, body, context) {
19
+ const opContext = {
20
+ ...context,
21
+ operation: "StatelessTransportManager.handleRequest",
22
+ };
23
+ logger.debug("Creating ephemeral server instance for stateless request.", opContext);
24
+ let server;
25
+ let transport;
26
+ try {
27
+ server = await this.createServerInstanceFn();
28
+ transport = new StreamableHTTPServerTransport({
29
+ sessionIdGenerator: undefined,
30
+ onsessioninitialized: undefined,
31
+ });
32
+ await server.connect(transport);
33
+ logger.debug("Ephemeral server connected to transport.", opContext);
34
+ const mockReq = {
35
+ headers,
36
+ method: "POST",
37
+ };
38
+ const mockRes = new HonoStreamResponse();
39
+ await transport.handleRequest(mockReq, mockRes, body);
40
+ logger.info("Stateless request handled successfully.", opContext);
41
+ const responseHeaders = new Headers();
42
+ for (const [key, value] of Object.entries(mockRes.getHeaders())) {
43
+ responseHeaders.set(key, Array.isArray(value) ? value.join(", ") : String(value));
44
+ }
45
+ // Bridge the Node.js stream (PassThrough) to a Web Stream (ReadableStream)
46
+ const webStream = Readable.toWeb(mockRes);
47
+ return {
48
+ headers: responseHeaders,
49
+ statusCode: mockRes.statusCode,
50
+ stream: webStream,
51
+ };
52
+ }
53
+ catch (error) {
54
+ throw ErrorHandler.handleError(error, {
55
+ operation: "StatelessTransportManager.handleRequest",
56
+ context: opContext,
57
+ rethrow: true,
58
+ });
59
+ }
60
+ finally {
61
+ if (server || transport) {
62
+ this.cleanup(server, transport, opContext);
63
+ }
64
+ }
65
+ }
66
+ async shutdown() {
67
+ const context = requestContextService.createRequestContext({
68
+ operation: "StatelessTransportManager.shutdown",
69
+ });
70
+ logger.info("Stateless transport manager shutdown - no persistent resources to clean up.", context);
71
+ return Promise.resolve();
72
+ }
73
+ cleanup(server, transport, context) {
74
+ const opContext = {
75
+ ...context,
76
+ operation: "StatelessTransportManager.cleanup",
77
+ };
78
+ logger.debug("Scheduling cleanup for ephemeral resources.", opContext);
79
+ Promise.all([transport?.close(), server?.close()])
80
+ .then(() => {
81
+ logger.debug("Ephemeral resources cleaned up successfully.", opContext);
82
+ })
83
+ .catch((cleanupError) => {
84
+ logger.warning("Error during stateless resource cleanup.", {
85
+ ...opContext,
86
+ error: cleanupError instanceof Error
87
+ ? cleanupError.message
88
+ : String(cleanupError),
89
+ });
90
+ });
91
+ }
92
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @fileoverview Core types and interfaces for the transport layer abstraction.
3
+ * @module src/mcp-server/transports/core/transportTypes
4
+ */
5
+ export {};
@@ -5,8 +5,8 @@
5
5
  * formats them into a consistent JSON-RPC error response.
6
6
  * @module src/mcp-server/transports/httpErrorHandler
7
7
  */
8
- import { BaseErrorCode, McpError } from "../../types-global/errors.js";
9
- import { ErrorHandler, requestContextService } from "../../utils/index.js";
8
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
9
+ import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
10
10
  /**
11
11
  * A centralized error handling middleware for Hono.
12
12
  * This function is registered with `app.onError()` and will catch any errors
@@ -22,6 +22,7 @@ export const httpErrorHandler = async (err, c) => {
22
22
  path: c.req.path,
23
23
  method: c.req.method,
24
24
  });
25
+ logger.debug("HTTP error handler invoked.", context);
25
26
  const handledError = ErrorHandler.handleError(err, {
26
27
  operation: "httpTransport",
27
28
  context,
@@ -39,6 +40,7 @@ export const httpErrorHandler = async (err, c) => {
39
40
  status = 403;
40
41
  break;
41
42
  case BaseErrorCode.VALIDATION_ERROR:
43
+ case BaseErrorCode.INVALID_INPUT:
42
44
  status = 400;
43
45
  break;
44
46
  case BaseErrorCode.CONFLICT:
@@ -51,23 +53,46 @@ export const httpErrorHandler = async (err, c) => {
51
53
  status = 500;
52
54
  }
53
55
  }
56
+ logger.debug(`Mapping error to HTTP status ${status}.`, {
57
+ ...context,
58
+ status,
59
+ errorCode: handledError.code,
60
+ });
54
61
  // Attempt to get the request ID from the body, but don't fail if it's not there or unreadable.
55
62
  let requestId = null;
56
- try {
57
- const body = await c.req.json();
58
- requestId = body?.id || null;
63
+ // Only attempt to read the body if it hasn't been consumed already.
64
+ if (c.req.raw.bodyUsed === false) {
65
+ try {
66
+ const body = await c.req.json();
67
+ requestId = body?.id || null;
68
+ logger.debug("Extracted JSON-RPC request ID from body.", {
69
+ ...context,
70
+ jsonRpcId: requestId,
71
+ });
72
+ }
73
+ catch {
74
+ logger.warning("Could not parse request body to extract JSON-RPC ID.", context);
75
+ // Ignore parsing errors, requestId will remain null
76
+ }
59
77
  }
60
- catch {
61
- // Ignore parsing errors, requestId will remain null
78
+ else {
79
+ logger.debug("Request body already consumed, cannot extract JSON-RPC ID.", context);
62
80
  }
63
81
  const errorCode = handledError instanceof McpError ? handledError.code : -32603;
64
82
  c.status(status);
65
- return c.json({
83
+ const errorResponse = {
66
84
  jsonrpc: "2.0",
67
85
  error: {
68
86
  code: errorCode,
69
87
  message: handledError.message,
70
88
  },
71
89
  id: requestId,
90
+ };
91
+ logger.info(`Sending formatted error response for request.`, {
92
+ ...context,
93
+ status,
94
+ errorCode,
95
+ jsonRpcId: requestId,
72
96
  });
97
+ return c.json(errorResponse);
73
98
  };