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 CHANGED
@@ -3,7 +3,7 @@
3
3
  [![TypeScript](https://img.shields.io/badge/TypeScript-^5.8.3-blue.svg)](https://www.typescriptlang.org/)
4
4
  [![Model Context Protocol SDK](https://img.shields.io/badge/MCP%20SDK-1.12.1-green.svg)](https://github.com/modelcontextprotocol/typescript-sdk)
5
5
  [![MCP Spec Version](https://img.shields.io/badge/MCP%20Spec-2025--03--26-lightgrey.svg)](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/changelog.mdx)
6
- [![Version](https://img.shields.io/badge/Version-1.4.5-blue.svg)](./CHANGELOG.md)
6
+ [![Version](https://img.shields.io/badge/Version-1.4.8-blue.svg)](./CHANGELOG.md)
7
7
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
8
8
  [![Status](https://img.shields.io/badge/Status-Stable-green.svg)](https://github.com/cyanheads/mcp-ts-template/issues)
9
9
  [![GitHub](https://img.shields.io/github/stars/cyanheads/mcp-ts-template?style=social)](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 | Express-based server implementing the MCP **Streamable HTTP** transport (utilizing Server-Sent Events for server-to-client streaming), 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). Includes a factory for client instantiation. Integrated with our utils (logger, etc.) | `src/services/llm-providers/openRouter/`, `src/services/llm-providers/llmFactory.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 Fallback | Safely falls back to `mcp-config.json.example` if primary client config is missing. | `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` |
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
 
@@ -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.
@@ -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 | http.Server>;
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
- * (expected to contain `token`, `clientId`, and `scopes`) is attached to `req.auth`.
11
- * If the token is missing, invalid, or expired, it sends an HTTP 401 Unauthorized response.
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 { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
17
- import { NextFunction, Request, Response } from "express";
18
- declare global {
19
- namespace Express {
20
- interface Request {
21
- /** Authentication information derived from the JWT, conforming to MCP SDK's AuthInfo. */
22
- auth?: AuthInfo;
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
- * Express middleware for verifying JWT Bearer token authentication.
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(req: Request, res: Response, next: NextFunction): void;
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
- * (expected to contain `token`, `clientId`, and `scopes`) is attached to `req.auth`.
11
- * If the token is missing, invalid, or expired, it sends an HTTP 401 Unauthorized response.
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
- * Express middleware for verifying JWT Bearer token authentication.
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(req, res, next) {
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
- // Populate req.auth strictly according to SDK's AuthInfo
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: req.auth,
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
- res.status(500).json({
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.headers.authorization;
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
- res.status(401).json({
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
- res
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
- res
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
- res.status(401).json({
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
- res.status(401).json({
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
- // Construct req.auth with only the properties defined in SDK's AuthInfo
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: req.auth.clientId,
151
- scopes: req.auth.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
- res.status(401).json({ error: `Unauthorized: ${errorMessage}.` });
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 an Express server, configuring middleware (CORS, Authentication),
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<http.Server>;
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 an Express server, configuring middleware (CORS, Authentication),
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 express from "express";
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 { logger, requestContextService, } from "../../utils/index.js";
20
- import { mcpAuthMiddleware } from "./authentication/authMiddleware.js";
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 serverInstance - The Node.js HTTP server instance.
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 port number the server successfully bound to.
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(serverInstance, initialPort, host, maxRetries, parentContext) {
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
- await new Promise((listenResolve, listenReject) => {
172
- serverInstance
173
- .listen(currentPort, host, () => {
174
- const serverAddress = `http://${host}:${currentPort}${MCP_ENDPOINT_PATH}`;
175
- logger.info(`HTTP transport successfully listening on host ${host} at ${serverAddress}`, { ...attemptContext, address: serverAddress });
176
- listenResolve();
177
- })
178
- .on("error", (err) => {
179
- listenReject(err);
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(currentPort);
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 = express();
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 Express app for HTTP transport...", transportContext);
220
- app.use(express.json());
221
- app.options(MCP_ENDPOINT_PATH, (req, res) => {
222
- const optionsContext = requestContextService.createRequestContext({
223
- ...transportContext,
224
- operation: "handleOptions",
225
- origin: req.headers.origin,
226
- method: req.method,
227
- path: req.path,
228
- });
229
- logger.debug(`Received OPTIONS request for ${MCP_ENDPOINT_PATH}`, optionsContext);
230
- if (isOriginAllowed(req, res)) {
231
- logger.debug("OPTIONS request origin allowed, sending 204.", optionsContext);
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.headers.origin,
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
- if (!isOriginAllowed(req, res)) {
249
- logger.debug("Origin check failed, sending 403.", securityContext);
250
- res.status(403).send("Forbidden: Invalid Origin");
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(mcpAuthMiddleware);
260
- app.post(MCP_ENDPOINT_PATH, async (req, res) => {
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.headers.origin,
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.headers,
271
- bodyPreview: JSON.stringify(req.body).substring(0, 100),
263
+ headers: c.req.header(),
264
+ bodyPreview: JSON.stringify(body).substring(0, 100),
272
265
  });
273
- const sessionId = req.headers["mcp-session-id"];
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(req.body);
276
+ const isInitReq = isInitializeRequest(body);
284
277
  logger.debug(`Is InitializeRequest: ${isInitReq}`, {
285
278
  ...basePostContext,
286
279
  sessionId,
287
280
  });
288
- const requestId = req.body?.id || null;
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
- res.status(404).json({
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(req, res, req.body);
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 (req, res) => {
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.headers.origin,
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.headers,
383
+ headers: c.req.header(),
393
384
  });
394
- const sessionId = req.headers["mcp-session-id"];
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
- res.status(404).json({
400
+ return c.json({
410
401
  jsonrpc: "2.0",
411
402
  error: { code: -32004, message: "Session not found or expired" },
412
- id: null, // Or a relevant request identifier if available from context
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(req, res);
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
- if (!res.headersSent) {
429
- res.status(500).json({
430
- jsonrpc: "2.0",
431
- error: { code: -32603, message: "Internal Server Error" },
432
- id: null, // Or a relevant request identifier
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 actualPort = await startHttpServerWithRetry(serverInstance, config.mcpHttpPort, config.mcpHttpHost, MAX_PORT_RETRIES, transportContext);
444
- let serverAddressLog = `http://${config.mcpHttpHost}:${actualPort}${MCP_ENDPOINT_PATH}`;
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; // Re-throw the error to be caught by the caller
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 = (key, value) => typeof value === "bigint" ? value.toString() : value;
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
- for (let i = 0; i < length; i++) {
66
- result += charset[bytes[i] % charset.length];
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
- if (trimmedInput.toLowerCase().startsWith("javascript:")) {
208
- throw new Error("JavaScript pseudo-protocol is not allowed in URLs.");
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.5",
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:generate": "MCP_LOG_LEVEL=debug tsc && node dist/storage/duckdbExample.js"
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.29",
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
- "express": "^5.1.0",
51
+ "hono": "^4.7.11",
49
52
  "ignore": "^7.0.5",
50
53
  "jsonwebtoken": "^9.0.2",
51
- "openai": "^5.1.0",
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/express": "^5.0.2",
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
  }