mcp-ts-template 1.4.5 → 1.4.8
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 +33 -33
- package/dist/config/index.d.ts +11 -0
- package/dist/config/index.js +16 -0
- package/dist/mcp-server/server.d.ts +2 -2
- package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +21 -13
- package/dist/mcp-server/transports/authentication/authMiddleware.js +28 -64
- package/dist/mcp-server/transports/httpTransport.d.ts +4 -4
- package/dist/mcp-server/transports/httpTransport.js +125 -148
- package/dist/services/supabase/supabaseClient.d.ts +24 -0
- package/dist/services/supabase/supabaseClient.js +67 -0
- package/dist/utils/internal/logger.js +7 -1
- package/dist/utils/security/idGenerator.js +11 -3
- package/dist/utils/security/sanitization.js +5 -2
- package/package.json +14 -8
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.typescriptlang.org/)
|
|
4
4
|
[](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
5
5
|
[](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/changelog.mdx)
|
|
6
|
-
[](./CHANGELOG.md)
|
|
7
7
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
8
8
|
[](https://github.com/cyanheads/mcp-ts-template/issues)
|
|
9
9
|
[](https://github.com/cyanheads/mcp-ts-template)
|
|
@@ -185,38 +185,38 @@ This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE
|
|
|
185
185
|
|
|
186
186
|
## 📊 Detailed Features Table
|
|
187
187
|
|
|
188
|
-
| Category | Feature | Description | Location(s)
|
|
189
|
-
| :----------------------- | :------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
190
|
-
| **Core Components** | MCP Server | Core server logic, tool/resource registration, transport handling. Includes Echo Tool & Resource examples. | `src/mcp-server/`
|
|
191
|
-
| | MCP Client | Logic for connecting to external MCP servers (updated to **MCP 2025-03-26 spec**). Refactored for modularity. | `src/mcp-client/` (see subdirs: `core/`, `client-config/`, `transports/`)
|
|
192
|
-
| | Configuration | Environment-aware settings with Zod validation. | `src/config/`, `src/mcp-client/client-config/configLoader.ts`
|
|
193
|
-
| | Streamable HTTP Transport |
|
|
194
|
-
| | Stdio Transport | Handles MCP communication over standard input/output. | `src/mcp-server/transports/stdioTransport.ts`
|
|
195
|
-
| **Utilities (Core)** | Logger | Structured, context-aware logging (files with rotation & MCP notifications). | `src/utils/internal/logger.ts`
|
|
196
|
-
| | ErrorHandler | Centralized error processing, classification, and logging. | `src/utils/internal/errorHandler.ts`
|
|
197
|
-
| | RequestContext | Request/operation tracking and correlation. | `src/utils/internal/requestContext.ts`
|
|
198
|
-
| **Utilities (Metrics)** | TokenCounter | Estimates token counts using `tiktoken`. | `src/utils/metrics/tokenCounter.ts`
|
|
199
|
-
| **Utilities (Parsing)** | DateParser | Parses natural language date strings using `chrono-node`. | `src/utils/parsing/dateParser.ts`
|
|
200
|
-
| | JsonParser | Parses potentially partial JSON, handles `<think>` blocks. | `src/utils/parsing/jsonParser.ts`
|
|
201
|
-
| **Utilities (Security)** | IdGenerator | Generates unique IDs (prefixed or UUIDs). | `src/utils/security/idGenerator.ts`
|
|
202
|
-
| | RateLimiter | Request throttling based on keys. | `src/utils/security/rateLimiter.ts`
|
|
203
|
-
| | Sanitization | Input validation/cleaning (HTML, paths, URLs, numbers, JSON) & log redaction (`validator`, `sanitize-html`). | `src/utils/security/sanitization.ts`
|
|
204
|
-
| **Services** | DuckDB Integration | Reusable module for in-process analytical data management using DuckDB. A storage layer that runs on the same level as the application. Includes connection management, query execution, and example usage. Integrated with our utils (logger, etc.) | `src/services/duck-db/`, `src/storage/duckdbExample.ts`
|
|
205
|
-
| | OpenRouter LLM Integration | Reusable module for interacting with various LLMs via the OpenRouter API (OpenAI SDK compatible).
|
|
206
|
-
| **Type Safety** | Global Types | Shared TypeScript definitions for consistent interfaces (Errors, MCP types). | `src/types-global/`
|
|
207
|
-
| | Zod Schemas | Used for robust validation of configuration files and tool/resource inputs. | Throughout (`config`, `mcp-client`, tools, etc.)
|
|
208
|
-
| **Error Handling** | Pattern-Based Classification | Automatically categorize errors based on message patterns. | `src/utils/internal/errorHandler.ts`
|
|
209
|
-
| | Consistent Formatting | Standardized error responses with additional context. | `src/utils/internal/errorHandler.ts`
|
|
210
|
-
| | Safe Try/Catch Patterns | Centralized error processing helpers (`ErrorHandler.tryCatch`). | `src/utils/internal/errorHandler.ts`
|
|
211
|
-
| | Client/Transport Error Handling | Specific handlers for MCP client and transport error handling. | `src/mcp-client/core/`, `src/mcp-client/transports/`
|
|
212
|
-
| **Security** | Input Validation | Using `validator` and `zod` for various data type checks. | `src/utils/security/sanitization.ts`, etc.
|
|
213
|
-
| | Input Sanitization | Using `sanitize-html` to prevent injection attacks. | `src/utils/security/sanitization.ts`
|
|
214
|
-
| | Sensitive Data Redaction | Automatic redaction in logs. | `src/utils/security/sanitization.ts`
|
|
215
|
-
| | Configuration
|
|
216
|
-
| **Scripts** | Clean Script | Removes `dist` and `logs` directories (or custom targets). | `scripts/clean.ts`
|
|
217
|
-
| | Make Executable Script | Sets executable permissions (`chmod +x`) on specified files (Unix-like only). | `scripts/make-executable.ts`
|
|
218
|
-
| | Tree Script | Generates a directory structure tree, respecting `.gitignore`. | `scripts/tree.ts`
|
|
219
|
-
| | Fetch OpenAPI Spec Script | Fetches an OpenAPI spec (YAML/JSON) from a URL with fallbacks, saves locally. | `scripts/fetch-openapi-spec.ts`
|
|
188
|
+
| Category | Feature | Description | Location(s) |
|
|
189
|
+
| :----------------------- | :------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------ |
|
|
190
|
+
| **Core Components** | MCP Server | Core server logic, tool/resource registration, transport handling. Includes Echo Tool & Resource examples. | `src/mcp-server/` |
|
|
191
|
+
| | MCP Client | Logic for connecting to external MCP servers (updated to **MCP 2025-03-26 spec**). Refactored for modularity. | `src/mcp-client/` (see subdirs: `core/`, `client-config/`, `transports/`) |
|
|
192
|
+
| | Configuration | Environment-aware settings with Zod validation. | `src/config/`, `src/mcp-client/client-config/configLoader.ts` |
|
|
193
|
+
| | Streamable HTTP Transport | Hono-based server implementing the MCP **Streamable HTTP** transport with session management, CORS, and port retries. | `src/mcp-server/transports/httpTransport.ts` |
|
|
194
|
+
| | Stdio Transport | Handles MCP communication over standard input/output. | `src/mcp-server/transports/stdioTransport.ts` |
|
|
195
|
+
| **Utilities (Core)** | Logger | Structured, context-aware logging (files with rotation & MCP notifications). | `src/utils/internal/logger.ts` |
|
|
196
|
+
| | ErrorHandler | Centralized error processing, classification, and logging. | `src/utils/internal/errorHandler.ts` |
|
|
197
|
+
| | RequestContext | Request/operation tracking and correlation. | `src/utils/internal/requestContext.ts` |
|
|
198
|
+
| **Utilities (Metrics)** | TokenCounter | Estimates token counts using `tiktoken`. | `src/utils/metrics/tokenCounter.ts` |
|
|
199
|
+
| **Utilities (Parsing)** | DateParser | Parses natural language date strings using `chrono-node`. | `src/utils/parsing/dateParser.ts` |
|
|
200
|
+
| | JsonParser | Parses potentially partial JSON, handles `<think>` blocks. | `src/utils/parsing/jsonParser.ts` |
|
|
201
|
+
| **Utilities (Security)** | IdGenerator | Generates unique IDs (prefixed or UUIDs). | `src/utils/security/idGenerator.ts` |
|
|
202
|
+
| | RateLimiter | Request throttling based on keys. | `src/utils/security/rateLimiter.ts` |
|
|
203
|
+
| | Sanitization | Input validation/cleaning (HTML, paths, URLs, numbers, JSON) & log redaction (`validator`, `sanitize-html`). | `src/utils/security/sanitization.ts` |
|
|
204
|
+
| **Services** | DuckDB Integration | Reusable module for in-process analytical data management using DuckDB. A storage layer that runs on the same level as the application. Includes connection management, query execution, and example usage. Integrated with our utils (logger, etc.) | `src/services/duck-db/`, `src/storage/duckdbExample.ts` |
|
|
205
|
+
| | OpenRouter LLM Integration | Reusable module for interacting with various LLMs via the OpenRouter API (OpenAI SDK compatible). Integrated with our utils (logger, etc.) | `src/services/llm-providers/openRouterProvider.ts` |
|
|
206
|
+
| **Type Safety** | Global Types | Shared TypeScript definitions for consistent interfaces (Errors, MCP types). | `src/types-global/` |
|
|
207
|
+
| | Zod Schemas | Used for robust validation of configuration files and tool/resource inputs. | Throughout (`config`, `mcp-client`, tools, etc.) |
|
|
208
|
+
| **Error Handling** | Pattern-Based Classification | Automatically categorize errors based on message patterns. | `src/utils/internal/errorHandler.ts` |
|
|
209
|
+
| | Consistent Formatting | Standardized error responses with additional context. | `src/utils/internal/errorHandler.ts` |
|
|
210
|
+
| | Safe Try/Catch Patterns | Centralized error processing helpers (`ErrorHandler.tryCatch`). | `src/utils/internal/errorHandler.ts` |
|
|
211
|
+
| | Client/Transport Error Handling | Specific handlers for MCP client and transport error handling. | `src/mcp-client/core/`, `src/mcp-client/transports/` |
|
|
212
|
+
| **Security** | Input Validation | Using `validator` and `zod` for various data type checks. | `src/utils/security/sanitization.ts`, etc. |
|
|
213
|
+
| | Input Sanitization | Using `sanitize-html` to prevent injection attacks. | `src/utils/security/sanitization.ts` |
|
|
214
|
+
| | Sensitive Data Redaction | Automatic redaction in logs. | `src/utils/security/sanitization.ts` |
|
|
215
|
+
| | Configuration Validation | Throws a descriptive error if the primary client config (`mcp-config.json`) is missing, preventing fallback to a potentially insecure example file. | `src/mcp-client/client-config/configLoader.ts` |
|
|
216
|
+
| **Scripts** | Clean Script | Removes `dist` and `logs` directories (or custom targets). | `scripts/clean.ts` |
|
|
217
|
+
| | Make Executable Script | Sets executable permissions (`chmod +x`) on specified files (Unix-like only). | `scripts/make-executable.ts` |
|
|
218
|
+
| | Tree Script | Generates a directory structure tree, respecting `.gitignore`. | `scripts/tree.ts` |
|
|
219
|
+
| | Fetch OpenAPI Spec Script | Fetches an OpenAPI spec (YAML/JSON) from a URL with fallbacks, saves locally. | `scripts/fetch-openapi-spec.ts` |
|
|
220
220
|
|
|
221
221
|
---
|
|
222
222
|
|
package/dist/config/index.d.ts
CHANGED
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
* Aggregates settings from validated environment variables and `package.json`.
|
|
20
20
|
*/
|
|
21
21
|
export declare const config: {
|
|
22
|
+
/** Information from package.json. */
|
|
23
|
+
pkg: {
|
|
24
|
+
name: string;
|
|
25
|
+
version: string;
|
|
26
|
+
};
|
|
22
27
|
/** MCP server name. Env `MCP_SERVER_NAME` > `package.json` name > "mcp-ts-template". */
|
|
23
28
|
mcpServerName: string;
|
|
24
29
|
/** MCP server version. Env `MCP_SERVER_VERSION` > `package.json` version > "0.0.0". */
|
|
@@ -66,6 +71,12 @@ export declare const config: {
|
|
|
66
71
|
serviceDocumentationUrl: string | undefined;
|
|
67
72
|
defaultClientRedirectUris: string[] | undefined;
|
|
68
73
|
} | undefined;
|
|
74
|
+
/** Supabase configuration. Undefined if no related env vars are set. */
|
|
75
|
+
supabase: {
|
|
76
|
+
url: string;
|
|
77
|
+
anonKey: string;
|
|
78
|
+
serviceRoleKey: string | undefined;
|
|
79
|
+
} | undefined;
|
|
69
80
|
};
|
|
70
81
|
/**
|
|
71
82
|
* Configured logging level for the application.
|
package/dist/config/index.js
CHANGED
|
@@ -145,6 +145,12 @@ const EnvSchema = z.object({
|
|
|
145
145
|
.optional(),
|
|
146
146
|
/** Optional. Comma-separated default OAuth client redirect URIs. */
|
|
147
147
|
OAUTH_PROXY_DEFAULT_CLIENT_REDIRECT_URIS: z.string().optional(),
|
|
148
|
+
/** Supabase Project URL. From `SUPABASE_URL`. */
|
|
149
|
+
SUPABASE_URL: z.string().url("SUPABASE_URL must be a valid URL.").optional(),
|
|
150
|
+
/** Supabase Anon Key (public). From `SUPABASE_ANON_KEY`. */
|
|
151
|
+
SUPABASE_ANON_KEY: z.string().optional(),
|
|
152
|
+
/** Supabase Service Role Key (secret). From `SUPABASE_SERVICE_ROLE_KEY`. */
|
|
153
|
+
SUPABASE_SERVICE_ROLE_KEY: z.string().optional(),
|
|
148
154
|
});
|
|
149
155
|
const parsedEnv = EnvSchema.safeParse(process.env);
|
|
150
156
|
if (!parsedEnv.success) {
|
|
@@ -223,6 +229,8 @@ if (!validatedLogsPath) {
|
|
|
223
229
|
* Aggregates settings from validated environment variables and `package.json`.
|
|
224
230
|
*/
|
|
225
231
|
export const config = {
|
|
232
|
+
/** Information from package.json. */
|
|
233
|
+
pkg,
|
|
226
234
|
/** MCP server name. Env `MCP_SERVER_NAME` > `package.json` name > "mcp-ts-template". */
|
|
227
235
|
mcpServerName: env.MCP_SERVER_NAME || pkg.name,
|
|
228
236
|
/** MCP server version. Env `MCP_SERVER_VERSION` > `package.json` version > "0.0.0". */
|
|
@@ -281,6 +289,14 @@ export const config = {
|
|
|
281
289
|
.filter(Boolean),
|
|
282
290
|
}
|
|
283
291
|
: undefined,
|
|
292
|
+
/** Supabase configuration. Undefined if no related env vars are set. */
|
|
293
|
+
supabase: env.SUPABASE_URL && env.SUPABASE_ANON_KEY
|
|
294
|
+
? {
|
|
295
|
+
url: env.SUPABASE_URL,
|
|
296
|
+
anonKey: env.SUPABASE_ANON_KEY,
|
|
297
|
+
serviceRoleKey: env.SUPABASE_SERVICE_ROLE_KEY,
|
|
298
|
+
}
|
|
299
|
+
: undefined,
|
|
284
300
|
};
|
|
285
301
|
/**
|
|
286
302
|
* Configured logging level for the application.
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* - Transports: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx
|
|
14
14
|
* @module src/mcp-server/server
|
|
15
15
|
*/
|
|
16
|
+
import { ServerType } from "@hono/node-server";
|
|
16
17
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
|
-
import http from "http";
|
|
18
18
|
/**
|
|
19
19
|
* Main application entry point. Initializes and starts the MCP server.
|
|
20
20
|
* Orchestrates server startup, transport selection, and top-level error handling.
|
|
@@ -26,4 +26,4 @@ import http from "http";
|
|
|
26
26
|
* @returns For 'stdio', resolves with `McpServer`. For 'http', resolves with `http.Server`.
|
|
27
27
|
* Rejects on critical failure, leading to process exit.
|
|
28
28
|
*/
|
|
29
|
-
export declare function initializeAndStartServer(): Promise<void | McpServer |
|
|
29
|
+
export declare function initializeAndStartServer(): Promise<void | McpServer | ServerType>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT).
|
|
2
|
+
* @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT) for Hono.
|
|
3
3
|
*
|
|
4
4
|
* This middleware validates JSON Web Tokens (JWT) passed via the 'Authorization' header
|
|
5
5
|
* using the 'Bearer' scheme (e.g., "Authorization: Bearer <your_token>").
|
|
@@ -7,23 +7,31 @@
|
|
|
7
7
|
* in the configuration (`config.mcpAuthSecretKey`).
|
|
8
8
|
*
|
|
9
9
|
* If the token is valid, an object conforming to the MCP SDK's `AuthInfo` type
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* is attached to `c.env.incoming.auth`. This direct attachment to the raw Node.js
|
|
11
|
+
* request object is for compatibility with the underlying SDK transport, which is
|
|
12
|
+
* not Hono-context-aware.
|
|
13
|
+
* If the token is missing, invalid, or expired, it returns an HTTP 401 Unauthorized response.
|
|
12
14
|
*
|
|
13
15
|
* @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
|
|
14
16
|
* @module src/mcp-server/transports/authentication/authMiddleware
|
|
15
17
|
*/
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
18
|
+
import { HttpBindings } from "@hono/node-server";
|
|
19
|
+
import { type AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
|
|
20
|
+
import { Context, Next } from "hono";
|
|
21
|
+
export type { AuthInfo };
|
|
22
|
+
declare module "http" {
|
|
23
|
+
interface IncomingMessage {
|
|
24
|
+
auth?: AuthInfo;
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
27
|
-
*
|
|
28
|
+
* Hono middleware for verifying JWT Bearer token authentication.
|
|
29
|
+
* It attaches authentication info to `c.env.incoming.auth` for SDK compatibility with the node server.
|
|
28
30
|
*/
|
|
29
|
-
export declare function mcpAuthMiddleware(
|
|
31
|
+
export declare function mcpAuthMiddleware(c: Context<{
|
|
32
|
+
Bindings: HttpBindings;
|
|
33
|
+
}>, next: Next): Promise<void | (Response & import("hono").TypedResponse<{
|
|
34
|
+
error: string;
|
|
35
|
+
}, 500, "json">) | (Response & import("hono").TypedResponse<{
|
|
36
|
+
error: string;
|
|
37
|
+
}, 401, "json">)>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT).
|
|
2
|
+
* @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT) for Hono.
|
|
3
3
|
*
|
|
4
4
|
* This middleware validates JSON Web Tokens (JWT) passed via the 'Authorization' header
|
|
5
5
|
* using the 'Bearer' scheme (e.g., "Authorization: Bearer <your_token>").
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* in the configuration (`config.mcpAuthSecretKey`).
|
|
8
8
|
*
|
|
9
9
|
* If the token is valid, an object conforming to the MCP SDK's `AuthInfo` type
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* is attached to `c.env.incoming.auth`. This direct attachment to the raw Node.js
|
|
11
|
+
* request object is for compatibility with the underlying SDK transport, which is
|
|
12
|
+
* not Hono-context-aware.
|
|
13
|
+
* If the token is missing, invalid, or expired, it returns an HTTP 401 Unauthorized response.
|
|
12
14
|
*
|
|
13
15
|
* @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
|
|
14
16
|
* @module src/mcp-server/transports/authentication/authMiddleware
|
|
@@ -25,67 +27,56 @@ else if (!config.mcpAuthSecretKey) {
|
|
|
25
27
|
logger.warning("MCP_AUTH_SECRET_KEY is not set. Authentication middleware will bypass checks (DEVELOPMENT ONLY). This is insecure for production.");
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
28
|
-
*
|
|
30
|
+
* Hono middleware for verifying JWT Bearer token authentication.
|
|
31
|
+
* It attaches authentication info to `c.env.incoming.auth` for SDK compatibility with the node server.
|
|
29
32
|
*/
|
|
30
|
-
export function mcpAuthMiddleware(
|
|
33
|
+
export async function mcpAuthMiddleware(c, next) {
|
|
31
34
|
const context = requestContextService.createRequestContext({
|
|
32
35
|
operation: "mcpAuthMiddleware",
|
|
33
|
-
method: req.method,
|
|
34
|
-
path: req.path,
|
|
36
|
+
method: c.req.method,
|
|
37
|
+
path: c.req.path,
|
|
35
38
|
});
|
|
36
39
|
logger.debug("Running MCP Authentication Middleware (Bearer Token Validation)...", context);
|
|
40
|
+
const reqWithAuth = c.env.incoming;
|
|
37
41
|
// Development Mode Bypass
|
|
38
42
|
if (!config.mcpAuthSecretKey) {
|
|
39
43
|
if (environment !== "production") {
|
|
40
44
|
logger.warning("Bypassing JWT authentication: MCP_AUTH_SECRET_KEY is not set (DEVELOPMENT ONLY).", context);
|
|
41
|
-
|
|
42
|
-
req.auth = {
|
|
45
|
+
reqWithAuth.auth = {
|
|
43
46
|
token: "dev-mode-placeholder-token",
|
|
44
47
|
clientId: "dev-client-id",
|
|
45
48
|
scopes: ["dev-scope"],
|
|
46
49
|
};
|
|
47
|
-
// Log dev mode details separately, not attaching to req.auth if not part of AuthInfo
|
|
48
50
|
logger.debug("Dev mode auth object created.", {
|
|
49
51
|
...context,
|
|
50
|
-
authDetails:
|
|
52
|
+
authDetails: reqWithAuth.auth,
|
|
51
53
|
});
|
|
52
|
-
return next();
|
|
54
|
+
return await next();
|
|
53
55
|
}
|
|
54
56
|
else {
|
|
55
57
|
logger.error("FATAL: MCP_AUTH_SECRET_KEY is missing in production. Cannot bypass auth.", context);
|
|
56
|
-
|
|
57
|
-
error: "Server configuration error: Authentication key missing.",
|
|
58
|
-
});
|
|
59
|
-
return;
|
|
58
|
+
return c.json({ error: "Server configuration error: Authentication key missing." }, 500);
|
|
60
59
|
}
|
|
61
60
|
}
|
|
62
|
-
const authHeader = req.
|
|
61
|
+
const authHeader = c.req.header("Authorization");
|
|
63
62
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
64
63
|
logger.warning("Authentication failed: Missing or malformed Authorization header (Bearer scheme required).", context);
|
|
65
|
-
|
|
64
|
+
return c.json({
|
|
66
65
|
error: "Unauthorized: Missing or invalid authentication token format.",
|
|
67
|
-
});
|
|
68
|
-
return;
|
|
66
|
+
}, 401);
|
|
69
67
|
}
|
|
70
68
|
const tokenParts = authHeader.split(" ");
|
|
71
69
|
if (tokenParts.length !== 2 || tokenParts[0] !== "Bearer" || !tokenParts[1]) {
|
|
72
70
|
logger.warning("Authentication failed: Malformed Bearer token.", context);
|
|
73
|
-
|
|
74
|
-
.status(401)
|
|
75
|
-
.json({ error: "Unauthorized: Malformed authentication token." });
|
|
76
|
-
return;
|
|
71
|
+
return c.json({ error: "Unauthorized: Malformed authentication token." }, 401);
|
|
77
72
|
}
|
|
78
73
|
const rawToken = tokenParts[1];
|
|
79
74
|
try {
|
|
80
75
|
const decoded = jwt.verify(rawToken, config.mcpAuthSecretKey);
|
|
81
76
|
if (typeof decoded === "string") {
|
|
82
77
|
logger.warning("Authentication failed: JWT decoded to a string, expected an object payload.", context);
|
|
83
|
-
|
|
84
|
-
.status(401)
|
|
85
|
-
.json({ error: "Unauthorized: Invalid token payload format." });
|
|
86
|
-
return;
|
|
78
|
+
return c.json({ error: "Unauthorized: Invalid token payload format." }, 401);
|
|
87
79
|
}
|
|
88
|
-
// Extract and validate fields for SDK's AuthInfo
|
|
89
80
|
const clientIdFromToken = typeof decoded.cid === "string"
|
|
90
81
|
? decoded.cid
|
|
91
82
|
: typeof decoded.client_id === "string"
|
|
@@ -93,12 +84,9 @@ export function mcpAuthMiddleware(req, res, next) {
|
|
|
93
84
|
: undefined;
|
|
94
85
|
if (!clientIdFromToken) {
|
|
95
86
|
logger.warning("Authentication failed: JWT 'cid' or 'client_id' claim is missing or not a string.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
|
|
96
|
-
|
|
97
|
-
error: "Unauthorized: Invalid token, missing client identifier.",
|
|
98
|
-
});
|
|
99
|
-
return;
|
|
87
|
+
return c.json({ error: "Unauthorized: Invalid token, missing client identifier." }, 401);
|
|
100
88
|
}
|
|
101
|
-
let scopesFromToken;
|
|
89
|
+
let scopesFromToken = [];
|
|
102
90
|
if (Array.isArray(decoded.scp) &&
|
|
103
91
|
decoded.scp.every((s) => typeof s === "string")) {
|
|
104
92
|
scopesFromToken = decoded.scp;
|
|
@@ -107,50 +95,26 @@ export function mcpAuthMiddleware(req, res, next) {
|
|
|
107
95
|
decoded.scope.trim() !== "") {
|
|
108
96
|
scopesFromToken = decoded.scope.split(" ").filter((s) => s);
|
|
109
97
|
if (scopesFromToken.length === 0 && decoded.scope.trim() !== "") {
|
|
110
|
-
// handles case " " -> [""]
|
|
111
98
|
scopesFromToken = [decoded.scope.trim()];
|
|
112
99
|
}
|
|
113
|
-
else if (scopesFromToken.length === 0 && decoded.scope.trim() === "") {
|
|
114
|
-
// If scope is an empty string, treat as no scopes.
|
|
115
|
-
// This will now lead to an error if scopes are considered mandatory.
|
|
116
|
-
logger.debug("JWT 'scope' claim was an empty string, resulting in empty scopes array.", context);
|
|
117
|
-
}
|
|
118
100
|
}
|
|
119
|
-
else {
|
|
120
|
-
// If scopes are strictly mandatory and not found or invalid format
|
|
121
|
-
logger.warning("Authentication failed: JWT 'scp' or 'scope' claim is missing, not an array of strings, or not a valid space-separated string.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
|
|
122
|
-
res.status(401).json({
|
|
123
|
-
error: "Unauthorized: Invalid token, missing or invalid scopes.",
|
|
124
|
-
});
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
// If, after parsing, scopesFromToken is empty and scopes are considered mandatory for any operation.
|
|
128
|
-
// This check assumes that all valid tokens must have at least one scope.
|
|
129
|
-
// If some tokens are legitimately allowed to have no scopes for certain operations,
|
|
130
|
-
// this check might need to be adjusted or handled downstream.
|
|
131
101
|
if (scopesFromToken.length === 0) {
|
|
132
102
|
logger.warning("Authentication failed: Token resulted in an empty scope array, and scopes are required.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
|
|
133
|
-
|
|
134
|
-
error: "Unauthorized: Token must contain valid, non-empty scopes.",
|
|
135
|
-
});
|
|
136
|
-
return;
|
|
103
|
+
return c.json({ error: "Unauthorized: Token must contain valid, non-empty scopes." }, 401);
|
|
137
104
|
}
|
|
138
|
-
|
|
139
|
-
// All other claims from 'decoded' are not part of req.auth for type safety.
|
|
140
|
-
req.auth = {
|
|
105
|
+
reqWithAuth.auth = {
|
|
141
106
|
token: rawToken,
|
|
142
107
|
clientId: clientIdFromToken,
|
|
143
108
|
scopes: scopesFromToken,
|
|
144
109
|
};
|
|
145
|
-
// Log separately if other JWT claims like 'sub' (sessionId) are needed for app logic
|
|
146
110
|
const subClaimForLogging = typeof decoded.sub === "string" ? decoded.sub : undefined;
|
|
147
111
|
logger.debug("JWT verified successfully. AuthInfo attached to request.", {
|
|
148
112
|
...context,
|
|
149
113
|
mcpSessionIdContext: subClaimForLogging,
|
|
150
|
-
clientId:
|
|
151
|
-
scopes:
|
|
114
|
+
clientId: reqWithAuth.auth.clientId,
|
|
115
|
+
scopes: reqWithAuth.auth.scopes,
|
|
152
116
|
});
|
|
153
|
-
next();
|
|
117
|
+
await next();
|
|
154
118
|
}
|
|
155
119
|
catch (error) {
|
|
156
120
|
let errorMessage = "Invalid token";
|
|
@@ -173,6 +137,6 @@ export function mcpAuthMiddleware(req, res, next) {
|
|
|
173
137
|
errorMessage = "Unknown verification error";
|
|
174
138
|
logger.error("Authentication failed: Unexpected non-error exception during token verification.", { ...context, error });
|
|
175
139
|
}
|
|
176
|
-
|
|
140
|
+
return c.json({ error: `Unauthorized: ${errorMessage}.` }, 401);
|
|
177
141
|
}
|
|
178
142
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Handles the setup and management of the Streamable HTTP MCP transport.
|
|
2
|
+
* @fileoverview Handles the setup and management of the Streamable HTTP MCP transport using Hono.
|
|
3
3
|
* Implements the MCP Specification 2025-03-26 for Streamable HTTP.
|
|
4
|
-
* This includes creating
|
|
4
|
+
* This includes creating a Hono server, configuring middleware (CORS, Authentication),
|
|
5
5
|
* defining request routing for the single MCP endpoint (POST/GET/DELETE),
|
|
6
6
|
* managing server-side sessions, handling Server-Sent Events (SSE) for streaming,
|
|
7
7
|
* and binding to a network port with retry logic for port conflicts.
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#streamable-http
|
|
11
11
|
* @module src/mcp-server/transports/httpTransport
|
|
12
12
|
*/
|
|
13
|
+
import { ServerType } from "@hono/node-server";
|
|
13
14
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
|
-
import http from "http";
|
|
15
15
|
import { RequestContext } from "../../utils/index.js";
|
|
16
16
|
/**
|
|
17
17
|
* Sets up and starts the Streamable HTTP transport layer for the MCP server.
|
|
@@ -21,4 +21,4 @@ import { RequestContext } from "../../utils/index.js";
|
|
|
21
21
|
* @returns A promise that resolves with the Node.js `http.Server` instance when the HTTP server is successfully listening.
|
|
22
22
|
* @throws {Error} If the server fails to start after all port retries.
|
|
23
23
|
*/
|
|
24
|
-
export declare function startHttpTransport(createServerInstanceFn: () => Promise<McpServer>, parentContext: RequestContext): Promise<
|
|
24
|
+
export declare function startHttpTransport(createServerInstanceFn: () => Promise<McpServer>, parentContext: RequestContext): Promise<ServerType>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Handles the setup and management of the Streamable HTTP MCP transport.
|
|
2
|
+
* @fileoverview Handles the setup and management of the Streamable HTTP MCP transport using Hono.
|
|
3
3
|
* Implements the MCP Specification 2025-03-26 for Streamable HTTP.
|
|
4
|
-
* This includes creating
|
|
4
|
+
* This includes creating a Hono server, configuring middleware (CORS, Authentication),
|
|
5
5
|
* defining request routing for the single MCP endpoint (POST/GET/DELETE),
|
|
6
6
|
* managing server-side sessions, handling Server-Sent Events (SSE) for streaming,
|
|
7
7
|
* and binding to a network port with retry logic for port conflicts.
|
|
@@ -10,14 +10,17 @@
|
|
|
10
10
|
* https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#streamable-http
|
|
11
11
|
* @module src/mcp-server/transports/httpTransport
|
|
12
12
|
*/
|
|
13
|
+
import { serve } from "@hono/node-server";
|
|
13
14
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
14
15
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
15
|
-
import
|
|
16
|
+
import { Hono } from "hono";
|
|
17
|
+
import { cors } from "hono/cors";
|
|
16
18
|
import http from "http";
|
|
17
19
|
import { randomUUID } from "node:crypto";
|
|
18
20
|
import { config } from "../../config/index.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
+
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
|
|
22
|
+
import { logger, rateLimiter, requestContextService, } from "../../utils/index.js";
|
|
23
|
+
import { mcpAuthMiddleware, } from "./authentication/authMiddleware.js";
|
|
21
24
|
/**
|
|
22
25
|
* The port number for the HTTP transport, configured via `MCP_HTTP_PORT` environment variable.
|
|
23
26
|
* Defaults to 3010 if not specified (default is managed by the config module).
|
|
@@ -54,45 +57,6 @@ const MAX_PORT_RETRIES = 15;
|
|
|
54
57
|
* @private
|
|
55
58
|
*/
|
|
56
59
|
const httpTransports = {};
|
|
57
|
-
/**
|
|
58
|
-
* Checks if an incoming HTTP request's `Origin` header is permissible based on configuration.
|
|
59
|
-
* MCP Spec Security: Servers MUST validate the `Origin` header for cross-origin requests.
|
|
60
|
-
* This function checks the request's origin against the `config.mcpAllowedOrigins` list.
|
|
61
|
-
* If the server is bound to localhost, requests from localhost or with no/null origin are also permitted.
|
|
62
|
-
* Sets appropriate CORS headers (`Access-Control-Allow-Origin`, etc.) if the origin is allowed.
|
|
63
|
-
*
|
|
64
|
-
* @param req - The Express request object.
|
|
65
|
-
* @param res - The Express response object.
|
|
66
|
-
* @returns True if the origin is allowed, false otherwise.
|
|
67
|
-
* @private
|
|
68
|
-
*/
|
|
69
|
-
function isOriginAllowed(req, res) {
|
|
70
|
-
const origin = req.headers.origin;
|
|
71
|
-
const host = req.hostname;
|
|
72
|
-
const isLocalhostBinding = ["127.0.0.1", "::1", "localhost"].includes(host);
|
|
73
|
-
const allowedOrigins = config.mcpAllowedOrigins || [];
|
|
74
|
-
const context = requestContextService.createRequestContext({
|
|
75
|
-
operation: "isOriginAllowed",
|
|
76
|
-
origin,
|
|
77
|
-
host,
|
|
78
|
-
isLocalhostBinding,
|
|
79
|
-
allowedOrigins,
|
|
80
|
-
});
|
|
81
|
-
logger.debug("Checking origin allowance", context);
|
|
82
|
-
const allowed = (origin && allowedOrigins.includes(origin)) ||
|
|
83
|
-
(isLocalhostBinding && (!origin || origin === "null"));
|
|
84
|
-
if (allowed && origin) {
|
|
85
|
-
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
86
|
-
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
87
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, Last-Event-ID, Authorization");
|
|
88
|
-
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
89
|
-
}
|
|
90
|
-
else if (!allowed && origin) {
|
|
91
|
-
logger.warning(`Origin denied: ${origin}`, context);
|
|
92
|
-
}
|
|
93
|
-
logger.debug(`Origin check result: ${allowed}`, { ...context, allowed });
|
|
94
|
-
return allowed;
|
|
95
|
-
}
|
|
96
60
|
/**
|
|
97
61
|
* Proactively checks if a specific network port is already in use.
|
|
98
62
|
* @param port - The port number to check.
|
|
@@ -132,16 +96,16 @@ async function isPortInUse(port, host, parentContext) {
|
|
|
132
96
|
/**
|
|
133
97
|
* Attempts to start the HTTP server, retrying on incrementing ports if `EADDRINUSE` occurs.
|
|
134
98
|
*
|
|
135
|
-
* @param
|
|
99
|
+
* @param app - The Hono application instance.
|
|
136
100
|
* @param initialPort - The initial port number to try.
|
|
137
101
|
* @param host - The host address to bind to.
|
|
138
102
|
* @param maxRetries - Maximum number of additional ports to attempt.
|
|
139
103
|
* @param parentContext - Logging context from the caller.
|
|
140
|
-
* @returns A promise that resolves with the
|
|
104
|
+
* @returns A promise that resolves with the Node.js `http.Server` instance the server successfully bound to.
|
|
141
105
|
* @throws {Error} If binding fails after all retries or for a non-EADDRINUSE error.
|
|
142
106
|
* @private
|
|
143
107
|
*/
|
|
144
|
-
function startHttpServerWithRetry(
|
|
108
|
+
function startHttpServerWithRetry(app, initialPort, host, maxRetries, parentContext) {
|
|
145
109
|
const startContext = requestContextService.createRequestContext({
|
|
146
110
|
...parentContext,
|
|
147
111
|
operation: "startHttpServerWithRetry",
|
|
@@ -168,18 +132,21 @@ function startHttpServerWithRetry(serverInstance, initialPort, host, maxRetries,
|
|
|
168
132
|
continue;
|
|
169
133
|
}
|
|
170
134
|
try {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
}
|
|
135
|
+
const serverInstance = serve({ fetch: app.fetch, port: currentPort, hostname: host }, (info) => {
|
|
136
|
+
const serverAddress = `http://${info.address}:${info.port}${MCP_ENDPOINT_PATH}`;
|
|
137
|
+
logger.info(`HTTP transport successfully listening on host ${host} at ${serverAddress}`, { ...attemptContext, address: serverAddress });
|
|
138
|
+
// Display user-friendly startup message only after server is confirmed listening
|
|
139
|
+
let serverAddressLog = serverAddress;
|
|
140
|
+
let productionNote = "";
|
|
141
|
+
if (config.environment === "production") {
|
|
142
|
+
serverAddressLog = `https://${info.address}:${info.port}${MCP_ENDPOINT_PATH}`;
|
|
143
|
+
productionNote = ` (via HTTPS, ensure reverse proxy is configured)`;
|
|
144
|
+
}
|
|
145
|
+
if (process.stdout.isTTY) {
|
|
146
|
+
console.log(`\n🚀 MCP Server running in HTTP mode at: ${serverAddressLog}${productionNote}\n (MCP Spec: 2025-03-26 Streamable HTTP Transport)\n`);
|
|
147
|
+
}
|
|
181
148
|
});
|
|
182
|
-
resolve(
|
|
149
|
+
resolve(serverInstance);
|
|
183
150
|
return;
|
|
184
151
|
}
|
|
185
152
|
catch (err) {
|
|
@@ -210,67 +177,93 @@ function startHttpServerWithRetry(serverInstance, initialPort, host, maxRetries,
|
|
|
210
177
|
* @throws {Error} If the server fails to start after all port retries.
|
|
211
178
|
*/
|
|
212
179
|
export async function startHttpTransport(createServerInstanceFn, parentContext) {
|
|
213
|
-
const app =
|
|
180
|
+
const app = new Hono();
|
|
214
181
|
const transportContext = requestContextService.createRequestContext({
|
|
215
182
|
...parentContext,
|
|
216
183
|
transportType: "HTTP",
|
|
217
184
|
component: "HttpTransportSetup",
|
|
218
185
|
});
|
|
219
|
-
logger.debug("Setting up
|
|
220
|
-
app.use(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
res.sendStatus(204);
|
|
233
|
-
}
|
|
234
|
-
else {
|
|
235
|
-
logger.debug("OPTIONS request origin denied, sending 403.", optionsContext);
|
|
236
|
-
res.status(403).send("Forbidden: Invalid Origin");
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
app.use((req, res, next) => {
|
|
186
|
+
logger.debug("Setting up Hono app for HTTP transport...", transportContext);
|
|
187
|
+
app.use("*", cors({
|
|
188
|
+
origin: config.mcpAllowedOrigins || [],
|
|
189
|
+
allowMethods: ["GET", "POST", "DELETE", "OPTIONS"],
|
|
190
|
+
allowHeaders: [
|
|
191
|
+
"Content-Type",
|
|
192
|
+
"Mcp-Session-Id",
|
|
193
|
+
"Last-Event-ID",
|
|
194
|
+
"Authorization",
|
|
195
|
+
],
|
|
196
|
+
credentials: true,
|
|
197
|
+
}));
|
|
198
|
+
app.use("*", async (c, next) => {
|
|
240
199
|
const securityContext = requestContextService.createRequestContext({
|
|
241
200
|
...transportContext,
|
|
242
201
|
operation: "securityMiddleware",
|
|
243
|
-
path: req.path,
|
|
244
|
-
method: req.method,
|
|
245
|
-
origin: req.
|
|
202
|
+
path: c.req.path,
|
|
203
|
+
method: c.req.method,
|
|
204
|
+
origin: c.req.header("origin"),
|
|
246
205
|
});
|
|
247
206
|
logger.debug(`Applying security middleware...`, securityContext);
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
254
|
-
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
255
|
-
res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'self'; frame-src 'none'; font-src 'self'; connect-src 'self'");
|
|
207
|
+
c.res.headers.set("X-Content-Type-Options", "nosniff");
|
|
208
|
+
c.res.headers.set("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
209
|
+
c.res.headers.set("Content-Security-Policy", "default-src 'self'; script-src 'self'; object-src 'none'; style-src 'self'; img-src 'self'; media-src 'self'; frame-src 'none'; font-src 'self'; connect-src 'self'");
|
|
256
210
|
logger.debug("Security middleware passed.", securityContext);
|
|
257
|
-
next();
|
|
211
|
+
await next();
|
|
258
212
|
});
|
|
259
|
-
app.use(
|
|
260
|
-
|
|
213
|
+
app.use(MCP_ENDPOINT_PATH, async (c, next) => {
|
|
214
|
+
const rateLimitKey = c.req.header("x-forwarded-for") ||
|
|
215
|
+
c.req.header("host") ||
|
|
216
|
+
"unknown_ip_for_rate_limit";
|
|
217
|
+
const context = requestContextService.createRequestContext({
|
|
218
|
+
operation: "httpRateLimitCheck",
|
|
219
|
+
ipAddress: rateLimitKey,
|
|
220
|
+
method: c.req.method,
|
|
221
|
+
path: c.req.path,
|
|
222
|
+
});
|
|
223
|
+
try {
|
|
224
|
+
rateLimiter.check(rateLimitKey, context);
|
|
225
|
+
logger.debug("Rate limit check passed.", context);
|
|
226
|
+
await next();
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
if (error instanceof McpError &&
|
|
230
|
+
error.code === BaseErrorCode.RATE_LIMITED) {
|
|
231
|
+
logger.warning(`Rate limit exceeded for IP: ${rateLimitKey}`, {
|
|
232
|
+
...context,
|
|
233
|
+
errorMessage: error.message,
|
|
234
|
+
details: error.details,
|
|
235
|
+
});
|
|
236
|
+
return c.json({
|
|
237
|
+
jsonrpc: "2.0",
|
|
238
|
+
error: { code: -32000, message: "Too Many Requests" },
|
|
239
|
+
id: (await c.req.json().catch(() => ({})))?.id || null,
|
|
240
|
+
}, 429);
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
logger.error("Unexpected error in rate limit middleware", {
|
|
244
|
+
...context,
|
|
245
|
+
error: error instanceof Error ? error.message : String(error),
|
|
246
|
+
});
|
|
247
|
+
throw error;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
app.use(MCP_ENDPOINT_PATH, mcpAuthMiddleware);
|
|
252
|
+
app.post(MCP_ENDPOINT_PATH, async (c) => {
|
|
261
253
|
const basePostContext = requestContextService.createRequestContext({
|
|
262
254
|
...transportContext,
|
|
263
255
|
operation: "handlePost",
|
|
264
256
|
method: "POST",
|
|
265
|
-
path: req.path,
|
|
266
|
-
origin: req.
|
|
257
|
+
path: c.req.path,
|
|
258
|
+
origin: c.req.header("origin"),
|
|
267
259
|
});
|
|
260
|
+
const body = await c.req.json();
|
|
268
261
|
logger.debug(`Received POST request on ${MCP_ENDPOINT_PATH}`, {
|
|
269
262
|
...basePostContext,
|
|
270
|
-
headers: req.
|
|
271
|
-
bodyPreview: JSON.stringify(
|
|
263
|
+
headers: c.req.header(),
|
|
264
|
+
bodyPreview: JSON.stringify(body).substring(0, 100),
|
|
272
265
|
});
|
|
273
|
-
const sessionId = req.
|
|
266
|
+
const sessionId = c.req.header("mcp-session-id");
|
|
274
267
|
logger.debug(`Extracted session ID: ${sessionId}`, {
|
|
275
268
|
...basePostContext,
|
|
276
269
|
sessionId,
|
|
@@ -280,12 +273,12 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
|
|
|
280
273
|
...basePostContext,
|
|
281
274
|
sessionId,
|
|
282
275
|
});
|
|
283
|
-
const isInitReq = isInitializeRequest(
|
|
276
|
+
const isInitReq = isInitializeRequest(body);
|
|
284
277
|
logger.debug(`Is InitializeRequest: ${isInitReq}`, {
|
|
285
278
|
...basePostContext,
|
|
286
279
|
sessionId,
|
|
287
280
|
});
|
|
288
|
-
const requestId =
|
|
281
|
+
const requestId = body?.id || null;
|
|
289
282
|
try {
|
|
290
283
|
if (isInitReq) {
|
|
291
284
|
if (transport) {
|
|
@@ -334,17 +327,17 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
|
|
|
334
327
|
}
|
|
335
328
|
else if (!transport) {
|
|
336
329
|
logger.warning("Invalid or missing session ID for non-initialize POST request.", { ...basePostContext, sessionId });
|
|
337
|
-
|
|
330
|
+
return c.json({
|
|
338
331
|
jsonrpc: "2.0",
|
|
339
332
|
error: { code: -32004, message: "Invalid or expired session ID" },
|
|
340
333
|
id: requestId,
|
|
341
|
-
});
|
|
342
|
-
return;
|
|
334
|
+
}, 404);
|
|
343
335
|
}
|
|
344
336
|
const currentSessionId = transport.sessionId;
|
|
345
337
|
logger.debug(`Processing POST request content for session ${currentSessionId}...`, { ...basePostContext, sessionId: currentSessionId, isInitReq });
|
|
346
|
-
await transport.handleRequest(
|
|
338
|
+
const response = await transport.handleRequest(c.env.incoming, c.env.outgoing, body);
|
|
347
339
|
logger.debug(`Finished processing POST request content for session ${currentSessionId}.`, { ...basePostContext, sessionId: currentSessionId });
|
|
340
|
+
return response;
|
|
348
341
|
}
|
|
349
342
|
catch (err) {
|
|
350
343
|
const errorSessionId = transport?.sessionId || sessionId;
|
|
@@ -355,16 +348,6 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
|
|
|
355
348
|
error: err instanceof Error ? err.message : String(err),
|
|
356
349
|
stack: err instanceof Error ? err.stack : undefined,
|
|
357
350
|
});
|
|
358
|
-
if (!res.headersSent) {
|
|
359
|
-
res.status(500).json({
|
|
360
|
-
jsonrpc: "2.0",
|
|
361
|
-
error: {
|
|
362
|
-
code: -32603,
|
|
363
|
-
message: "Internal server error during POST handling",
|
|
364
|
-
},
|
|
365
|
-
id: requestId,
|
|
366
|
-
});
|
|
367
|
-
}
|
|
368
351
|
if (isInitReq && transport && !transport.sessionId) {
|
|
369
352
|
logger.debug("Cleaning up transport after initialization failure.", {
|
|
370
353
|
...basePostContext,
|
|
@@ -376,22 +359,30 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
|
|
|
376
359
|
closeError: closeErr,
|
|
377
360
|
}));
|
|
378
361
|
}
|
|
362
|
+
return c.json({
|
|
363
|
+
jsonrpc: "2.0",
|
|
364
|
+
error: {
|
|
365
|
+
code: -32603,
|
|
366
|
+
message: "Internal server error during POST handling",
|
|
367
|
+
},
|
|
368
|
+
id: requestId,
|
|
369
|
+
}, 500);
|
|
379
370
|
}
|
|
380
371
|
});
|
|
381
|
-
const handleSessionReq = async (
|
|
382
|
-
const method = req.method;
|
|
372
|
+
const handleSessionReq = async (c) => {
|
|
373
|
+
const method = c.req.method;
|
|
383
374
|
const baseSessionReqContext = requestContextService.createRequestContext({
|
|
384
375
|
...transportContext,
|
|
385
376
|
operation: `handle${method}`,
|
|
386
377
|
method,
|
|
387
|
-
path: req.path,
|
|
388
|
-
origin: req.
|
|
378
|
+
path: c.req.path,
|
|
379
|
+
origin: c.req.header("origin"),
|
|
389
380
|
});
|
|
390
381
|
logger.debug(`Received ${method} request on ${MCP_ENDPOINT_PATH}`, {
|
|
391
382
|
...baseSessionReqContext,
|
|
392
|
-
headers: req.
|
|
383
|
+
headers: c.req.header(),
|
|
393
384
|
});
|
|
394
|
-
const sessionId = req.
|
|
385
|
+
const sessionId = c.req.header("mcp-session-id");
|
|
395
386
|
logger.debug(`Extracted session ID: ${sessionId}`, {
|
|
396
387
|
...baseSessionReqContext,
|
|
397
388
|
sessionId,
|
|
@@ -406,17 +397,17 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
|
|
|
406
397
|
...baseSessionReqContext,
|
|
407
398
|
sessionId,
|
|
408
399
|
});
|
|
409
|
-
|
|
400
|
+
return c.json({
|
|
410
401
|
jsonrpc: "2.0",
|
|
411
402
|
error: { code: -32004, message: "Session not found or expired" },
|
|
412
|
-
id: null,
|
|
413
|
-
});
|
|
414
|
-
return;
|
|
403
|
+
id: null,
|
|
404
|
+
}, 404);
|
|
415
405
|
}
|
|
416
406
|
try {
|
|
417
407
|
logger.debug(`Delegating ${method} request to transport for session ${sessionId}...`, { ...baseSessionReqContext, sessionId });
|
|
418
|
-
await transport.handleRequest(
|
|
408
|
+
const response = await transport.handleRequest(c.env.incoming, c.env.outgoing);
|
|
419
409
|
logger.info(`Successfully handled ${method} request for session ${sessionId}`, { ...baseSessionReqContext, sessionId });
|
|
410
|
+
return response;
|
|
420
411
|
}
|
|
421
412
|
catch (err) {
|
|
422
413
|
logger.error(`Error handling ${method} request for session ${sessionId}`, {
|
|
@@ -425,40 +416,26 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
|
|
|
425
416
|
error: err instanceof Error ? err.message : String(err),
|
|
426
417
|
stack: err instanceof Error ? err.stack : undefined,
|
|
427
418
|
});
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
});
|
|
434
|
-
}
|
|
419
|
+
return c.json({
|
|
420
|
+
jsonrpc: "2.0",
|
|
421
|
+
error: { code: -32603, message: "Internal Server Error" },
|
|
422
|
+
id: null,
|
|
423
|
+
}, 500);
|
|
435
424
|
}
|
|
436
425
|
};
|
|
437
426
|
app.get(MCP_ENDPOINT_PATH, handleSessionReq);
|
|
438
427
|
app.delete(MCP_ENDPOINT_PATH, handleSessionReq);
|
|
439
428
|
logger.debug("Creating HTTP server instance...", transportContext);
|
|
440
|
-
const serverInstance = http.createServer(app);
|
|
441
429
|
try {
|
|
442
430
|
logger.debug("Attempting to start HTTP server with retry logic...", transportContext);
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
let productionNote = "";
|
|
446
|
-
if (config.environment === "production") {
|
|
447
|
-
// The server itself runs HTTP, but it's expected to be behind an HTTPS proxy in production.
|
|
448
|
-
// The log reflects the effective public-facing URL.
|
|
449
|
-
serverAddressLog = `https://${config.mcpHttpHost}:${actualPort}${MCP_ENDPOINT_PATH}`;
|
|
450
|
-
productionNote = ` (via HTTPS, ensure reverse proxy is configured)`;
|
|
451
|
-
}
|
|
452
|
-
if (process.stdout.isTTY) {
|
|
453
|
-
console.log(`\n🚀 MCP Server running in HTTP mode at: ${serverAddressLog}${productionNote}\n (MCP Spec: 2025-03-26 Streamable HTTP Transport)\n`);
|
|
454
|
-
}
|
|
455
|
-
return serverInstance; // Return the created server instance
|
|
431
|
+
const serverInstance = await startHttpServerWithRetry(app, config.mcpHttpPort, config.mcpHttpHost, MAX_PORT_RETRIES, transportContext);
|
|
432
|
+
return serverInstance;
|
|
456
433
|
}
|
|
457
434
|
catch (err) {
|
|
458
435
|
logger.fatal("HTTP server failed to start after multiple port retries.", {
|
|
459
436
|
...transportContext,
|
|
460
437
|
error: err instanceof Error ? err.message : String(err),
|
|
461
438
|
});
|
|
462
|
-
throw err;
|
|
439
|
+
throw err;
|
|
463
440
|
}
|
|
464
441
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Initializes and exports a singleton Supabase client instance.
|
|
3
|
+
* This module ensures that the Supabase client is initialized once and shared
|
|
4
|
+
* across the application, using credentials from the central configuration.
|
|
5
|
+
* It handles both the standard client and the admin client (using the service role key).
|
|
6
|
+
*
|
|
7
|
+
* @module src/services/supabase/supabaseClient
|
|
8
|
+
*/
|
|
9
|
+
import { SupabaseClient } from "@supabase/supabase-js";
|
|
10
|
+
type Database = any;
|
|
11
|
+
/**
|
|
12
|
+
* Returns the singleton Supabase client instance.
|
|
13
|
+
* Throws an McpError if the client is not initialized.
|
|
14
|
+
* @returns The Supabase client.
|
|
15
|
+
*/
|
|
16
|
+
export declare const getSupabaseClient: () => SupabaseClient<Database>;
|
|
17
|
+
/**
|
|
18
|
+
* Returns the singleton Supabase admin client instance.
|
|
19
|
+
* This client uses the service role key and bypasses RLS.
|
|
20
|
+
* Throws an McpError if the admin client is not initialized.
|
|
21
|
+
* @returns The Supabase admin client.
|
|
22
|
+
*/
|
|
23
|
+
export declare const getSupabaseAdminClient: () => SupabaseClient<Database>;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Initializes and exports a singleton Supabase client instance.
|
|
3
|
+
* This module ensures that the Supabase client is initialized once and shared
|
|
4
|
+
* across the application, using credentials from the central configuration.
|
|
5
|
+
* It handles both the standard client and the admin client (using the service role key).
|
|
6
|
+
*
|
|
7
|
+
* @module src/services/supabase/supabaseClient
|
|
8
|
+
*/
|
|
9
|
+
import { createClient } from "@supabase/supabase-js";
|
|
10
|
+
import { config } from "../../config/index.js";
|
|
11
|
+
import { BaseErrorCode, McpError } from "../../types-global/errors.js";
|
|
12
|
+
import { logger, requestContextService } from "../../utils/index.js";
|
|
13
|
+
let supabase = null;
|
|
14
|
+
let supabaseAdmin = null;
|
|
15
|
+
const initializeSupabase = () => {
|
|
16
|
+
const context = requestContextService.createRequestContext({
|
|
17
|
+
operation: "initializeSupabase",
|
|
18
|
+
});
|
|
19
|
+
if (config.supabase?.url && config.supabase?.anonKey) {
|
|
20
|
+
if (!supabase) {
|
|
21
|
+
supabase = createClient(config.supabase.url, config.supabase.anonKey, {
|
|
22
|
+
auth: {
|
|
23
|
+
persistSession: false,
|
|
24
|
+
autoRefreshToken: false,
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
logger.info("Supabase client initialized.", context);
|
|
28
|
+
}
|
|
29
|
+
if (!supabaseAdmin && config.supabase.serviceRoleKey) {
|
|
30
|
+
supabaseAdmin = createClient(config.supabase.url, config.supabase.serviceRoleKey, {
|
|
31
|
+
auth: {
|
|
32
|
+
persistSession: false,
|
|
33
|
+
autoRefreshToken: false,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
logger.info("Supabase admin client initialized.", context);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
logger.warning("Supabase URL or anon key is missing. Supabase clients not initialized.", context);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
// Initialize on load
|
|
44
|
+
initializeSupabase();
|
|
45
|
+
/**
|
|
46
|
+
* Returns the singleton Supabase client instance.
|
|
47
|
+
* Throws an McpError if the client is not initialized.
|
|
48
|
+
* @returns The Supabase client.
|
|
49
|
+
*/
|
|
50
|
+
export const getSupabaseClient = () => {
|
|
51
|
+
if (!supabase) {
|
|
52
|
+
throw new McpError(BaseErrorCode.SERVICE_NOT_INITIALIZED, "Supabase client has not been initialized. Please check your SUPABASE_URL and SUPABASE_ANON_KEY environment variables.");
|
|
53
|
+
}
|
|
54
|
+
return supabase;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Returns the singleton Supabase admin client instance.
|
|
58
|
+
* This client uses the service role key and bypasses RLS.
|
|
59
|
+
* Throws an McpError if the admin client is not initialized.
|
|
60
|
+
* @returns The Supabase admin client.
|
|
61
|
+
*/
|
|
62
|
+
export const getSupabaseAdminClient = () => {
|
|
63
|
+
if (!supabaseAdmin) {
|
|
64
|
+
throw new McpError(BaseErrorCode.SERVICE_NOT_INITIALIZED, "Supabase admin client has not been initialized. Please check your SUPABASE_SERVICE_ROLE_KEY environment variable.");
|
|
65
|
+
}
|
|
66
|
+
return supabaseAdmin;
|
|
67
|
+
};
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Provides a singleton Logger class that wraps Winston for file logging
|
|
3
|
+
* and supports sending MCP (Model Context Protocol) `notifications/message`.
|
|
4
|
+
* It handles different log levels compliant with RFC 5424 and MCP specifications.
|
|
5
|
+
* @module src/utils/internal/logger
|
|
6
|
+
*/
|
|
1
7
|
import path from "path";
|
|
2
8
|
import winston from "winston";
|
|
3
9
|
import { config } from "../../config/index.js";
|
|
@@ -54,7 +60,7 @@ function createWinstonConsoleFormat() {
|
|
|
54
60
|
}
|
|
55
61
|
if (Object.keys(metaCopy).length > 0) {
|
|
56
62
|
try {
|
|
57
|
-
const replacer = (
|
|
63
|
+
const replacer = (_key, value) => typeof value === "bigint" ? value.toString() : value;
|
|
58
64
|
const remainingMetaJson = JSON.stringify(metaCopy, replacer, 2);
|
|
59
65
|
if (remainingMetaJson !== "{}")
|
|
60
66
|
metaString += `\n Meta: ${remainingMetaJson}`;
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$schema": "http://json.schemastore.org/package",
|
|
3
2
|
"name": "mcp-ts-template",
|
|
4
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.8",
|
|
5
4
|
"description": "TypeScript template for building Model Context Protocol (MCP) Servers & Clients. Features extensive utilities (logger, requestContext, etc.), STDIO & Streamable HTTP (with authMiddleware), examples, and type safety. Ideal starting point for creating production-ready MCP Servers & Clients.",
|
|
6
5
|
"main": "dist/index.js",
|
|
7
6
|
"files": [
|
|
@@ -31,25 +30,30 @@
|
|
|
31
30
|
"tree": "ts-node --esm scripts/tree.ts",
|
|
32
31
|
"fetch-spec": "ts-node --esm scripts/fetch-openapi-spec.ts",
|
|
33
32
|
"format": "prettier --write \"**/*.{ts,js,json,md,html,css}\"",
|
|
34
|
-
"inspector": "mcp-inspector --config mcp.json --server mcp-ts-template",
|
|
35
|
-
"db:
|
|
33
|
+
"inspector": "npx mcp-inspector --config mcp.json --server mcp-ts-template",
|
|
34
|
+
"db:duckdb-example": "MCP_LOG_LEVEL=debug tsc && node dist/storage/duckdbExample.js"
|
|
36
35
|
},
|
|
37
36
|
"dependencies": {
|
|
38
37
|
"@duckdb/node-api": "^1.3.0-alpha.21",
|
|
38
|
+
"@hono/node-server": "^1.14.3",
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
40
|
+
"@node-oauth/oauth2-server": "^5.2.0",
|
|
41
|
+
"@supabase/supabase-js": "^2.49.10",
|
|
40
42
|
"@types/jsonwebtoken": "^9.0.9",
|
|
41
|
-
"@types/node": "^22.15.
|
|
43
|
+
"@types/node": "^22.15.30",
|
|
42
44
|
"@types/sanitize-html": "^2.16.0",
|
|
43
45
|
"@types/validator": "13.15.1",
|
|
46
|
+
"bcryptjs": "^3.0.2",
|
|
44
47
|
"chalk": "^5.4.1",
|
|
45
48
|
"chrono-node": "^2.8.0",
|
|
46
49
|
"cli-table3": "^0.6.5",
|
|
47
50
|
"dotenv": "^16.5.0",
|
|
48
|
-
"
|
|
51
|
+
"hono": "^4.7.11",
|
|
49
52
|
"ignore": "^7.0.5",
|
|
50
53
|
"jsonwebtoken": "^9.0.2",
|
|
51
|
-
"openai": "^5.1.
|
|
54
|
+
"openai": "^5.1.1",
|
|
52
55
|
"partial-json": "^0.1.7",
|
|
56
|
+
"pg": "^8.16.0",
|
|
53
57
|
"sanitize-html": "^2.17.0",
|
|
54
58
|
"tiktoken": "^1.0.21",
|
|
55
59
|
"ts-node": "^10.9.2",
|
|
@@ -90,12 +94,14 @@
|
|
|
90
94
|
"node": ">=20.0.0"
|
|
91
95
|
},
|
|
92
96
|
"devDependencies": {
|
|
93
|
-
"@types/
|
|
97
|
+
"@types/bcryptjs": "^3.0.0",
|
|
94
98
|
"@types/js-yaml": "^4.0.9",
|
|
95
99
|
"@types/node-fetch": "^2.6.12",
|
|
100
|
+
"@types/pg": "^8.15.4",
|
|
96
101
|
"axios": "^1.9.0",
|
|
97
102
|
"js-yaml": "^4.1.0",
|
|
98
103
|
"prettier": "^3.5.3",
|
|
104
|
+
"supabase": "^2.24.3",
|
|
99
105
|
"typedoc": "^0.28.5"
|
|
100
106
|
}
|
|
101
107
|
}
|