mcp-ts-template 1.4.6 → 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.6-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,15 +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 { BaseErrorCode, McpError } from "../../types-global/errors.js"; // For McpError type check
21
+ import { BaseErrorCode, McpError } from "../../types-global/errors.js";
20
22
  import { logger, rateLimiter, requestContextService, } from "../../utils/index.js";
21
- import { mcpAuthMiddleware } from "./authentication/authMiddleware.js";
23
+ import { mcpAuthMiddleware, } from "./authentication/authMiddleware.js";
22
24
  /**
23
25
  * The port number for the HTTP transport, configured via `MCP_HTTP_PORT` environment variable.
24
26
  * Defaults to 3010 if not specified (default is managed by the config module).
@@ -55,69 +57,6 @@ const MAX_PORT_RETRIES = 15;
55
57
  * @private
56
58
  */
57
59
  const httpTransports = {};
58
- /**
59
- * Checks if an incoming HTTP request's `Origin` header is permissible based on configuration.
60
- * MCP Spec Security: Servers MUST validate the `Origin` header for cross-origin requests.
61
- * This function checks the request's origin against the `config.mcpAllowedOrigins` list.
62
- * If the server is bound to localhost, requests from localhost or with no/null origin are also permitted.
63
- * Sets appropriate CORS headers (`Access-Control-Allow-Origin`, etc.) if the origin is allowed.
64
- *
65
- * @param req - The Express request object.
66
- * @param res - The Express response object.
67
- * @returns True if the origin is allowed, false otherwise.
68
- * @private
69
- */
70
- function isOriginAllowed(req, res) {
71
- const origin = req.headers.origin;
72
- // Determine if the server is bound to a localhost interface using the configured HTTP_HOST.
73
- const isLocalhostBinding = ["127.0.0.1", "::1", "localhost"].includes(config.mcpHttpHost);
74
- const allowedOrigins = config.mcpAllowedOrigins || [];
75
- const context = requestContextService.createRequestContext({
76
- operation: "isOriginAllowed",
77
- requestOrigin: origin, // Use a more descriptive key for the request's origin
78
- serverBindingHost: config.mcpHttpHost, // Log the server's binding host for context
79
- isLocalhostBinding,
80
- configuredAllowedOrigins: allowedOrigins, // Use a more descriptive key
81
- });
82
- logger.debug("Checking origin allowance", context);
83
- const allowed = (origin && allowedOrigins.includes(origin)) || // Origin is explicitly in the whitelist
84
- (isLocalhostBinding && (!origin || origin === "null")); // Or server is localhost and origin is missing or "null"
85
- if (allowed && origin) {
86
- // If the origin is "null", we must not set Access-Control-Allow-Origin to "null"
87
- // when Access-Control-Allow-Credentials is also true (which it is in this block).
88
- // This combination is a security risk. By not setting ACAO to "null",
89
- // a credentialed request from a "null" origin will likely be blocked by the browser, which is safer.
90
- if (origin === "null") {
91
- logger.debug(`Origin is "null". Not setting Access-Control-Allow-Origin to "null" due to Access-Control-Allow-Credentials being true.`, context);
92
- // Note: Access-Control-Allow-Origin is NOT set to "null".
93
- }
94
- else {
95
- // For any other allowed, non-null origin, reflect it.
96
- res.setHeader("Access-Control-Allow-Origin", origin);
97
- }
98
- // These headers are set for any allowed & originated request.
99
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
100
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, Last-Event-ID, Authorization");
101
- res.setHeader("Access-Control-Allow-Credentials", "true");
102
- }
103
- else if (allowed && !origin && isLocalhostBinding) {
104
- // Case: No origin header, but server is localhost-bound (e.g., same-origin, curl).
105
- // 'allowed' is true. We can allow credentials. ACAO is not strictly needed for non-browser or same-origin.
106
- // If it's a browser in a weird state sending no origin but expecting CORS for credentials,
107
- // it will likely fail without ACAO, which is fine.
108
- logger.debug(`No origin header, but request allowed due to localhost binding. Setting Access-Control-Allow-Credentials to true.`, context);
109
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
110
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, Last-Event-ID, Authorization");
111
- res.setHeader("Access-Control-Allow-Credentials", "true");
112
- }
113
- else if (!allowed && origin) {
114
- // Origin was present but not allowed by any rule.
115
- logger.warning(`Origin denied: ${origin}`, context);
116
- }
117
- // If !allowed and !origin, no specific CORS headers needed, request proceeds to be potentially denied by other logic or auth.
118
- logger.debug(`Origin check result: ${allowed}`, { ...context, allowed });
119
- return allowed;
120
- }
121
60
  /**
122
61
  * Proactively checks if a specific network port is already in use.
123
62
  * @param port - The port number to check.
@@ -157,16 +96,16 @@ async function isPortInUse(port, host, parentContext) {
157
96
  /**
158
97
  * Attempts to start the HTTP server, retrying on incrementing ports if `EADDRINUSE` occurs.
159
98
  *
160
- * @param serverInstance - The Node.js HTTP server instance.
99
+ * @param app - The Hono application instance.
161
100
  * @param initialPort - The initial port number to try.
162
101
  * @param host - The host address to bind to.
163
102
  * @param maxRetries - Maximum number of additional ports to attempt.
164
103
  * @param parentContext - Logging context from the caller.
165
- * @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.
166
105
  * @throws {Error} If binding fails after all retries or for a non-EADDRINUSE error.
167
106
  * @private
168
107
  */
169
- function startHttpServerWithRetry(serverInstance, initialPort, host, maxRetries, parentContext) {
108
+ function startHttpServerWithRetry(app, initialPort, host, maxRetries, parentContext) {
170
109
  const startContext = requestContextService.createRequestContext({
171
110
  ...parentContext,
172
111
  operation: "startHttpServerWithRetry",
@@ -193,18 +132,21 @@ function startHttpServerWithRetry(serverInstance, initialPort, host, maxRetries,
193
132
  continue;
194
133
  }
195
134
  try {
196
- await new Promise((listenResolve, listenReject) => {
197
- serverInstance
198
- .listen(currentPort, host, () => {
199
- const serverAddress = `http://${host}:${currentPort}${MCP_ENDPOINT_PATH}`;
200
- logger.info(`HTTP transport successfully listening on host ${host} at ${serverAddress}`, { ...attemptContext, address: serverAddress });
201
- listenResolve();
202
- })
203
- .on("error", (err) => {
204
- listenReject(err);
205
- });
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
+ }
206
148
  });
207
- resolve(currentPort);
149
+ resolve(serverInstance);
208
150
  return;
209
151
  }
210
152
  catch (err) {
@@ -235,109 +177,93 @@ function startHttpServerWithRetry(serverInstance, initialPort, host, maxRetries,
235
177
  * @throws {Error} If the server fails to start after all port retries.
236
178
  */
237
179
  export async function startHttpTransport(createServerInstanceFn, parentContext) {
238
- const app = express();
180
+ const app = new Hono();
239
181
  const transportContext = requestContextService.createRequestContext({
240
182
  ...parentContext,
241
183
  transportType: "HTTP",
242
184
  component: "HttpTransportSetup",
243
185
  });
244
- logger.debug("Setting up Express app for HTTP transport...", transportContext);
245
- app.use(express.json());
246
- // Rate Limiting Middleware
247
- // Apply this before more expensive operations like auth or request processing.
248
- const httpRateLimitMiddleware = (req, res, next) => {
249
- // Determine a reliable key for rate limiting. Prioritize req.ip,
250
- // then fall back to req.socket.remoteAddress, and finally to a default string.
251
- const rateLimitKey = req.ip || req.socket.remoteAddress || "unknown_ip_for_rate_limit";
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) => {
199
+ const securityContext = requestContextService.createRequestContext({
200
+ ...transportContext,
201
+ operation: "securityMiddleware",
202
+ path: c.req.path,
203
+ method: c.req.method,
204
+ origin: c.req.header("origin"),
205
+ });
206
+ logger.debug(`Applying security middleware...`, securityContext);
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'");
210
+ logger.debug("Security middleware passed.", securityContext);
211
+ await next();
212
+ });
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";
252
217
  const context = requestContextService.createRequestContext({
253
218
  operation: "httpRateLimitCheck",
254
- ipAddress: rateLimitKey, // Log the actual key being used
255
- method: req.method,
256
- path: req.path,
219
+ ipAddress: rateLimitKey,
220
+ method: c.req.method,
221
+ path: c.req.path,
257
222
  });
258
223
  try {
259
- rateLimiter.check(rateLimitKey, context); // Use the guaranteed string key
224
+ rateLimiter.check(rateLimitKey, context);
260
225
  logger.debug("Rate limit check passed.", context);
261
- next();
226
+ await next();
262
227
  }
263
228
  catch (error) {
264
- if (error instanceof McpError && error.code === BaseErrorCode.RATE_LIMITED) {
229
+ if (error instanceof McpError &&
230
+ error.code === BaseErrorCode.RATE_LIMITED) {
265
231
  logger.warning(`Rate limit exceeded for IP: ${rateLimitKey}`, {
266
232
  ...context,
267
233
  errorMessage: error.message,
268
234
  details: error.details,
269
235
  });
270
- res.status(429).json({
236
+ return c.json({
271
237
  jsonrpc: "2.0",
272
- error: { code: -32000, message: "Too Many Requests" }, // Generic JSON-RPC error for rate limit
273
- id: req.body?.id || null,
274
- });
238
+ error: { code: -32000, message: "Too Many Requests" },
239
+ id: (await c.req.json().catch(() => ({})))?.id || null,
240
+ }, 429);
275
241
  }
276
242
  else {
277
- // For other errors, pass them to the default error handler
278
243
  logger.error("Unexpected error in rate limit middleware", {
279
244
  ...context,
280
245
  error: error instanceof Error ? error.message : String(error),
281
246
  });
282
- next(error);
247
+ throw error;
283
248
  }
284
249
  }
285
- };
286
- // Apply rate limiter to the MCP endpoint for all methods
287
- app.use(MCP_ENDPOINT_PATH, httpRateLimitMiddleware);
288
- app.options(MCP_ENDPOINT_PATH, (req, res) => {
289
- const optionsContext = requestContextService.createRequestContext({
290
- ...transportContext,
291
- operation: "handleOptions",
292
- origin: req.headers.origin,
293
- method: req.method,
294
- path: req.path,
295
- });
296
- logger.debug(`Received OPTIONS request for ${MCP_ENDPOINT_PATH}`, optionsContext);
297
- if (isOriginAllowed(req, res)) {
298
- logger.debug("OPTIONS request origin allowed, sending 204.", optionsContext);
299
- res.sendStatus(204);
300
- }
301
- else {
302
- logger.debug("OPTIONS request origin denied, sending 403.", optionsContext);
303
- res.status(403).send("Forbidden: Invalid Origin");
304
- }
305
- });
306
- app.use((req, res, next) => {
307
- const securityContext = requestContextService.createRequestContext({
308
- ...transportContext,
309
- operation: "securityMiddleware",
310
- path: req.path,
311
- method: req.method,
312
- origin: req.headers.origin,
313
- });
314
- logger.debug(`Applying security middleware...`, securityContext);
315
- if (!isOriginAllowed(req, res)) {
316
- logger.debug("Origin check failed, sending 403.", securityContext);
317
- res.status(403).send("Forbidden: Invalid Origin");
318
- return;
319
- }
320
- res.setHeader("X-Content-Type-Options", "nosniff");
321
- res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
322
- 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'");
323
- logger.debug("Security middleware passed.", securityContext);
324
- next();
325
250
  });
326
- app.use(mcpAuthMiddleware);
327
- app.post(MCP_ENDPOINT_PATH, async (req, res) => {
251
+ app.use(MCP_ENDPOINT_PATH, mcpAuthMiddleware);
252
+ app.post(MCP_ENDPOINT_PATH, async (c) => {
328
253
  const basePostContext = requestContextService.createRequestContext({
329
254
  ...transportContext,
330
255
  operation: "handlePost",
331
256
  method: "POST",
332
- path: req.path,
333
- origin: req.headers.origin,
257
+ path: c.req.path,
258
+ origin: c.req.header("origin"),
334
259
  });
260
+ const body = await c.req.json();
335
261
  logger.debug(`Received POST request on ${MCP_ENDPOINT_PATH}`, {
336
262
  ...basePostContext,
337
- headers: req.headers,
338
- bodyPreview: JSON.stringify(req.body).substring(0, 100),
263
+ headers: c.req.header(),
264
+ bodyPreview: JSON.stringify(body).substring(0, 100),
339
265
  });
340
- const sessionId = req.headers["mcp-session-id"];
266
+ const sessionId = c.req.header("mcp-session-id");
341
267
  logger.debug(`Extracted session ID: ${sessionId}`, {
342
268
  ...basePostContext,
343
269
  sessionId,
@@ -347,12 +273,12 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
347
273
  ...basePostContext,
348
274
  sessionId,
349
275
  });
350
- const isInitReq = isInitializeRequest(req.body);
276
+ const isInitReq = isInitializeRequest(body);
351
277
  logger.debug(`Is InitializeRequest: ${isInitReq}`, {
352
278
  ...basePostContext,
353
279
  sessionId,
354
280
  });
355
- const requestId = req.body?.id || null;
281
+ const requestId = body?.id || null;
356
282
  try {
357
283
  if (isInitReq) {
358
284
  if (transport) {
@@ -401,17 +327,17 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
401
327
  }
402
328
  else if (!transport) {
403
329
  logger.warning("Invalid or missing session ID for non-initialize POST request.", { ...basePostContext, sessionId });
404
- res.status(404).json({
330
+ return c.json({
405
331
  jsonrpc: "2.0",
406
332
  error: { code: -32004, message: "Invalid or expired session ID" },
407
333
  id: requestId,
408
- });
409
- return;
334
+ }, 404);
410
335
  }
411
336
  const currentSessionId = transport.sessionId;
412
337
  logger.debug(`Processing POST request content for session ${currentSessionId}...`, { ...basePostContext, sessionId: currentSessionId, isInitReq });
413
- await transport.handleRequest(req, res, req.body);
338
+ const response = await transport.handleRequest(c.env.incoming, c.env.outgoing, body);
414
339
  logger.debug(`Finished processing POST request content for session ${currentSessionId}.`, { ...basePostContext, sessionId: currentSessionId });
340
+ return response;
415
341
  }
416
342
  catch (err) {
417
343
  const errorSessionId = transport?.sessionId || sessionId;
@@ -422,16 +348,6 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
422
348
  error: err instanceof Error ? err.message : String(err),
423
349
  stack: err instanceof Error ? err.stack : undefined,
424
350
  });
425
- if (!res.headersSent) {
426
- res.status(500).json({
427
- jsonrpc: "2.0",
428
- error: {
429
- code: -32603,
430
- message: "Internal server error during POST handling",
431
- },
432
- id: requestId,
433
- });
434
- }
435
351
  if (isInitReq && transport && !transport.sessionId) {
436
352
  logger.debug("Cleaning up transport after initialization failure.", {
437
353
  ...basePostContext,
@@ -443,22 +359,30 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
443
359
  closeError: closeErr,
444
360
  }));
445
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);
446
370
  }
447
371
  });
448
- const handleSessionReq = async (req, res) => {
449
- const method = req.method;
372
+ const handleSessionReq = async (c) => {
373
+ const method = c.req.method;
450
374
  const baseSessionReqContext = requestContextService.createRequestContext({
451
375
  ...transportContext,
452
376
  operation: `handle${method}`,
453
377
  method,
454
- path: req.path,
455
- origin: req.headers.origin,
378
+ path: c.req.path,
379
+ origin: c.req.header("origin"),
456
380
  });
457
381
  logger.debug(`Received ${method} request on ${MCP_ENDPOINT_PATH}`, {
458
382
  ...baseSessionReqContext,
459
- headers: req.headers,
383
+ headers: c.req.header(),
460
384
  });
461
- const sessionId = req.headers["mcp-session-id"];
385
+ const sessionId = c.req.header("mcp-session-id");
462
386
  logger.debug(`Extracted session ID: ${sessionId}`, {
463
387
  ...baseSessionReqContext,
464
388
  sessionId,
@@ -473,17 +397,17 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
473
397
  ...baseSessionReqContext,
474
398
  sessionId,
475
399
  });
476
- res.status(404).json({
400
+ return c.json({
477
401
  jsonrpc: "2.0",
478
402
  error: { code: -32004, message: "Session not found or expired" },
479
- id: null, // Or a relevant request identifier if available from context
480
- });
481
- return;
403
+ id: null,
404
+ }, 404);
482
405
  }
483
406
  try {
484
407
  logger.debug(`Delegating ${method} request to transport for session ${sessionId}...`, { ...baseSessionReqContext, sessionId });
485
- await transport.handleRequest(req, res);
408
+ const response = await transport.handleRequest(c.env.incoming, c.env.outgoing);
486
409
  logger.info(`Successfully handled ${method} request for session ${sessionId}`, { ...baseSessionReqContext, sessionId });
410
+ return response;
487
411
  }
488
412
  catch (err) {
489
413
  logger.error(`Error handling ${method} request for session ${sessionId}`, {
@@ -492,40 +416,26 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
492
416
  error: err instanceof Error ? err.message : String(err),
493
417
  stack: err instanceof Error ? err.stack : undefined,
494
418
  });
495
- if (!res.headersSent) {
496
- res.status(500).json({
497
- jsonrpc: "2.0",
498
- error: { code: -32603, message: "Internal Server Error" },
499
- id: null, // Or a relevant request identifier
500
- });
501
- }
419
+ return c.json({
420
+ jsonrpc: "2.0",
421
+ error: { code: -32603, message: "Internal Server Error" },
422
+ id: null,
423
+ }, 500);
502
424
  }
503
425
  };
504
426
  app.get(MCP_ENDPOINT_PATH, handleSessionReq);
505
427
  app.delete(MCP_ENDPOINT_PATH, handleSessionReq);
506
428
  logger.debug("Creating HTTP server instance...", transportContext);
507
- const serverInstance = http.createServer(app);
508
429
  try {
509
430
  logger.debug("Attempting to start HTTP server with retry logic...", transportContext);
510
- const actualPort = await startHttpServerWithRetry(serverInstance, config.mcpHttpPort, config.mcpHttpHost, MAX_PORT_RETRIES, transportContext);
511
- let serverAddressLog = `http://${config.mcpHttpHost}:${actualPort}${MCP_ENDPOINT_PATH}`;
512
- let productionNote = "";
513
- if (config.environment === "production") {
514
- // The server itself runs HTTP, but it's expected to be behind an HTTPS proxy in production.
515
- // The log reflects the effective public-facing URL.
516
- serverAddressLog = `https://${config.mcpHttpHost}:${actualPort}${MCP_ENDPOINT_PATH}`;
517
- productionNote = ` (via HTTPS, ensure reverse proxy is configured)`;
518
- }
519
- if (process.stdout.isTTY) {
520
- console.log(`\n🚀 MCP Server running in HTTP mode at: ${serverAddressLog}${productionNote}\n (MCP Spec: 2025-03-26 Streamable HTTP Transport)\n`);
521
- }
522
- return serverInstance; // Return the created server instance
431
+ const serverInstance = await startHttpServerWithRetry(app, config.mcpHttpPort, config.mcpHttpHost, MAX_PORT_RETRIES, transportContext);
432
+ return serverInstance;
523
433
  }
524
434
  catch (err) {
525
435
  logger.fatal("HTTP server failed to start after multiple port retries.", {
526
436
  ...transportContext,
527
437
  error: err instanceof Error ? err.message : String(err),
528
438
  });
529
- throw err; // Re-throw the error to be caught by the caller
439
+ throw err;
530
440
  }
531
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}`;
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.6",
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
  }