obsidian-mcp-server 2.0.4 → 2.0.6

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 (44) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +80 -88
  3. package/dist/mcp-server/server.js +8 -8
  4. package/dist/mcp-server/tools/{obsidianDeleteFileTool → obsidianDeleteNoteTool}/index.d.ts +4 -4
  5. package/dist/mcp-server/tools/{obsidianDeleteFileTool → obsidianDeleteNoteTool}/index.js +4 -4
  6. package/dist/mcp-server/tools/{obsidianDeleteFileTool → obsidianDeleteNoteTool}/logic.d.ts +9 -9
  7. package/dist/mcp-server/tools/{obsidianDeleteFileTool → obsidianDeleteNoteTool}/logic.js +6 -6
  8. package/dist/mcp-server/tools/{obsidianDeleteFileTool → obsidianDeleteNoteTool}/registration.d.ts +2 -2
  9. package/dist/mcp-server/tools/{obsidianDeleteFileTool → obsidianDeleteNoteTool}/registration.js +12 -12
  10. package/dist/mcp-server/tools/{obsidianListFilesTool → obsidianListNotesTool}/index.d.ts +4 -4
  11. package/dist/mcp-server/tools/{obsidianListFilesTool → obsidianListNotesTool}/index.js +4 -4
  12. package/dist/mcp-server/tools/{obsidianListFilesTool → obsidianListNotesTool}/logic.d.ts +10 -10
  13. package/dist/mcp-server/tools/{obsidianListFilesTool → obsidianListNotesTool}/logic.js +14 -13
  14. package/dist/mcp-server/tools/{obsidianListFilesTool → obsidianListNotesTool}/registration.d.ts +4 -4
  15. package/dist/mcp-server/tools/{obsidianListFilesTool → obsidianListNotesTool}/registration.js +14 -14
  16. package/dist/mcp-server/tools/{obsidianReadFileTool → obsidianReadNoteTool}/index.d.ts +4 -4
  17. package/dist/mcp-server/tools/{obsidianReadFileTool → obsidianReadNoteTool}/index.js +4 -4
  18. package/dist/mcp-server/tools/{obsidianReadFileTool → obsidianReadNoteTool}/logic.d.ts +9 -9
  19. package/dist/mcp-server/tools/{obsidianReadFileTool → obsidianReadNoteTool}/logic.js +8 -8
  20. package/dist/mcp-server/tools/{obsidianReadFileTool → obsidianReadNoteTool}/registration.d.ts +2 -2
  21. package/dist/mcp-server/tools/{obsidianReadFileTool → obsidianReadNoteTool}/registration.js +12 -12
  22. package/dist/mcp-server/tools/{obsidianUpdateFileTool → obsidianUpdateNoteTool}/index.d.ts +4 -4
  23. package/dist/mcp-server/tools/{obsidianUpdateFileTool → obsidianUpdateNoteTool}/index.js +4 -4
  24. package/dist/mcp-server/tools/{obsidianUpdateFileTool → obsidianUpdateNoteTool}/logic.d.ts +8 -8
  25. package/dist/mcp-server/tools/{obsidianUpdateFileTool → obsidianUpdateNoteTool}/logic.js +7 -7
  26. package/dist/mcp-server/tools/{obsidianUpdateFileTool → obsidianUpdateNoteTool}/registration.d.ts +2 -2
  27. package/dist/mcp-server/tools/{obsidianUpdateFileTool → obsidianUpdateNoteTool}/registration.js +13 -13
  28. package/dist/mcp-server/transports/{authentication → auth/core}/authContext.d.ts +2 -2
  29. package/dist/mcp-server/transports/{authentication → auth/core}/authContext.js +1 -1
  30. package/dist/mcp-server/transports/{authentication/types.d.ts → auth/core/authTypes.d.ts} +1 -1
  31. package/dist/mcp-server/transports/{authentication/types.js → auth/core/authTypes.js} +1 -1
  32. package/dist/mcp-server/transports/{authentication → auth/core}/authUtils.d.ts +1 -1
  33. package/dist/mcp-server/transports/{authentication → auth/core}/authUtils.js +3 -3
  34. package/dist/mcp-server/transports/auth/index.d.ts +10 -0
  35. package/dist/mcp-server/transports/auth/index.js +9 -0
  36. package/dist/mcp-server/transports/{authentication/authMiddleware.d.ts → auth/strategies/jwt/jwtMiddleware.d.ts} +4 -7
  37. package/dist/mcp-server/transports/{authentication/authMiddleware.js → auth/strategies/jwt/jwtMiddleware.js} +40 -36
  38. package/dist/mcp-server/transports/{authentication → auth/strategies/oauth}/oauthMiddleware.d.ts +2 -6
  39. package/dist/mcp-server/transports/{authentication → auth/strategies/oauth}/oauthMiddleware.js +33 -18
  40. package/dist/mcp-server/transports/httpErrorHandler.d.ts +26 -0
  41. package/dist/mcp-server/transports/httpErrorHandler.js +73 -0
  42. package/dist/mcp-server/transports/httpTransport.d.ts +11 -14
  43. package/dist/mcp-server/transports/httpTransport.js +91 -379
  44. package/package.json +14 -19
@@ -1,9 +1,9 @@
1
1
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
2
2
  import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
3
3
  // Import the Zod schema for validation and the core processing logic
4
- import { ObsidianUpdateFileInputSchema, ObsidianUpdateFileInputSchemaShape, processObsidianUpdateFile, } from "./logic.js";
4
+ import { ObsidianUpdateNoteInputSchema, ObsidianUpdateNoteInputSchemaShape, processObsidianUpdateNote, } from "./logic.js";
5
5
  /**
6
- * Registers the 'obsidian_update_file' tool with the MCP server.
6
+ * Registers the 'obsidian_update_note' tool with the MCP server.
7
7
  *
8
8
  * This tool allows modification of Obsidian notes (specified by file path,
9
9
  * the active file, or a periodic note) using whole-file operations:
@@ -20,26 +20,26 @@ import { ObsidianUpdateFileInputSchema, ObsidianUpdateFileInputSchemaShape, proc
20
20
  * @returns {Promise<void>} A promise that resolves when the tool registration is complete or rejects on error.
21
21
  * @throws {McpError} Throws an McpError if registration fails critically.
22
22
  */
23
- export const registerObsidianUpdateFileTool = async (server, obsidianService, vaultCacheService) => {
24
- const toolName = "obsidian_update_file";
23
+ export const registerObsidianUpdateNoteTool = async (server, obsidianService, vaultCacheService) => {
24
+ const toolName = "obsidian_update_note";
25
25
  const toolDescription = "Tool to modify Obsidian notes (specified by file path, the active file, or a periodic note) using whole-file operations: 'append', 'prepend', or 'overwrite'. Options allow creating missing files/targets and controlling overwrite behavior. Returns success status, message, a formatted timestamp string, file stats (stats), and optionally the final file content.";
26
26
  // Create a context for the registration process itself for better traceability.
27
27
  const registrationContext = requestContextService.createRequestContext({
28
- operation: "RegisterObsidianUpdateFileTool",
28
+ operation: "RegisterObsidianUpdateNoteTool",
29
29
  toolName: toolName,
30
- module: "ObsidianUpdateFileRegistration", // Identify the module performing registration
30
+ module: "ObsidianUpdateNoteRegistration", // Identify the module performing registration
31
31
  });
32
32
  logger.info(`Attempting to register tool: ${toolName}`, registrationContext);
33
33
  // Wrap the registration in a tryCatch block for robust error handling during setup.
34
34
  await ErrorHandler.tryCatch(async () => {
35
35
  // Use the high-level SDK method for tool registration.
36
36
  // This handles schema generation, validation, and routing automatically.
37
- server.tool(toolName, toolDescription, ObsidianUpdateFileInputSchemaShape, // Provide the Zod schema shape for input validation.
37
+ server.tool(toolName, toolDescription, ObsidianUpdateNoteInputSchemaShape, // Provide the Zod schema shape for input validation.
38
38
  /**
39
- * The handler function executed when the 'obsidian_update_file' tool is called.
39
+ * The handler function executed when the 'obsidian_update_note' tool is called.
40
40
  *
41
- * @param {ObsidianUpdateFileRegistrationInput} params - The raw input parameters received from the client,
42
- * matching the structure defined by ObsidianUpdateFileInputSchemaShape.
41
+ * @param {ObsidianUpdateNoteRegistrationInput} params - The raw input parameters received from the client,
42
+ * matching the structure defined by ObsidianUpdateNoteInputSchemaShape.
43
43
  * @returns {Promise<CallToolResult>} A promise resolving to the structured result for the MCP client,
44
44
  * containing either the successful response data or an error indication.
45
45
  */
@@ -47,7 +47,7 @@ export const registerObsidianUpdateFileTool = async (server, obsidianService, va
47
47
  // Create a specific context for this handler invocation.
48
48
  const handlerContext = requestContextService.createRequestContext({
49
49
  parentContext: registrationContext, // Link to the registration context
50
- operation: "HandleObsidianUpdateFileRequest",
50
+ operation: "HandleObsidianUpdateNoteRequest",
51
51
  toolName: toolName,
52
52
  params: {
53
53
  // Log key parameters for easier debugging, content is omitted for brevity/security
@@ -67,10 +67,10 @@ export const registerObsidianUpdateFileTool = async (server, obsidianService, va
67
67
  // This ensures type safety and adherence to constraints defined in logic.ts.
68
68
  // While server.tool performs initial validation based on the shape,
69
69
  // this step applies any stricter rules or refinements from the full schema.
70
- const validatedParams = ObsidianUpdateFileInputSchema.parse(params);
70
+ const validatedParams = ObsidianUpdateNoteInputSchema.parse(params);
71
71
  // Delegate the actual file update logic to the dedicated processing function.
72
72
  // Pass the validated parameters, the handler context, and the Obsidian service instance.
73
- const response = await processObsidianUpdateFile(validatedParams, handlerContext, obsidianService, vaultCacheService);
73
+ const response = await processObsidianUpdateNote(validatedParams, handlerContext, obsidianService, vaultCacheService);
74
74
  logger.debug(`'${toolName}' (wholeFile mode) processed successfully`, handlerContext);
75
75
  // Format the successful response from the logic function into the MCP CallToolResult structure.
76
76
  // The response object (containing status, message, timestamp, stat, etc.) is serialized to JSON.
@@ -5,10 +5,10 @@
5
5
  * from the middleware layer down to the tool and resource handlers without
6
6
  * drilling props.
7
7
  *
8
- * @module src/mcp-server/transports/authentication/authContext
8
+ * @module src/mcp-server/transports/auth/core/authContext
9
9
  */
10
10
  import { AsyncLocalStorage } from "async_hooks";
11
- import type { AuthInfo } from "./types.js";
11
+ import type { AuthInfo } from "./authTypes.js";
12
12
  /**
13
13
  * Defines the structure of the store used within the AsyncLocalStorage.
14
14
  * It holds the authentication information for the current request context.
@@ -5,7 +5,7 @@
5
5
  * from the middleware layer down to the tool and resource handlers without
6
6
  * drilling props.
7
7
  *
8
- * @module src/mcp-server/transports/authentication/authContext
8
+ * @module src/mcp-server/transports/auth/core/authContext
9
9
  */
10
10
  import { AsyncLocalStorage } from "async_hooks";
11
11
  /**
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * @fileoverview Shared types for authentication middleware.
3
- * @module src/mcp-server/transports/authentication/types
3
+ * @module src/mcp-server/transports/auth/core/auth.types
4
4
  */
5
5
  import type { AuthInfo as SdkAuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
6
6
  /**
@@ -1,5 +1,5 @@
1
1
  /**
2
2
  * @fileoverview Shared types for authentication middleware.
3
- * @module src/mcp-server/transports/authentication/types
3
+ * @module src/mcp-server/transports/auth/core/auth.types
4
4
  */
5
5
  export {};
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * @fileoverview Provides utility functions for authorization, specifically for
3
3
  * checking token scopes against required permissions for a given operation.
4
- * @module src/mcp-server/transports/authentication/authUtils
4
+ * @module src/mcp-server/transports/auth/core/authUtils
5
5
  */
6
6
  /**
7
7
  * Checks if the current authentication context contains all the specified scopes.
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * @fileoverview Provides utility functions for authorization, specifically for
3
3
  * checking token scopes against required permissions for a given operation.
4
- * @module src/mcp-server/transports/authentication/authUtils
4
+ * @module src/mcp-server/transports/auth/core/authUtils
5
5
  */
6
- import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
7
- import { logger, requestContextService } from "../../../utils/index.js";
6
+ import { BaseErrorCode, McpError } from "../../../../types-global/errors.js";
7
+ import { logger, requestContextService } from "../../../../utils/index.js";
8
8
  import { authContext } from "./authContext.js";
9
9
  /**
10
10
  * Checks if the current authentication context contains all the specified scopes.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @fileoverview Barrel file for the auth module.
3
+ * Exports core utilities and middleware strategies for easier imports.
4
+ * @module src/mcp-server/transports/auth/index
5
+ */
6
+ export { authContext } from "./core/authContext.js";
7
+ export { withRequiredScopes } from "./core/authUtils.js";
8
+ export type { AuthInfo } from "./core/authTypes.js";
9
+ export { mcpAuthMiddleware as jwtAuthMiddleware } from "./strategies/jwt/jwtMiddleware.js";
10
+ export { oauthMiddleware } from "./strategies/oauth/oauthMiddleware.js";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @fileoverview Barrel file for the auth module.
3
+ * Exports core utilities and middleware strategies for easier imports.
4
+ * @module src/mcp-server/transports/auth/index
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";
@@ -10,10 +10,11 @@
10
10
  * is attached to `c.env.incoming.auth`. This direct attachment to the raw Node.js
11
11
  * request object is for compatibility with the underlying SDK transport, which is
12
12
  * not Hono-context-aware.
13
- * If the token is missing, invalid, or expired, it returns an HTTP 401 Unauthorized response.
13
+ * If the token is missing, invalid, or expired, it throws an `McpError`, which is
14
+ * then handled by the centralized `httpErrorHandler`.
14
15
  *
15
16
  * @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
16
- * @module src/mcp-server/transports/authentication/authMiddleware
17
+ * @module src/mcp-server/transports/auth/strategies/jwt/jwtMiddleware
17
18
  */
18
19
  import { HttpBindings } from "@hono/node-server";
19
20
  import { Context, Next } from "hono";
@@ -23,8 +24,4 @@ import { Context, Next } from "hono";
23
24
  */
24
25
  export declare function mcpAuthMiddleware(c: Context<{
25
26
  Bindings: HttpBindings;
26
- }>, next: Next): Promise<void | (Response & import("hono").TypedResponse<{
27
- error: string;
28
- }, 500, "json">) | (Response & import("hono").TypedResponse<{
29
- error: string;
30
- }, 401, "json">)>;
27
+ }>, next: Next): Promise<void>;
@@ -10,22 +10,26 @@
10
10
  * is attached to `c.env.incoming.auth`. This direct attachment to the raw Node.js
11
11
  * request object is for compatibility with the underlying SDK transport, which is
12
12
  * not Hono-context-aware.
13
- * If the token is missing, invalid, or expired, it returns an HTTP 401 Unauthorized response.
13
+ * If the token is missing, invalid, or expired, it throws an `McpError`, which is
14
+ * then handled by the centralized `httpErrorHandler`.
14
15
  *
15
16
  * @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
16
- * @module src/mcp-server/transports/authentication/authMiddleware
17
+ * @module src/mcp-server/transports/auth/strategies/jwt/jwtMiddleware
17
18
  */
18
- import jwt from "jsonwebtoken";
19
- import { config, environment } from "../../../config/index.js";
20
- import { logger, requestContextService } from "../../../utils/index.js";
21
- import { authContext } from "./authContext.js";
19
+ import { jwtVerify } from "jose";
20
+ import { config, environment } from "../../../../../config/index.js";
21
+ import { logger, requestContextService } from "../../../../../utils/index.js";
22
+ import { BaseErrorCode, McpError } from "../../../../../types-global/errors.js";
23
+ import { authContext } from "../../core/authContext.js";
22
24
  // Startup Validation: Validate secret key presence on module load.
23
- if (environment === "production" && !config.mcpAuthSecretKey) {
24
- logger.fatal("CRITICAL: MCP_AUTH_SECRET_KEY is not set in production environment. Authentication cannot proceed securely.");
25
- throw new Error("MCP_AUTH_SECRET_KEY must be set in production environment for JWT authentication.");
26
- }
27
- else if (!config.mcpAuthSecretKey) {
28
- logger.warning("MCP_AUTH_SECRET_KEY is not set. Authentication middleware will bypass checks (DEVELOPMENT ONLY). This is insecure for production.");
25
+ if (config.mcpAuthMode === "jwt") {
26
+ if (environment === "production" && !config.mcpAuthSecretKey) {
27
+ logger.fatal("CRITICAL: MCP_AUTH_SECRET_KEY is not set in production environment for JWT auth. Authentication cannot proceed securely.");
28
+ throw new Error("MCP_AUTH_SECRET_KEY must be set in production environment for JWT authentication.");
29
+ }
30
+ else if (!config.mcpAuthSecretKey) {
31
+ logger.warning("MCP_AUTH_SECRET_KEY is not set. JWT auth middleware will bypass checks (DEVELOPMENT ONLY). This is insecure for production.");
32
+ }
29
33
  }
30
34
  /**
31
35
  * Hono middleware for verifying JWT Bearer token authentication.
@@ -39,6 +43,10 @@ export async function mcpAuthMiddleware(c, next) {
39
43
  });
40
44
  logger.debug("Running MCP Authentication Middleware (Bearer Token Validation)...", context);
41
45
  const reqWithAuth = c.env.incoming;
46
+ // If JWT auth is not enabled, skip the middleware.
47
+ if (config.mcpAuthMode !== "jwt") {
48
+ return await next();
49
+ }
42
50
  // Development Mode Bypass
43
51
  if (!config.mcpAuthSecretKey) {
44
52
  if (environment !== "production") {
@@ -57,28 +65,23 @@ export async function mcpAuthMiddleware(c, next) {
57
65
  }
58
66
  else {
59
67
  logger.error("FATAL: MCP_AUTH_SECRET_KEY is missing in production. Cannot bypass auth.", context);
60
- return c.json({ error: "Server configuration error: Authentication key missing." }, 500);
68
+ throw new McpError(BaseErrorCode.INTERNAL_ERROR, "Server configuration error: Authentication key missing.");
61
69
  }
62
70
  }
71
+ const secretKey = new TextEncoder().encode(config.mcpAuthSecretKey);
63
72
  const authHeader = c.req.header("Authorization");
64
73
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
65
74
  logger.warning("Authentication failed: Missing or malformed Authorization header (Bearer scheme required).", context);
66
- return c.json({
67
- error: "Unauthorized: Missing or invalid authentication token format.",
68
- }, 401);
75
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, "Missing or invalid authentication token format.");
69
76
  }
70
77
  const tokenParts = authHeader.split(" ");
71
78
  if (tokenParts.length !== 2 || tokenParts[0] !== "Bearer" || !tokenParts[1]) {
72
79
  logger.warning("Authentication failed: Malformed Bearer token.", context);
73
- return c.json({ error: "Unauthorized: Malformed authentication token." }, 401);
80
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, "Malformed authentication token.");
74
81
  }
75
82
  const rawToken = tokenParts[1];
76
83
  try {
77
- const decoded = jwt.verify(rawToken, config.mcpAuthSecretKey);
78
- if (typeof decoded === "string") {
79
- logger.warning("Authentication failed: JWT decoded to a string, expected an object payload.", context);
80
- return c.json({ error: "Unauthorized: Invalid token payload format." }, 401);
81
- }
84
+ const { payload: decoded } = await jwtVerify(rawToken, secretKey);
82
85
  const clientIdFromToken = typeof decoded.cid === "string"
83
86
  ? decoded.cid
84
87
  : typeof decoded.client_id === "string"
@@ -86,7 +89,7 @@ export async function mcpAuthMiddleware(c, next) {
86
89
  : undefined;
87
90
  if (!clientIdFromToken) {
88
91
  logger.warning("Authentication failed: JWT 'cid' or 'client_id' claim is missing or not a string.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
89
- return c.json({ error: "Unauthorized: Invalid token, missing client identifier." }, 401);
92
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, "Invalid token, missing client identifier.");
90
93
  }
91
94
  let scopesFromToken = [];
92
95
  if (Array.isArray(decoded.scp) &&
@@ -102,7 +105,7 @@ export async function mcpAuthMiddleware(c, next) {
102
105
  }
103
106
  if (scopesFromToken.length === 0) {
104
107
  logger.warning("Authentication failed: Token resulted in an empty scope array, and scopes are required.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
105
- return c.json({ error: "Unauthorized: Token must contain valid, non-empty scopes." }, 401);
108
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, "Token must contain valid, non-empty scopes.");
106
109
  }
107
110
  reqWithAuth.auth = {
108
111
  token: rawToken,
@@ -120,26 +123,27 @@ export async function mcpAuthMiddleware(c, next) {
120
123
  await authContext.run({ authInfo }, next);
121
124
  }
122
125
  catch (error) {
123
- let errorMessage = "Invalid token";
124
- if (error instanceof jwt.TokenExpiredError) {
125
- errorMessage = "Token expired";
126
+ let errorMessage = "Invalid token.";
127
+ let errorCode = BaseErrorCode.UNAUTHORIZED;
128
+ if (error instanceof Error && error.name === "JWTExpired") {
129
+ errorMessage = "Token expired.";
126
130
  logger.warning("Authentication failed: Token expired.", {
127
131
  ...context,
128
- expiredAt: error.expiredAt,
132
+ errorName: error.name,
129
133
  });
130
134
  }
131
- else if (error instanceof jwt.JsonWebTokenError) {
132
- errorMessage = `Invalid token: ${error.message}`;
133
- logger.warning(`Authentication failed: ${errorMessage}`, { ...context });
134
- }
135
135
  else if (error instanceof Error) {
136
- errorMessage = `Verification error: ${error.message}`;
137
- logger.error("Authentication failed: Unexpected error during token verification.", { ...context, error: error.message });
136
+ errorMessage = `Invalid token: ${error.message}`;
137
+ logger.warning(`Authentication failed: ${errorMessage}`, {
138
+ ...context,
139
+ errorName: error.name,
140
+ });
138
141
  }
139
142
  else {
140
- errorMessage = "Unknown verification error";
143
+ errorMessage = "Unknown verification error.";
144
+ errorCode = BaseErrorCode.INTERNAL_ERROR;
141
145
  logger.error("Authentication failed: Unexpected non-error exception during token verification.", { ...context, error });
142
146
  }
143
- return c.json({ error: `Unauthorized: ${errorMessage}.` }, 401);
147
+ throw new McpError(errorCode, errorMessage);
144
148
  }
145
149
  }
@@ -5,7 +5,7 @@
5
5
  * On success, it populates an AuthInfo object and stores it in an AsyncLocalStorage
6
6
  * context for use in downstream handlers.
7
7
  *
8
- * @module src/mcp-server/transports/authentication/oauthMiddleware
8
+ * @module src/mcp-server/transports/auth/strategies/oauth/oauthMiddleware
9
9
  */
10
10
  import { HttpBindings } from "@hono/node-server";
11
11
  import { Context, Next } from "hono";
@@ -17,8 +17,4 @@ import { Context, Next } from "hono";
17
17
  */
18
18
  export declare function oauthMiddleware(c: Context<{
19
19
  Bindings: HttpBindings;
20
- }>, next: Next): Promise<(Response & import("hono").TypedResponse<{
21
- error: string;
22
- }, 500, "json">) | (Response & import("hono").TypedResponse<{
23
- error: string;
24
- }, 401, "json">) | undefined>;
20
+ }>, next: Next): Promise<void>;
@@ -5,14 +5,14 @@
5
5
  * On success, it populates an AuthInfo object and stores it in an AsyncLocalStorage
6
6
  * context for use in downstream handlers.
7
7
  *
8
- * @module src/mcp-server/transports/authentication/oauthMiddleware
8
+ * @module src/mcp-server/transports/auth/strategies/oauth/oauthMiddleware
9
9
  */
10
10
  import { createRemoteJWKSet, jwtVerify } from "jose";
11
- import { config } from "../../../config/index.js";
12
- import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
13
- import { ErrorHandler } from "../../../utils/internal/errorHandler.js";
14
- import { logger, requestContextService } from "../../../utils/index.js";
15
- import { authContext } from "./authContext.js";
11
+ import { config } from "../../../../../config/index.js";
12
+ import { BaseErrorCode, McpError } from "../../../../../types-global/errors.js";
13
+ import { logger, requestContextService } from "../../../../../utils/index.js";
14
+ import { ErrorHandler } from "../../../../../utils/internal/errorHandler.js";
15
+ import { authContext } from "../../core/authContext.js";
16
16
  // --- Startup Validation ---
17
17
  // Ensures that necessary OAuth configuration is present when the mode is 'oauth'.
18
18
  if (config.mcpAuthMode === "oauth") {
@@ -57,6 +57,10 @@ if (config.mcpAuthMode === "oauth" && config.oauthIssuerUrl) {
57
57
  * @param next - The function to call to proceed to the next middleware.
58
58
  */
59
59
  export async function oauthMiddleware(c, next) {
60
+ // If OAuth is not the configured auth mode, skip this middleware.
61
+ if (config.mcpAuthMode !== "oauth") {
62
+ return await next();
63
+ }
60
64
  const context = requestContextService.createRequestContext({
61
65
  operation: "oauthMiddleware",
62
66
  httpMethod: c.req.method,
@@ -64,13 +68,12 @@ export async function oauthMiddleware(c, next) {
64
68
  });
65
69
  if (!jwks) {
66
70
  // This should not happen if startup validation is correct, but it's a safeguard.
67
- const error = new McpError(BaseErrorCode.CONFIGURATION_ERROR, "OAuth middleware is active, but JWKS client is not initialized.", context);
68
- ErrorHandler.handleError(error, { operation: "oauthMiddleware", context });
69
- return c.json({ error: "Server configuration error." }, 500);
71
+ // This should not happen if startup validation is correct, but it's a safeguard.
72
+ throw new McpError(BaseErrorCode.CONFIGURATION_ERROR, "OAuth middleware is active, but JWKS client is not initialized.", context);
70
73
  }
71
74
  const authHeader = c.req.header("Authorization");
72
75
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
73
- return c.json({ error: "Unauthorized: Missing or invalid token format." }, 401);
76
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, "Missing or invalid token format.");
74
77
  }
75
78
  const token = authHeader.substring(7);
76
79
  try {
@@ -80,10 +83,14 @@ export async function oauthMiddleware(c, next) {
80
83
  });
81
84
  // The 'scope' claim is typically a space-delimited string in OAuth 2.1.
82
85
  const scopes = typeof payload.scope === "string" ? payload.scope.split(" ") : [];
86
+ if (scopes.length === 0) {
87
+ logger.warning("Authentication failed: Token contains no scopes, but scopes are required.", { ...context, jwtPayloadKeys: Object.keys(payload) });
88
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, "Token must contain valid, non-empty scopes.");
89
+ }
83
90
  const clientId = typeof payload.client_id === "string" ? payload.client_id : undefined;
84
91
  if (!clientId) {
85
92
  logger.warning("Authentication failed: OAuth token 'client_id' claim is missing or not a string.", { ...context, jwtPayloadKeys: Object.keys(payload) });
86
- return c.json({ error: "Unauthorized: Invalid token, missing client identifier." }, 401);
93
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, "Invalid token, missing client identifier.");
87
94
  }
88
95
  const authInfo = {
89
96
  token,
@@ -97,13 +104,21 @@ export async function oauthMiddleware(c, next) {
97
104
  await authContext.run({ authInfo }, next);
98
105
  }
99
106
  catch (error) {
100
- logger.warning("OAuth token validation failed", {
101
- ...context,
102
- errorName: error.name,
103
- errorMessage: error.message,
107
+ if (error instanceof Error && error.name === "JWTExpired") {
108
+ logger.warning("Authentication failed: OAuth token expired.", context);
109
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, "Token expired.");
110
+ }
111
+ const handledError = ErrorHandler.handleError(error, {
112
+ operation: "oauthMiddleware",
113
+ context,
114
+ rethrow: false, // We will throw a new McpError below
104
115
  });
105
- // The `jose` library provides specific error codes like 'ERR_JWT_EXPIRED' or 'ERR_JWS_INVALID'
106
- const message = `Unauthorized: ${error.message || "Invalid token"}`;
107
- return c.json({ error: message }, 401);
116
+ // Ensure we always throw an McpError for consistency
117
+ if (handledError instanceof McpError) {
118
+ throw handledError;
119
+ }
120
+ else {
121
+ throw new McpError(BaseErrorCode.UNAUTHORIZED, `Unauthorized: ${handledError.message || "Invalid token"}`, { originalError: handledError.name });
122
+ }
108
123
  }
109
124
  }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * @fileoverview Centralized error handler for the Hono HTTP transport.
3
+ * This middleware intercepts errors that occur during request processing,
4
+ * standardizes them using the application's ErrorHandler utility, and
5
+ * formats them into a consistent JSON-RPC error response.
6
+ * @module src/mcp-server/transports/httpErrorHandler
7
+ */
8
+ import { Context } from "hono";
9
+ import { BaseErrorCode } from "../../types-global/errors.js";
10
+ /**
11
+ * A centralized error handling middleware for Hono.
12
+ * This function is registered with `app.onError()` and will catch any errors
13
+ * thrown from preceding middleware or route handlers.
14
+ *
15
+ * @param err - The error that was thrown.
16
+ * @param c - The Hono context object for the request.
17
+ * @returns A Response object containing the formatted JSON-RPC error.
18
+ */
19
+ export declare const httpErrorHandler: (err: Error, c: Context) => Promise<Response & import("hono").TypedResponse<{
20
+ jsonrpc: string;
21
+ error: {
22
+ code: number | BaseErrorCode;
23
+ message: string;
24
+ };
25
+ id: string | number | null;
26
+ }, import("hono/utils/http-status").ContentfulStatusCode, "json">>;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * @fileoverview Centralized error handler for the Hono HTTP transport.
3
+ * This middleware intercepts errors that occur during request processing,
4
+ * standardizes them using the application's ErrorHandler utility, and
5
+ * formats them into a consistent JSON-RPC error response.
6
+ * @module src/mcp-server/transports/httpErrorHandler
7
+ */
8
+ import { BaseErrorCode, McpError } from "../../types-global/errors.js";
9
+ import { ErrorHandler, requestContextService } from "../../utils/index.js";
10
+ /**
11
+ * A centralized error handling middleware for Hono.
12
+ * This function is registered with `app.onError()` and will catch any errors
13
+ * thrown from preceding middleware or route handlers.
14
+ *
15
+ * @param err - The error that was thrown.
16
+ * @param c - The Hono context object for the request.
17
+ * @returns A Response object containing the formatted JSON-RPC error.
18
+ */
19
+ export const httpErrorHandler = async (err, c) => {
20
+ const context = requestContextService.createRequestContext({
21
+ operation: "httpErrorHandler",
22
+ path: c.req.path,
23
+ method: c.req.method,
24
+ });
25
+ const handledError = ErrorHandler.handleError(err, {
26
+ operation: "httpTransport",
27
+ context,
28
+ });
29
+ let status = 500;
30
+ if (handledError instanceof McpError) {
31
+ switch (handledError.code) {
32
+ case BaseErrorCode.NOT_FOUND:
33
+ status = 404;
34
+ break;
35
+ case BaseErrorCode.UNAUTHORIZED:
36
+ status = 401;
37
+ break;
38
+ case BaseErrorCode.FORBIDDEN:
39
+ status = 403;
40
+ break;
41
+ case BaseErrorCode.VALIDATION_ERROR:
42
+ status = 400;
43
+ break;
44
+ case BaseErrorCode.CONFLICT:
45
+ status = 409;
46
+ break;
47
+ case BaseErrorCode.RATE_LIMITED:
48
+ status = 429;
49
+ break;
50
+ default:
51
+ status = 500;
52
+ }
53
+ }
54
+ // Attempt to get the request ID from the body, but don't fail if it's not there or unreadable.
55
+ let requestId = null;
56
+ try {
57
+ const body = await c.req.json();
58
+ requestId = body?.id || null;
59
+ }
60
+ catch {
61
+ // Ignore parsing errors, requestId will remain null
62
+ }
63
+ const errorCode = handledError instanceof McpError ? handledError.code : -32603;
64
+ c.status(status);
65
+ return c.json({
66
+ jsonrpc: "2.0",
67
+ error: {
68
+ code: errorCode,
69
+ message: handledError.message,
70
+ },
71
+ id: requestId,
72
+ });
73
+ };
@@ -1,10 +1,15 @@
1
1
  /**
2
- * @fileoverview Handles the setup and management of the Streamable HTTP MCP transport using Hono.
3
- * Implements the MCP Specification 2025-03-26 for Streamable HTTP.
4
- * This includes creating a Hono server, configuring middleware (CORS, Authentication),
5
- * defining request routing for the single MCP endpoint (POST/GET/DELETE),
6
- * managing server-side sessions, handling Server-Sent Events (SSE) for streaming,
7
- * and binding to a network port with retry logic for port conflicts.
2
+ * @fileoverview Configures and starts the Streamable HTTP MCP transport using Hono.
3
+ * This module integrates the `@modelcontextprotocol/sdk`'s `StreamableHTTPServerTransport`
4
+ * into a Hono web server. Its responsibilities include:
5
+ * - Creating a Hono server instance.
6
+ * - Applying and configuring middleware for CORS, rate limiting, and authentication (JWT/OAuth).
7
+ * - Defining the routes (`/mcp` endpoint for POST, GET, DELETE) to handle the MCP lifecycle.
8
+ * - Orchestrating session management by mapping session IDs to SDK transport instances.
9
+ * - Implementing port-binding logic with automatic retry on conflicts.
10
+ *
11
+ * The underlying implementation of the MCP Streamable HTTP specification, including
12
+ * Server-Sent Events (SSE) for streaming, is handled by the SDK's transport class.
8
13
  *
9
14
  * Specification Reference:
10
15
  * https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#streamable-http
@@ -13,12 +18,4 @@
13
18
  import { ServerType } from "@hono/node-server";
14
19
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
20
  import { RequestContext } from "../../utils/index.js";
16
- /**
17
- * Sets up and starts the Streamable HTTP transport layer for the MCP server.
18
- *
19
- * @param createServerInstanceFn - An asynchronous factory function that returns a new `McpServer` instance.
20
- * @param parentContext - Logging context from the main server startup process.
21
- * @returns A promise that resolves with the Node.js `http.Server` instance when the HTTP server is successfully listening.
22
- * @throws {Error} If the server fails to start after all port retries.
23
- */
24
21
  export declare function startHttpTransport(createServerInstanceFn: () => Promise<McpServer>, parentContext: RequestContext): Promise<ServerType>;