mcp-ts-template 1.7.4 โ 1.7.7
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 +42 -46
- package/dist/config/index.d.ts +14 -0
- package/dist/config/index.js +37 -3
- package/dist/mcp-server/server.js +10 -7
- package/dist/mcp-server/transports/auth/authFactory.js +10 -0
- package/dist/mcp-server/transports/auth/authMiddleware.js +16 -4
- package/dist/mcp-server/transports/auth/lib/authUtils.js +21 -14
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js +48 -15
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js +48 -12
- package/dist/mcp-server/transports/core/baseTransportManager.d.ts +17 -0
- package/dist/mcp-server/transports/core/baseTransportManager.js +18 -0
- package/dist/mcp-server/transports/core/honoNodeBridge.d.ts +23 -0
- package/dist/mcp-server/transports/core/honoNodeBridge.js +51 -0
- package/dist/mcp-server/transports/core/statefulTransportManager.d.ts +31 -0
- package/dist/mcp-server/transports/core/statefulTransportManager.js +233 -0
- package/dist/mcp-server/transports/core/statelessTransportManager.d.ts +20 -0
- package/dist/mcp-server/transports/core/statelessTransportManager.js +92 -0
- package/dist/mcp-server/transports/core/transportTypes.d.ts +23 -29
- package/dist/mcp-server/transports/http/httpErrorHandler.d.ts +4 -9
- package/dist/mcp-server/transports/http/httpErrorHandler.js +25 -3
- package/dist/mcp-server/transports/http/httpTransport.d.ts +6 -30
- package/dist/mcp-server/transports/http/httpTransport.js +141 -104
- package/dist/mcp-server/transports/http/httpTypes.d.ts +3 -1
- package/dist/mcp-server/transports/http/mcpTransportMiddleware.d.ts +25 -0
- package/dist/mcp-server/transports/http/mcpTransportMiddleware.js +63 -0
- package/dist/mcp-server/transports/stdio/stdioTransport.js +8 -3
- package/package.json +1 -1
- package/dist/mcp-server/transports/core/mcpTransportManager.d.ts +0 -32
- package/dist/mcp-server/transports/core/mcpTransportManager.js +0 -148
package/README.md
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
[](https://www.typescriptlang.org/)
|
|
8
8
|
[](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
9
9
|
[](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-06-18/changelog.mdx)
|
|
10
|
-
[](./CHANGELOG.md)
|
|
11
|
+
[](./vitest.config.ts)
|
|
12
12
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
13
13
|
[](https://github.com/cyanheads/mcp-ts-template/issues)
|
|
14
14
|
[](https://github.com/cyanheads/mcp-ts-template)
|
|
@@ -30,36 +30,30 @@ Building a robust server for AI agents is more than just writing code. It requir
|
|
|
30
30
|
|
|
31
31
|
## โจ Key Features
|
|
32
32
|
|
|
33
|
-
| Feature Area | Description
|
|
34
|
-
| :-------------------------- |
|
|
35
|
-
| **๐ MCP Server** |
|
|
36
|
-
| **๐ Production Utilities** | Logging, Error Handling, ID Generation, Rate Limiting, Request Context tracking, Input Sanitization.
|
|
37
|
-
| **๐ Type Safety/Security** | Strong type checking via TypeScript & Zod validation. Built-in security utilities (sanitization, auth middleware for HTTP).
|
|
38
|
-
| **โ๏ธ Error Handling** | Consistent error categorization (`BaseErrorCode`), detailed logging, centralized handling (`ErrorHandler`).
|
|
39
|
-
| **๐ Documentation** | Comprehensive `README.md`, structured JSDoc comments, API references.
|
|
40
|
-
| **๐ต๏ธ Interaction Logging** | Captures raw requests and responses for all external LLM provider interactions to a dedicated `interactions.log` file for full traceability.
|
|
41
|
-
| **๐ค Agent Ready** | Includes a [.clinerules](.clinerules) developer cheatsheet tailored for LLM coding agents.
|
|
42
|
-
| **๐ ๏ธ Utility Scripts** | Scripts for cleaning builds, setting executable permissions, generating directory trees, and fetching OpenAPI specs.
|
|
43
|
-
| **๐งฉ Services** | Reusable modules for LLM (OpenRouter) and data storage (DuckDB) integration, with examples.
|
|
44
|
-
| **๐งช
|
|
45
|
-
|
|
46
|
-
##
|
|
47
|
-
|
|
48
|
-
This template
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
| [**filesystem-mcp-server**](https://github.com/cyanheads/filesystem-mcp-server) | Offers platform-agnostic file system capabilities for AI agents, including advanced search and directory traversal. |
|
|
58
|
-
| [**workflows-mcp-server**](https://github.com/cyanheads/workflows-mcp-server) | A declarative workflow engine that allows agents to execute complex, multi-step automations from simple YAML files. |
|
|
59
|
-
|
|
60
|
-
_Note: [**toolkit-mcp-server**](https://github.com/cyanheads/toolkit-mcp-server) was built on an older version of this template and is pending updates._
|
|
61
|
-
|
|
62
|
-
You can also **see my [GitHub profile](https://github.com/cyanheads/)** for additional MCP servers I've created.
|
|
33
|
+
| Feature Area | Description | Key Components / Location |
|
|
34
|
+
| :-------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------- |
|
|
35
|
+
| **๐ MCP Server** | A functional server with example tools and resources. Supports `stdio` and a **Streamable HTTP** transport built with [**Hono**](https://hono.dev/). | `src/mcp-server/`, `src/mcp-server/transports/` |
|
|
36
|
+
| **๐ Production Utilities** | Logging, Error Handling, ID Generation, Rate Limiting, Request Context tracking, Input Sanitization. | `src/utils/` |
|
|
37
|
+
| **๐ Type Safety/Security** | Strong type checking via TypeScript & Zod validation. Built-in security utilities (sanitization, auth middleware for HTTP). | Throughout, `src/utils/security/`, `src/mcp-server/transports/auth/` |
|
|
38
|
+
| **โ๏ธ Error Handling** | Consistent error categorization (`BaseErrorCode`), detailed logging, centralized handling (`ErrorHandler`). | `src/utils/internal/errorHandler.ts`, `src/types-global/` |
|
|
39
|
+
| **๐ Documentation** | Comprehensive `README.md`, structured JSDoc comments, API references. | `README.md`, Codebase, `tsdoc.json`, `docs/api-references/` |
|
|
40
|
+
| **๐ต๏ธ Interaction Logging** | Captures raw requests and responses for all external LLM provider interactions to a dedicated `interactions.log` file for full traceability. | `src/utils/internal/logger.ts` |
|
|
41
|
+
| **๐ค Agent Ready** | Includes a [.clinerules](.clinerules) developer cheatsheet tailored for LLM coding agents. | `.clinerules` |
|
|
42
|
+
| **๐ ๏ธ Utility Scripts** | Scripts for cleaning builds, setting executable permissions, generating directory trees, and fetching OpenAPI specs. | `scripts/` |
|
|
43
|
+
| **๐งฉ Services** | Reusable modules for LLM (OpenRouter) and data storage (DuckDB) integration, with examples. | `src/services/`, `src/storage/duckdbExample.ts` |
|
|
44
|
+
| **๐งช Integration Testing** | Integrated with Vitest for fast and reliable integration testing. Includes example tests for core logic and a coverage reporter. | `vitest.config.ts`, `tests/` |
|
|
45
|
+
|
|
46
|
+
## Architecture Overview
|
|
47
|
+
|
|
48
|
+
This template employs a modular, transport-agnostic architecture to ensure a clean separation of concerns.
|
|
49
|
+
|
|
50
|
+
- **Core Server (`src/mcp-server/server.ts`)**: The central point where tools and resources are registered. It remains independent of how the server is accessed.
|
|
51
|
+
- **Transports (`src/mcp-server/transports/`)**: The transport layer connects the core server to the outside world.
|
|
52
|
+
- **StdioTransport**: For direct process-to-process communication.
|
|
53
|
+
- **HttpTransport**: A modern, streamable HTTP server built with **Hono**. It uses a middleware-based approach for handling CORS, rate limiting, authentication, and MCP request processing.
|
|
54
|
+
- **Transport Managers (`src/mcp-server/transports/core/`)**: These managers bridge the gap between the transport layer (like Hono) and the MCP SDK, handling session management (stateful vs. stateless) and request lifecycle.
|
|
55
|
+
|
|
56
|
+
This design allows you to add new tools and logic to the core server without worrying about the underlying transport details.
|
|
63
57
|
|
|
64
58
|
## Quick Start
|
|
65
59
|
|
|
@@ -93,7 +87,7 @@ npm run build
|
|
|
93
87
|
|
|
94
88
|
### 4. Running Tests
|
|
95
89
|
|
|
96
|
-
This template uses [Vitest](https://vitest.dev/) for
|
|
90
|
+
This template uses [Vitest](https://vitest.dev/) for testing, with a strong emphasis on **integration testing** to ensure all components work together correctly.
|
|
97
91
|
|
|
98
92
|
- **Run all tests once:**
|
|
99
93
|
```bash
|
|
@@ -110,16 +104,15 @@ This template uses [Vitest](https://vitest.dev/) for unit testing. Tests are loc
|
|
|
110
104
|
|
|
111
105
|
## โ๏ธ Configuration
|
|
112
106
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
Configure the MCP server's behavior using these environment variables:
|
|
107
|
+
Configure the server using these environment variables (or a `.env` file):
|
|
116
108
|
|
|
117
109
|
| Variable | Description | Default |
|
|
118
110
|
| :-------------------- | :---------------------------------------------------------------------------------------- | :------------------------------------- |
|
|
119
111
|
| `MCP_TRANSPORT_TYPE` | Server transport: `stdio` or `http`. | `stdio` |
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
112
|
+
| `MCP_SESSION_MODE` | Session mode for HTTP: `stateless`, `stateful`, or `auto`. | `auto` |
|
|
113
|
+
| `MCP_HTTP_PORT` | Port for the HTTP server. | `3010` |
|
|
114
|
+
| `MCP_HTTP_HOST` | Host address for the HTTP server. | `127.0.0.1` |
|
|
115
|
+
| `MCP_ALLOWED_ORIGINS` | Comma-separated allowed origins for CORS. | (none) |
|
|
123
116
|
| `MCP_AUTH_MODE` | Authentication mode for HTTP: `jwt`, `oauth`, or `none`. | `none` |
|
|
124
117
|
| `MCP_AUTH_SECRET_KEY` | **Required for `jwt` mode.** Secret key (min 32 chars) for signing/verifying auth tokens. | (none - **MUST be set in production**) |
|
|
125
118
|
| `OAUTH_ISSUER_URL` | **Required for `oauth` mode.** The issuer URL of your authorization server. | (none) |
|
|
@@ -128,12 +121,12 @@ Configure the MCP server's behavior using these environment variables:
|
|
|
128
121
|
|
|
129
122
|
## ๐๏ธ Project Structure
|
|
130
123
|
|
|
131
|
-
- **`src/mcp-server/`**: Contains the MCP server
|
|
132
|
-
- **`src/config/`**: Handles loading and validation of environment variables
|
|
133
|
-
- **`src/services/`**:
|
|
124
|
+
- **`src/mcp-server/`**: Contains the core MCP server, tools, resources, and transport handlers.
|
|
125
|
+
- **`src/config/`**: Handles loading and validation of environment variables.
|
|
126
|
+
- **`src/services/`**: Reusable modules for integrating with external services (DuckDB, OpenRouter).
|
|
134
127
|
- **`src/types-global/`**: Defines shared TypeScript interfaces and type definitions.
|
|
135
|
-
- **`src/utils/`**:
|
|
136
|
-
- **`src/index.ts`**: The main entry point
|
|
128
|
+
- **`src/utils/`**: Core utilities (logging, error handling, security, etc.).
|
|
129
|
+
- **`src/index.ts`**: The main entry point that initializes and starts the server.
|
|
137
130
|
|
|
138
131
|
**Explore the full structure yourself:**
|
|
139
132
|
|
|
@@ -145,9 +138,12 @@ npm run tree
|
|
|
145
138
|
|
|
146
139
|
## ๐งฉ Extending the System
|
|
147
140
|
|
|
148
|
-
|
|
141
|
+
The canonical pattern for adding new tools and resources is defined in the [.clinerules](.clinerules) file. It mandates a strict separation of concerns:
|
|
142
|
+
|
|
143
|
+
1. **`logic.ts`**: Contains the pure business logic, Zod schemas, and type definitions. This file throws structured errors on failure.
|
|
144
|
+
2. **`registration.ts`**: Acts as the "handler." It registers the tool with the server, wraps the logic call in a `try...catch` block, and formats the final success or error response.
|
|
149
145
|
|
|
150
|
-
|
|
146
|
+
This "Logic Throws, Handler Catches" pattern ensures that core logic remains pure and testable, while the registration layer handles all side effects and response formatting.
|
|
151
147
|
|
|
152
148
|
## ๐ Explore More MCP Resources
|
|
153
149
|
|
package/dist/config/index.d.ts
CHANGED
|
@@ -36,10 +36,20 @@ export declare const config: {
|
|
|
36
36
|
environment: string;
|
|
37
37
|
/** MCP transport type ('stdio' or 'http'). From `MCP_TRANSPORT_TYPE` env var. Default: "stdio". */
|
|
38
38
|
mcpTransportType: "stdio" | "http";
|
|
39
|
+
/** MCP session mode ('stateless', 'stateful', 'auto'). From `MCP_SESSION_MODE` env var. Default: "auto". */
|
|
40
|
+
mcpSessionMode: "stateless" | "stateful" | "auto";
|
|
39
41
|
/** HTTP server port (if http transport). From `MCP_HTTP_PORT` env var. Default: 3010. */
|
|
40
42
|
mcpHttpPort: number;
|
|
41
43
|
/** HTTP server host (if http transport). From `MCP_HTTP_HOST` env var. Default: "127.0.0.1". */
|
|
42
44
|
mcpHttpHost: string;
|
|
45
|
+
/** MCP endpoint path for HTTP transport. From `MCP_HTTP_ENDPOINT_PATH`. Default: "/mcp". */
|
|
46
|
+
mcpHttpEndpointPath: string;
|
|
47
|
+
/** Max retries for port binding. From `MCP_HTTP_MAX_PORT_RETRIES`. Default: 15. */
|
|
48
|
+
mcpHttpMaxPortRetries: number;
|
|
49
|
+
/** Delay between port binding retries. From `MCP_HTTP_PORT_RETRY_DELAY_MS`. Default: 50. */
|
|
50
|
+
mcpHttpPortRetryDelayMs: number;
|
|
51
|
+
/** Timeout for stale stateful sessions. From `MCP_STATEFUL_SESSION_STALE_TIMEOUT_MS`. Default: 1800000. */
|
|
52
|
+
mcpStatefulSessionStaleTimeoutMs: number;
|
|
43
53
|
/** Array of allowed CORS origins (http transport). From `MCP_ALLOWED_ORIGINS` (comma-separated). */
|
|
44
54
|
mcpAllowedOrigins: string[] | undefined;
|
|
45
55
|
/** Auth secret key (JWTs, http transport). From `MCP_AUTH_SECRET_KEY`. CRITICAL. */
|
|
@@ -52,6 +62,10 @@ export declare const config: {
|
|
|
52
62
|
oauthJwksUri: string | undefined;
|
|
53
63
|
/** OAuth 2.1 Audience. From `OAUTH_AUDIENCE`. */
|
|
54
64
|
oauthAudience: string | undefined;
|
|
65
|
+
/** Development mode client ID. From `DEV_MCP_CLIENT_ID`. */
|
|
66
|
+
devMcpClientId: string | undefined;
|
|
67
|
+
/** Development mode scopes. From `DEV_MCP_SCOPES`. */
|
|
68
|
+
devMcpScopes: string[] | undefined;
|
|
55
69
|
/** OpenRouter App URL. From `OPENROUTER_APP_URL`. Default: "http://localhost:3000". */
|
|
56
70
|
openrouterAppUrl: string;
|
|
57
71
|
/** OpenRouter App Name. From `OPENROUTER_APP_NAME`. Defaults to `mcpServerName`. */
|
package/dist/config/index.js
CHANGED
|
@@ -85,10 +85,28 @@ const EnvSchema = z.object({
|
|
|
85
85
|
NODE_ENV: z.string().default("development"),
|
|
86
86
|
/** MCP communication transport ("stdio" or "http"). Default: "stdio". */
|
|
87
87
|
MCP_TRANSPORT_TYPE: z.enum(["stdio", "http"]).default("stdio"),
|
|
88
|
+
/** MCP session mode ('stateless', 'stateful', 'auto'). Default: 'auto'. */
|
|
89
|
+
MCP_SESSION_MODE: z.enum(["stateless", "stateful", "auto"]).default("auto"),
|
|
88
90
|
/** HTTP server port (if MCP_TRANSPORT_TYPE is "http"). Default: 3010. */
|
|
89
91
|
MCP_HTTP_PORT: z.coerce.number().int().positive().default(3010),
|
|
90
92
|
/** HTTP server host (if MCP_TRANSPORT_TYPE is "http"). Default: "127.0.0.1". */
|
|
91
93
|
MCP_HTTP_HOST: z.string().default("127.0.0.1"),
|
|
94
|
+
/** The endpoint path for the MCP server. Default: "/mcp". */
|
|
95
|
+
MCP_HTTP_ENDPOINT_PATH: z.string().default("/mcp"),
|
|
96
|
+
/** Max retries for binding to a port if the initial one is in use. Default: 15. */
|
|
97
|
+
MCP_HTTP_MAX_PORT_RETRIES: z.coerce.number().int().nonnegative().default(15),
|
|
98
|
+
/** Delay in ms between port binding retries. Default: 50. */
|
|
99
|
+
MCP_HTTP_PORT_RETRY_DELAY_MS: z.coerce
|
|
100
|
+
.number()
|
|
101
|
+
.int()
|
|
102
|
+
.nonnegative()
|
|
103
|
+
.default(50),
|
|
104
|
+
/** Timeout in ms for considering a stateful session stale and eligible for cleanup. Default: 1800000 (30 minutes). */
|
|
105
|
+
MCP_STATEFUL_SESSION_STALE_TIMEOUT_MS: z.coerce
|
|
106
|
+
.number()
|
|
107
|
+
.int()
|
|
108
|
+
.positive()
|
|
109
|
+
.default(1800000),
|
|
92
110
|
/** Optional. Comma-separated allowed origins for CORS (HTTP transport). */
|
|
93
111
|
MCP_ALLOWED_ORIGINS: z.string().optional(),
|
|
94
112
|
/** Optional. Secret key (min 32 chars) for auth tokens (HTTP transport). CRITICAL for production. */
|
|
@@ -104,6 +122,10 @@ const EnvSchema = z.object({
|
|
|
104
122
|
OAUTH_JWKS_URI: z.string().url().optional(),
|
|
105
123
|
/** The audience claim for the OAuth 2.1 access tokens. This server will reject tokens not intended for it. */
|
|
106
124
|
OAUTH_AUDIENCE: z.string().optional(),
|
|
125
|
+
/** Optional. Client ID to use in development mode for JWT strategy. Default: "dev-client-id". */
|
|
126
|
+
DEV_MCP_CLIENT_ID: z.string().optional(),
|
|
127
|
+
/** Optional. Comma-separated scopes for development mode JWT strategy. Default: "dev-scope". */
|
|
128
|
+
DEV_MCP_SCOPES: z.string().optional(),
|
|
107
129
|
/** Optional. Application URL for OpenRouter integration. */
|
|
108
130
|
OPENROUTER_APP_URL: z
|
|
109
131
|
.string()
|
|
@@ -114,9 +136,7 @@ const EnvSchema = z.object({
|
|
|
114
136
|
/** Optional. API key for OpenRouter services. */
|
|
115
137
|
OPENROUTER_API_KEY: z.string().optional(),
|
|
116
138
|
/** Default LLM model. Default: "google/gemini-2.5-flash". */
|
|
117
|
-
LLM_DEFAULT_MODEL: z
|
|
118
|
-
.string()
|
|
119
|
-
.default("google/gemini-2.5-flash"),
|
|
139
|
+
LLM_DEFAULT_MODEL: z.string().default("google/gemini-2.5-flash"),
|
|
120
140
|
/** Optional. Default LLM temperature (0.0-2.0). */
|
|
121
141
|
LLM_DEFAULT_TEMPERATURE: z.coerce.number().min(0).max(2).optional(),
|
|
122
142
|
/** Optional. Default LLM top_p (0.0-1.0). */
|
|
@@ -262,10 +282,20 @@ export const config = {
|
|
|
262
282
|
environment: env.NODE_ENV,
|
|
263
283
|
/** MCP transport type ('stdio' or 'http'). From `MCP_TRANSPORT_TYPE` env var. Default: "stdio". */
|
|
264
284
|
mcpTransportType: env.MCP_TRANSPORT_TYPE,
|
|
285
|
+
/** MCP session mode ('stateless', 'stateful', 'auto'). From `MCP_SESSION_MODE` env var. Default: "auto". */
|
|
286
|
+
mcpSessionMode: env.MCP_SESSION_MODE,
|
|
265
287
|
/** HTTP server port (if http transport). From `MCP_HTTP_PORT` env var. Default: 3010. */
|
|
266
288
|
mcpHttpPort: env.MCP_HTTP_PORT,
|
|
267
289
|
/** HTTP server host (if http transport). From `MCP_HTTP_HOST` env var. Default: "127.0.0.1". */
|
|
268
290
|
mcpHttpHost: env.MCP_HTTP_HOST,
|
|
291
|
+
/** MCP endpoint path for HTTP transport. From `MCP_HTTP_ENDPOINT_PATH`. Default: "/mcp". */
|
|
292
|
+
mcpHttpEndpointPath: env.MCP_HTTP_ENDPOINT_PATH,
|
|
293
|
+
/** Max retries for port binding. From `MCP_HTTP_MAX_PORT_RETRIES`. Default: 15. */
|
|
294
|
+
mcpHttpMaxPortRetries: env.MCP_HTTP_MAX_PORT_RETRIES,
|
|
295
|
+
/** Delay between port binding retries. From `MCP_HTTP_PORT_RETRY_DELAY_MS`. Default: 50. */
|
|
296
|
+
mcpHttpPortRetryDelayMs: env.MCP_HTTP_PORT_RETRY_DELAY_MS,
|
|
297
|
+
/** Timeout for stale stateful sessions. From `MCP_STATEFUL_SESSION_STALE_TIMEOUT_MS`. Default: 1800000. */
|
|
298
|
+
mcpStatefulSessionStaleTimeoutMs: env.MCP_STATEFUL_SESSION_STALE_TIMEOUT_MS,
|
|
269
299
|
/** Array of allowed CORS origins (http transport). From `MCP_ALLOWED_ORIGINS` (comma-separated). */
|
|
270
300
|
mcpAllowedOrigins: env.MCP_ALLOWED_ORIGINS?.split(",")
|
|
271
301
|
.map((origin) => origin.trim())
|
|
@@ -280,6 +310,10 @@ export const config = {
|
|
|
280
310
|
oauthJwksUri: env.OAUTH_JWKS_URI,
|
|
281
311
|
/** OAuth 2.1 Audience. From `OAUTH_AUDIENCE`. */
|
|
282
312
|
oauthAudience: env.OAUTH_AUDIENCE,
|
|
313
|
+
/** Development mode client ID. From `DEV_MCP_CLIENT_ID`. */
|
|
314
|
+
devMcpClientId: env.DEV_MCP_CLIENT_ID,
|
|
315
|
+
/** Development mode scopes. From `DEV_MCP_SCOPES`. */
|
|
316
|
+
devMcpScopes: env.DEV_MCP_SCOPES?.split(",").map((s) => s.trim()),
|
|
283
317
|
/** OpenRouter App URL. From `OPENROUTER_APP_URL`. Default: "http://localhost:3000". */
|
|
284
318
|
openrouterAppUrl: env.OPENROUTER_APP_URL || "http://localhost:3000",
|
|
285
319
|
/** OpenRouter App Name. From `OPENROUTER_APP_NAME`. Defaults to `mcpServerName`. */
|
|
@@ -35,11 +35,6 @@ async function createMcpServerInstance() {
|
|
|
35
35
|
operation: "createMcpServerInstance",
|
|
36
36
|
});
|
|
37
37
|
logger.info("Initializing MCP server instance", context);
|
|
38
|
-
requestContextService.configure({
|
|
39
|
-
appName: config.mcpServerName,
|
|
40
|
-
appVersion: config.mcpServerVersion,
|
|
41
|
-
environment,
|
|
42
|
-
});
|
|
43
38
|
const server = new McpServer({ name: config.mcpServerName, version: config.mcpServerVersion }, {
|
|
44
39
|
capabilities: {
|
|
45
40
|
logging: {},
|
|
@@ -76,12 +71,14 @@ async function startTransport() {
|
|
|
76
71
|
transport: transportType,
|
|
77
72
|
});
|
|
78
73
|
logger.info(`Starting transport: ${transportType}`, context);
|
|
74
|
+
// Always use the factory pattern
|
|
75
|
+
const serverFactory = createMcpServerInstance;
|
|
79
76
|
if (transportType === "http") {
|
|
80
|
-
const { server } = await startHttpTransport(
|
|
77
|
+
const { server } = await startHttpTransport(serverFactory, context);
|
|
81
78
|
return server;
|
|
82
79
|
}
|
|
83
80
|
if (transportType === "stdio") {
|
|
84
|
-
const server = await
|
|
81
|
+
const server = await serverFactory(); // Call the factory once here
|
|
85
82
|
await startStdioTransport(server, context);
|
|
86
83
|
return server;
|
|
87
84
|
}
|
|
@@ -95,6 +92,12 @@ export async function initializeAndStartServer() {
|
|
|
95
92
|
operation: "initializeAndStartServer",
|
|
96
93
|
});
|
|
97
94
|
logger.info("MCP Server initialization sequence started.", context);
|
|
95
|
+
// Configure the global request context service once at startup.
|
|
96
|
+
requestContextService.configure({
|
|
97
|
+
appName: config.mcpServerName,
|
|
98
|
+
appVersion: config.mcpServerVersion,
|
|
99
|
+
environment,
|
|
100
|
+
});
|
|
98
101
|
try {
|
|
99
102
|
const result = await startTransport();
|
|
100
103
|
logger.info("MCP Server initialization sequence completed successfully.", context);
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* @module src/mcp-server/transports/auth/authFactory
|
|
6
6
|
*/
|
|
7
7
|
import { config } from "../../../config/index.js";
|
|
8
|
+
import { logger, requestContextService } from "../../../utils/index.js";
|
|
8
9
|
import { JwtStrategy } from "./strategies/jwtStrategy.js";
|
|
9
10
|
import { OauthStrategy } from "./strategies/oauthStrategy.js";
|
|
10
11
|
/**
|
|
@@ -16,16 +17,25 @@ import { OauthStrategy } from "./strategies/oauthStrategy.js";
|
|
|
16
17
|
* @throws {Error} If the auth mode is unknown or misconfigured.
|
|
17
18
|
*/
|
|
18
19
|
export function createAuthStrategy() {
|
|
20
|
+
const context = requestContextService.createRequestContext({
|
|
21
|
+
operation: "createAuthStrategy",
|
|
22
|
+
authMode: config.mcpAuthMode,
|
|
23
|
+
});
|
|
24
|
+
logger.info("Creating authentication strategy...", context);
|
|
19
25
|
switch (config.mcpAuthMode) {
|
|
20
26
|
case "jwt":
|
|
27
|
+
logger.debug("Instantiating JWT authentication strategy.", context);
|
|
21
28
|
return new JwtStrategy();
|
|
22
29
|
case "oauth":
|
|
30
|
+
logger.debug("Instantiating OAuth authentication strategy.", context);
|
|
23
31
|
return new OauthStrategy();
|
|
24
32
|
case "none":
|
|
33
|
+
logger.info("Authentication is disabled ('none' mode).", context);
|
|
25
34
|
return null; // No authentication
|
|
26
35
|
default:
|
|
27
36
|
// This ensures that if a new auth mode is added to the config type
|
|
28
37
|
// but not to this factory, we get a compile-time or runtime error.
|
|
38
|
+
logger.error(`Unknown authentication mode: ${config.mcpAuthMode}`, context);
|
|
29
39
|
throw new Error(`Unknown authentication mode: ${config.mcpAuthMode}`);
|
|
30
40
|
}
|
|
31
41
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
|
|
2
|
-
import { logger, requestContextService } from "../../../utils/index.js";
|
|
2
|
+
import { ErrorHandler, logger, requestContextService, } from "../../../utils/index.js";
|
|
3
3
|
import { authContext } from "./lib/authContext.js";
|
|
4
4
|
/**
|
|
5
5
|
* Creates a Hono middleware function that enforces authentication using a given strategy.
|
|
@@ -14,21 +14,27 @@ export function createAuthMiddleware(strategy) {
|
|
|
14
14
|
method: c.req.method,
|
|
15
15
|
path: c.req.path,
|
|
16
16
|
});
|
|
17
|
+
logger.debug("Initiating authentication check.", context);
|
|
17
18
|
const authHeader = c.req.header("Authorization");
|
|
18
19
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
20
|
+
logger.warning("Authorization header missing or invalid.", context);
|
|
19
21
|
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Missing or invalid Authorization header. Bearer scheme required.", context);
|
|
20
22
|
}
|
|
21
23
|
const token = authHeader.substring(7);
|
|
22
24
|
if (!token) {
|
|
25
|
+
logger.warning("Bearer token is missing from Authorization header.", context);
|
|
23
26
|
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Authentication token is missing.", context);
|
|
24
27
|
}
|
|
28
|
+
logger.debug("Extracted Bearer token, proceeding to verification.", context);
|
|
25
29
|
try {
|
|
26
30
|
const authInfo = await strategy.verify(token);
|
|
27
|
-
|
|
31
|
+
const authLogContext = {
|
|
28
32
|
...context,
|
|
29
33
|
clientId: authInfo.clientId,
|
|
34
|
+
subject: authInfo.subject,
|
|
30
35
|
scopes: authInfo.scopes,
|
|
31
|
-
}
|
|
36
|
+
};
|
|
37
|
+
logger.info("Authentication successful. Auth context populated.", authLogContext);
|
|
32
38
|
// Run the next middleware in the chain within the populated auth context.
|
|
33
39
|
await authContext.run({ authInfo }, next);
|
|
34
40
|
}
|
|
@@ -39,7 +45,13 @@ export function createAuthMiddleware(strategy) {
|
|
|
39
45
|
...context,
|
|
40
46
|
error: error instanceof Error ? error.message : String(error),
|
|
41
47
|
});
|
|
42
|
-
|
|
48
|
+
// Ensure consistent error handling
|
|
49
|
+
throw ErrorHandler.handleError(error, {
|
|
50
|
+
operation: "authMiddlewareVerification",
|
|
51
|
+
context,
|
|
52
|
+
rethrow: true, // Rethrow to be caught by Hono's global error handler
|
|
53
|
+
errorCode: BaseErrorCode.UNAUTHORIZED, // Default to unauthorized if not more specific
|
|
54
|
+
});
|
|
43
55
|
}
|
|
44
56
|
};
|
|
45
57
|
}
|
|
@@ -19,27 +19,34 @@ import { authContext } from "./authContext.js";
|
|
|
19
19
|
* more required scopes are not present in the validated token.
|
|
20
20
|
*/
|
|
21
21
|
export function withRequiredScopes(requiredScopes) {
|
|
22
|
+
const operationName = "withRequiredScopesCheck";
|
|
23
|
+
const initialContext = requestContextService.createRequestContext({
|
|
24
|
+
operation: operationName,
|
|
25
|
+
requiredScopes,
|
|
26
|
+
});
|
|
27
|
+
logger.debug("Performing scope authorization check.", initialContext);
|
|
22
28
|
const store = authContext.getStore();
|
|
23
29
|
if (!store || !store.authInfo) {
|
|
30
|
+
logger.crit("Authentication context is missing in withRequiredScopes. This is a server configuration error.", initialContext);
|
|
24
31
|
// This is a server-side logic error; the auth middleware should always populate this.
|
|
25
|
-
throw new McpError(BaseErrorCode.INTERNAL_ERROR, "Authentication context is missing. This indicates a server configuration error.",
|
|
26
|
-
|
|
32
|
+
throw new McpError(BaseErrorCode.INTERNAL_ERROR, "Authentication context is missing. This indicates a server configuration error.", {
|
|
33
|
+
...initialContext,
|
|
27
34
|
error: "AuthStore not found in AsyncLocalStorage.",
|
|
28
|
-
})
|
|
35
|
+
});
|
|
29
36
|
}
|
|
30
|
-
const { scopes: grantedScopes, clientId } = store.authInfo;
|
|
37
|
+
const { scopes: grantedScopes, clientId, subject } = store.authInfo;
|
|
31
38
|
const grantedScopeSet = new Set(grantedScopes);
|
|
32
39
|
const missingScopes = requiredScopes.filter((scope) => !grantedScopeSet.has(scope));
|
|
40
|
+
const finalContext = {
|
|
41
|
+
...initialContext,
|
|
42
|
+
grantedScopes,
|
|
43
|
+
clientId,
|
|
44
|
+
subject,
|
|
45
|
+
};
|
|
33
46
|
if (missingScopes.length > 0) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
granted: grantedScopes,
|
|
38
|
-
missing: missingScopes,
|
|
39
|
-
clientId: clientId,
|
|
40
|
-
subject: store.authInfo.subject,
|
|
41
|
-
});
|
|
42
|
-
logger.warning("Authorization failed: Missing required scopes.", context);
|
|
43
|
-
throw new McpError(BaseErrorCode.FORBIDDEN, `Insufficient permissions. Missing required scopes: ${missingScopes.join(", ")}`, { requiredScopes, missingScopes });
|
|
47
|
+
const errorContext = { ...finalContext, missingScopes };
|
|
48
|
+
logger.warning("Authorization failed: Missing required scopes.", errorContext);
|
|
49
|
+
throw new McpError(BaseErrorCode.FORBIDDEN, `Insufficient permissions. Missing required scopes: ${missingScopes.join(", ")}`, errorContext);
|
|
44
50
|
}
|
|
51
|
+
logger.debug("Scope authorization successful.", finalContext);
|
|
45
52
|
}
|
|
@@ -8,19 +8,24 @@
|
|
|
8
8
|
import { jwtVerify } from "jose";
|
|
9
9
|
import { config, environment } from "../../../../config/index.js";
|
|
10
10
|
import { BaseErrorCode, McpError } from "../../../../types-global/errors.js";
|
|
11
|
-
import { logger } from "../../../../utils/index.js";
|
|
11
|
+
import { ErrorHandler, logger, requestContextService, } from "../../../../utils/index.js";
|
|
12
12
|
export class JwtStrategy {
|
|
13
13
|
constructor() {
|
|
14
|
+
const context = requestContextService.createRequestContext({
|
|
15
|
+
operation: "JwtStrategy.constructor",
|
|
16
|
+
});
|
|
17
|
+
logger.debug("Initializing JwtStrategy...", context);
|
|
14
18
|
if (config.mcpAuthMode === "jwt") {
|
|
15
19
|
if (environment === "production" && !config.mcpAuthSecretKey) {
|
|
16
|
-
logger.fatal("CRITICAL: MCP_AUTH_SECRET_KEY is not set in production for JWT auth.");
|
|
17
|
-
throw new McpError(BaseErrorCode.CONFIGURATION_ERROR, "MCP_AUTH_SECRET_KEY must be set for JWT auth in production.");
|
|
20
|
+
logger.fatal("CRITICAL: MCP_AUTH_SECRET_KEY is not set in production for JWT auth.", context);
|
|
21
|
+
throw new McpError(BaseErrorCode.CONFIGURATION_ERROR, "MCP_AUTH_SECRET_KEY must be set for JWT auth in production.", context);
|
|
18
22
|
}
|
|
19
23
|
else if (!config.mcpAuthSecretKey) {
|
|
20
|
-
logger.warning("MCP_AUTH_SECRET_KEY is not set. JWT auth will be bypassed (DEV ONLY).");
|
|
24
|
+
logger.warning("MCP_AUTH_SECRET_KEY is not set. JWT auth will be bypassed (DEV ONLY).", context);
|
|
21
25
|
this.secretKey = null;
|
|
22
26
|
}
|
|
23
27
|
else {
|
|
28
|
+
logger.info("JWT secret key loaded successfully.", context);
|
|
24
29
|
this.secretKey = new TextEncoder().encode(config.mcpAuthSecretKey);
|
|
25
30
|
}
|
|
26
31
|
}
|
|
@@ -29,28 +34,38 @@ export class JwtStrategy {
|
|
|
29
34
|
}
|
|
30
35
|
}
|
|
31
36
|
async verify(token) {
|
|
37
|
+
const context = requestContextService.createRequestContext({
|
|
38
|
+
operation: "JwtStrategy.verify",
|
|
39
|
+
});
|
|
40
|
+
logger.debug("Attempting to verify JWT.", context);
|
|
32
41
|
// Handle development mode bypass
|
|
33
42
|
if (!this.secretKey) {
|
|
34
43
|
if (environment !== "production") {
|
|
35
|
-
logger.warning("Bypassing JWT verification: No secret key (DEV ONLY).");
|
|
44
|
+
logger.warning("Bypassing JWT verification: No secret key (DEV ONLY).", context);
|
|
36
45
|
return {
|
|
37
46
|
token: "dev-mode-placeholder-token",
|
|
38
|
-
clientId: "dev-client-id",
|
|
39
|
-
scopes: ["dev-scope"],
|
|
47
|
+
clientId: config.devMcpClientId || "dev-client-id",
|
|
48
|
+
scopes: config.devMcpScopes || ["dev-scope"],
|
|
40
49
|
};
|
|
41
50
|
}
|
|
42
51
|
// This path is defensive. The constructor should prevent this state in production.
|
|
43
|
-
|
|
52
|
+
logger.crit("Auth secret key is missing in production.", context);
|
|
53
|
+
throw new McpError(BaseErrorCode.CONFIGURATION_ERROR, "Auth secret key is missing in production. This indicates a server configuration error.", context);
|
|
44
54
|
}
|
|
45
55
|
try {
|
|
46
56
|
const { payload: decoded } = await jwtVerify(token, this.secretKey);
|
|
57
|
+
logger.debug("JWT signature verified successfully.", {
|
|
58
|
+
...context,
|
|
59
|
+
claims: decoded,
|
|
60
|
+
});
|
|
47
61
|
const clientId = typeof decoded.cid === "string"
|
|
48
62
|
? decoded.cid
|
|
49
63
|
: typeof decoded.client_id === "string"
|
|
50
64
|
? decoded.client_id
|
|
51
65
|
: undefined;
|
|
52
66
|
if (!clientId) {
|
|
53
|
-
|
|
67
|
+
logger.warning("Invalid token: missing 'cid' or 'client_id' claim.", context);
|
|
68
|
+
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Invalid token: missing 'cid' or 'client_id' claim.", context);
|
|
54
69
|
}
|
|
55
70
|
let scopes = [];
|
|
56
71
|
if (Array.isArray(decoded.scp) &&
|
|
@@ -61,18 +76,36 @@ export class JwtStrategy {
|
|
|
61
76
|
scopes = decoded.scope.split(" ").filter(Boolean);
|
|
62
77
|
}
|
|
63
78
|
if (scopes.length === 0) {
|
|
64
|
-
|
|
79
|
+
logger.warning("Invalid token: missing or empty 'scp' or 'scope' claim.", context);
|
|
80
|
+
throw new McpError(BaseErrorCode.UNAUTHORIZED, "Token must contain valid, non-empty scopes.", context);
|
|
65
81
|
}
|
|
66
|
-
|
|
82
|
+
const authInfo = {
|
|
83
|
+
token,
|
|
84
|
+
clientId,
|
|
85
|
+
scopes,
|
|
86
|
+
subject: decoded.sub,
|
|
87
|
+
};
|
|
88
|
+
logger.info("JWT verification successful.", {
|
|
89
|
+
...context,
|
|
90
|
+
clientId,
|
|
91
|
+
scopes,
|
|
92
|
+
});
|
|
93
|
+
return authInfo;
|
|
67
94
|
}
|
|
68
95
|
catch (error) {
|
|
69
|
-
if (error instanceof McpError)
|
|
70
|
-
throw error;
|
|
71
96
|
const message = error instanceof Error && error.name === "JWTExpired"
|
|
72
97
|
? "Token has expired."
|
|
73
98
|
: "Token verification failed.";
|
|
74
|
-
|
|
75
|
-
|
|
99
|
+
logger.warning(`JWT verification failed: ${message}`, {
|
|
100
|
+
...context,
|
|
101
|
+
errorName: error instanceof Error ? error.name : "Unknown",
|
|
102
|
+
});
|
|
103
|
+
throw ErrorHandler.handleError(error, {
|
|
104
|
+
operation: "JwtStrategy.verify",
|
|
105
|
+
context,
|
|
106
|
+
rethrow: true,
|
|
107
|
+
errorCode: BaseErrorCode.UNAUTHORIZED,
|
|
108
|
+
errorMapper: () => new McpError(BaseErrorCode.UNAUTHORIZED, message, context),
|
|
76
109
|
});
|
|
77
110
|
}
|
|
78
111
|
}
|