fa-mcp-sdk 0.4.142 → 0.11.2
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 +5 -0
- package/cli-template/.dockerignore +16 -0
- package/cli-template/.gitlab-ci.yml +135 -0
- package/cli-template/AGENTS.md +1 -0
- package/cli-template/CHANGELOG.md +64 -0
- package/cli-template/FA-MCP-SDK-DOC/00-FA-MCP-SDK-index.md +27 -4
- package/cli-template/FA-MCP-SDK-DOC/02-1-tools-and-api.md +195 -0
- package/cli-template/FA-MCP-SDK-DOC/02-2-prompts-and-resources.md +172 -9
- package/cli-template/FA-MCP-SDK-DOC/03-configuration.md +170 -12
- package/cli-template/FA-MCP-SDK-DOC/04-authentication.md +158 -8
- package/cli-template/FA-MCP-SDK-DOC/06-utilities.md +67 -6
- package/cli-template/FA-MCP-SDK-DOC/07-testing-and-operations.md +31 -15
- package/cli-template/FA-MCP-SDK-DOC/10-mcp-apps.md +1 -1
- package/cli-template/FA-MCP-SDK-DOC/11-public-contract.md +342 -0
- package/cli-template/README.md +37 -0
- package/cli-template/deploy/docker/.env.example +10 -0
- package/cli-template/deploy/docker/Dockerfile +44 -0
- package/cli-template/deploy/docker/Dockerfile.local +29 -0
- package/cli-template/deploy/docker/README.md +94 -0
- package/cli-template/deploy/docker/config/local.docker.yaml +14 -0
- package/cli-template/deploy/docker/docker-compose.yml +31 -0
- package/cli-template/deploy/gitlab-runner/.env.example +16 -0
- package/cli-template/deploy/gitlab-runner/README.md +65 -0
- package/cli-template/deploy/gitlab-runner/config/config.toml.template +26 -0
- package/cli-template/deploy/gitlab-runner/docker-compose.yml +39 -0
- package/cli-template/deploy/gitlab-runner/entrypoint.sh +27 -0
- package/cli-template/deploy/gitlab-runner/start.sh +47 -0
- package/cli-template/gitignore +96 -95
- package/cli-template/package.json +1 -1
- package/config/_local.yaml +73 -11
- package/config/custom-environment-variables.yaml +102 -0
- package/config/default.yaml +164 -11
- package/config/local.yaml +20 -19
- package/dist/core/_types_/config.d.ts +119 -0
- package/dist/core/_types_/config.d.ts.map +1 -1
- package/dist/core/_types_/types.d.ts +137 -4
- package/dist/core/_types_/types.d.ts.map +1 -1
- package/dist/core/agent-tester/agent-tester-router.d.ts.map +1 -1
- package/dist/core/agent-tester/agent-tester-router.js +25 -11
- package/dist/core/agent-tester/agent-tester-router.js.map +1 -1
- package/dist/core/agent-tester/services/TesterMcpClientService.d.ts.map +1 -1
- package/dist/core/agent-tester/services/TesterMcpClientService.js +6 -4
- package/dist/core/agent-tester/services/TesterMcpClientService.js.map +1 -1
- package/dist/core/auth/admin-auth.js +4 -4
- package/dist/core/auth/admin-auth.js.map +1 -1
- package/dist/core/auth/agent-tester-auth.d.ts +1 -1
- package/dist/core/auth/agent-tester-auth.d.ts.map +1 -1
- package/dist/core/auth/agent-tester-auth.js +8 -4
- package/dist/core/auth/agent-tester-auth.js.map +1 -1
- package/dist/core/auth/auth-profile.d.ts +38 -0
- package/dist/core/auth/auth-profile.d.ts.map +1 -0
- package/dist/core/auth/auth-profile.js +101 -0
- package/dist/core/auth/auth-profile.js.map +1 -0
- package/dist/core/auth/jwt-v2.d.ts +27 -0
- package/dist/core/auth/jwt-v2.d.ts.map +1 -0
- package/dist/core/auth/jwt-v2.js +180 -0
- package/dist/core/auth/jwt-v2.js.map +1 -0
- package/dist/core/auth/jwt.d.ts +27 -13
- package/dist/core/auth/jwt.d.ts.map +1 -1
- package/dist/core/auth/jwt.js +36 -13
- package/dist/core/auth/jwt.js.map +1 -1
- package/dist/core/auth/key-resolver.d.ts +74 -0
- package/dist/core/auth/key-resolver.d.ts.map +1 -0
- package/dist/core/auth/key-resolver.js +330 -0
- package/dist/core/auth/key-resolver.js.map +1 -0
- package/dist/core/auth/middleware.d.ts.map +1 -1
- package/dist/core/auth/middleware.js +66 -0
- package/dist/core/auth/middleware.js.map +1 -1
- package/dist/core/auth/multi-auth.d.ts +1 -1
- package/dist/core/auth/multi-auth.d.ts.map +1 -1
- package/dist/core/auth/multi-auth.js +7 -7
- package/dist/core/auth/multi-auth.js.map +1 -1
- package/dist/core/auth/token-generator/server.js +4 -4
- package/dist/core/auth/token-generator/server.js.map +1 -1
- package/dist/core/auth/types.d.ts +5 -0
- package/dist/core/auth/types.d.ts.map +1 -1
- package/dist/core/db/pg-db.d.ts +7 -0
- package/dist/core/db/pg-db.d.ts.map +1 -1
- package/dist/core/db/pg-db.js +54 -3
- package/dist/core/db/pg-db.js.map +1 -1
- package/dist/core/errors/BaseMcpError.d.ts +21 -1
- package/dist/core/errors/BaseMcpError.d.ts.map +1 -1
- package/dist/core/errors/BaseMcpError.js +20 -1
- package/dist/core/errors/BaseMcpError.js.map +1 -1
- package/dist/core/errors/ValidationError.d.ts +5 -0
- package/dist/core/errors/ValidationError.d.ts.map +1 -1
- package/dist/core/errors/ValidationError.js +6 -1
- package/dist/core/errors/ValidationError.js.map +1 -1
- package/dist/core/errors/errors.d.ts +31 -3
- package/dist/core/errors/errors.d.ts.map +1 -1
- package/dist/core/errors/errors.js +86 -6
- package/dist/core/errors/errors.js.map +1 -1
- package/dist/core/errors/specific-errors.d.ts +54 -0
- package/dist/core/errors/specific-errors.d.ts.map +1 -0
- package/dist/core/errors/specific-errors.js +82 -0
- package/dist/core/errors/specific-errors.js.map +1 -0
- package/dist/core/index.d.ts +10 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +9 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/init-mcp-server.d.ts.map +1 -1
- package/dist/core/init-mcp-server.js +39 -0
- package/dist/core/init-mcp-server.js.map +1 -1
- package/dist/core/mcp/create-mcp-server.d.ts +12 -6
- package/dist/core/mcp/create-mcp-server.d.ts.map +1 -1
- package/dist/core/mcp/create-mcp-server.js +592 -33
- package/dist/core/mcp/create-mcp-server.js.map +1 -1
- package/dist/core/mcp/debug-trace.d.ts +3 -1
- package/dist/core/mcp/debug-trace.d.ts.map +1 -1
- package/dist/core/mcp/debug-trace.js +17 -2
- package/dist/core/mcp/debug-trace.js.map +1 -1
- package/dist/core/mcp/deprecation.d.ts +31 -0
- package/dist/core/mcp/deprecation.d.ts.map +1 -0
- package/dist/core/mcp/deprecation.js +96 -0
- package/dist/core/mcp/deprecation.js.map +1 -0
- package/dist/core/mcp/mcp-logging.d.ts +32 -0
- package/dist/core/mcp/mcp-logging.d.ts.map +1 -0
- package/dist/core/mcp/mcp-logging.js +97 -0
- package/dist/core/mcp/mcp-logging.js.map +1 -0
- package/dist/core/mcp/pagination.d.ts +13 -0
- package/dist/core/mcp/pagination.d.ts.map +1 -0
- package/dist/core/mcp/pagination.js +50 -0
- package/dist/core/mcp/pagination.js.map +1 -0
- package/dist/core/mcp/prompts.d.ts +5 -1
- package/dist/core/mcp/prompts.d.ts.map +1 -1
- package/dist/core/mcp/prompts.js +3 -1
- package/dist/core/mcp/prompts.js.map +1 -1
- package/dist/core/mcp/resources.d.ts +9 -0
- package/dist/core/mcp/resources.d.ts.map +1 -1
- package/dist/core/mcp/resources.js +158 -11
- package/dist/core/mcp/resources.js.map +1 -1
- package/dist/core/mcp/server-stdio.d.ts +7 -1
- package/dist/core/mcp/server-stdio.d.ts.map +1 -1
- package/dist/core/mcp/server-stdio.js +8 -3
- package/dist/core/mcp/server-stdio.js.map +1 -1
- package/dist/core/mcp/task-store.d.ts +97 -0
- package/dist/core/mcp/task-store.d.ts.map +1 -0
- package/dist/core/mcp/task-store.js +175 -0
- package/dist/core/mcp/task-store.js.map +1 -0
- package/dist/core/mcp/tool-limits.d.ts +22 -0
- package/dist/core/mcp/tool-limits.d.ts.map +1 -0
- package/dist/core/mcp/tool-limits.js +115 -0
- package/dist/core/mcp/tool-limits.js.map +1 -0
- package/dist/core/mcp/validate-tool-args.d.ts +16 -0
- package/dist/core/mcp/validate-tool-args.d.ts.map +1 -0
- package/dist/core/mcp/validate-tool-args.js +67 -0
- package/dist/core/mcp/validate-tool-args.js.map +1 -0
- package/dist/core/mcp/validate-tool-names.d.ts +11 -0
- package/dist/core/mcp/validate-tool-names.d.ts.map +1 -0
- package/dist/core/mcp/validate-tool-names.js +23 -0
- package/dist/core/mcp/validate-tool-names.js.map +1 -0
- package/dist/core/metrics/metrics.d.ts +45 -0
- package/dist/core/metrics/metrics.d.ts.map +1 -0
- package/dist/core/metrics/metrics.js +119 -0
- package/dist/core/metrics/metrics.js.map +1 -0
- package/dist/core/utils/mask-sensitive.d.ts +44 -0
- package/dist/core/utils/mask-sensitive.d.ts.map +1 -0
- package/dist/core/utils/mask-sensitive.js +64 -0
- package/dist/core/utils/mask-sensitive.js.map +1 -0
- package/dist/core/utils/testing/McpHttpClient.d.ts +8 -33
- package/dist/core/utils/testing/McpHttpClient.d.ts.map +1 -1
- package/dist/core/utils/testing/McpHttpClient.js +8 -74
- package/dist/core/utils/testing/McpHttpClient.js.map +1 -1
- package/dist/core/utils/testing/McpStreamableHttpClient.d.ts +24 -30
- package/dist/core/utils/testing/McpStreamableHttpClient.d.ts.map +1 -1
- package/dist/core/utils/testing/McpStreamableHttpClient.js +36 -198
- package/dist/core/utils/testing/McpStreamableHttpClient.js.map +1 -1
- package/dist/core/utils/utils.d.ts.map +1 -1
- package/dist/core/utils/utils.js +2 -0
- package/dist/core/utils/utils.js.map +1 -1
- package/dist/core/web/admin-router.js +3 -3
- package/dist/core/web/admin-router.js.map +1 -1
- package/dist/core/web/cors.d.ts +9 -1
- package/dist/core/web/cors.d.ts.map +1 -1
- package/dist/core/web/cors.js +26 -5
- package/dist/core/web/cors.js.map +1 -1
- package/dist/core/web/event-store.d.ts +33 -0
- package/dist/core/web/event-store.d.ts.map +1 -0
- package/dist/core/web/event-store.js +65 -0
- package/dist/core/web/event-store.js.map +1 -0
- package/dist/core/web/oauth-router.d.ts +37 -0
- package/dist/core/web/oauth-router.d.ts.map +1 -0
- package/dist/core/web/oauth-router.js +207 -0
- package/dist/core/web/oauth-router.js.map +1 -0
- package/dist/core/web/request-id.d.ts +44 -0
- package/dist/core/web/request-id.d.ts.map +1 -0
- package/dist/core/web/request-id.js +82 -0
- package/dist/core/web/request-id.js.map +1 -0
- package/dist/core/web/server-http.d.ts.map +1 -1
- package/dist/core/web/server-http.js +322 -182
- package/dist/core/web/server-http.js.map +1 -1
- package/package.json +15 -2
- package/scripts/claude-2-agents-symlink.js +10 -1
- package/scripts/generate-jwt.js +129 -51
- package/src/template/custom-resources.ts +14 -0
- package/src/template/prompts/custom-prompts.ts +4 -0
- package/src/template/tools/handle-tool-call.ts +59 -3
- package/src/template/tools/tools.ts +92 -31
- package/src/tests/mcp/test-http.js +1 -1
- package/src/tests/mcp/test-sse.js +1 -1
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { dirname, join } from 'path';
|
|
2
3
|
import { fileURLToPath } from 'url';
|
|
3
4
|
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
|
|
4
|
-
import {
|
|
5
|
+
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
6
|
+
import { InMemoryEventStore } from './event-store.js';
|
|
7
|
+
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, isInitializeRequest, } from '@modelcontextprotocol/sdk/types.js';
|
|
5
8
|
import chalk from 'chalk';
|
|
6
9
|
import express from 'express';
|
|
7
10
|
import helmet from 'helmet';
|
|
@@ -10,24 +13,28 @@ import { createAgentTesterRouter } from '../agent-tester/agent-tester-router.js'
|
|
|
10
13
|
import { validateAdminAuthConfig } from '../auth/admin-auth.js';
|
|
11
14
|
import { createAgentTesterSessionMW } from '../auth/agent-tester-auth.js';
|
|
12
15
|
import { checkJwtToken, generateToken, MIN_ENCRYPT_KEY_LENGTH } from '../auth/jwt.js';
|
|
16
|
+
import { canLocallyIssueJwt, getJwtRuntimeConfig } from '../auth/key-resolver.js';
|
|
13
17
|
import { createAuthMW } from '../auth/middleware.js';
|
|
14
18
|
import { checkPermanentToken } from '../auth/permanent.js';
|
|
15
19
|
import { appConfig, getProjectData } from '../bootstrap/init-config.js';
|
|
16
|
-
import { debugMcpNotification } from '../debug.js';
|
|
17
20
|
import { getMainDBConnectionStatus } from '../db/pg-db.js';
|
|
18
|
-
import { BaseMcpError } from '../errors/BaseMcpError.js';
|
|
19
21
|
import { createJsonRpcErrorResponse, ServerError, toError, toStr } from '../errors/errors.js';
|
|
22
|
+
import { PayloadTooLargeError, RateLimitedError, ResourceNotFoundError, TimeoutError, } from '../errors/specific-errors.js';
|
|
20
23
|
import { logger as lgr } from '../logger.js';
|
|
24
|
+
import { getMetrics, getMetricsRegistry, initMetrics } from '../metrics/metrics.js';
|
|
21
25
|
import { createMcpServer } from '../mcp/create-mcp-server.js';
|
|
22
|
-
import {
|
|
26
|
+
import { getPromptsList } from '../mcp/prompts.js';
|
|
23
27
|
import { getResource, getResourcesList } from '../mcp/resources.js';
|
|
28
|
+
import { truncateToolResponse, withToolTimeout } from '../mcp/tool-limits.js';
|
|
24
29
|
import { formatRateLimitError, isRateLimitError } from '../utils/rate-limit.js';
|
|
25
30
|
import { getTools, normalizeHeaders } from '../utils/utils.js';
|
|
26
31
|
import { createAdminRouter } from './admin-router.js';
|
|
27
32
|
import { applyCors } from './cors.js';
|
|
28
33
|
import { faviconSvg } from './favicon-svg.js';
|
|
29
34
|
import { handleHomeInfo } from './home-api.js';
|
|
35
|
+
import { createOAuthRouter } from './oauth-router.js';
|
|
30
36
|
import { configureOpenAPI, createSwaggerUIAssetsMiddleware } from './openapi.js';
|
|
37
|
+
import { requestIdMW } from './request-id.js';
|
|
31
38
|
import { createSvgRouter } from './svg-icons.js';
|
|
32
39
|
const __filename = fileURLToPath(import.meta.url);
|
|
33
40
|
const __dirname = dirname(__filename);
|
|
@@ -35,6 +42,26 @@ const __dirname = dirname(__filename);
|
|
|
35
42
|
const staticPath = join(__dirname, 'static');
|
|
36
43
|
const logger = lgr.getSubLogger({ name: chalk.bgYellow('server-http') });
|
|
37
44
|
export const isAdminEnabled = appConfig.adminPanel?.enabled === true;
|
|
45
|
+
/**
|
|
46
|
+
* Standard §14 — pick the rate-limit bucket key from the request:
|
|
47
|
+
* - `scope: 'subject'` → JWT `sub`/`user` claim, falling back to req.ip when no auth payload
|
|
48
|
+
* - `scope: 'ip'` → req.ip / unknown
|
|
49
|
+
*/
|
|
50
|
+
function resolveRateLimitKey(req, suffix = '') {
|
|
51
|
+
const scope = appConfig.mcp.rateLimit?.scope ?? 'subject';
|
|
52
|
+
let key = '';
|
|
53
|
+
if (scope === 'subject') {
|
|
54
|
+
const payload = req.auth?.payload ?? req.authInfo?.payload;
|
|
55
|
+
const sub = payload?.sub ?? payload?.user ?? req.authInfo?.username;
|
|
56
|
+
if (sub && String(sub).trim()) {
|
|
57
|
+
key = `sub:${String(sub).trim().toLowerCase()}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (!key) {
|
|
61
|
+
key = `ip:${req.ip || 'unknown'}`;
|
|
62
|
+
}
|
|
63
|
+
return suffix ? `${suffix}-${key}` : key;
|
|
64
|
+
}
|
|
38
65
|
/**
|
|
39
66
|
* Handle rate limiting with consistent error response
|
|
40
67
|
*/
|
|
@@ -46,19 +73,19 @@ async function handleRateLimit(rateLimiter, clientId, ip, context = '', res, id)
|
|
|
46
73
|
if (isRateLimitError(rateLimitError)) {
|
|
47
74
|
const rateLimitMessage = formatRateLimitError(rateLimitError, appConfig.mcp.rateLimit.maxRequests);
|
|
48
75
|
logger.warn(`Rate limit exceeded${context ? ` in ${context}` : ''}: ip: ${ip}`);
|
|
76
|
+
const scope = (appConfig.mcp.rateLimit?.scope ?? 'subject');
|
|
77
|
+
getMetrics()?.rateLimitHits.inc({ scope });
|
|
78
|
+
// Standard §14 + Appendix B: HTTP 429, JSON-RPC code -32003, `Retry-After` header in
|
|
79
|
+
// seconds (also mirrored under `error.data.retryAfter` per Appendix B.3).
|
|
80
|
+
const retryAfterSec = Math.max(1, Math.ceil((rateLimitError.msBeforeNext ?? 1000) / 1000));
|
|
81
|
+
const error = new RateLimitedError(rateLimitMessage, retryAfterSec);
|
|
49
82
|
if (res) {
|
|
50
|
-
res.
|
|
51
|
-
|
|
52
|
-
id: id ?? 1,
|
|
53
|
-
error: {
|
|
54
|
-
code: -32000,
|
|
55
|
-
message: rateLimitMessage,
|
|
56
|
-
},
|
|
57
|
-
});
|
|
83
|
+
res.setHeader('Retry-After', String(retryAfterSec));
|
|
84
|
+
res.status(error.statusCode).json(createJsonRpcErrorResponse(error, id ?? null));
|
|
58
85
|
return;
|
|
59
86
|
}
|
|
60
87
|
else {
|
|
61
|
-
throw
|
|
88
|
+
throw error;
|
|
62
89
|
}
|
|
63
90
|
}
|
|
64
91
|
throw rateLimitError;
|
|
@@ -69,6 +96,11 @@ async function handleRateLimit(rateLimiter, clientId, ip, context = '', res, id)
|
|
|
69
96
|
*/
|
|
70
97
|
export async function startHttpServer() {
|
|
71
98
|
const app = express();
|
|
99
|
+
// Express `trust proxy`. Required for /.well-known/openid-configuration when the server
|
|
100
|
+
// sits behind HTTPS reverse proxy (X-Forwarded-Proto / X-Forwarded-Host).
|
|
101
|
+
if (appConfig.webServer.trustProxy !== undefined) {
|
|
102
|
+
app.set('trust proxy', appConfig.webServer.trustProxy);
|
|
103
|
+
}
|
|
72
104
|
// Initialize rate limiter
|
|
73
105
|
const rateLimiter = new RateLimiterMemory({
|
|
74
106
|
keyPrefix: appConfig.shortName,
|
|
@@ -77,15 +109,34 @@ export async function startHttpServer() {
|
|
|
77
109
|
});
|
|
78
110
|
// Create universal auth middleware for all endpoints
|
|
79
111
|
const authMW = createAuthMW();
|
|
112
|
+
// Standard §15.1 — sticky `X-Request-Id` (+ W3C traceparent/tracestate) MUST be installed
|
|
113
|
+
// before CORS, auth or any handler that may shortcut the chain — otherwise 401/403
|
|
114
|
+
// responses would land without a correlation id, breaking downstream debugging.
|
|
115
|
+
app.use(requestIdMW());
|
|
116
|
+
// Standard §15.3 — Prometheus metrics. Opt-in: enabled flag drives both `prom-client`
|
|
117
|
+
// registry initialisation and `GET /metrics` mounting below.
|
|
118
|
+
const metricsCfg = appConfig.webServer.metrics;
|
|
119
|
+
const metricsEnabled = metricsCfg?.enabled === true;
|
|
120
|
+
if (metricsEnabled) {
|
|
121
|
+
initMetrics();
|
|
122
|
+
}
|
|
80
123
|
// Security middleware
|
|
81
124
|
app.use(helmet({
|
|
82
125
|
contentSecurityPolicy: false, // Allow for SSE
|
|
83
126
|
crossOriginEmbedderPolicy: false,
|
|
84
127
|
}));
|
|
85
|
-
// JSON parsing
|
|
86
|
-
|
|
87
|
-
|
|
128
|
+
// JSON parsing. Body size is capped by `mcp.limits.maxPayloadBytes` (standard §14, default 1 MiB).
|
|
129
|
+
// Anything above is intercepted by the error-handling middleware below and converted to
|
|
130
|
+
// a JSON-RPC `-32005` / HTTP 413 response.
|
|
131
|
+
const { maxPayloadBytes } = appConfig.mcp.limits;
|
|
132
|
+
app.use(express.json({ limit: maxPayloadBytes }));
|
|
133
|
+
app.use(express.urlencoded({ extended: true, limit: maxPayloadBytes }));
|
|
88
134
|
applyCors(app);
|
|
135
|
+
// OAuth discovery + token endpoints (mounted before auth MW so they remain public).
|
|
136
|
+
// Active only when jwtToken.mode !== 'legacyAesCtr'.
|
|
137
|
+
if (getJwtRuntimeConfig().mode !== 'legacyAesCtr') {
|
|
138
|
+
app.use(createOAuthRouter());
|
|
139
|
+
}
|
|
89
140
|
app.use(faviconSvg());
|
|
90
141
|
// Serve static files (CSS, JS, SVG)
|
|
91
142
|
app.use('/static', express.static(staticPath));
|
|
@@ -97,12 +148,14 @@ export async function startHttpServer() {
|
|
|
97
148
|
app.get('/', (req, res) => {
|
|
98
149
|
res.sendFile(join(staticPath, 'home', 'index.html'));
|
|
99
150
|
});
|
|
100
|
-
// Health check endpoint
|
|
151
|
+
// Health check endpoint. Standard §16.1 mandates `status`, `version` and `uptime`. An
|
|
152
|
+
// `unhealthy` body is paired with HTTP 503 so platform health probes pick up the failure.
|
|
101
153
|
app.get('/health', async (req, res) => {
|
|
102
154
|
let health = {
|
|
103
155
|
status: 'healthy',
|
|
156
|
+
version: appConfig.version,
|
|
157
|
+
uptime: process.uptime(),
|
|
104
158
|
details: {
|
|
105
|
-
uptime: process.uptime(),
|
|
106
159
|
timestamp: new Date().toISOString(),
|
|
107
160
|
},
|
|
108
161
|
};
|
|
@@ -112,15 +165,56 @@ export async function startHttpServer() {
|
|
|
112
165
|
health.status = 'unhealthy';
|
|
113
166
|
}
|
|
114
167
|
}
|
|
115
|
-
res.json(health);
|
|
168
|
+
res.status(health.status === 'healthy' ? 200 : 503).json(health);
|
|
116
169
|
});
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
170
|
+
// Standard §15.3 — Prometheus metrics endpoint. Opt-in (default off). Public by design —
|
|
171
|
+
// protect via network policy / reverse proxy if the server is reachable from the network.
|
|
172
|
+
if (metricsEnabled) {
|
|
173
|
+
const metricsPath = metricsCfg?.path || '/metrics';
|
|
174
|
+
app.get(metricsPath, async (_req, res) => {
|
|
175
|
+
try {
|
|
176
|
+
const reg = getMetricsRegistry();
|
|
177
|
+
res.setHeader('Content-Type', reg.contentType);
|
|
178
|
+
res.end(await reg.metrics());
|
|
179
|
+
}
|
|
180
|
+
catch (err) {
|
|
181
|
+
logger.error('Failed to render Prometheus metrics', err);
|
|
182
|
+
res.status(500).send('Failed to render metrics');
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// Readiness probe (standard §16.2) — no authentication; reports whether every dependency
|
|
187
|
+
// the server needs to serve traffic is up. Empty / sensitive details are NEVER returned —
|
|
188
|
+
// each check is reduced to `ok` / `error`.
|
|
189
|
+
app.get('/ready', async (req, res) => {
|
|
190
|
+
const checks = {};
|
|
191
|
+
let ready = true;
|
|
192
|
+
if (appConfig.isMainDBUsed) {
|
|
193
|
+
const dbStatus = await getMainDBConnectionStatus();
|
|
194
|
+
if (dbStatus === 'connected') {
|
|
195
|
+
checks.db = 'ok';
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
checks.db = 'error';
|
|
199
|
+
ready = false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
// Cache singleton: trivially available; surface it for diagnostic completeness.
|
|
203
|
+
checks.cache = 'ok';
|
|
204
|
+
// JWKS check is a placeholder until Phase 5 introduces the OAuth profile.
|
|
205
|
+
checks.jwks = 'skipped';
|
|
206
|
+
res.status(ready ? 200 : 503).json({
|
|
207
|
+
status: ready ? 'ready' : 'not_ready',
|
|
208
|
+
checks,
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
// Token check endpoint: POST /ct {"t": "<token>"}. Standard §7.1 forbids secrets in URL.
|
|
212
|
+
// GET /ct?t=<token> is gated behind webServer.tokenCheck.allowQueryToken (non-prod only).
|
|
213
|
+
const handleTokenCheck = async (req, res) => {
|
|
120
214
|
const raw = req.method === 'GET' ? req.query.t : req.body?.t;
|
|
121
215
|
const token = typeof raw === 'string' ? raw.trim() : '';
|
|
122
216
|
if (!token) {
|
|
123
|
-
return res.status(400).json({ success: false, error: 'Token not provided. Pass via "t"
|
|
217
|
+
return res.status(400).json({ success: false, error: 'Token not provided. Pass via "t" body parameter' });
|
|
124
218
|
}
|
|
125
219
|
const { errorReason: permError } = checkPermanentToken(token);
|
|
126
220
|
if (!permError) {
|
|
@@ -129,13 +223,21 @@ export async function startHttpServer() {
|
|
|
129
223
|
const xff = req.headers['x-forwarded-for'];
|
|
130
224
|
const xffStr = (Array.isArray(xff) ? (xff[0] ?? '') : (xff ?? '')).split(',').shift() ?? '';
|
|
131
225
|
const clientIp = req.ip ?? (xffStr.trim() || (req.socket?.remoteAddress ?? ''));
|
|
132
|
-
const jwtResult = checkJwtToken({ token, clientIp });
|
|
226
|
+
const jwtResult = await checkJwtToken({ token, clientIp });
|
|
133
227
|
if (!jwtResult.errorReason) {
|
|
134
228
|
return res.json({ success: true, type: 'JWT', payload: jwtResult.payload });
|
|
135
229
|
}
|
|
136
230
|
return res.status(401).json({ success: false, error: jwtResult.errorReason });
|
|
137
231
|
};
|
|
138
|
-
|
|
232
|
+
const allowQueryToken = appConfig.webServer.tokenCheck?.allowQueryToken === true && process.env.NODE_ENV !== 'production';
|
|
233
|
+
if (allowQueryToken) {
|
|
234
|
+
app.get('/ct', handleTokenCheck);
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
app.get('/ct', (_req, res) => res.status(405).json({
|
|
238
|
+
error: 'GET /ct is disabled by standard §7.1. Use POST /ct with JSON body {"t": "<token>"}.',
|
|
239
|
+
}));
|
|
240
|
+
}
|
|
139
241
|
app.post('/ct', handleTokenCheck);
|
|
140
242
|
// Public endpoint: returns used HTTP headers configured in the template (optional)
|
|
141
243
|
app.get('/used-http-headers', (req, res) => {
|
|
@@ -172,13 +274,26 @@ export async function startHttpServer() {
|
|
|
172
274
|
}
|
|
173
275
|
// JWT generation API endpoint
|
|
174
276
|
if (appConfig.webServer.genJwtApiEnable) {
|
|
277
|
+
const jwtMode = getJwtRuntimeConfig().mode;
|
|
175
278
|
const encryptKey = appConfig.webServer.auth?.jwtToken?.encryptKey;
|
|
176
|
-
|
|
279
|
+
const legacyKeyMissing = jwtMode === 'legacyAesCtr' && (!encryptKey || encryptKey.length < MIN_ENCRYPT_KEY_LENGTH || encryptKey === '***');
|
|
280
|
+
if (legacyKeyMissing) {
|
|
177
281
|
logger.error('genJwtApiEnable is true but webServer.auth.jwtToken.encryptKey is not configured');
|
|
178
282
|
}
|
|
283
|
+
else if (jwtMode === 'remoteJwks') {
|
|
284
|
+
app.post('/gen-jwt', authMW, (_req, res) => {
|
|
285
|
+
const { jwksUri } = getJwtRuntimeConfig();
|
|
286
|
+
res.status(501).json({
|
|
287
|
+
success: false,
|
|
288
|
+
error: 'cannot_issue_token',
|
|
289
|
+
error_description: 'This server runs in mode=remoteJwks and does not issue JWTs. ' +
|
|
290
|
+
(jwksUri ? `Obtain tokens from the IdP at ${jwksUri}.` : 'Obtain tokens from the configured IdP.'),
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
}
|
|
179
294
|
else {
|
|
180
295
|
const TTL_MULTIPLIERS = { s: 1, m: 60, d: 86400, y: 31536000 };
|
|
181
|
-
app.post('/gen-jwt', authMW, (req, res) => {
|
|
296
|
+
app.post('/gen-jwt', authMW, async (req, res) => {
|
|
182
297
|
try {
|
|
183
298
|
const { username, ttl, service, params } = req.body;
|
|
184
299
|
if (!username || !username.trim()) {
|
|
@@ -224,7 +339,14 @@ export async function startHttpServer() {
|
|
|
224
339
|
Object.assign(payload, params);
|
|
225
340
|
}
|
|
226
341
|
}
|
|
227
|
-
|
|
342
|
+
if (jwtMode !== 'legacyAesCtr' && !canLocallyIssueJwt()) {
|
|
343
|
+
return res.status(501).json({
|
|
344
|
+
success: false,
|
|
345
|
+
error: 'cannot_issue_token',
|
|
346
|
+
error_description: `Current jwtToken.mode=${jwtMode} cannot sign tokens locally.`,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
const token = await generateToken(username.trim(), liveTimeSec, payload);
|
|
228
350
|
const expire = Date.now() + liveTimeSec * 1000;
|
|
229
351
|
return res.json({
|
|
230
352
|
success: true,
|
|
@@ -270,7 +392,7 @@ export async function startHttpServer() {
|
|
|
270
392
|
// Client capabilities are read lazily on every call via `sseServer.getClientCapabilities()` so the
|
|
271
393
|
// value reflects the post-handshake state for every list/read/call.
|
|
272
394
|
async function createSseServer(preservedHeaders, mcpAuthPayload) {
|
|
273
|
-
const sseServer = createMcpServer();
|
|
395
|
+
const sseServer = createMcpServer('sse');
|
|
274
396
|
const sseCtx = () => {
|
|
275
397
|
const caps = sseServer.getClientCapabilities();
|
|
276
398
|
return {
|
|
@@ -298,16 +420,20 @@ export async function startHttpServer() {
|
|
|
298
420
|
return (await getResource(request.params.uri, sseCtx()));
|
|
299
421
|
});
|
|
300
422
|
// Override the tool call handler to include rate limiting, preserved headers and auth payload
|
|
301
|
-
sseServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
423
|
+
sseServer.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
302
424
|
// Apply rate limiting for each SSE tool call
|
|
303
425
|
const toolCallClientId = 'sse-tool-unknown';
|
|
304
426
|
await handleRateLimit(rateLimiter, toolCallClientId, 'unknown', `SSE tool call | tool: ${request.params.name}`);
|
|
305
|
-
// Execute the tool call with preserved headers and payload from SSE connection establishment
|
|
427
|
+
// Execute the tool call with preserved headers and payload from SSE connection establishment.
|
|
428
|
+
// Same `mcp.limits` enforcement as the Streamable HTTP path.
|
|
306
429
|
const { toolHandler } = getProjectData();
|
|
307
|
-
|
|
430
|
+
const sseToolName = request.params?.name ?? 'unknown';
|
|
431
|
+
const response = (await withToolTimeout(sseToolName, () => toolHandler({
|
|
308
432
|
...request.params,
|
|
309
433
|
...sseCtx(),
|
|
310
|
-
|
|
434
|
+
signal: extra?.signal,
|
|
435
|
+
})));
|
|
436
|
+
return truncateToolResponse(response);
|
|
311
437
|
});
|
|
312
438
|
return sseServer;
|
|
313
439
|
}
|
|
@@ -315,7 +441,7 @@ export async function startHttpServer() {
|
|
|
315
441
|
app.get('/sse', authMW, async (req, res) => {
|
|
316
442
|
try {
|
|
317
443
|
// Apply rate limiting for SSE connection
|
|
318
|
-
const clientId =
|
|
444
|
+
const clientId = resolveRateLimitKey(req, 'sse');
|
|
319
445
|
await handleRateLimit(rateLimiter, clientId, req.ip || 'unknown', 'SSE', res, 1);
|
|
320
446
|
logger.info('SSE client connected');
|
|
321
447
|
// Preserve normalized headers from SSE connection establishment
|
|
@@ -366,14 +492,8 @@ export async function startHttpServer() {
|
|
|
366
492
|
}
|
|
367
493
|
const transportData = sseTransports.get(sessionId);
|
|
368
494
|
if (!transportData) {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
error: {
|
|
372
|
-
code: -32001,
|
|
373
|
-
message: 'Session not found',
|
|
374
|
-
},
|
|
375
|
-
id: null,
|
|
376
|
-
});
|
|
495
|
+
const err = new ResourceNotFoundError('SSE session not found');
|
|
496
|
+
res.status(err.statusCode).json(createJsonRpcErrorResponse(err, null));
|
|
377
497
|
return;
|
|
378
498
|
}
|
|
379
499
|
const { transport } = transportData;
|
|
@@ -398,18 +518,12 @@ export async function startHttpServer() {
|
|
|
398
518
|
break;
|
|
399
519
|
}
|
|
400
520
|
if (!targetTransport) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
error: {
|
|
404
|
-
code: -32001,
|
|
405
|
-
message: 'No SSE connection established. Connect via GET /sse first.',
|
|
406
|
-
},
|
|
407
|
-
id: req.body?.id ?? null,
|
|
408
|
-
});
|
|
521
|
+
const err = new ResourceNotFoundError('No SSE connection established. Connect via GET /sse first.');
|
|
522
|
+
res.status(err.statusCode).json(createJsonRpcErrorResponse(err, req.body?.id ?? null));
|
|
409
523
|
return;
|
|
410
524
|
}
|
|
411
525
|
// Apply rate limiting
|
|
412
|
-
const clientId = req
|
|
526
|
+
const clientId = resolveRateLimitKey(req);
|
|
413
527
|
await handleRateLimit(rateLimiter, clientId, req.ip || 'unknown', 'SSE POST', res, req.body?.id);
|
|
414
528
|
logger.info(`Direct SSE POST request received: ${req.body.method} | id: ${req.body.id}`);
|
|
415
529
|
// Use the transport's built-in handlePostMessage method
|
|
@@ -422,158 +536,169 @@ export async function startHttpServer() {
|
|
|
422
536
|
}
|
|
423
537
|
}
|
|
424
538
|
});
|
|
425
|
-
// Streamable HTTP
|
|
426
|
-
//
|
|
427
|
-
// by the `Mcp-Session-Id
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
const httpClientCapabilitiesBySession = new Map();
|
|
539
|
+
// Streamable HTTP runs **stateful**: each MCP session owns a `StreamableHTTPServerTransport`
|
|
540
|
+
// bound to its own `Server` instance (so `getClientCapabilities()` works like on stdio). The
|
|
541
|
+
// transport is created on `initialize`, keyed by the server-generated `Mcp-Session-Id`, and the
|
|
542
|
+
// SDK transport handles protocol-version negotiation, error codes, notifications (202) and the
|
|
543
|
+
// GET-SSE / DELETE-teardown semantics for us.
|
|
431
544
|
const HTTP_SESSION_HEADER = 'mcp-session-id';
|
|
432
|
-
// Soft cap to bound memory; oldest session
|
|
545
|
+
// Soft cap to bound memory; oldest session is evicted (FIFO via Map insertion order).
|
|
433
546
|
const MAX_HTTP_SESSIONS = 4096;
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
547
|
+
const mcpTransports = new Map();
|
|
548
|
+
// Standard §6 (MAY) — SSE resumability. A single in-memory EventStore is shared across sessions
|
|
549
|
+
// (each stream owns its own streamId). Created only when opted in; otherwise the transport is
|
|
550
|
+
// built without an eventStore and behavior is unchanged.
|
|
551
|
+
const sseResumability = appConfig.mcp.sse?.resumability === true;
|
|
552
|
+
const sseEventStore = sseResumability
|
|
553
|
+
? new InMemoryEventStore(appConfig.mcp.sse?.maxStoredEvents ?? 1000)
|
|
554
|
+
: undefined;
|
|
555
|
+
if (sseResumability) {
|
|
556
|
+
logger.info(`MCP SSE resumability enabled (in-memory, max ${appConfig.mcp.sse?.maxStoredEvents ?? 1000} events)`);
|
|
557
|
+
}
|
|
558
|
+
const evictOldestSession = (keep) => {
|
|
559
|
+
while (mcpTransports.size > MAX_HTTP_SESSIONS) {
|
|
560
|
+
const oldest = mcpTransports.keys().next().value;
|
|
561
|
+
if (!oldest || oldest === keep) {
|
|
562
|
+
break;
|
|
563
|
+
}
|
|
564
|
+
const t = mcpTransports.get(oldest);
|
|
565
|
+
mcpTransports.delete(oldest);
|
|
566
|
+
void t?.close();
|
|
567
|
+
}
|
|
568
|
+
};
|
|
569
|
+
// Race the underlying transport against `mcp.limits.toolTimeoutMs` for `tools/call` requests.
|
|
570
|
+
// The standard (§14) requires HTTP 504 + JSON-RPC -32004 on timeout. The SDK's transport
|
|
571
|
+
// doesn't surface HTTP-level timeouts, so we monitor at this layer — whoever writes the
|
|
572
|
+
// response first wins. The tool promise itself is still bounded inside the SDK handler by
|
|
573
|
+
// `withToolTimeout`, which throws an `McpError(-32004)` to keep the JSON-RPC code correct
|
|
574
|
+
// for the SSE branch and for the (unlikely) case the handler beats the HTTP timer.
|
|
575
|
+
const runHttpToolCall = async (req, res, exec) => {
|
|
576
|
+
if (req.body?.method !== 'tools/call') {
|
|
577
|
+
await exec();
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const timeoutMs = appConfig.mcp.limits.toolTimeoutMs;
|
|
581
|
+
let timer;
|
|
582
|
+
const timerPromise = new Promise((resolve) => {
|
|
583
|
+
timer = setTimeout(() => resolve('timeout'), timeoutMs);
|
|
584
|
+
if (typeof timer?.unref === 'function') {
|
|
585
|
+
timer.unref();
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
try {
|
|
589
|
+
const winner = await Promise.race([exec().then(() => 'done'), timerPromise]);
|
|
590
|
+
if (winner === 'timeout' && !res.headersSent) {
|
|
591
|
+
const toolName = req.body?.params?.name ?? 'unknown';
|
|
592
|
+
const err = new TimeoutError(`Tool '${toolName}' exceeded ${timeoutMs} ms timeout`, {
|
|
593
|
+
reason: 'tool_timeout',
|
|
594
|
+
});
|
|
595
|
+
logger.warn(`Tool timeout (${timeoutMs} ms) for tool: ${toolName}`);
|
|
596
|
+
res.status(err.statusCode).json(createJsonRpcErrorResponse(err, req.body?.id ?? null));
|
|
440
597
|
}
|
|
441
598
|
}
|
|
442
|
-
|
|
599
|
+
finally {
|
|
600
|
+
if (timer) {
|
|
601
|
+
clearTimeout(timer);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
const noSessionError = (req, res) => {
|
|
606
|
+
res.status(400).json({
|
|
607
|
+
jsonrpc: '2.0',
|
|
608
|
+
id: req.body?.id ?? null,
|
|
609
|
+
error: { code: -32600, message: 'No valid MCP session. Send `initialize` first.' },
|
|
610
|
+
});
|
|
443
611
|
};
|
|
444
|
-
//
|
|
612
|
+
// GET (server→client SSE stream) and DELETE (session teardown) operate on an existing session.
|
|
613
|
+
const routeToSession = async (req, res) => {
|
|
614
|
+
const sessionId = req.headers[HTTP_SESSION_HEADER];
|
|
615
|
+
const transport = sessionId ? mcpTransports.get(sessionId) : undefined;
|
|
616
|
+
if (!transport) {
|
|
617
|
+
noSessionError(req, res);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
await transport.handleRequest(req, res, req.body);
|
|
621
|
+
};
|
|
622
|
+
// POST endpoint for MCP requests — handshake + all JSON-RPC calls go through the SDK transport.
|
|
445
623
|
app.post('/mcp', authMW, async (req, res) => {
|
|
446
624
|
try {
|
|
447
|
-
//
|
|
448
|
-
const clientId = req
|
|
625
|
+
// Rate limiting and the body-size limit stay here, before the transport takes over.
|
|
626
|
+
const clientId = resolveRateLimitKey(req);
|
|
449
627
|
await handleRateLimit(rateLimiter, clientId, req.ip || 'unknown', 'HTTP MCP', res, req.body?.id);
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
//
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
logger.info('MCP client initialization completed');
|
|
460
|
-
}
|
|
461
|
-
else {
|
|
462
|
-
logger.debug(`MCP notification received: ${method}`);
|
|
463
|
-
}
|
|
464
|
-
if (debugMcpNotification.enabled) {
|
|
465
|
-
debugMcpNotification(`← ${method}\n${JSON.stringify(params ?? {}, null, 2)}`);
|
|
628
|
+
if (res.headersSent) {
|
|
629
|
+
return; // rate limit already responded
|
|
630
|
+
}
|
|
631
|
+
// Dedicated rate-limit bucket for tool calls (was inline in the old switch).
|
|
632
|
+
if (req.body?.method === 'tools/call') {
|
|
633
|
+
const toolCallClientId = resolveRateLimitKey(req, 'tool');
|
|
634
|
+
await handleRateLimit(rateLimiter, toolCallClientId, req.ip || 'unknown', `tool call | tool: ${req.body?.params?.name || 'unknown'}`, res, req.body?.id);
|
|
635
|
+
if (res.headersSent) {
|
|
636
|
+
return;
|
|
466
637
|
}
|
|
467
|
-
return res.status(204).send();
|
|
468
638
|
}
|
|
469
|
-
const
|
|
470
|
-
const
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
639
|
+
const sessionId = req.headers[HTTP_SESSION_HEADER];
|
|
640
|
+
const existing = sessionId ? mcpTransports.get(sessionId) : undefined;
|
|
641
|
+
if (existing) {
|
|
642
|
+
await runHttpToolCall(req, res, () => existing.handleRequest(req, res, req.body));
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
if (isInitializeRequest(req.body)) {
|
|
646
|
+
logger.info(`MCP client initializing: protocolVersion: ${req.body?.params?.protocolVersion} | clientInfo: ${JSON.stringify(req.body?.params?.clientInfo)}`);
|
|
647
|
+
const transport = new StreamableHTTPServerTransport({
|
|
648
|
+
sessionIdGenerator: () => randomUUID(),
|
|
649
|
+
// Standard §6 (MAY) — attach the EventStore only when resumability is enabled.
|
|
650
|
+
...(sseEventStore ? { eventStore: sseEventStore } : {}),
|
|
651
|
+
onsessioninitialized: (sid) => {
|
|
652
|
+
mcpTransports.set(sid, transport);
|
|
653
|
+
evictOldestSession(sid);
|
|
654
|
+
},
|
|
655
|
+
onsessionclosed: (sid) => {
|
|
656
|
+
mcpTransports.delete(sid);
|
|
657
|
+
},
|
|
658
|
+
});
|
|
659
|
+
// SDK `Transport` exposes `onclose` as a plain setter (not an EventTarget), so
|
|
660
|
+
// `addEventListener` does not apply here — this is the canonical SDK pattern.
|
|
661
|
+
// oxlint-disable-next-line unicorn/prefer-add-event-listener
|
|
662
|
+
transport.onclose = () => {
|
|
663
|
+
const sid = transport.sessionId;
|
|
664
|
+
if (sid) {
|
|
665
|
+
mcpTransports.delete(sid);
|
|
488
666
|
}
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
serverInfo: {
|
|
497
|
-
name: appConfig.name,
|
|
498
|
-
version: appConfig.version,
|
|
499
|
-
},
|
|
500
|
-
};
|
|
501
|
-
break;
|
|
502
|
-
case 'tools/list':
|
|
503
|
-
const tools = await getTools(httpArgs);
|
|
504
|
-
result = { tools };
|
|
505
|
-
break;
|
|
506
|
-
case 'tools/call':
|
|
507
|
-
// Apply rate limiting for tool calls
|
|
508
|
-
const toolCallClientId = `tool-${req.ip || 'unknown'}`;
|
|
509
|
-
await handleRateLimit(rateLimiter, toolCallClientId, req.ip || 'unknown', `tool call | tool: ${params?.name || 'unknown'}`, res, id);
|
|
510
|
-
const { toolHandler } = getProjectData();
|
|
511
|
-
result = await toolHandler({ ...params, ...httpArgs });
|
|
512
|
-
break;
|
|
513
|
-
case 'prompts/list':
|
|
514
|
-
result = await getPromptsList(httpArgs);
|
|
515
|
-
break;
|
|
516
|
-
case 'prompts/get': {
|
|
517
|
-
result = await getPrompt(request, httpArgs);
|
|
518
|
-
break;
|
|
519
|
-
}
|
|
520
|
-
case 'resources/list':
|
|
521
|
-
result = await getResourcesList(httpArgs);
|
|
522
|
-
break;
|
|
523
|
-
case 'resources/read': {
|
|
524
|
-
result = await getResource(params.uri, httpArgs);
|
|
525
|
-
break;
|
|
526
|
-
}
|
|
527
|
-
case 'ping':
|
|
528
|
-
result = { pong: true };
|
|
529
|
-
break;
|
|
530
|
-
default:
|
|
531
|
-
throw new Error(`Unknown method: ${method}`);
|
|
667
|
+
};
|
|
668
|
+
const server = createMcpServer('http');
|
|
669
|
+
// Cast: SDK `Transport` types are stricter under `exactOptionalPropertyTypes`, but
|
|
670
|
+
// `StreamableHTTPServerTransport` is a valid transport.
|
|
671
|
+
await server.connect(transport);
|
|
672
|
+
await runHttpToolCall(req, res, () => transport.handleRequest(req, res, req.body));
|
|
673
|
+
return;
|
|
532
674
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
id,
|
|
536
|
-
result,
|
|
537
|
-
});
|
|
675
|
+
// No session and not an `initialize` request → 400 per MCP transport semantics.
|
|
676
|
+
noSessionError(req, res);
|
|
538
677
|
}
|
|
539
678
|
catch (error) {
|
|
540
679
|
if (!error.printed) {
|
|
541
680
|
logger.error('MCP request failed', toError(error));
|
|
542
681
|
error.printed = true;
|
|
543
682
|
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
errorResponse = {
|
|
549
|
-
code: -1,
|
|
550
|
-
message: errorObj.message,
|
|
551
|
-
data: {
|
|
552
|
-
code: errorObj.code,
|
|
553
|
-
details: errorObj.details,
|
|
554
|
-
// stack: process.env.NODE_ENV === 'development' ? errorObj.stack : undefined
|
|
555
|
-
},
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
else {
|
|
559
|
-
// Standard error handling for non-MCP errors
|
|
560
|
-
errorResponse = {
|
|
561
|
-
code: -1,
|
|
562
|
-
message: toStr(error),
|
|
563
|
-
};
|
|
683
|
+
if (!res.headersSent) {
|
|
684
|
+
res
|
|
685
|
+
.status(500)
|
|
686
|
+
.json(createJsonRpcErrorResponse(error instanceof ServerError ? error : new ServerError(toStr(error))));
|
|
564
687
|
}
|
|
565
|
-
return res.json({
|
|
566
|
-
jsonrpc: '2.0',
|
|
567
|
-
id: req.body?.id ?? 1,
|
|
568
|
-
error: errorResponse,
|
|
569
|
-
});
|
|
570
688
|
}
|
|
571
689
|
});
|
|
690
|
+
app.get('/mcp', authMW, async (req, res) => {
|
|
691
|
+
await routeToSession(req, res);
|
|
692
|
+
});
|
|
693
|
+
app.delete('/mcp', authMW, async (req, res) => {
|
|
694
|
+
await routeToSession(req, res);
|
|
695
|
+
});
|
|
572
696
|
// 404 handler for unknown routes
|
|
573
697
|
app.use((req, res) => {
|
|
574
698
|
const availableEndpoints = {
|
|
575
699
|
home: 'GET /',
|
|
576
700
|
health: 'GET /health',
|
|
701
|
+
ready: 'GET /ready',
|
|
577
702
|
checkToken: 'GET /ct?t=<token>, POST /ct',
|
|
578
703
|
sse: 'GET /sse, POST /sse',
|
|
579
704
|
messages: 'POST /messages',
|
|
@@ -599,17 +724,32 @@ export async function startHttpServer() {
|
|
|
599
724
|
availableEndpoints,
|
|
600
725
|
});
|
|
601
726
|
});
|
|
602
|
-
// Error handling middleware (must have 4 parameters for Express to recognize it)
|
|
727
|
+
// Error handling middleware (must have 4 parameters for Express to recognize it).
|
|
728
|
+
// Special case: `express.json()` raises `entity.too.large` with `error.type === 'entity.too.large'`
|
|
729
|
+
// when the request body exceeds `mcp.limits.maxPayloadBytes`. Standard §14 maps that to
|
|
730
|
+
// JSON-RPC `-32005` / HTTP 413.
|
|
603
731
|
app.use((error, req, res, _next) => {
|
|
732
|
+
if (error && error.type === 'entity.too.large') {
|
|
733
|
+
logger.warn(`Payload too large from ip: ${req.ip}, limit: ${maxPayloadBytes}`);
|
|
734
|
+
if (!res.headersSent) {
|
|
735
|
+
const err = new PayloadTooLargeError(`Request body exceeds ${maxPayloadBytes} bytes`, {
|
|
736
|
+
reason: 'payload_too_large',
|
|
737
|
+
});
|
|
738
|
+
res.status(err.statusCode).json(createJsonRpcErrorResponse(err, req.body?.id ?? null));
|
|
739
|
+
}
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
604
742
|
logger.error('Express error handler', error);
|
|
605
743
|
if (!res.headersSent) {
|
|
606
744
|
res.status(500).json(createJsonRpcErrorResponse(error));
|
|
607
745
|
}
|
|
608
746
|
});
|
|
609
|
-
// Start HTTP server
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
747
|
+
// Start HTTP server. Bind address is driven by config — default is the loopback interface
|
|
748
|
+
// (`127.0.0.1`) so the server is unreachable from the network until an operator opts in to
|
|
749
|
+
// `0.0.0.0` or a specific NIC address. Standard §6.
|
|
750
|
+
const { port, host } = appConfig.webServer;
|
|
751
|
+
app.listen(port, host || '127.0.0.1', () => {
|
|
752
|
+
let msg = `${chalk.magenta(appConfig.productName)} started with ${chalk.blue('HTTP')} transport on ${chalk.blue(host)}:${chalk.blue(port)}
|
|
613
753
|
Home page: http://localhost:${port}/`;
|
|
614
754
|
if (isAdminEnabled) {
|
|
615
755
|
msg += `\nAdmin panel: http://localhost:${port}/admin`;
|