clinicaltrialsgov-mcp-server 1.0.0 → 1.0.2
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 +26 -27
- package/dist/mcp-server/tools/getStudy/logic.js +5 -14
- package/dist/mcp-server/tools/listStudies/logic.js +5 -14
- 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 -7
- package/dist/mcp-server/transports/{authentication/authMiddleware.js → auth/strategies/jwt/jwtMiddleware.js} +40 -36
- 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 +81 -131
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
# ClinicalTrials.gov 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/clinicaltrialsgov-mcp-server/issues)
|
|
8
8
|
[](https://github.com/cyanheads/clinicaltrialsgov-mcp-server)
|
|
9
9
|
|
|
10
10
|
**Empower your AI agents with direct access to the official ClinicalTrials.gov database!**
|
|
11
11
|
|
|
12
|
-
An MCP (Model Context Protocol) server providing a robust, developer-friendly interface to the official [ClinicalTrials.gov v2 API](https://clinicaltrials.gov/api/
|
|
12
|
+
An MCP (Model Context Protocol) server providing a robust, developer-friendly interface to the official [ClinicalTrials.gov v2 API](https://clinicaltrials.gov/data-api/api). Enables LLMs and AI agents to search, retrieve, and analyze clinical study data programmatically.
|
|
13
13
|
|
|
14
14
|
Built on the [`cyanheads/mcp-ts-template`](https://github.com/cyanheads/mcp-ts-template), this server follows a modular architecture with robust error handling, logging, and security features.
|
|
15
15
|
|
|
@@ -59,7 +59,7 @@ Leverages the robust utilities provided by the `mcp-ts-template`:
|
|
|
59
59
|
- **Input Validation/Sanitization**: Uses `zod` for schema validation and custom sanitization logic.
|
|
60
60
|
- **Request Context**: Tracking and correlation of operations via unique request IDs using `AsyncLocalStorage`.
|
|
61
61
|
- **Type Safety**: Strong typing enforced by TypeScript and Zod schemas.
|
|
62
|
-
- **HTTP Transport**: High-performance HTTP server using **Hono**, featuring session management with garbage collection
|
|
62
|
+
- **HTTP Transport**: High-performance HTTP server using **Hono**, featuring session management with garbage collection and authentication support.
|
|
63
63
|
- **Authentication**: Robust authentication layer supporting JWT and OAuth 2.1, with fine-grained scope enforcement.
|
|
64
64
|
- **Deployment**: Multi-stage `Dockerfile` for creating small, secure production images with native dependency support.
|
|
65
65
|
|
|
@@ -80,12 +80,31 @@ Leverages the robust utilities provided by the `mcp-ts-template`:
|
|
|
80
80
|
|
|
81
81
|
- [Node.js (>=18.0.0)](https://nodejs.org/)
|
|
82
82
|
- [npm](https://www.npmjs.com/) (comes with Node.js)
|
|
83
|
-
- [Docker](https://www.docker.com/) (optional, for containerized deployment)
|
|
84
83
|
|
|
85
|
-
###
|
|
84
|
+
### MCP Client Settings
|
|
85
|
+
|
|
86
|
+
Add the following to your MCP client's configuration file (e.g., `cline_mcp_settings.json`). This configuration uses `npx` to run the server, which will automatically install the package if not already present:
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"mcpServers": {
|
|
91
|
+
"clinicaltrialsgov-mcp-server": {
|
|
92
|
+
"command": "npx",
|
|
93
|
+
"args": ["clinicaltrialsgov-mcp-server"],
|
|
94
|
+
"env": {
|
|
95
|
+
"MCP_LOG_LEVEL": "info"
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## If running manually (not via MCP client for development or testing)
|
|
103
|
+
|
|
104
|
+
### Install via npm
|
|
86
105
|
|
|
87
106
|
```bash
|
|
88
|
-
npm install
|
|
107
|
+
npm install clinicaltrialsgov-mcp-server
|
|
89
108
|
```
|
|
90
109
|
|
|
91
110
|
### Alternatively Install from Source
|
|
@@ -129,26 +148,6 @@ Configure the server using environment variables. These environmental variables
|
|
|
129
148
|
| `LOGS_DIR` | Directory for log file storage (if `LOG_OUTPUT_MODE=file`). | `logs/` |
|
|
130
149
|
| `NODE_ENV` | Runtime environment (`development`, `production`). | `development` |
|
|
131
150
|
|
|
132
|
-
### MCP Client Settings
|
|
133
|
-
|
|
134
|
-
Add the following to your MCP client's configuration file (e.g., `cline_mcp_settings.json`). This configuration uses `npx` to run the server, which will automatically install the package if not already present:
|
|
135
|
-
|
|
136
|
-
```json
|
|
137
|
-
{
|
|
138
|
-
"mcpServers": {
|
|
139
|
-
"clinicaltrialsgov-mcp-server": {
|
|
140
|
-
"command": "npx",
|
|
141
|
-
"args": ["@cyanheads/clinicaltrialsgov-mcp-server"],
|
|
142
|
-
"env": {
|
|
143
|
-
"MCP_LOG_LEVEL": "info"
|
|
144
|
-
},
|
|
145
|
-
"disabled": false,
|
|
146
|
-
"autoApprove": []
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
```
|
|
151
|
-
|
|
152
151
|
## Project Structure
|
|
153
152
|
|
|
154
153
|
The codebase follows a modular structure within the `src/` directory:
|
|
@@ -38,19 +38,10 @@ export async function getStudyLogic(params, context) {
|
|
|
38
38
|
toolInput: params,
|
|
39
39
|
});
|
|
40
40
|
const service = new ClinicalTrialsGovService();
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, `Study with NCT ID '${params.nctId}' not found.`, { nctId: params.nctId });
|
|
45
|
-
}
|
|
46
|
-
logger.info(`Successfully fetched study ${params.nctId}`, { ...context });
|
|
47
|
-
return cleanStudy(study);
|
|
48
|
-
}
|
|
49
|
-
catch (error) {
|
|
50
|
-
// Re-throw service errors or wrap unexpected errors
|
|
51
|
-
if (error instanceof McpError) {
|
|
52
|
-
throw error;
|
|
53
|
-
}
|
|
54
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, `An unexpected error occurred while fetching study ${params.nctId}.`, { originalError: error });
|
|
41
|
+
const study = await service.fetchStudy(params.nctId, context);
|
|
42
|
+
if (!study) {
|
|
43
|
+
throw new McpError(BaseErrorCode.NOT_FOUND, `Study with NCT ID '${params.nctId}' not found.`, { nctId: params.nctId });
|
|
55
44
|
}
|
|
45
|
+
logger.info(`Successfully fetched study ${params.nctId}`, { ...context });
|
|
46
|
+
return cleanStudy(study);
|
|
56
47
|
}
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { ClinicalTrialsGovService, Status, } from "../../../services/clinical-trials-gov/index.js";
|
|
7
|
-
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
|
|
8
7
|
import { cleanStudy } from "../../../utils/clinicaltrials/jsonCleaner.js";
|
|
9
8
|
import { logger } from "../../../utils/index.js";
|
|
10
9
|
/**
|
|
@@ -90,18 +89,10 @@ export const ListStudiesInputSchema = z.object({
|
|
|
90
89
|
export async function listStudiesLogic(params, context) {
|
|
91
90
|
logger.debug("Executing listStudiesLogic", { ...context, toolInput: params });
|
|
92
91
|
const service = new ClinicalTrialsGovService();
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
pagedStudies.studies = pagedStudies.studies.map(cleanStudy);
|
|
98
|
-
}
|
|
99
|
-
return pagedStudies;
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
if (error instanceof McpError) {
|
|
103
|
-
throw error;
|
|
104
|
-
}
|
|
105
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, "An unexpected error occurred while listing studies.", { originalError: error });
|
|
92
|
+
const pagedStudies = await service.listStudies(params, context);
|
|
93
|
+
logger.info("Successfully listed studies.", { ...context });
|
|
94
|
+
if (pagedStudies.studies) {
|
|
95
|
+
pagedStudies.studies = pagedStudies.studies.map(cleanStudy);
|
|
106
96
|
}
|
|
97
|
+
return pagedStudies;
|
|
107
98
|
}
|
|
@@ -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,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
|
|
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";
|
|
@@ -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
|
|
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
|
|
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 {
|
|
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 (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
+
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Malformed authentication token.");
|
|
74
81
|
}
|
|
75
82
|
const rawToken = tokenParts[1];
|
|
76
83
|
try {
|
|
77
|
-
const decoded =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
125
|
-
|
|
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
|
-
|
|
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 = `
|
|
137
|
-
logger.
|
|
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
|
-
|
|
147
|
+
throw new McpError(errorCode, errorMessage);
|
|
144
148
|
}
|
|
145
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,
|
|
@@ -96,22 +103,6 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
|
|
|
96
103
|
...parentContext,
|
|
97
104
|
component: "HttpTransportSetup",
|
|
98
105
|
});
|
|
99
|
-
setInterval(() => {
|
|
100
|
-
const now = Date.now();
|
|
101
|
-
const gcContext = requestContextService.createRequestContext({
|
|
102
|
-
operation: "SessionGarbageCollector",
|
|
103
|
-
});
|
|
104
|
-
for (const sessionId in sessionActivity) {
|
|
105
|
-
if (now - sessionActivity[sessionId] > SESSION_TIMEOUT_MS) {
|
|
106
|
-
logger.info(`Session ${sessionId} timed out. Cleaning up.`, {
|
|
107
|
-
...gcContext,
|
|
108
|
-
sessionId,
|
|
109
|
-
});
|
|
110
|
-
httpTransports[sessionId]?.close();
|
|
111
|
-
delete sessionActivity[sessionId];
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
}, SESSION_GC_INTERVAL_MS);
|
|
115
106
|
app.use("*", cors({
|
|
116
107
|
origin: config.mcpAllowedOrigins || [],
|
|
117
108
|
allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
|
|
@@ -128,131 +119,90 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
|
|
|
128
119
|
await next();
|
|
129
120
|
});
|
|
130
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.
|
|
131
125
|
const clientIp = c.req.header("x-forwarded-for")?.split(",")[0].trim() || "unknown_ip";
|
|
132
126
|
const context = requestContextService.createRequestContext({
|
|
133
127
|
operation: "httpRateLimitCheck",
|
|
134
128
|
ipAddress: clientIp,
|
|
135
129
|
});
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
}
|
|
140
|
-
catch (error) {
|
|
141
|
-
const handledError = ErrorHandler.handleError(error, {
|
|
142
|
-
operation: "rateLimitMiddleware",
|
|
143
|
-
context,
|
|
144
|
-
});
|
|
145
|
-
return c.json({
|
|
146
|
-
jsonrpc: "2.0",
|
|
147
|
-
error: { code: -32000, message: handledError.message },
|
|
148
|
-
id: (await c.req.json().catch(() => ({})))?.id || null,
|
|
149
|
-
}, 429);
|
|
150
|
-
}
|
|
130
|
+
// Let the centralized error handler catch rate limit errors
|
|
131
|
+
rateLimiter.check(clientIp, context);
|
|
132
|
+
await next();
|
|
151
133
|
});
|
|
152
134
|
if (config.mcpAuthMode === "oauth") {
|
|
153
135
|
app.use(MCP_ENDPOINT_PATH, oauthMiddleware);
|
|
154
136
|
}
|
|
155
137
|
else {
|
|
156
|
-
app.use(MCP_ENDPOINT_PATH,
|
|
138
|
+
app.use(MCP_ENDPOINT_PATH, jwtAuthMiddleware);
|
|
157
139
|
}
|
|
140
|
+
// Centralized Error Handling
|
|
141
|
+
app.onError(httpErrorHandler);
|
|
158
142
|
app.post(MCP_ENDPOINT_PATH, async (c) => {
|
|
159
143
|
const postContext = requestContextService.createRequestContext({
|
|
160
144
|
...transportContext,
|
|
161
145
|
operation: "handlePost",
|
|
162
146
|
});
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if (
|
|
171
|
-
|
|
172
|
-
|
|
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,
|
|
158
|
+
});
|
|
159
|
+
await transport.close(); // This will trigger the onclose handler.
|
|
160
|
+
}
|
|
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}`, {
|
|
173
167
|
...postContext,
|
|
174
|
-
|
|
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,
|
|
175
180
|
});
|
|
176
|
-
await transport.close();
|
|
177
181
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
logger.info(`HTTP Session created: ${newId}`, {
|
|
184
|
-
...postContext,
|
|
185
|
-
newSessionId: newId,
|
|
186
|
-
});
|
|
187
|
-
},
|
|
188
|
-
});
|
|
189
|
-
transport.onclose = () => {
|
|
190
|
-
const closedSessionId = transport.sessionId;
|
|
191
|
-
if (closedSessionId) {
|
|
192
|
-
delete httpTransports[closedSessionId];
|
|
193
|
-
delete sessionActivity[closedSessionId];
|
|
194
|
-
logger.info(`HTTP Session closed: ${closedSessionId}`, {
|
|
195
|
-
...postContext,
|
|
196
|
-
closedSessionId,
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
const server = await createServerInstanceFn();
|
|
201
|
-
await server.connect(transport);
|
|
202
|
-
}
|
|
203
|
-
else if (!transport) {
|
|
204
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, "Invalid or expired session ID.");
|
|
205
|
-
}
|
|
206
|
-
return await transport.handleRequest(c.env.incoming, c.env.outgoing, body);
|
|
182
|
+
};
|
|
183
|
+
// Connect the new transport to a new server instance.
|
|
184
|
+
const server = await createServerInstanceFn();
|
|
185
|
+
await server.connect(newTransport);
|
|
186
|
+
transport = newTransport;
|
|
207
187
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
context: postContext,
|
|
212
|
-
});
|
|
213
|
-
const requestId = (await c.req.json().catch(() => ({})))?.id || null;
|
|
214
|
-
return c.json({
|
|
215
|
-
jsonrpc: "2.0",
|
|
216
|
-
error: { code: -32603, message: handledError.message },
|
|
217
|
-
id: requestId,
|
|
218
|
-
}, handledError instanceof McpError &&
|
|
219
|
-
handledError.code === BaseErrorCode.NOT_FOUND
|
|
220
|
-
? 404
|
|
221
|
-
: 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.");
|
|
222
191
|
}
|
|
192
|
+
// Pass the request to the transport to handle.
|
|
193
|
+
return await transport.handleRequest(c.env.incoming, c.env.outgoing, body);
|
|
223
194
|
});
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
try {
|
|
231
|
-
const sessionId = c.req.header("mcp-session-id");
|
|
232
|
-
const transport = sessionId ? httpTransports[sessionId] : undefined;
|
|
233
|
-
if (!transport) {
|
|
234
|
-
throw new McpError(BaseErrorCode.NOT_FOUND, "Session not found or expired.");
|
|
235
|
-
}
|
|
236
|
-
if (sessionId)
|
|
237
|
-
sessionActivity[sessionId] = Date.now();
|
|
238
|
-
return await transport.handleRequest(c.env.incoming, c.env.outgoing);
|
|
239
|
-
}
|
|
240
|
-
catch (err) {
|
|
241
|
-
const handledError = ErrorHandler.handleError(err, {
|
|
242
|
-
operation: `handle${method}`,
|
|
243
|
-
context: sessionReqContext,
|
|
244
|
-
});
|
|
245
|
-
return c.json({
|
|
246
|
-
jsonrpc: "2.0",
|
|
247
|
-
error: { code: -32603, message: handledError.message },
|
|
248
|
-
id: null,
|
|
249
|
-
}, handledError instanceof McpError &&
|
|
250
|
-
handledError.code === BaseErrorCode.NOT_FOUND
|
|
251
|
-
? 404
|
|
252
|
-
: 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.");
|
|
253
201
|
}
|
|
202
|
+
// Let the transport handle the streaming (GET) or termination (DELETE) request.
|
|
203
|
+
return await transport.handleRequest(c.env.incoming, c.env.outgoing);
|
|
254
204
|
};
|
|
255
|
-
app.get(MCP_ENDPOINT_PATH,
|
|
256
|
-
app.delete(MCP_ENDPOINT_PATH,
|
|
205
|
+
app.get(MCP_ENDPOINT_PATH, handleSessionRequest);
|
|
206
|
+
app.delete(MCP_ENDPOINT_PATH, handleSessionRequest);
|
|
257
207
|
return startHttpServerWithRetry(app, HTTP_PORT, HTTP_HOST, MAX_PORT_RETRIES, transportContext);
|
|
258
208
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clinicaltrialsgov-mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A Model Context Protocol (MCP) Server providing LLM tools for the official ClinicalTrials.gov REST API. Search and retrieve clinical trial data, including study details and more",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": [
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@hono/node-server": "^1.14.4",
|
|
39
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
39
|
+
"@modelcontextprotocol/sdk": "^1.13.0",
|
|
40
40
|
"@supabase/supabase-js": "^2.50.0",
|
|
41
41
|
"@types/jsonwebtoken": "^9.0.10",
|
|
42
42
|
"@types/node": "^24.0.3",
|
|
@@ -44,11 +44,12 @@
|
|
|
44
44
|
"@types/validator": "13.15.2",
|
|
45
45
|
"chrono-node": "^2.8.0",
|
|
46
46
|
"dotenv": "^16.5.0",
|
|
47
|
-
"hono": "^4.
|
|
47
|
+
"hono": "^4.8.2",
|
|
48
48
|
"ignore": "^7.0.5",
|
|
49
49
|
"jose": "^6.0.11",
|
|
50
50
|
"jsonwebtoken": "^9.0.2",
|
|
51
|
-
"
|
|
51
|
+
"npm": "^11.4.2",
|
|
52
|
+
"openai": "^5.6.0",
|
|
52
53
|
"partial-json": "^0.1.7",
|
|
53
54
|
"sanitize-html": "^2.17.0",
|
|
54
55
|
"tiktoken": "^1.0.21",
|