@cyanheads/pubmed-mcp-server 1.2.1 → 1.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/dist/mcp-server/transports/{authentication → auth/core}/authContext.d.ts +2 -2
- package/dist/mcp-server/transports/{authentication → auth/core}/authContext.js +1 -1
- package/dist/mcp-server/transports/{authentication/types.d.ts → auth/core/authTypes.d.ts} +1 -1
- package/dist/mcp-server/transports/{authentication/types.js → auth/core/authTypes.js} +1 -1
- package/dist/mcp-server/transports/{authentication → auth/core}/authUtils.d.ts +1 -1
- package/dist/mcp-server/transports/{authentication → auth/core}/authUtils.js +3 -3
- package/dist/mcp-server/transports/auth/index.d.ts +10 -0
- package/dist/mcp-server/transports/auth/index.js +9 -0
- package/dist/mcp-server/transports/{authentication/authMiddleware.d.ts → auth/strategies/jwt/jwtMiddleware.d.ts} +4 -12
- package/dist/mcp-server/transports/{authentication/authMiddleware.js → auth/strategies/jwt/jwtMiddleware.js} +36 -43
- package/dist/mcp-server/transports/{authentication → auth/strategies/oauth}/oauthMiddleware.d.ts +2 -6
- package/dist/mcp-server/transports/{authentication → auth/strategies/oauth}/oauthMiddleware.js +33 -18
- package/dist/mcp-server/transports/httpErrorHandler.d.ts +26 -0
- package/dist/mcp-server/transports/httpErrorHandler.js +73 -0
- package/dist/mcp-server/transports/httpTransport.d.ts +11 -6
- package/dist/mcp-server/transports/httpTransport.js +105 -106
- package/dist/utils/internal/logger.js +51 -49
- package/dist/utils/security/idGenerator.d.ts +4 -1
- package/dist/utils/security/idGenerator.js +24 -7
- package/dist/utils/security/rateLimiter.d.ts +0 -4
- package/dist/utils/security/rateLimiter.js +0 -4
- package/dist/utils/security/sanitization.d.ts +11 -0
- package/dist/utils/security/sanitization.js +16 -2
- package/package.json +8 -8
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# PubMed MCP Server
|
|
2
2
|
|
|
3
3
|
[](https://www.typescriptlang.org/)
|
|
4
|
-
[](https://modelcontextprotocol.io/)
|
|
5
|
+
[](./CHANGELOG.md)
|
|
6
6
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
7
7
|
[](https://github.com/cyanheads/pubmed-mcp-server/issues)
|
|
8
8
|
[](https://github.com/cyanheads/pubmed-mcp-server)
|
|
@@ -62,7 +62,7 @@ Leverages the robust utilities provided by the `mcp-ts-template`:
|
|
|
62
62
|
- **Input Validation/Sanitization**: Uses `zod` for schema validation and custom sanitization logic.
|
|
63
63
|
- **Request Context**: Tracking and correlation of operations via unique request IDs using `AsyncLocalStorage`.
|
|
64
64
|
- **Type Safety**: Strong typing enforced by TypeScript and Zod schemas.
|
|
65
|
-
- **HTTP Transport**: High-performance HTTP server using **Hono**, featuring session management with garbage collection
|
|
65
|
+
- **HTTP Transport**: High-performance HTTP server using **Hono**, featuring session management with garbage collection and CORS support.
|
|
66
66
|
- **Authentication**: Robust authentication layer supporting JWT and OAuth 2.1, with fine-grained scope enforcement.
|
|
67
67
|
- **Deployment**: Multi-stage `Dockerfile` for creating small, secure production images with native dependency support.
|
|
68
68
|
|
|
@@ -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/
|
|
8
|
+
* @module src/mcp-server/transports/auth/core/authContext
|
|
9
9
|
*/
|
|
10
10
|
import { AsyncLocalStorage } from "async_hooks";
|
|
11
|
-
import type { AuthInfo } from "./
|
|
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/
|
|
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/
|
|
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,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/
|
|
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/
|
|
4
|
+
* @module src/mcp-server/transports/auth/core/authUtils
|
|
5
5
|
*/
|
|
6
|
-
import { BaseErrorCode, McpError } from "
|
|
7
|
-
import { logger, requestContextService } from "
|
|
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,26 +10,18 @@
|
|
|
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
|
|
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/
|
|
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";
|
|
20
|
-
/**
|
|
21
|
-
* Validates the presence of the MCP_AUTH_SECRET_KEY at startup.
|
|
22
|
-
* This should be called once when the application is initializing.
|
|
23
|
-
*/
|
|
24
|
-
export declare function initializeAuthMiddleware(): void;
|
|
25
21
|
/**
|
|
26
22
|
* Hono middleware for verifying JWT Bearer token authentication.
|
|
27
23
|
* It attaches authentication info to `c.env.incoming.auth` for SDK compatibility with the node server.
|
|
28
24
|
*/
|
|
29
25
|
export declare function mcpAuthMiddleware(c: Context<{
|
|
30
26
|
Bindings: HttpBindings;
|
|
31
|
-
}>, next: Next): Promise<void
|
|
32
|
-
error: string;
|
|
33
|
-
}, 500, "json">) | (Response & import("hono").TypedResponse<{
|
|
34
|
-
error: string;
|
|
35
|
-
}, 401, "json">)>;
|
|
27
|
+
}>, next: Next): Promise<void>;
|
|
@@ -10,32 +10,25 @@
|
|
|
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
|
|
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/
|
|
17
|
+
* @module src/mcp-server/transports/auth/strategies/jwt/jwtMiddleware
|
|
17
18
|
*/
|
|
18
|
-
import
|
|
19
|
-
import { config, environment } from "
|
|
20
|
-
import { logger, requestContextService } from "
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
*/
|
|
26
|
-
export function initializeAuthMiddleware() {
|
|
27
|
-
const context = requestContextService.createRequestContext({
|
|
28
|
-
operation: "initializeAuthMiddleware",
|
|
29
|
-
});
|
|
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";
|
|
24
|
+
// Startup Validation: Validate secret key presence on module load.
|
|
25
|
+
if (config.mcpAuthMode === "jwt") {
|
|
30
26
|
if (environment === "production" && !config.mcpAuthSecretKey) {
|
|
31
|
-
logger.fatal("CRITICAL: MCP_AUTH_SECRET_KEY is not set in production environment. Authentication cannot proceed securely."
|
|
27
|
+
logger.fatal("CRITICAL: MCP_AUTH_SECRET_KEY is not set in production environment for JWT auth. Authentication cannot proceed securely.");
|
|
32
28
|
throw new Error("MCP_AUTH_SECRET_KEY must be set in production environment for JWT authentication.");
|
|
33
29
|
}
|
|
34
30
|
else if (!config.mcpAuthSecretKey) {
|
|
35
|
-
logger.warning("MCP_AUTH_SECRET_KEY is not set.
|
|
36
|
-
}
|
|
37
|
-
else {
|
|
38
|
-
logger.debug("Auth middleware secret key check passed.", context);
|
|
31
|
+
logger.warning("MCP_AUTH_SECRET_KEY is not set. JWT auth middleware will bypass checks (DEVELOPMENT ONLY). This is insecure for production.");
|
|
39
32
|
}
|
|
40
33
|
}
|
|
41
34
|
/**
|
|
@@ -50,6 +43,10 @@ export async function mcpAuthMiddleware(c, next) {
|
|
|
50
43
|
});
|
|
51
44
|
logger.debug("Running MCP Authentication Middleware (Bearer Token Validation)...", context);
|
|
52
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
|
+
}
|
|
53
50
|
// Development Mode Bypass
|
|
54
51
|
if (!config.mcpAuthSecretKey) {
|
|
55
52
|
if (environment !== "production") {
|
|
@@ -68,28 +65,23 @@ export async function mcpAuthMiddleware(c, next) {
|
|
|
68
65
|
}
|
|
69
66
|
else {
|
|
70
67
|
logger.error("FATAL: MCP_AUTH_SECRET_KEY is missing in production. Cannot bypass auth.", context);
|
|
71
|
-
|
|
68
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, "Server configuration error: Authentication key missing.");
|
|
72
69
|
}
|
|
73
70
|
}
|
|
71
|
+
const secretKey = new TextEncoder().encode(config.mcpAuthSecretKey);
|
|
74
72
|
const authHeader = c.req.header("Authorization");
|
|
75
73
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
76
74
|
logger.warning("Authentication failed: Missing or malformed Authorization header (Bearer scheme required).", context);
|
|
77
|
-
|
|
78
|
-
error: "Unauthorized: Missing or invalid authentication token format.",
|
|
79
|
-
}, 401);
|
|
75
|
+
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Missing or invalid authentication token format.");
|
|
80
76
|
}
|
|
81
77
|
const tokenParts = authHeader.split(" ");
|
|
82
78
|
if (tokenParts.length !== 2 || tokenParts[0] !== "Bearer" || !tokenParts[1]) {
|
|
83
79
|
logger.warning("Authentication failed: Malformed Bearer token.", context);
|
|
84
|
-
|
|
80
|
+
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Malformed authentication token.");
|
|
85
81
|
}
|
|
86
82
|
const rawToken = tokenParts[1];
|
|
87
83
|
try {
|
|
88
|
-
const decoded =
|
|
89
|
-
if (typeof decoded === "string") {
|
|
90
|
-
logger.warning("Authentication failed: JWT decoded to a string, expected an object payload.", context);
|
|
91
|
-
return c.json({ error: "Unauthorized: Invalid token payload format." }, 401);
|
|
92
|
-
}
|
|
84
|
+
const { payload: decoded } = await jwtVerify(rawToken, secretKey);
|
|
93
85
|
const clientIdFromToken = typeof decoded.cid === "string"
|
|
94
86
|
? decoded.cid
|
|
95
87
|
: typeof decoded.client_id === "string"
|
|
@@ -97,7 +89,7 @@ export async function mcpAuthMiddleware(c, next) {
|
|
|
97
89
|
: undefined;
|
|
98
90
|
if (!clientIdFromToken) {
|
|
99
91
|
logger.warning("Authentication failed: JWT 'cid' or 'client_id' claim is missing or not a string.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
|
|
100
|
-
|
|
92
|
+
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Invalid token, missing client identifier.");
|
|
101
93
|
}
|
|
102
94
|
let scopesFromToken = [];
|
|
103
95
|
if (Array.isArray(decoded.scp) &&
|
|
@@ -113,7 +105,7 @@ export async function mcpAuthMiddleware(c, next) {
|
|
|
113
105
|
}
|
|
114
106
|
if (scopesFromToken.length === 0) {
|
|
115
107
|
logger.warning("Authentication failed: Token resulted in an empty scope array, and scopes are required.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
|
|
116
|
-
|
|
108
|
+
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Token must contain valid, non-empty scopes.");
|
|
117
109
|
}
|
|
118
110
|
reqWithAuth.auth = {
|
|
119
111
|
token: rawToken,
|
|
@@ -131,26 +123,27 @@ export async function mcpAuthMiddleware(c, next) {
|
|
|
131
123
|
await authContext.run({ authInfo }, next);
|
|
132
124
|
}
|
|
133
125
|
catch (error) {
|
|
134
|
-
let errorMessage = "Invalid token";
|
|
135
|
-
|
|
136
|
-
|
|
126
|
+
let errorMessage = "Invalid token.";
|
|
127
|
+
let errorCode = BaseErrorCode.UNAUTHORIZED;
|
|
128
|
+
if (error instanceof Error && error.name === "JWTExpired") {
|
|
129
|
+
errorMessage = "Token expired.";
|
|
137
130
|
logger.warning("Authentication failed: Token expired.", {
|
|
138
131
|
...context,
|
|
139
|
-
|
|
132
|
+
errorName: error.name,
|
|
140
133
|
});
|
|
141
134
|
}
|
|
142
|
-
else if (error instanceof jwt.JsonWebTokenError) {
|
|
143
|
-
errorMessage = `Invalid token: ${error.message}`;
|
|
144
|
-
logger.warning(`Authentication failed: ${errorMessage}`, { ...context });
|
|
145
|
-
}
|
|
146
135
|
else if (error instanceof Error) {
|
|
147
|
-
errorMessage = `
|
|
148
|
-
logger.
|
|
136
|
+
errorMessage = `Invalid token: ${error.message}`;
|
|
137
|
+
logger.warning(`Authentication failed: ${errorMessage}`, {
|
|
138
|
+
...context,
|
|
139
|
+
errorName: error.name,
|
|
140
|
+
});
|
|
149
141
|
}
|
|
150
142
|
else {
|
|
151
|
-
errorMessage = "Unknown verification error";
|
|
143
|
+
errorMessage = "Unknown verification error.";
|
|
144
|
+
errorCode = BaseErrorCode.INTERNAL_ERROR;
|
|
152
145
|
logger.error("Authentication failed: Unexpected non-error exception during token verification.", { ...context, error });
|
|
153
146
|
}
|
|
154
|
-
|
|
147
|
+
throw new McpError(errorCode, errorMessage);
|
|
155
148
|
}
|
|
156
149
|
}
|
package/dist/mcp-server/transports/{authentication → auth/strategies/oauth}/oauthMiddleware.d.ts
RENAMED
|
@@ -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/
|
|
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<
|
|
21
|
-
error: string;
|
|
22
|
-
}, 500, "json">) | (Response & import("hono").TypedResponse<{
|
|
23
|
-
error: string;
|
|
24
|
-
}, 401, "json">) | undefined>;
|
|
20
|
+
}>, next: Next): Promise<void>;
|
package/dist/mcp-server/transports/{authentication → auth/strategies/oauth}/oauthMiddleware.js
RENAMED
|
@@ -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/
|
|
8
|
+
* @module src/mcp-server/transports/auth/strategies/oauth/oauthMiddleware
|
|
9
9
|
*/
|
|
10
10
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
11
|
-
import { config } from "
|
|
12
|
-
import { BaseErrorCode, McpError } from "
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import { authContext } from "
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
//
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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
|
|
@@ -19,17 +24,19 @@ import http from "http";
|
|
|
19
24
|
import { randomUUID } from "node:crypto";
|
|
20
25
|
import { config } from "../../config/index.js";
|
|
21
26
|
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
27
|
+
import { logger, rateLimiter, requestContextService, } from "../../utils/index.js";
|
|
28
|
+
import { jwtAuthMiddleware, oauthMiddleware, } from "./auth/index.js";
|
|
29
|
+
import { httpErrorHandler } from "./httpErrorHandler.js";
|
|
25
30
|
const HTTP_PORT = config.mcpHttpPort;
|
|
26
31
|
const HTTP_HOST = config.mcpHttpHost;
|
|
27
32
|
const MCP_ENDPOINT_PATH = "/mcp";
|
|
28
33
|
const MAX_PORT_RETRIES = 15;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
34
|
+
// The transports map will store active sessions, keyed by session ID.
|
|
35
|
+
// NOTE: This is an in-memory session store, which is a known limitation for scalability.
|
|
36
|
+
// It will not work in a multi-process (clustered) or serverless environment.
|
|
37
|
+
// For a scalable deployment, this would need to be replaced with a distributed
|
|
38
|
+
// store like Redis or Memcached.
|
|
39
|
+
const transports = {};
|
|
33
40
|
async function isPortInUse(port, host, parentContext) {
|
|
34
41
|
const checkContext = requestContextService.createRequestContext({
|
|
35
42
|
...parentContext,
|
|
@@ -57,7 +64,11 @@ function startHttpServerWithRetry(app, initialPort, host, maxRetries, parentCont
|
|
|
57
64
|
return new Promise(async (resolve, reject) => {
|
|
58
65
|
for (let i = 0; i <= maxRetries; i++) {
|
|
59
66
|
const currentPort = initialPort + i;
|
|
60
|
-
const attemptContext = {
|
|
67
|
+
const attemptContext = {
|
|
68
|
+
...startContext,
|
|
69
|
+
port: currentPort,
|
|
70
|
+
attempt: i + 1,
|
|
71
|
+
};
|
|
61
72
|
if (await isPortInUse(currentPort, host, attemptContext)) {
|
|
62
73
|
logger.warning(`Port ${currentPort} is in use, retrying...`, attemptContext);
|
|
63
74
|
continue;
|
|
@@ -65,7 +76,10 @@ function startHttpServerWithRetry(app, initialPort, host, maxRetries, parentCont
|
|
|
65
76
|
try {
|
|
66
77
|
const serverInstance = serve({ fetch: app.fetch, port: currentPort, hostname: host }, (info) => {
|
|
67
78
|
const serverAddress = `http://${info.address}:${info.port}${MCP_ENDPOINT_PATH}`;
|
|
68
|
-
logger.info(`HTTP transport listening at ${serverAddress}`, {
|
|
79
|
+
logger.info(`HTTP transport listening at ${serverAddress}`, {
|
|
80
|
+
...attemptContext,
|
|
81
|
+
address: serverAddress,
|
|
82
|
+
});
|
|
69
83
|
if (process.stdout.isTTY) {
|
|
70
84
|
console.log(`\n🚀 MCP Server running at: ${serverAddress}\n`);
|
|
71
85
|
}
|
|
@@ -84,27 +98,20 @@ function startHttpServerWithRetry(app, initialPort, host, maxRetries, parentCont
|
|
|
84
98
|
});
|
|
85
99
|
}
|
|
86
100
|
export async function startHttpTransport(createServerInstanceFn, parentContext) {
|
|
87
|
-
initializeAuthMiddleware();
|
|
88
101
|
const app = new Hono();
|
|
89
102
|
const transportContext = requestContextService.createRequestContext({
|
|
90
103
|
...parentContext,
|
|
91
104
|
component: "HttpTransportSetup",
|
|
92
105
|
});
|
|
93
|
-
setInterval(() => {
|
|
94
|
-
const now = Date.now();
|
|
95
|
-
const gcContext = requestContextService.createRequestContext({ operation: "SessionGarbageCollector" });
|
|
96
|
-
for (const sessionId in sessionActivity) {
|
|
97
|
-
if (now - sessionActivity[sessionId] > SESSION_TIMEOUT_MS) {
|
|
98
|
-
logger.info(`Session ${sessionId} timed out. Cleaning up.`, { ...gcContext, sessionId });
|
|
99
|
-
httpTransports[sessionId]?.close();
|
|
100
|
-
delete sessionActivity[sessionId];
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
}, SESSION_GC_INTERVAL_MS);
|
|
104
106
|
app.use("*", cors({
|
|
105
107
|
origin: config.mcpAllowedOrigins || [],
|
|
106
108
|
allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
|
|
107
|
-
allowHeaders: [
|
|
109
|
+
allowHeaders: [
|
|
110
|
+
"Content-Type",
|
|
111
|
+
"Mcp-Session-Id",
|
|
112
|
+
"Last-Event-ID",
|
|
113
|
+
"Authorization",
|
|
114
|
+
],
|
|
108
115
|
credentials: true,
|
|
109
116
|
}));
|
|
110
117
|
app.use("*", async (c, next) => {
|
|
@@ -112,98 +119,90 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
|
|
|
112
119
|
await next();
|
|
113
120
|
});
|
|
114
121
|
app.use(MCP_ENDPOINT_PATH, async (c, next) => {
|
|
122
|
+
// NOTE (Security): The 'x-forwarded-for' header is used for rate limiting.
|
|
123
|
+
// This is only secure if the server is run behind a trusted proxy that
|
|
124
|
+
// correctly sets or validates this header.
|
|
115
125
|
const clientIp = c.req.header("x-forwarded-for")?.split(",")[0].trim() || "unknown_ip";
|
|
116
|
-
const context = requestContextService.createRequestContext({
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return c.json({
|
|
124
|
-
jsonrpc: "2.0",
|
|
125
|
-
error: { code: -32000, message: handledError.message },
|
|
126
|
-
id: (await c.req.json().catch(() => ({})))?.id || null,
|
|
127
|
-
}, 429);
|
|
128
|
-
}
|
|
126
|
+
const context = requestContextService.createRequestContext({
|
|
127
|
+
operation: "httpRateLimitCheck",
|
|
128
|
+
ipAddress: clientIp,
|
|
129
|
+
});
|
|
130
|
+
// Let the centralized error handler catch rate limit errors
|
|
131
|
+
rateLimiter.check(clientIp, context);
|
|
132
|
+
await next();
|
|
129
133
|
});
|
|
130
134
|
if (config.mcpAuthMode === "oauth") {
|
|
131
135
|
app.use(MCP_ENDPOINT_PATH, oauthMiddleware);
|
|
132
136
|
}
|
|
133
137
|
else {
|
|
134
|
-
app.use(MCP_ENDPOINT_PATH,
|
|
138
|
+
app.use(MCP_ENDPOINT_PATH, jwtAuthMiddleware);
|
|
135
139
|
}
|
|
140
|
+
// Centralized Error Handling
|
|
141
|
+
app.onError(httpErrorHandler);
|
|
136
142
|
app.post(MCP_ENDPOINT_PATH, async (c) => {
|
|
137
|
-
const postContext = requestContextService.createRequestContext({
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
onsessioninitialized: (newId) => {
|
|
153
|
-
httpTransports[newId] = transport;
|
|
154
|
-
sessionActivity[newId] = Date.now();
|
|
155
|
-
logger.info(`HTTP Session created: ${newId}`, { ...postContext, newSessionId: newId });
|
|
156
|
-
},
|
|
143
|
+
const postContext = requestContextService.createRequestContext({
|
|
144
|
+
...transportContext,
|
|
145
|
+
operation: "handlePost",
|
|
146
|
+
});
|
|
147
|
+
const body = await c.req.json();
|
|
148
|
+
const sessionId = c.req.header("mcp-session-id");
|
|
149
|
+
let transport = sessionId
|
|
150
|
+
? transports[sessionId]
|
|
151
|
+
: undefined;
|
|
152
|
+
if (isInitializeRequest(body)) {
|
|
153
|
+
// If a transport already exists for a session, it's a re-initialization.
|
|
154
|
+
if (transport) {
|
|
155
|
+
logger.warning("Re-initializing existing session.", {
|
|
156
|
+
...postContext,
|
|
157
|
+
sessionId,
|
|
157
158
|
});
|
|
158
|
-
transport.
|
|
159
|
-
const closedSessionId = transport.sessionId;
|
|
160
|
-
if (closedSessionId) {
|
|
161
|
-
delete httpTransports[closedSessionId];
|
|
162
|
-
delete sessionActivity[closedSessionId];
|
|
163
|
-
logger.info(`HTTP Session closed: ${closedSessionId}`, { ...postContext, closedSessionId });
|
|
164
|
-
}
|
|
165
|
-
};
|
|
166
|
-
const server = await createServerInstanceFn();
|
|
167
|
-
await server.connect(transport);
|
|
168
|
-
}
|
|
169
|
-
else if (!transport) {
|
|
170
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, "Invalid or expired session ID.");
|
|
159
|
+
await transport.close(); // This will trigger the onclose handler.
|
|
171
160
|
}
|
|
172
|
-
|
|
161
|
+
// Create a new transport for a new session.
|
|
162
|
+
const newTransport = new StreamableHTTPServerTransport({
|
|
163
|
+
sessionIdGenerator: () => randomUUID(),
|
|
164
|
+
onsessioninitialized: (newId) => {
|
|
165
|
+
transports[newId] = newTransport;
|
|
166
|
+
logger.info(`HTTP Session created: ${newId}`, {
|
|
167
|
+
...postContext,
|
|
168
|
+
newSessionId: newId,
|
|
169
|
+
});
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
// Set up cleanup logic for when the transport is closed.
|
|
173
|
+
newTransport.onclose = () => {
|
|
174
|
+
const closedSessionId = newTransport.sessionId;
|
|
175
|
+
if (closedSessionId && transports[closedSessionId]) {
|
|
176
|
+
delete transports[closedSessionId];
|
|
177
|
+
logger.info(`HTTP Session closed: ${closedSessionId}`, {
|
|
178
|
+
...postContext,
|
|
179
|
+
closedSessionId,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
// Connect the new transport to a new server instance.
|
|
184
|
+
const server = await createServerInstanceFn();
|
|
185
|
+
await server.connect(newTransport);
|
|
186
|
+
transport = newTransport;
|
|
173
187
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return c.json({
|
|
178
|
-
jsonrpc: "2.0",
|
|
179
|
-
error: { code: -32603, message: handledError.message },
|
|
180
|
-
id: requestId,
|
|
181
|
-
}, handledError instanceof McpError && handledError.code === BaseErrorCode.NOT_FOUND ? 404 : 500);
|
|
188
|
+
else if (!transport) {
|
|
189
|
+
// If it's not an initialization request and no transport was found, it's an error.
|
|
190
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, "Invalid or expired session ID.");
|
|
182
191
|
}
|
|
192
|
+
// Pass the request to the transport to handle.
|
|
193
|
+
return await transport.handleRequest(c.env.incoming, c.env.outgoing, body);
|
|
183
194
|
});
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
if (!transport) {
|
|
191
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, "Session not found or expired.");
|
|
192
|
-
}
|
|
193
|
-
if (sessionId)
|
|
194
|
-
sessionActivity[sessionId] = Date.now();
|
|
195
|
-
return await transport.handleRequest(c.env.incoming, c.env.outgoing);
|
|
196
|
-
}
|
|
197
|
-
catch (err) {
|
|
198
|
-
const handledError = ErrorHandler.handleError(err, { operation: `handle${method}`, context: sessionReqContext });
|
|
199
|
-
return c.json({
|
|
200
|
-
jsonrpc: "2.0",
|
|
201
|
-
error: { code: -32603, message: handledError.message },
|
|
202
|
-
id: null,
|
|
203
|
-
}, handledError instanceof McpError && handledError.code === BaseErrorCode.NOT_FOUND ? 404 : 500);
|
|
195
|
+
// A reusable handler for GET and DELETE requests which operate on existing sessions.
|
|
196
|
+
const handleSessionRequest = async (c) => {
|
|
197
|
+
const sessionId = c.req.header("mcp-session-id");
|
|
198
|
+
const transport = sessionId ? transports[sessionId] : undefined;
|
|
199
|
+
if (!transport) {
|
|
200
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, "Session not found or expired.");
|
|
204
201
|
}
|
|
202
|
+
// Let the transport handle the streaming (GET) or termination (DELETE) request.
|
|
203
|
+
return await transport.handleRequest(c.env.incoming, c.env.outgoing);
|
|
205
204
|
};
|
|
206
|
-
app.get(MCP_ENDPOINT_PATH,
|
|
207
|
-
app.delete(MCP_ENDPOINT_PATH,
|
|
205
|
+
app.get(MCP_ENDPOINT_PATH, handleSessionRequest);
|
|
206
|
+
app.delete(MCP_ENDPOINT_PATH, handleSessionRequest);
|
|
208
207
|
return startHttpServerWithRetry(app, HTTP_PORT, HTTP_HOST, MAX_PORT_RETRIES, transportContext);
|
|
209
208
|
}
|
|
@@ -35,8 +35,11 @@ const mcpToWinstonLevel = {
|
|
|
35
35
|
alert: "error",
|
|
36
36
|
emerg: "error",
|
|
37
37
|
};
|
|
38
|
+
// The logsPath from config is already resolved and validated by src/config/index.ts
|
|
39
|
+
const resolvedLogsDir = config.logsPath;
|
|
40
|
+
const isLogsDirSafe = !!resolvedLogsDir; // If logsPath is set, it's considered safe by config logic.
|
|
38
41
|
/**
|
|
39
|
-
* Creates the Winston console log format
|
|
42
|
+
* Creates the Winston console log format.
|
|
40
43
|
* @returns The Winston log format for console output.
|
|
41
44
|
* @private
|
|
42
45
|
*/
|
|
@@ -100,45 +103,47 @@ export class Logger {
|
|
|
100
103
|
});
|
|
101
104
|
return;
|
|
102
105
|
}
|
|
106
|
+
// Set initialized to true at the beginning of the initialization process.
|
|
103
107
|
this.initialized = true;
|
|
104
108
|
this.currentMcpLevel = level;
|
|
105
109
|
this.currentWinstonLevel = mcpToWinstonLevel[level];
|
|
110
|
+
// The logs directory (config.logsPath / resolvedLogsDir) is expected to be created and validated
|
|
111
|
+
// by the configuration module (src/config/index.ts) before logger initialization.
|
|
112
|
+
// If isLogsDirSafe is true, we assume resolvedLogsDir exists and is usable.
|
|
113
|
+
// No redundant directory creation logic here.
|
|
114
|
+
const fileFormat = winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json());
|
|
106
115
|
const transports = [];
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
116
|
+
const fileTransportOptions = {
|
|
117
|
+
format: fileFormat,
|
|
118
|
+
maxsize: this.LOG_FILE_MAX_SIZE,
|
|
119
|
+
maxFiles: this.LOG_MAX_FILES,
|
|
120
|
+
tailable: true,
|
|
121
|
+
};
|
|
122
|
+
if (isLogsDirSafe) {
|
|
123
|
+
transports.push(new winston.transports.File({
|
|
124
|
+
filename: path.join(resolvedLogsDir, "error.log"),
|
|
125
|
+
level: "error",
|
|
126
|
+
...fileTransportOptions,
|
|
127
|
+
}), new winston.transports.File({
|
|
128
|
+
filename: path.join(resolvedLogsDir, "warn.log"),
|
|
129
|
+
level: "warn",
|
|
130
|
+
...fileTransportOptions,
|
|
131
|
+
}), new winston.transports.File({
|
|
132
|
+
filename: path.join(resolvedLogsDir, "info.log"),
|
|
133
|
+
level: "info",
|
|
134
|
+
...fileTransportOptions,
|
|
135
|
+
}), new winston.transports.File({
|
|
136
|
+
filename: path.join(resolvedLogsDir, "debug.log"),
|
|
137
|
+
level: "debug",
|
|
138
|
+
...fileTransportOptions,
|
|
139
|
+
}), new winston.transports.File({
|
|
140
|
+
filename: path.join(resolvedLogsDir, "combined.log"),
|
|
141
|
+
...fileTransportOptions,
|
|
110
142
|
}));
|
|
111
143
|
}
|
|
112
144
|
else {
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const fileFormat = winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json());
|
|
116
|
-
const fileTransportOptions = {
|
|
117
|
-
format: fileFormat,
|
|
118
|
-
maxsize: this.LOG_FILE_MAX_SIZE,
|
|
119
|
-
maxFiles: this.LOG_MAX_FILES,
|
|
120
|
-
tailable: true,
|
|
121
|
-
};
|
|
122
|
-
transports.push(new winston.transports.File({
|
|
123
|
-
filename: path.join(resolvedLogsDir, "error.log"),
|
|
124
|
-
level: "error",
|
|
125
|
-
...fileTransportOptions,
|
|
126
|
-
}), new winston.transports.File({
|
|
127
|
-
filename: path.join(resolvedLogsDir, "warn.log"),
|
|
128
|
-
level: "warn",
|
|
129
|
-
...fileTransportOptions,
|
|
130
|
-
}), new winston.transports.File({
|
|
131
|
-
filename: path.join(resolvedLogsDir, "info.log"),
|
|
132
|
-
level: "info",
|
|
133
|
-
...fileTransportOptions,
|
|
134
|
-
}), new winston.transports.File({
|
|
135
|
-
filename: path.join(resolvedLogsDir, "debug.log"),
|
|
136
|
-
level: "debug",
|
|
137
|
-
...fileTransportOptions,
|
|
138
|
-
}), new winston.transports.File({
|
|
139
|
-
filename: path.join(resolvedLogsDir, "combined.log"),
|
|
140
|
-
...fileTransportOptions,
|
|
141
|
-
}));
|
|
145
|
+
if (process.stdout.isTTY) {
|
|
146
|
+
console.warn("File logging disabled as logsPath is not configured or invalid.");
|
|
142
147
|
}
|
|
143
148
|
}
|
|
144
149
|
this.winstonLogger = winston.createLogger({
|
|
@@ -146,20 +151,23 @@ export class Logger {
|
|
|
146
151
|
transports,
|
|
147
152
|
exitOnError: false,
|
|
148
153
|
});
|
|
154
|
+
// Configure console transport after Winston logger is created
|
|
149
155
|
const consoleStatus = this._configureConsoleTransport();
|
|
150
156
|
const initialContext = {
|
|
151
157
|
loggerSetup: true,
|
|
152
158
|
requestId: "logger-init-deferred",
|
|
153
159
|
timestamp: new Date().toISOString(),
|
|
154
160
|
};
|
|
161
|
+
// Removed logging of logsDirCreatedMessage as it's no longer set
|
|
155
162
|
if (consoleStatus.message) {
|
|
156
163
|
this.info(consoleStatus.message, initialContext);
|
|
157
164
|
}
|
|
158
|
-
this.
|
|
165
|
+
this.initialized = true; // Ensure this is set after successful setup
|
|
166
|
+
this.info(`Logger initialized. File logging level: ${this.currentWinstonLevel}. MCP logging level: ${this.currentMcpLevel}. Console logging: ${consoleStatus.enabled ? "enabled" : "disabled"}`, {
|
|
159
167
|
loggerSetup: true,
|
|
160
168
|
requestId: "logger-post-init",
|
|
161
169
|
timestamp: new Date().toISOString(),
|
|
162
|
-
logsPathUsed:
|
|
170
|
+
logsPathUsed: resolvedLogsDir,
|
|
163
171
|
});
|
|
164
172
|
}
|
|
165
173
|
/**
|
|
@@ -199,6 +207,7 @@ export class Logger {
|
|
|
199
207
|
this.currentMcpLevel = newLevel;
|
|
200
208
|
this.currentWinstonLevel = mcpToWinstonLevel[newLevel];
|
|
201
209
|
if (this.winstonLogger) {
|
|
210
|
+
// Ensure winstonLogger is defined
|
|
202
211
|
this.winstonLogger.level = this.currentWinstonLevel;
|
|
203
212
|
}
|
|
204
213
|
const consoleStatus = this._configureConsoleTransport();
|
|
@@ -217,12 +226,6 @@ export class Logger {
|
|
|
217
226
|
* @private
|
|
218
227
|
*/
|
|
219
228
|
_configureConsoleTransport() {
|
|
220
|
-
if (config.logOutputMode === "stdout") {
|
|
221
|
-
return {
|
|
222
|
-
enabled: true,
|
|
223
|
-
message: "Stdout logging is enabled by configuration.",
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
229
|
if (!this.winstonLogger) {
|
|
227
230
|
return {
|
|
228
231
|
enabled: false,
|
|
@@ -235,19 +238,17 @@ export class Logger {
|
|
|
235
238
|
if (shouldHaveConsole && !consoleTransport) {
|
|
236
239
|
const consoleFormat = createWinstonConsoleFormat();
|
|
237
240
|
this.winstonLogger.add(new winston.transports.Console({
|
|
238
|
-
level: "debug",
|
|
241
|
+
level: "debug", // Console always logs debug if enabled
|
|
239
242
|
format: consoleFormat,
|
|
240
243
|
}));
|
|
241
|
-
message =
|
|
242
|
-
"Interactive console logging enabled (level: debug, stdout is TTY).";
|
|
244
|
+
message = "Console logging enabled (level: debug, stdout is TTY).";
|
|
243
245
|
}
|
|
244
246
|
else if (!shouldHaveConsole && consoleTransport) {
|
|
245
247
|
this.winstonLogger.remove(consoleTransport);
|
|
246
|
-
message =
|
|
247
|
-
"Interactive console logging disabled (level not debug or stdout not TTY).";
|
|
248
|
+
message = "Console logging disabled (level not debug or stdout not TTY).";
|
|
248
249
|
}
|
|
249
250
|
else {
|
|
250
|
-
message = "
|
|
251
|
+
message = "Console logging status unchanged.";
|
|
251
252
|
}
|
|
252
253
|
return { enabled: shouldHaveConsole, message };
|
|
253
254
|
}
|
|
@@ -287,7 +288,7 @@ export class Logger {
|
|
|
287
288
|
if (!this.ensureInitialized())
|
|
288
289
|
return;
|
|
289
290
|
if (mcpLevelSeverity[level] > mcpLevelSeverity[this.currentMcpLevel]) {
|
|
290
|
-
return;
|
|
291
|
+
return; // Do not log if message level is less severe than currentMcpLevel
|
|
291
292
|
}
|
|
292
293
|
const logData = { ...context };
|
|
293
294
|
const winstonLevel = mcpToWinstonLevel[level];
|
|
@@ -303,6 +304,7 @@ export class Logger {
|
|
|
303
304
|
mcpDataPayload.context = context;
|
|
304
305
|
if (error) {
|
|
305
306
|
mcpDataPayload.error = { message: error.message };
|
|
307
|
+
// Include stack trace in debug mode for MCP notifications, truncated for brevity
|
|
306
308
|
if (this.currentMcpLevel === "debug" && error.stack) {
|
|
307
309
|
mcpDataPayload.error.stack = error.stack.substring(0, this.MCP_NOTIFICATION_STACK_TRACE_MAX_LENGTH);
|
|
308
310
|
}
|
|
@@ -319,7 +321,7 @@ export class Logger {
|
|
|
319
321
|
originalLevel: level,
|
|
320
322
|
originalMessage: msg,
|
|
321
323
|
sendError: errorMessage,
|
|
322
|
-
mcpPayload: JSON.stringify(mcpDataPayload).substring(0, 500),
|
|
324
|
+
mcpPayload: JSON.stringify(mcpDataPayload).substring(0, 500), // Log a preview
|
|
323
325
|
};
|
|
324
326
|
this.winstonLogger.error("Failed to send MCP log notification", internalErrorContext);
|
|
325
327
|
}
|
|
@@ -85,6 +85,7 @@ export declare class IdGenerator {
|
|
|
85
85
|
* @param id - The ID string to validate.
|
|
86
86
|
* @param entityType - The expected entity type of the ID.
|
|
87
87
|
* @param options - Optional parameters used during generation for validation consistency.
|
|
88
|
+
* The `charset` from these options will be used for validation.
|
|
88
89
|
* @returns `true` if the ID is valid, `false` otherwise.
|
|
89
90
|
*/
|
|
90
91
|
isValid(id: string, entityType: string, options?: IdGenerationOptions): boolean;
|
|
@@ -112,7 +113,9 @@ export declare class IdGenerator {
|
|
|
112
113
|
getEntityType(id: string, separator?: string): string;
|
|
113
114
|
/**
|
|
114
115
|
* Normalizes an entity ID to ensure the prefix matches the registered case
|
|
115
|
-
* and the random part is uppercase.
|
|
116
|
+
* and the random part is uppercase. Note: This assumes the charset characters
|
|
117
|
+
* have a meaningful uppercase version if case-insensitivity is desired for the random part.
|
|
118
|
+
* For default charset (A-Z0-9), this is fine. For custom charsets, behavior might vary.
|
|
116
119
|
* @param id - The ID to normalize (e.g., "proj_a6b3j0").
|
|
117
120
|
* @param separator - The separator used in the ID. Defaults to `IdGenerator.DEFAULT_SEPARATOR`.
|
|
118
121
|
* @returns The normalized ID (e.g., "PROJ_A6B3J0").
|
|
@@ -60,10 +60,18 @@ export class IdGenerator {
|
|
|
60
60
|
* @returns The generated random string.
|
|
61
61
|
*/
|
|
62
62
|
generateRandomString(length = IdGenerator.DEFAULT_LENGTH, charset = IdGenerator.DEFAULT_CHARSET) {
|
|
63
|
-
const bytes = randomBytes(length);
|
|
64
63
|
let result = "";
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
// Determine the largest multiple of charset.length that is less than or equal to 256
|
|
65
|
+
// This is the threshold for rejection sampling to avoid bias.
|
|
66
|
+
const maxValidByteValue = Math.floor(256 / charset.length) * charset.length;
|
|
67
|
+
while (result.length < length) {
|
|
68
|
+
const byteBuffer = randomBytes(1); // Get one random byte
|
|
69
|
+
const byte = byteBuffer[0];
|
|
70
|
+
// If the byte is within the valid range (i.e., it won't introduce bias),
|
|
71
|
+
// use it to select a character from the charset. Otherwise, discard and try again.
|
|
72
|
+
if (byte < maxValidByteValue) {
|
|
73
|
+
result += charset[byte % charset.length];
|
|
74
|
+
}
|
|
67
75
|
}
|
|
68
76
|
return result;
|
|
69
77
|
}
|
|
@@ -101,16 +109,21 @@ export class IdGenerator {
|
|
|
101
109
|
* @param id - The ID string to validate.
|
|
102
110
|
* @param entityType - The expected entity type of the ID.
|
|
103
111
|
* @param options - Optional parameters used during generation for validation consistency.
|
|
112
|
+
* The `charset` from these options will be used for validation.
|
|
104
113
|
* @returns `true` if the ID is valid, `false` otherwise.
|
|
105
114
|
*/
|
|
106
115
|
isValid(id, entityType, options = {}) {
|
|
107
116
|
const prefix = this.entityPrefixes[entityType];
|
|
108
|
-
const { length = IdGenerator.DEFAULT_LENGTH, separator = IdGenerator.DEFAULT_SEPARATOR,
|
|
117
|
+
const { length = IdGenerator.DEFAULT_LENGTH, separator = IdGenerator.DEFAULT_SEPARATOR, charset = IdGenerator.DEFAULT_CHARSET, // Use charset from options or default
|
|
118
|
+
} = options;
|
|
109
119
|
if (!prefix) {
|
|
110
120
|
return false;
|
|
111
121
|
}
|
|
112
|
-
//
|
|
113
|
-
|
|
122
|
+
// Build regex character class from the charset
|
|
123
|
+
// Escape characters that have special meaning inside a regex character class `[]`
|
|
124
|
+
const escapedCharsetForClass = charset.replace(/[[\]\\^-]/g, "\\$&");
|
|
125
|
+
const charsetRegexPart = `[${escapedCharsetForClass}]`;
|
|
126
|
+
const pattern = new RegExp(`^${this.escapeRegex(prefix)}${this.escapeRegex(separator)}${charsetRegexPart}{${length}}$`);
|
|
114
127
|
return pattern.test(id);
|
|
115
128
|
}
|
|
116
129
|
/**
|
|
@@ -153,7 +166,9 @@ export class IdGenerator {
|
|
|
153
166
|
}
|
|
154
167
|
/**
|
|
155
168
|
* Normalizes an entity ID to ensure the prefix matches the registered case
|
|
156
|
-
* and the random part is uppercase.
|
|
169
|
+
* and the random part is uppercase. Note: This assumes the charset characters
|
|
170
|
+
* have a meaningful uppercase version if case-insensitivity is desired for the random part.
|
|
171
|
+
* For default charset (A-Z0-9), this is fine. For custom charsets, behavior might vary.
|
|
157
172
|
* @param id - The ID to normalize (e.g., "proj_a6b3j0").
|
|
158
173
|
* @param separator - The separator used in the ID. Defaults to `IdGenerator.DEFAULT_SEPARATOR`.
|
|
159
174
|
* @returns The normalized ID (e.g., "PROJ_A6B3J0").
|
|
@@ -164,6 +179,8 @@ export class IdGenerator {
|
|
|
164
179
|
const registeredPrefix = this.entityPrefixes[entityType];
|
|
165
180
|
const idParts = id.split(separator);
|
|
166
181
|
const randomPart = idParts.slice(1).join(separator);
|
|
182
|
+
// Consider if randomPart.toUpperCase() is always correct for custom charsets.
|
|
183
|
+
// For now, maintaining existing behavior.
|
|
167
184
|
return `${registeredPrefix}${separator}${randomPart.toUpperCase()}`;
|
|
168
185
|
}
|
|
169
186
|
}
|
|
@@ -28,10 +28,6 @@ export interface RateLimitEntry {
|
|
|
28
28
|
/**
|
|
29
29
|
* A generic rate limiter class using an in-memory store.
|
|
30
30
|
* Controls frequency of operations based on unique keys.
|
|
31
|
-
*
|
|
32
|
-
* @scalability Note: This is an in-memory store. For horizontal scaling across
|
|
33
|
-
* multiple processes or machines, this state would need to be moved to a shared,
|
|
34
|
-
* distributed store like Redis or a database.
|
|
35
31
|
*/
|
|
36
32
|
export declare class RateLimiter {
|
|
37
33
|
private config;
|
|
@@ -9,10 +9,6 @@ import { logger, requestContextService } from "../index.js";
|
|
|
9
9
|
/**
|
|
10
10
|
* A generic rate limiter class using an in-memory store.
|
|
11
11
|
* Controls frequency of operations based on unique keys.
|
|
12
|
-
*
|
|
13
|
-
* @scalability Note: This is an in-memory store. For horizontal scaling across
|
|
14
|
-
* multiple processes or machines, this state would need to be moved to a shared,
|
|
15
|
-
* distributed store like Redis or a database.
|
|
16
12
|
*/
|
|
17
13
|
export class RateLimiter {
|
|
18
14
|
/**
|
|
@@ -148,6 +148,17 @@ export declare class Sanitization {
|
|
|
148
148
|
* Sanitizes input for logging by redacting sensitive fields.
|
|
149
149
|
* Creates a deep clone and replaces values of fields matching `this.sensitiveFields`
|
|
150
150
|
* (case-insensitive substring match) with "[REDACTED]".
|
|
151
|
+
*
|
|
152
|
+
* It uses `structuredClone` if available for a high-fidelity deep clone.
|
|
153
|
+
* If `structuredClone` is not available (e.g., in older Node.js environments),
|
|
154
|
+
* it falls back to `JSON.parse(JSON.stringify(input))`. This fallback has limitations:
|
|
155
|
+
* - `Date` objects are converted to ISO date strings.
|
|
156
|
+
* - `undefined` values within objects are removed.
|
|
157
|
+
* - `Map`, `Set`, `RegExp` objects are converted to empty objects (`{}`).
|
|
158
|
+
* - Functions are removed.
|
|
159
|
+
* - `BigInt` values will throw an error during `JSON.stringify` unless a `toJSON` method is provided.
|
|
160
|
+
* - Circular references will cause `JSON.stringify` to throw an error.
|
|
161
|
+
*
|
|
151
162
|
* @param input - The input data to sanitize for logging.
|
|
152
163
|
* @returns A sanitized (deep cloned) version of the input, safe for logging.
|
|
153
164
|
* Returns original input if not object/array, or "[Log Sanitization Failed]" on error.
|
|
@@ -204,8 +204,11 @@ export class Sanitization {
|
|
|
204
204
|
})) {
|
|
205
205
|
throw new Error("Invalid URL format or protocol not in allowed list.");
|
|
206
206
|
}
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
const lowercasedInput = trimmedInput.toLowerCase();
|
|
208
|
+
if (lowercasedInput.startsWith("javascript:") ||
|
|
209
|
+
lowercasedInput.startsWith("data:") ||
|
|
210
|
+
lowercasedInput.startsWith("vbscript:")) {
|
|
211
|
+
throw new Error("Disallowed pseudo-protocol (javascript:, data:, or vbscript:) in URL.");
|
|
209
212
|
}
|
|
210
213
|
return trimmedInput;
|
|
211
214
|
}
|
|
@@ -377,6 +380,17 @@ export class Sanitization {
|
|
|
377
380
|
* Sanitizes input for logging by redacting sensitive fields.
|
|
378
381
|
* Creates a deep clone and replaces values of fields matching `this.sensitiveFields`
|
|
379
382
|
* (case-insensitive substring match) with "[REDACTED]".
|
|
383
|
+
*
|
|
384
|
+
* It uses `structuredClone` if available for a high-fidelity deep clone.
|
|
385
|
+
* If `structuredClone` is not available (e.g., in older Node.js environments),
|
|
386
|
+
* it falls back to `JSON.parse(JSON.stringify(input))`. This fallback has limitations:
|
|
387
|
+
* - `Date` objects are converted to ISO date strings.
|
|
388
|
+
* - `undefined` values within objects are removed.
|
|
389
|
+
* - `Map`, `Set`, `RegExp` objects are converted to empty objects (`{}`).
|
|
390
|
+
* - Functions are removed.
|
|
391
|
+
* - `BigInt` values will throw an error during `JSON.stringify` unless a `toJSON` method is provided.
|
|
392
|
+
* - Circular references will cause `JSON.stringify` to throw an error.
|
|
393
|
+
*
|
|
380
394
|
* @param input - The input data to sanitize for logging.
|
|
381
395
|
* @returns A sanitized (deep cloned) version of the input, safe for logging.
|
|
382
396
|
* Returns original input if not object/array, or "[Log Sanitization Failed]" on error.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyanheads/pubmed-mcp-server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "A Model Context Protocol (MCP) server enabling AI agents to intelligently search, retrieve, and analyze biomedical literature from PubMed via NCBI E-utilities. Built on the mcp-ts-template for robust, production-ready performance.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -33,19 +33,19 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@hono/node-server": "^1.14.4",
|
|
36
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
37
|
-
"@types/jsonwebtoken": "^9.0.
|
|
38
|
-
"@types/node": "^24.0.
|
|
36
|
+
"@modelcontextprotocol/sdk": "^1.13.0",
|
|
37
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
38
|
+
"@types/node": "^24.0.3",
|
|
39
39
|
"@types/sanitize-html": "^2.16.0",
|
|
40
|
-
"@types/validator": "13.15.
|
|
40
|
+
"@types/validator": "13.15.2",
|
|
41
41
|
"axios": "^1.10.0",
|
|
42
42
|
"chrono-node": "^2.8.3",
|
|
43
43
|
"dotenv": "^16.5.0",
|
|
44
44
|
"fast-xml-parser": "^5.2.5",
|
|
45
|
-
"hono": "^4.
|
|
45
|
+
"hono": "^4.8.2",
|
|
46
46
|
"jose": "^6.0.11",
|
|
47
47
|
"jsonwebtoken": "^9.0.2",
|
|
48
|
-
"openai": "^5.
|
|
48
|
+
"openai": "^5.6.0",
|
|
49
49
|
"partial-json": "^0.1.7",
|
|
50
50
|
"sanitize-html": "^2.17.0",
|
|
51
51
|
"tiktoken": "^1.0.21",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"chartjs-node-canvas": "^5.0.0",
|
|
57
57
|
"winston": "^3.17.0",
|
|
58
58
|
"winston-transport": "^4.9.0",
|
|
59
|
-
"zod": "^3.25.
|
|
59
|
+
"zod": "^3.25.67"
|
|
60
60
|
},
|
|
61
61
|
"keywords": [
|
|
62
62
|
"mcp",
|