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 +33 -33
- package/dist/config/index.d.ts +11 -0
- package/dist/config/index.js +16 -0
- package/dist/mcp-server/server.d.ts +2 -2
- package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +21 -13
- package/dist/mcp-server/transports/authentication/authMiddleware.js +28 -64
- package/dist/mcp-server/transports/httpTransport.d.ts +4 -4
- package/dist/mcp-server/transports/httpTransport.js +109 -199
- package/dist/services/supabase/supabaseClient.d.ts +24 -0
- package/dist/services/supabase/supabaseClient.js +67 -0
- package/dist/utils/internal/logger.js +7 -1
- package/package.json +14 -8
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.typescriptlang.org/)
|
|
4
4
|
[](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
5
5
|
[](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/changelog.mdx)
|
|
6
|
-
[](./CHANGELOG.md)
|
|
7
7
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
8
8
|
[](https://github.com/cyanheads/mcp-ts-template/issues)
|
|
9
9
|
[](https://github.com/cyanheads/mcp-ts-template)
|
|
@@ -185,38 +185,38 @@ This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE
|
|
|
185
185
|
|
|
186
186
|
## 📊 Detailed Features Table
|
|
187
187
|
|
|
188
|
-
| Category | Feature | Description | Location(s)
|
|
189
|
-
| :----------------------- | :------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
190
|
-
| **Core Components** | MCP Server | Core server logic, tool/resource registration, transport handling. Includes Echo Tool & Resource examples. | `src/mcp-server/`
|
|
191
|
-
| | MCP Client | Logic for connecting to external MCP servers (updated to **MCP 2025-03-26 spec**). Refactored for modularity. | `src/mcp-client/` (see subdirs: `core/`, `client-config/`, `transports/`)
|
|
192
|
-
| | Configuration | Environment-aware settings with Zod validation. | `src/config/`, `src/mcp-client/client-config/configLoader.ts`
|
|
193
|
-
| | Streamable HTTP Transport |
|
|
194
|
-
| | Stdio Transport | Handles MCP communication over standard input/output. | `src/mcp-server/transports/stdioTransport.ts`
|
|
195
|
-
| **Utilities (Core)** | Logger | Structured, context-aware logging (files with rotation & MCP notifications). | `src/utils/internal/logger.ts`
|
|
196
|
-
| | ErrorHandler | Centralized error processing, classification, and logging. | `src/utils/internal/errorHandler.ts`
|
|
197
|
-
| | RequestContext | Request/operation tracking and correlation. | `src/utils/internal/requestContext.ts`
|
|
198
|
-
| **Utilities (Metrics)** | TokenCounter | Estimates token counts using `tiktoken`. | `src/utils/metrics/tokenCounter.ts`
|
|
199
|
-
| **Utilities (Parsing)** | DateParser | Parses natural language date strings using `chrono-node`. | `src/utils/parsing/dateParser.ts`
|
|
200
|
-
| | JsonParser | Parses potentially partial JSON, handles `<think>` blocks. | `src/utils/parsing/jsonParser.ts`
|
|
201
|
-
| **Utilities (Security)** | IdGenerator | Generates unique IDs (prefixed or UUIDs). | `src/utils/security/idGenerator.ts`
|
|
202
|
-
| | RateLimiter | Request throttling based on keys. | `src/utils/security/rateLimiter.ts`
|
|
203
|
-
| | Sanitization | Input validation/cleaning (HTML, paths, URLs, numbers, JSON) & log redaction (`validator`, `sanitize-html`). | `src/utils/security/sanitization.ts`
|
|
204
|
-
| **Services** | DuckDB Integration | Reusable module for in-process analytical data management using DuckDB. A storage layer that runs on the same level as the application. Includes connection management, query execution, and example usage. Integrated with our utils (logger, etc.) | `src/services/duck-db/`, `src/storage/duckdbExample.ts`
|
|
205
|
-
| | OpenRouter LLM Integration | Reusable module for interacting with various LLMs via the OpenRouter API (OpenAI SDK compatible).
|
|
206
|
-
| **Type Safety** | Global Types | Shared TypeScript definitions for consistent interfaces (Errors, MCP types). | `src/types-global/`
|
|
207
|
-
| | Zod Schemas | Used for robust validation of configuration files and tool/resource inputs. | Throughout (`config`, `mcp-client`, tools, etc.)
|
|
208
|
-
| **Error Handling** | Pattern-Based Classification | Automatically categorize errors based on message patterns. | `src/utils/internal/errorHandler.ts`
|
|
209
|
-
| | Consistent Formatting | Standardized error responses with additional context. | `src/utils/internal/errorHandler.ts`
|
|
210
|
-
| | Safe Try/Catch Patterns | Centralized error processing helpers (`ErrorHandler.tryCatch`). | `src/utils/internal/errorHandler.ts`
|
|
211
|
-
| | Client/Transport Error Handling | Specific handlers for MCP client and transport error handling. | `src/mcp-client/core/`, `src/mcp-client/transports/`
|
|
212
|
-
| **Security** | Input Validation | Using `validator` and `zod` for various data type checks. | `src/utils/security/sanitization.ts`, etc.
|
|
213
|
-
| | Input Sanitization | Using `sanitize-html` to prevent injection attacks. | `src/utils/security/sanitization.ts`
|
|
214
|
-
| | Sensitive Data Redaction | Automatic redaction in logs. | `src/utils/security/sanitization.ts`
|
|
215
|
-
| | Configuration
|
|
216
|
-
| **Scripts** | Clean Script | Removes `dist` and `logs` directories (or custom targets). | `scripts/clean.ts`
|
|
217
|
-
| | Make Executable Script | Sets executable permissions (`chmod +x`) on specified files (Unix-like only). | `scripts/make-executable.ts`
|
|
218
|
-
| | Tree Script | Generates a directory structure tree, respecting `.gitignore`. | `scripts/tree.ts`
|
|
219
|
-
| | Fetch OpenAPI Spec Script | Fetches an OpenAPI spec (YAML/JSON) from a URL with fallbacks, saves locally. | `scripts/fetch-openapi-spec.ts`
|
|
188
|
+
| Category | Feature | Description | Location(s) |
|
|
189
|
+
| :----------------------- | :------------------------------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------ |
|
|
190
|
+
| **Core Components** | MCP Server | Core server logic, tool/resource registration, transport handling. Includes Echo Tool & Resource examples. | `src/mcp-server/` |
|
|
191
|
+
| | MCP Client | Logic for connecting to external MCP servers (updated to **MCP 2025-03-26 spec**). Refactored for modularity. | `src/mcp-client/` (see subdirs: `core/`, `client-config/`, `transports/`) |
|
|
192
|
+
| | Configuration | Environment-aware settings with Zod validation. | `src/config/`, `src/mcp-client/client-config/configLoader.ts` |
|
|
193
|
+
| | Streamable HTTP Transport | Hono-based server implementing the MCP **Streamable HTTP** transport with session management, CORS, and port retries. | `src/mcp-server/transports/httpTransport.ts` |
|
|
194
|
+
| | Stdio Transport | Handles MCP communication over standard input/output. | `src/mcp-server/transports/stdioTransport.ts` |
|
|
195
|
+
| **Utilities (Core)** | Logger | Structured, context-aware logging (files with rotation & MCP notifications). | `src/utils/internal/logger.ts` |
|
|
196
|
+
| | ErrorHandler | Centralized error processing, classification, and logging. | `src/utils/internal/errorHandler.ts` |
|
|
197
|
+
| | RequestContext | Request/operation tracking and correlation. | `src/utils/internal/requestContext.ts` |
|
|
198
|
+
| **Utilities (Metrics)** | TokenCounter | Estimates token counts using `tiktoken`. | `src/utils/metrics/tokenCounter.ts` |
|
|
199
|
+
| **Utilities (Parsing)** | DateParser | Parses natural language date strings using `chrono-node`. | `src/utils/parsing/dateParser.ts` |
|
|
200
|
+
| | JsonParser | Parses potentially partial JSON, handles `<think>` blocks. | `src/utils/parsing/jsonParser.ts` |
|
|
201
|
+
| **Utilities (Security)** | IdGenerator | Generates unique IDs (prefixed or UUIDs). | `src/utils/security/idGenerator.ts` |
|
|
202
|
+
| | RateLimiter | Request throttling based on keys. | `src/utils/security/rateLimiter.ts` |
|
|
203
|
+
| | Sanitization | Input validation/cleaning (HTML, paths, URLs, numbers, JSON) & log redaction (`validator`, `sanitize-html`). | `src/utils/security/sanitization.ts` |
|
|
204
|
+
| **Services** | DuckDB Integration | Reusable module for in-process analytical data management using DuckDB. A storage layer that runs on the same level as the application. Includes connection management, query execution, and example usage. Integrated with our utils (logger, etc.) | `src/services/duck-db/`, `src/storage/duckdbExample.ts` |
|
|
205
|
+
| | OpenRouter LLM Integration | Reusable module for interacting with various LLMs via the OpenRouter API (OpenAI SDK compatible). Integrated with our utils (logger, etc.) | `src/services/llm-providers/openRouterProvider.ts` |
|
|
206
|
+
| **Type Safety** | Global Types | Shared TypeScript definitions for consistent interfaces (Errors, MCP types). | `src/types-global/` |
|
|
207
|
+
| | Zod Schemas | Used for robust validation of configuration files and tool/resource inputs. | Throughout (`config`, `mcp-client`, tools, etc.) |
|
|
208
|
+
| **Error Handling** | Pattern-Based Classification | Automatically categorize errors based on message patterns. | `src/utils/internal/errorHandler.ts` |
|
|
209
|
+
| | Consistent Formatting | Standardized error responses with additional context. | `src/utils/internal/errorHandler.ts` |
|
|
210
|
+
| | Safe Try/Catch Patterns | Centralized error processing helpers (`ErrorHandler.tryCatch`). | `src/utils/internal/errorHandler.ts` |
|
|
211
|
+
| | Client/Transport Error Handling | Specific handlers for MCP client and transport error handling. | `src/mcp-client/core/`, `src/mcp-client/transports/` |
|
|
212
|
+
| **Security** | Input Validation | Using `validator` and `zod` for various data type checks. | `src/utils/security/sanitization.ts`, etc. |
|
|
213
|
+
| | Input Sanitization | Using `sanitize-html` to prevent injection attacks. | `src/utils/security/sanitization.ts` |
|
|
214
|
+
| | Sensitive Data Redaction | Automatic redaction in logs. | `src/utils/security/sanitization.ts` |
|
|
215
|
+
| | Configuration Validation | Throws a descriptive error if the primary client config (`mcp-config.json`) is missing, preventing fallback to a potentially insecure example file. | `src/mcp-client/client-config/configLoader.ts` |
|
|
216
|
+
| **Scripts** | Clean Script | Removes `dist` and `logs` directories (or custom targets). | `scripts/clean.ts` |
|
|
217
|
+
| | Make Executable Script | Sets executable permissions (`chmod +x`) on specified files (Unix-like only). | `scripts/make-executable.ts` |
|
|
218
|
+
| | Tree Script | Generates a directory structure tree, respecting `.gitignore`. | `scripts/tree.ts` |
|
|
219
|
+
| | Fetch OpenAPI Spec Script | Fetches an OpenAPI spec (YAML/JSON) from a URL with fallbacks, saves locally. | `scripts/fetch-openapi-spec.ts` |
|
|
220
220
|
|
|
221
221
|
---
|
|
222
222
|
|
package/dist/config/index.d.ts
CHANGED
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
* Aggregates settings from validated environment variables and `package.json`.
|
|
20
20
|
*/
|
|
21
21
|
export declare const config: {
|
|
22
|
+
/** Information from package.json. */
|
|
23
|
+
pkg: {
|
|
24
|
+
name: string;
|
|
25
|
+
version: string;
|
|
26
|
+
};
|
|
22
27
|
/** MCP server name. Env `MCP_SERVER_NAME` > `package.json` name > "mcp-ts-template". */
|
|
23
28
|
mcpServerName: string;
|
|
24
29
|
/** MCP server version. Env `MCP_SERVER_VERSION` > `package.json` version > "0.0.0". */
|
|
@@ -66,6 +71,12 @@ export declare const config: {
|
|
|
66
71
|
serviceDocumentationUrl: string | undefined;
|
|
67
72
|
defaultClientRedirectUris: string[] | undefined;
|
|
68
73
|
} | undefined;
|
|
74
|
+
/** Supabase configuration. Undefined if no related env vars are set. */
|
|
75
|
+
supabase: {
|
|
76
|
+
url: string;
|
|
77
|
+
anonKey: string;
|
|
78
|
+
serviceRoleKey: string | undefined;
|
|
79
|
+
} | undefined;
|
|
69
80
|
};
|
|
70
81
|
/**
|
|
71
82
|
* Configured logging level for the application.
|
package/dist/config/index.js
CHANGED
|
@@ -145,6 +145,12 @@ const EnvSchema = z.object({
|
|
|
145
145
|
.optional(),
|
|
146
146
|
/** Optional. Comma-separated default OAuth client redirect URIs. */
|
|
147
147
|
OAUTH_PROXY_DEFAULT_CLIENT_REDIRECT_URIS: z.string().optional(),
|
|
148
|
+
/** Supabase Project URL. From `SUPABASE_URL`. */
|
|
149
|
+
SUPABASE_URL: z.string().url("SUPABASE_URL must be a valid URL.").optional(),
|
|
150
|
+
/** Supabase Anon Key (public). From `SUPABASE_ANON_KEY`. */
|
|
151
|
+
SUPABASE_ANON_KEY: z.string().optional(),
|
|
152
|
+
/** Supabase Service Role Key (secret). From `SUPABASE_SERVICE_ROLE_KEY`. */
|
|
153
|
+
SUPABASE_SERVICE_ROLE_KEY: z.string().optional(),
|
|
148
154
|
});
|
|
149
155
|
const parsedEnv = EnvSchema.safeParse(process.env);
|
|
150
156
|
if (!parsedEnv.success) {
|
|
@@ -223,6 +229,8 @@ if (!validatedLogsPath) {
|
|
|
223
229
|
* Aggregates settings from validated environment variables and `package.json`.
|
|
224
230
|
*/
|
|
225
231
|
export const config = {
|
|
232
|
+
/** Information from package.json. */
|
|
233
|
+
pkg,
|
|
226
234
|
/** MCP server name. Env `MCP_SERVER_NAME` > `package.json` name > "mcp-ts-template". */
|
|
227
235
|
mcpServerName: env.MCP_SERVER_NAME || pkg.name,
|
|
228
236
|
/** MCP server version. Env `MCP_SERVER_VERSION` > `package.json` version > "0.0.0". */
|
|
@@ -281,6 +289,14 @@ export const config = {
|
|
|
281
289
|
.filter(Boolean),
|
|
282
290
|
}
|
|
283
291
|
: undefined,
|
|
292
|
+
/** Supabase configuration. Undefined if no related env vars are set. */
|
|
293
|
+
supabase: env.SUPABASE_URL && env.SUPABASE_ANON_KEY
|
|
294
|
+
? {
|
|
295
|
+
url: env.SUPABASE_URL,
|
|
296
|
+
anonKey: env.SUPABASE_ANON_KEY,
|
|
297
|
+
serviceRoleKey: env.SUPABASE_SERVICE_ROLE_KEY,
|
|
298
|
+
}
|
|
299
|
+
: undefined,
|
|
284
300
|
};
|
|
285
301
|
/**
|
|
286
302
|
* Configured logging level for the application.
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* - Transports: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx
|
|
14
14
|
* @module src/mcp-server/server
|
|
15
15
|
*/
|
|
16
|
+
import { ServerType } from "@hono/node-server";
|
|
16
17
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
17
|
-
import http from "http";
|
|
18
18
|
/**
|
|
19
19
|
* Main application entry point. Initializes and starts the MCP server.
|
|
20
20
|
* Orchestrates server startup, transport selection, and top-level error handling.
|
|
@@ -26,4 +26,4 @@ import http from "http";
|
|
|
26
26
|
* @returns For 'stdio', resolves with `McpServer`. For 'http', resolves with `http.Server`.
|
|
27
27
|
* Rejects on critical failure, leading to process exit.
|
|
28
28
|
*/
|
|
29
|
-
export declare function initializeAndStartServer(): Promise<void | McpServer |
|
|
29
|
+
export declare function initializeAndStartServer(): Promise<void | McpServer | ServerType>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT).
|
|
2
|
+
* @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT) for Hono.
|
|
3
3
|
*
|
|
4
4
|
* This middleware validates JSON Web Tokens (JWT) passed via the 'Authorization' header
|
|
5
5
|
* using the 'Bearer' scheme (e.g., "Authorization: Bearer <your_token>").
|
|
@@ -7,23 +7,31 @@
|
|
|
7
7
|
* in the configuration (`config.mcpAuthSecretKey`).
|
|
8
8
|
*
|
|
9
9
|
* If the token is valid, an object conforming to the MCP SDK's `AuthInfo` type
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* is attached to `c.env.incoming.auth`. This direct attachment to the raw Node.js
|
|
11
|
+
* request object is for compatibility with the underlying SDK transport, which is
|
|
12
|
+
* not Hono-context-aware.
|
|
13
|
+
* If the token is missing, invalid, or expired, it returns an HTTP 401 Unauthorized response.
|
|
12
14
|
*
|
|
13
15
|
* @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
|
|
14
16
|
* @module src/mcp-server/transports/authentication/authMiddleware
|
|
15
17
|
*/
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
18
|
+
import { HttpBindings } from "@hono/node-server";
|
|
19
|
+
import { type AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
|
|
20
|
+
import { Context, Next } from "hono";
|
|
21
|
+
export type { AuthInfo };
|
|
22
|
+
declare module "http" {
|
|
23
|
+
interface IncomingMessage {
|
|
24
|
+
auth?: AuthInfo;
|
|
24
25
|
}
|
|
25
26
|
}
|
|
26
27
|
/**
|
|
27
|
-
*
|
|
28
|
+
* Hono middleware for verifying JWT Bearer token authentication.
|
|
29
|
+
* It attaches authentication info to `c.env.incoming.auth` for SDK compatibility with the node server.
|
|
28
30
|
*/
|
|
29
|
-
export declare function mcpAuthMiddleware(
|
|
31
|
+
export declare function mcpAuthMiddleware(c: Context<{
|
|
32
|
+
Bindings: HttpBindings;
|
|
33
|
+
}>, next: Next): Promise<void | (Response & import("hono").TypedResponse<{
|
|
34
|
+
error: string;
|
|
35
|
+
}, 500, "json">) | (Response & import("hono").TypedResponse<{
|
|
36
|
+
error: string;
|
|
37
|
+
}, 401, "json">)>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT).
|
|
2
|
+
* @fileoverview MCP Authentication Middleware for Bearer Token Validation (JWT) for Hono.
|
|
3
3
|
*
|
|
4
4
|
* This middleware validates JSON Web Tokens (JWT) passed via the 'Authorization' header
|
|
5
5
|
* using the 'Bearer' scheme (e.g., "Authorization: Bearer <your_token>").
|
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* in the configuration (`config.mcpAuthSecretKey`).
|
|
8
8
|
*
|
|
9
9
|
* If the token is valid, an object conforming to the MCP SDK's `AuthInfo` type
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* is attached to `c.env.incoming.auth`. This direct attachment to the raw Node.js
|
|
11
|
+
* request object is for compatibility with the underlying SDK transport, which is
|
|
12
|
+
* not Hono-context-aware.
|
|
13
|
+
* If the token is missing, invalid, or expired, it returns an HTTP 401 Unauthorized response.
|
|
12
14
|
*
|
|
13
15
|
* @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
|
|
14
16
|
* @module src/mcp-server/transports/authentication/authMiddleware
|
|
@@ -25,67 +27,56 @@ else if (!config.mcpAuthSecretKey) {
|
|
|
25
27
|
logger.warning("MCP_AUTH_SECRET_KEY is not set. Authentication middleware will bypass checks (DEVELOPMENT ONLY). This is insecure for production.");
|
|
26
28
|
}
|
|
27
29
|
/**
|
|
28
|
-
*
|
|
30
|
+
* Hono middleware for verifying JWT Bearer token authentication.
|
|
31
|
+
* It attaches authentication info to `c.env.incoming.auth` for SDK compatibility with the node server.
|
|
29
32
|
*/
|
|
30
|
-
export function mcpAuthMiddleware(
|
|
33
|
+
export async function mcpAuthMiddleware(c, next) {
|
|
31
34
|
const context = requestContextService.createRequestContext({
|
|
32
35
|
operation: "mcpAuthMiddleware",
|
|
33
|
-
method: req.method,
|
|
34
|
-
path: req.path,
|
|
36
|
+
method: c.req.method,
|
|
37
|
+
path: c.req.path,
|
|
35
38
|
});
|
|
36
39
|
logger.debug("Running MCP Authentication Middleware (Bearer Token Validation)...", context);
|
|
40
|
+
const reqWithAuth = c.env.incoming;
|
|
37
41
|
// Development Mode Bypass
|
|
38
42
|
if (!config.mcpAuthSecretKey) {
|
|
39
43
|
if (environment !== "production") {
|
|
40
44
|
logger.warning("Bypassing JWT authentication: MCP_AUTH_SECRET_KEY is not set (DEVELOPMENT ONLY).", context);
|
|
41
|
-
|
|
42
|
-
req.auth = {
|
|
45
|
+
reqWithAuth.auth = {
|
|
43
46
|
token: "dev-mode-placeholder-token",
|
|
44
47
|
clientId: "dev-client-id",
|
|
45
48
|
scopes: ["dev-scope"],
|
|
46
49
|
};
|
|
47
|
-
// Log dev mode details separately, not attaching to req.auth if not part of AuthInfo
|
|
48
50
|
logger.debug("Dev mode auth object created.", {
|
|
49
51
|
...context,
|
|
50
|
-
authDetails:
|
|
52
|
+
authDetails: reqWithAuth.auth,
|
|
51
53
|
});
|
|
52
|
-
return next();
|
|
54
|
+
return await next();
|
|
53
55
|
}
|
|
54
56
|
else {
|
|
55
57
|
logger.error("FATAL: MCP_AUTH_SECRET_KEY is missing in production. Cannot bypass auth.", context);
|
|
56
|
-
|
|
57
|
-
error: "Server configuration error: Authentication key missing.",
|
|
58
|
-
});
|
|
59
|
-
return;
|
|
58
|
+
return c.json({ error: "Server configuration error: Authentication key missing." }, 500);
|
|
60
59
|
}
|
|
61
60
|
}
|
|
62
|
-
const authHeader = req.
|
|
61
|
+
const authHeader = c.req.header("Authorization");
|
|
63
62
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
64
63
|
logger.warning("Authentication failed: Missing or malformed Authorization header (Bearer scheme required).", context);
|
|
65
|
-
|
|
64
|
+
return c.json({
|
|
66
65
|
error: "Unauthorized: Missing or invalid authentication token format.",
|
|
67
|
-
});
|
|
68
|
-
return;
|
|
66
|
+
}, 401);
|
|
69
67
|
}
|
|
70
68
|
const tokenParts = authHeader.split(" ");
|
|
71
69
|
if (tokenParts.length !== 2 || tokenParts[0] !== "Bearer" || !tokenParts[1]) {
|
|
72
70
|
logger.warning("Authentication failed: Malformed Bearer token.", context);
|
|
73
|
-
|
|
74
|
-
.status(401)
|
|
75
|
-
.json({ error: "Unauthorized: Malformed authentication token." });
|
|
76
|
-
return;
|
|
71
|
+
return c.json({ error: "Unauthorized: Malformed authentication token." }, 401);
|
|
77
72
|
}
|
|
78
73
|
const rawToken = tokenParts[1];
|
|
79
74
|
try {
|
|
80
75
|
const decoded = jwt.verify(rawToken, config.mcpAuthSecretKey);
|
|
81
76
|
if (typeof decoded === "string") {
|
|
82
77
|
logger.warning("Authentication failed: JWT decoded to a string, expected an object payload.", context);
|
|
83
|
-
|
|
84
|
-
.status(401)
|
|
85
|
-
.json({ error: "Unauthorized: Invalid token payload format." });
|
|
86
|
-
return;
|
|
78
|
+
return c.json({ error: "Unauthorized: Invalid token payload format." }, 401);
|
|
87
79
|
}
|
|
88
|
-
// Extract and validate fields for SDK's AuthInfo
|
|
89
80
|
const clientIdFromToken = typeof decoded.cid === "string"
|
|
90
81
|
? decoded.cid
|
|
91
82
|
: typeof decoded.client_id === "string"
|
|
@@ -93,12 +84,9 @@ export function mcpAuthMiddleware(req, res, next) {
|
|
|
93
84
|
: undefined;
|
|
94
85
|
if (!clientIdFromToken) {
|
|
95
86
|
logger.warning("Authentication failed: JWT 'cid' or 'client_id' claim is missing or not a string.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
|
|
96
|
-
|
|
97
|
-
error: "Unauthorized: Invalid token, missing client identifier.",
|
|
98
|
-
});
|
|
99
|
-
return;
|
|
87
|
+
return c.json({ error: "Unauthorized: Invalid token, missing client identifier." }, 401);
|
|
100
88
|
}
|
|
101
|
-
let scopesFromToken;
|
|
89
|
+
let scopesFromToken = [];
|
|
102
90
|
if (Array.isArray(decoded.scp) &&
|
|
103
91
|
decoded.scp.every((s) => typeof s === "string")) {
|
|
104
92
|
scopesFromToken = decoded.scp;
|
|
@@ -107,50 +95,26 @@ export function mcpAuthMiddleware(req, res, next) {
|
|
|
107
95
|
decoded.scope.trim() !== "") {
|
|
108
96
|
scopesFromToken = decoded.scope.split(" ").filter((s) => s);
|
|
109
97
|
if (scopesFromToken.length === 0 && decoded.scope.trim() !== "") {
|
|
110
|
-
// handles case " " -> [""]
|
|
111
98
|
scopesFromToken = [decoded.scope.trim()];
|
|
112
99
|
}
|
|
113
|
-
else if (scopesFromToken.length === 0 && decoded.scope.trim() === "") {
|
|
114
|
-
// If scope is an empty string, treat as no scopes.
|
|
115
|
-
// This will now lead to an error if scopes are considered mandatory.
|
|
116
|
-
logger.debug("JWT 'scope' claim was an empty string, resulting in empty scopes array.", context);
|
|
117
|
-
}
|
|
118
100
|
}
|
|
119
|
-
else {
|
|
120
|
-
// If scopes are strictly mandatory and not found or invalid format
|
|
121
|
-
logger.warning("Authentication failed: JWT 'scp' or 'scope' claim is missing, not an array of strings, or not a valid space-separated string.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
|
|
122
|
-
res.status(401).json({
|
|
123
|
-
error: "Unauthorized: Invalid token, missing or invalid scopes.",
|
|
124
|
-
});
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
// If, after parsing, scopesFromToken is empty and scopes are considered mandatory for any operation.
|
|
128
|
-
// This check assumes that all valid tokens must have at least one scope.
|
|
129
|
-
// If some tokens are legitimately allowed to have no scopes for certain operations,
|
|
130
|
-
// this check might need to be adjusted or handled downstream.
|
|
131
101
|
if (scopesFromToken.length === 0) {
|
|
132
102
|
logger.warning("Authentication failed: Token resulted in an empty scope array, and scopes are required.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
|
|
133
|
-
|
|
134
|
-
error: "Unauthorized: Token must contain valid, non-empty scopes.",
|
|
135
|
-
});
|
|
136
|
-
return;
|
|
103
|
+
return c.json({ error: "Unauthorized: Token must contain valid, non-empty scopes." }, 401);
|
|
137
104
|
}
|
|
138
|
-
|
|
139
|
-
// All other claims from 'decoded' are not part of req.auth for type safety.
|
|
140
|
-
req.auth = {
|
|
105
|
+
reqWithAuth.auth = {
|
|
141
106
|
token: rawToken,
|
|
142
107
|
clientId: clientIdFromToken,
|
|
143
108
|
scopes: scopesFromToken,
|
|
144
109
|
};
|
|
145
|
-
// Log separately if other JWT claims like 'sub' (sessionId) are needed for app logic
|
|
146
110
|
const subClaimForLogging = typeof decoded.sub === "string" ? decoded.sub : undefined;
|
|
147
111
|
logger.debug("JWT verified successfully. AuthInfo attached to request.", {
|
|
148
112
|
...context,
|
|
149
113
|
mcpSessionIdContext: subClaimForLogging,
|
|
150
|
-
clientId:
|
|
151
|
-
scopes:
|
|
114
|
+
clientId: reqWithAuth.auth.clientId,
|
|
115
|
+
scopes: reqWithAuth.auth.scopes,
|
|
152
116
|
});
|
|
153
|
-
next();
|
|
117
|
+
await next();
|
|
154
118
|
}
|
|
155
119
|
catch (error) {
|
|
156
120
|
let errorMessage = "Invalid token";
|
|
@@ -173,6 +137,6 @@ export function mcpAuthMiddleware(req, res, next) {
|
|
|
173
137
|
errorMessage = "Unknown verification error";
|
|
174
138
|
logger.error("Authentication failed: Unexpected non-error exception during token verification.", { ...context, error });
|
|
175
139
|
}
|
|
176
|
-
|
|
140
|
+
return c.json({ error: `Unauthorized: ${errorMessage}.` }, 401);
|
|
177
141
|
}
|
|
178
142
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Handles the setup and management of the Streamable HTTP MCP transport.
|
|
2
|
+
* @fileoverview Handles the setup and management of the Streamable HTTP MCP transport using Hono.
|
|
3
3
|
* Implements the MCP Specification 2025-03-26 for Streamable HTTP.
|
|
4
|
-
* This includes creating
|
|
4
|
+
* This includes creating a Hono server, configuring middleware (CORS, Authentication),
|
|
5
5
|
* defining request routing for the single MCP endpoint (POST/GET/DELETE),
|
|
6
6
|
* managing server-side sessions, handling Server-Sent Events (SSE) for streaming,
|
|
7
7
|
* and binding to a network port with retry logic for port conflicts.
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx#streamable-http
|
|
11
11
|
* @module src/mcp-server/transports/httpTransport
|
|
12
12
|
*/
|
|
13
|
+
import { ServerType } from "@hono/node-server";
|
|
13
14
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
|
-
import http from "http";
|
|
15
15
|
import { RequestContext } from "../../utils/index.js";
|
|
16
16
|
/**
|
|
17
17
|
* Sets up and starts the Streamable HTTP transport layer for the MCP server.
|
|
@@ -21,4 +21,4 @@ import { RequestContext } from "../../utils/index.js";
|
|
|
21
21
|
* @returns A promise that resolves with the Node.js `http.Server` instance when the HTTP server is successfully listening.
|
|
22
22
|
* @throws {Error} If the server fails to start after all port retries.
|
|
23
23
|
*/
|
|
24
|
-
export declare function startHttpTransport(createServerInstanceFn: () => Promise<McpServer>, parentContext: RequestContext): Promise<
|
|
24
|
+
export declare function startHttpTransport(createServerInstanceFn: () => Promise<McpServer>, parentContext: RequestContext): Promise<ServerType>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Handles the setup and management of the Streamable HTTP MCP transport.
|
|
2
|
+
* @fileoverview Handles the setup and management of the Streamable HTTP MCP transport using Hono.
|
|
3
3
|
* Implements the MCP Specification 2025-03-26 for Streamable HTTP.
|
|
4
|
-
* This includes creating
|
|
4
|
+
* This includes creating a Hono server, configuring middleware (CORS, Authentication),
|
|
5
5
|
* defining request routing for the single MCP endpoint (POST/GET/DELETE),
|
|
6
6
|
* managing server-side sessions, handling Server-Sent Events (SSE) for streaming,
|
|
7
7
|
* and binding to a network port with retry logic for port conflicts.
|
|
@@ -10,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
|
|
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";
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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(
|
|
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 =
|
|
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
|
|
245
|
-
app.use(
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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,
|
|
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);
|
|
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 &&
|
|
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
|
-
|
|
236
|
+
return c.json({
|
|
271
237
|
jsonrpc: "2.0",
|
|
272
|
-
error: { code: -32000, message: "Too Many Requests" },
|
|
273
|
-
id: req.
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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.
|
|
338
|
-
bodyPreview: JSON.stringify(
|
|
263
|
+
headers: c.req.header(),
|
|
264
|
+
bodyPreview: JSON.stringify(body).substring(0, 100),
|
|
339
265
|
});
|
|
340
|
-
const sessionId = req.
|
|
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(
|
|
276
|
+
const isInitReq = isInitializeRequest(body);
|
|
351
277
|
logger.debug(`Is InitializeRequest: ${isInitReq}`, {
|
|
352
278
|
...basePostContext,
|
|
353
279
|
sessionId,
|
|
354
280
|
});
|
|
355
|
-
const requestId =
|
|
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
|
-
|
|
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(
|
|
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 (
|
|
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.
|
|
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.
|
|
383
|
+
headers: c.req.header(),
|
|
460
384
|
});
|
|
461
|
-
const sessionId = req.
|
|
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
|
-
|
|
400
|
+
return c.json({
|
|
477
401
|
jsonrpc: "2.0",
|
|
478
402
|
error: { code: -32004, message: "Session not found or expired" },
|
|
479
|
-
id: null,
|
|
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(
|
|
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
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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
|
|
511
|
-
|
|
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;
|
|
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 = (
|
|
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.
|
|
3
|
+
"version": "1.4.8",
|
|
5
4
|
"description": "TypeScript template for building Model Context Protocol (MCP) Servers & Clients. Features extensive utilities (logger, requestContext, etc.), STDIO & Streamable HTTP (with authMiddleware), examples, and type safety. Ideal starting point for creating production-ready MCP Servers & Clients.",
|
|
6
5
|
"main": "dist/index.js",
|
|
7
6
|
"files": [
|
|
@@ -31,25 +30,30 @@
|
|
|
31
30
|
"tree": "ts-node --esm scripts/tree.ts",
|
|
32
31
|
"fetch-spec": "ts-node --esm scripts/fetch-openapi-spec.ts",
|
|
33
32
|
"format": "prettier --write \"**/*.{ts,js,json,md,html,css}\"",
|
|
34
|
-
"inspector": "mcp-inspector --config mcp.json --server mcp-ts-template",
|
|
35
|
-
"db:
|
|
33
|
+
"inspector": "npx mcp-inspector --config mcp.json --server mcp-ts-template",
|
|
34
|
+
"db:duckdb-example": "MCP_LOG_LEVEL=debug tsc && node dist/storage/duckdbExample.js"
|
|
36
35
|
},
|
|
37
36
|
"dependencies": {
|
|
38
37
|
"@duckdb/node-api": "^1.3.0-alpha.21",
|
|
38
|
+
"@hono/node-server": "^1.14.3",
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.12.1",
|
|
40
|
+
"@node-oauth/oauth2-server": "^5.2.0",
|
|
41
|
+
"@supabase/supabase-js": "^2.49.10",
|
|
40
42
|
"@types/jsonwebtoken": "^9.0.9",
|
|
41
|
-
"@types/node": "^22.15.
|
|
43
|
+
"@types/node": "^22.15.30",
|
|
42
44
|
"@types/sanitize-html": "^2.16.0",
|
|
43
45
|
"@types/validator": "13.15.1",
|
|
46
|
+
"bcryptjs": "^3.0.2",
|
|
44
47
|
"chalk": "^5.4.1",
|
|
45
48
|
"chrono-node": "^2.8.0",
|
|
46
49
|
"cli-table3": "^0.6.5",
|
|
47
50
|
"dotenv": "^16.5.0",
|
|
48
|
-
"
|
|
51
|
+
"hono": "^4.7.11",
|
|
49
52
|
"ignore": "^7.0.5",
|
|
50
53
|
"jsonwebtoken": "^9.0.2",
|
|
51
|
-
"openai": "^5.1.
|
|
54
|
+
"openai": "^5.1.1",
|
|
52
55
|
"partial-json": "^0.1.7",
|
|
56
|
+
"pg": "^8.16.0",
|
|
53
57
|
"sanitize-html": "^2.17.0",
|
|
54
58
|
"tiktoken": "^1.0.21",
|
|
55
59
|
"ts-node": "^10.9.2",
|
|
@@ -90,12 +94,14 @@
|
|
|
90
94
|
"node": ">=20.0.0"
|
|
91
95
|
},
|
|
92
96
|
"devDependencies": {
|
|
93
|
-
"@types/
|
|
97
|
+
"@types/bcryptjs": "^3.0.0",
|
|
94
98
|
"@types/js-yaml": "^4.0.9",
|
|
95
99
|
"@types/node-fetch": "^2.6.12",
|
|
100
|
+
"@types/pg": "^8.15.4",
|
|
96
101
|
"axios": "^1.9.0",
|
|
97
102
|
"js-yaml": "^4.1.0",
|
|
98
103
|
"prettier": "^3.5.3",
|
|
104
|
+
"supabase": "^2.24.3",
|
|
99
105
|
"typedoc": "^0.28.5"
|
|
100
106
|
}
|
|
101
107
|
}
|