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,55 +1,614 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
-
import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
3
|
+
import { CallToolRequestSchema, CompleteRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, SubscribeRequestSchema, UnsubscribeRequestSchema, ListTasksRequestSchema, GetTaskRequestSchema, GetTaskPayloadRequestSchema, CancelTaskRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
|
|
3
4
|
import { appConfig, getProjectData } from '../bootstrap/init-config.js';
|
|
4
|
-
import {
|
|
5
|
+
import { sanitizeOutwardMessage, toMcpError } from '../errors/errors.js';
|
|
6
|
+
import { RateLimitedError, ResourceNotFoundError } from '../errors/specific-errors.js';
|
|
7
|
+
import { getTools, normalizeHeaders } from '../utils/utils.js';
|
|
8
|
+
import { getCurrentRequestContext, runWithRequestContext } from '../web/request-id.js';
|
|
9
|
+
import { getMetrics } from '../metrics/metrics.js';
|
|
10
|
+
import { applyDeprecationToDescription, assertDeprecationConsistency, readDeprecation, warnDeprecatedUsage, } from './deprecation.js';
|
|
11
|
+
import { registerLoggingCapability } from './mcp-logging.js';
|
|
12
|
+
import { paginate, parsePageSize } from './pagination.js';
|
|
5
13
|
import { getPrompt, getPromptsList } from './prompts.js';
|
|
6
|
-
import { getResource, getResourcesList } from './resources.js';
|
|
14
|
+
import { getResource, getResourcesList, getResourceTemplatesList, subscribeResource, unsubscribeResource, } from './resources.js';
|
|
15
|
+
import { getTaskStore, isTerminalTaskStatus, toTaskDto } from './task-store.js';
|
|
16
|
+
import { truncateToolResponse, withToolTimeout } from './tool-limits.js';
|
|
17
|
+
import { validateToolInput, validateToolOutput } from './validate-tool-args.js';
|
|
18
|
+
/**
|
|
19
|
+
* Standard §14 — per-subject in-flight counter for tools/call. Keys are the JWT `sub`
|
|
20
|
+
* (or 'anonymous' when auth is disabled / token is absent). Excess concurrent calls
|
|
21
|
+
* raise RateLimitedError with the standard `Retry-After` semantics.
|
|
22
|
+
*/
|
|
23
|
+
const inFlightBySubject = new Map();
|
|
24
|
+
function subjectKeyFromAuth(authInfo) {
|
|
25
|
+
const sub = authInfo?.payload?.sub ?? authInfo?.payload?.user ?? authInfo?.username;
|
|
26
|
+
if (typeof sub === 'string' && sub.trim()) {
|
|
27
|
+
return sub.trim().toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
return 'anonymous';
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Standard §14 — try to claim a per-subject in-flight slot. Returns false when the subject is at
|
|
33
|
+
* its `maxConcurrentPerSubject` cap (caller raises RateLimitedError). Shared by the synchronous
|
|
34
|
+
* tools/call path and the task path so a `working` task occupies a slot exactly like a sync call.
|
|
35
|
+
*/
|
|
36
|
+
function tryAcquireSlot(subjectKey, maxConcurrent) {
|
|
37
|
+
const current = inFlightBySubject.get(subjectKey) ?? 0;
|
|
38
|
+
if (current >= maxConcurrent) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
inFlightBySubject.set(subjectKey, current + 1);
|
|
42
|
+
getMetrics()?.concurrentCalls.set({ subject: subjectKey }, current + 1);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
function releaseSlot(subjectKey) {
|
|
46
|
+
const after = (inFlightBySubject.get(subjectKey) ?? 1) - 1;
|
|
47
|
+
if (after <= 0) {
|
|
48
|
+
inFlightBySubject.delete(subjectKey);
|
|
49
|
+
getMetrics()?.concurrentCalls.set({ subject: subjectKey }, 0);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
inFlightBySubject.set(subjectKey, after);
|
|
53
|
+
getMetrics()?.concurrentCalls.set({ subject: subjectKey }, after);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
7
56
|
/**
|
|
8
57
|
* Create MCP Server instance with registered tool and prompt handlers.
|
|
9
58
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* `
|
|
59
|
+
* The same `Server` is driven by every SDK transport (stdio + Streamable HTTP), so handlers build
|
|
60
|
+
* their {@link ITransportContext} from the per-request `extra` (`RequestHandlerExtra`) that the SDK
|
|
61
|
+
* passes as the second argument:
|
|
62
|
+
* - `extra.requestInfo.headers` — full request headers (HTTP only; absent on stdio);
|
|
63
|
+
* - `extra.authInfo` — whatever the transport read from `req.auth` (HTTP auth middleware bridge);
|
|
64
|
+
* - `server.getClientCapabilities()` — capabilities reported during the `initialize` handshake,
|
|
65
|
+
* reliable because each HTTP session owns its own `Server` instance (stateful transport).
|
|
66
|
+
*
|
|
67
|
+
* @param transportType — transport that owns this server instance, surfaced to handlers as
|
|
68
|
+
* `ITransportContext.transport`.
|
|
15
69
|
*/
|
|
16
|
-
export function createMcpServer() {
|
|
70
|
+
export function createMcpServer(transportType) {
|
|
71
|
+
const resourcesCfg = appConfig.mcp.resources;
|
|
72
|
+
const subscribeEnabled = resourcesCfg?.subscribeEnabled === true;
|
|
73
|
+
const templatesEnabled = resourcesCfg?.templatesEnabled === true;
|
|
74
|
+
const resourceCapability = {};
|
|
75
|
+
if (subscribeEnabled) {
|
|
76
|
+
resourceCapability.subscribe = true;
|
|
77
|
+
resourceCapability.listChanged = true;
|
|
78
|
+
}
|
|
79
|
+
const loggingCapEnabled = appConfig.mcp.logging?.enabled !== false;
|
|
80
|
+
// Standard §8.2 — advertise only the capabilities the server actually supports.
|
|
81
|
+
//
|
|
82
|
+
// `resources` and `tools` are always advertised: built-in resources (project://*, use://auth,
|
|
83
|
+
// doc://readme) are present in every configuration, and an MCP SDK without tools has no purpose.
|
|
84
|
+
//
|
|
85
|
+
// `prompts` is conditional: a server configured without agent briefs and without customPrompts
|
|
86
|
+
// serves no prompts, so advertising the capability (and registering its handlers) would violate
|
|
87
|
+
// §8.2 — instead the prompts/* methods stay unregistered and return -32601 per §8.3.
|
|
88
|
+
const projectData = getProjectData();
|
|
89
|
+
const hasPrompts = Boolean((projectData?.agentBrief && projectData?.agentPrompt) ||
|
|
90
|
+
typeof projectData?.customPrompts === 'function' ||
|
|
91
|
+
(Array.isArray(projectData?.customPrompts) && projectData.customPrompts.length > 0));
|
|
92
|
+
// Standard §8.2 (MAY) — completion/complete is opt-in: requires both the config flag and a
|
|
93
|
+
// project-supplied provider. Without a provider there is nothing to serve, so the capability
|
|
94
|
+
// is not advertised and completion/complete returns -32601.
|
|
95
|
+
const completionsEnabled = appConfig.mcp.completions?.enabled === true && typeof projectData?.completionProvider === 'function';
|
|
96
|
+
// Standard §8.7 (MAY) — task-augmented execution is opt-in. When off (default) the capability is
|
|
97
|
+
// NOT advertised and the tasks/* methods stay unregistered (returning -32601), exactly as §8.7
|
|
98
|
+
// requires for a server that does not support tasks. When on, the server advertises that it can
|
|
99
|
+
// list and cancel tasks, and that task creation is supported for `tools/call`.
|
|
100
|
+
const tasksEnabled = appConfig.mcp.tasks?.enabled === true;
|
|
17
101
|
const server = new Server({
|
|
18
102
|
name: appConfig.name,
|
|
19
103
|
version: appConfig.version,
|
|
20
104
|
}, {
|
|
21
105
|
capabilities: {
|
|
22
106
|
tools: {},
|
|
23
|
-
prompts: {},
|
|
24
|
-
resources:
|
|
107
|
+
...(hasPrompts ? { prompts: {} } : {}),
|
|
108
|
+
resources: resourceCapability,
|
|
109
|
+
...(loggingCapEnabled ? { logging: {} } : {}),
|
|
110
|
+
...(completionsEnabled ? { completions: {} } : {}),
|
|
111
|
+
...(tasksEnabled
|
|
112
|
+
? {
|
|
113
|
+
tasks: {
|
|
114
|
+
list: {},
|
|
115
|
+
cancel: {},
|
|
116
|
+
// Task creation is supported only for tools/call (the only long-running path here).
|
|
117
|
+
// Shape per ServerTasksCapabilitySchema: requests.tools.call.
|
|
118
|
+
requests: { tools: { call: {} } },
|
|
119
|
+
},
|
|
120
|
+
}
|
|
121
|
+
: {}),
|
|
25
122
|
},
|
|
26
123
|
});
|
|
27
|
-
|
|
124
|
+
if (loggingCapEnabled) {
|
|
125
|
+
registerLoggingCapability(server);
|
|
126
|
+
}
|
|
127
|
+
const ctx = (extra) => {
|
|
128
|
+
const headers = extra.requestInfo?.headers ? normalizeHeaders(extra.requestInfo.headers) : undefined;
|
|
129
|
+
const payload = extra.authInfo?.payload;
|
|
28
130
|
const caps = server.getClientCapabilities();
|
|
29
|
-
return
|
|
131
|
+
return {
|
|
132
|
+
transport: transportType,
|
|
133
|
+
...(headers ? { headers } : {}),
|
|
134
|
+
...(payload ? { payload } : {}),
|
|
135
|
+
...(caps ? { clientCapabilities: caps } : {}),
|
|
136
|
+
};
|
|
30
137
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
138
|
+
const pageSize = parsePageSize(appConfig.mcp.pagination?.pageSize);
|
|
139
|
+
/**
|
|
140
|
+
* Standard §15.1 — every handler runs inside an {@link IRequestContext}.
|
|
141
|
+
* HTTP path: middleware already entered the AsyncLocalStorage scope, so we
|
|
142
|
+
* only enrich the existing context with `jsonRpcId`.
|
|
143
|
+
* Stdio path: no middleware ran — we mint a `stdio-<uuid>` request id so
|
|
144
|
+
* logs, errors and notifications can be correlated end-to-end.
|
|
145
|
+
*/
|
|
146
|
+
const withRequestContext = (handler) => {
|
|
147
|
+
return (async (req, extra) => {
|
|
148
|
+
const jsonRpcId = extra?.requestId ?? null;
|
|
149
|
+
const existing = getCurrentRequestContext();
|
|
150
|
+
const reqCtx = existing
|
|
151
|
+
? { ...existing, jsonRpcId }
|
|
152
|
+
: { requestId: `stdio-${randomUUID()}`, jsonRpcId };
|
|
153
|
+
return runWithRequestContext(reqCtx, async () => {
|
|
154
|
+
try {
|
|
155
|
+
return await handler(req, extra);
|
|
156
|
+
}
|
|
157
|
+
catch (err) {
|
|
158
|
+
// Standard §13.3 — every handler error is mapped to an SDK McpError with the correct
|
|
159
|
+
// numeric JSON-RPC code and a sanitized message before the transport serializes it.
|
|
160
|
+
throw toMcpError(err);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
// Handler for listing available tools (standard §8.4 — server-side pagination).
|
|
166
|
+
server.setRequestHandler(ListToolsRequestSchema, withRequestContext(async (request, extra) => {
|
|
167
|
+
const raw = await getTools(ctx(extra));
|
|
168
|
+
const tools = raw.map((t) => {
|
|
169
|
+
const info = readDeprecation(t);
|
|
170
|
+
if (!info) {
|
|
171
|
+
return t;
|
|
172
|
+
}
|
|
173
|
+
assertDeprecationConsistency('tool', t.name, info);
|
|
174
|
+
return { ...t, description: applyDeprecationToDescription(t.description, info) };
|
|
175
|
+
});
|
|
176
|
+
const cursor = request.params?.cursor;
|
|
177
|
+
const { page, nextCursor } = paginate(tools, cursor, pageSize, (t) => t.name);
|
|
178
|
+
return nextCursor ? { tools: page, nextCursor } : { tools: page };
|
|
179
|
+
}));
|
|
180
|
+
const progressThrottleMs = appConfig.mcp.progress?.throttleMs ?? 100;
|
|
181
|
+
/**
|
|
182
|
+
* Build a `sendProgress` emitter scoped to a single tools/call. Active only when the request
|
|
183
|
+
* carried `_meta.progressToken`; otherwise returns a no-op so handlers can call it
|
|
184
|
+
* unconditionally. Enforces monotonic increase and `throttleMs` server-side per §8.6.
|
|
185
|
+
*/
|
|
186
|
+
const buildSendProgress = (progressToken) => {
|
|
187
|
+
if (progressToken === undefined || progressToken === null) {
|
|
188
|
+
return () => { };
|
|
189
|
+
}
|
|
190
|
+
let lastEmit = 0;
|
|
191
|
+
let lastProgress = -Infinity;
|
|
192
|
+
return (progress, total, message) => {
|
|
193
|
+
if (typeof progress !== 'number' || Number.isNaN(progress)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (progress < lastProgress) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const now = Date.now();
|
|
200
|
+
if (now - lastEmit < progressThrottleMs) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
lastEmit = now;
|
|
204
|
+
lastProgress = progress;
|
|
205
|
+
const params = { progressToken, progress };
|
|
206
|
+
if (total !== undefined) {
|
|
207
|
+
params.total = total;
|
|
208
|
+
}
|
|
209
|
+
if (message !== undefined) {
|
|
210
|
+
params.message = message;
|
|
211
|
+
}
|
|
212
|
+
void server.notification({ method: 'notifications/progress', params }).catch(() => { });
|
|
213
|
+
};
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Post-process a raw tool response (shared by the synchronous and task paths): validate
|
|
217
|
+
* `structuredContent` against `outputSchema` (§9.4 — throws -32603 on violation), mirror it into
|
|
218
|
+
* `content[0]` as JSON text for legacy clients (§12.4), then truncate oversized payloads (§12.2)
|
|
219
|
+
* and record the serialized size metric. Returns the wire-ready result.
|
|
220
|
+
*/
|
|
221
|
+
const finalizeToolResponse = (tool, response) => {
|
|
222
|
+
if (response && typeof response === 'object' && 'structuredContent' in response) {
|
|
223
|
+
const outputCheck = validateToolOutput(tool, response.structuredContent);
|
|
224
|
+
if (!outputCheck.valid) {
|
|
225
|
+
throw new McpError(-32603, 'Tool produced result that violates outputSchema', {
|
|
226
|
+
field: outputCheck.field,
|
|
227
|
+
reason: outputCheck.reason,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
// §12.4 — mirror structuredContent in content[0] as JSON text for legacy clients.
|
|
231
|
+
const existingContent = Array.isArray(response.content) ? response.content : undefined;
|
|
232
|
+
const hasText = existingContent?.some((p) => p?.type === 'text' && typeof p?.text === 'string');
|
|
233
|
+
if (!hasText) {
|
|
234
|
+
let serialized;
|
|
235
|
+
try {
|
|
236
|
+
serialized = JSON.stringify(response.structuredContent ?? null, null, 2);
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
serialized = '';
|
|
240
|
+
}
|
|
241
|
+
response.content = [{ type: 'text', text: serialized }, ...(existingContent ?? [])];
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const truncated = truncateToolResponse(response);
|
|
245
|
+
try {
|
|
246
|
+
const resultBytes = JSON.stringify(truncated ?? null).length;
|
|
247
|
+
getMetrics()?.resultBytes.observe(resultBytes);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// ignore serialization-only failures
|
|
251
|
+
}
|
|
252
|
+
return truncated;
|
|
253
|
+
};
|
|
254
|
+
const taskStore = tasksEnabled ? getTaskStore() : undefined;
|
|
255
|
+
/** Standard §8.7 — emit notifications/tasks/status for a record's current state. */
|
|
256
|
+
const notifyTaskStatus = (record) => {
|
|
257
|
+
if (!taskStore) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
void server
|
|
261
|
+
.notification({ method: 'notifications/tasks/status', params: toTaskDto(record, taskStore.pollIntervalMs) })
|
|
262
|
+
.catch(() => { });
|
|
263
|
+
};
|
|
264
|
+
/**
|
|
265
|
+
* Standard §8.7 — start a tool call as a task: persist a `working` record, return its id
|
|
266
|
+
* immediately, and run the handler in the background. On completion the record transitions to
|
|
267
|
+
* `completed` (with the same result a synchronous call would return) or `failed` (with a
|
|
268
|
+
* sanitized message); cancellation is handled by the tasks/cancel path. The in-flight slot is
|
|
269
|
+
* held for the whole background run and released on the terminal transition.
|
|
270
|
+
*/
|
|
271
|
+
const startTask = (tool, toolName, request, extra, subjectKey, progressToken) => {
|
|
272
|
+
const store = taskStore;
|
|
38
273
|
const { toolHandler } = getProjectData();
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
274
|
+
const reqCtx = getCurrentRequestContext();
|
|
275
|
+
const ttlMs = request.params?.task?.ttl;
|
|
276
|
+
const record = store.create({
|
|
277
|
+
method: 'tools/call',
|
|
278
|
+
toolName,
|
|
279
|
+
subjectKey,
|
|
280
|
+
...(reqCtx?.requestId ? { requestId: reqCtx.requestId } : {}),
|
|
281
|
+
...(ttlMs !== undefined ? { ttlMs } : {}),
|
|
282
|
+
});
|
|
283
|
+
getMetrics()?.tasks.inc({ status: 'created' });
|
|
284
|
+
const sendProgress = buildSendProgress(progressToken);
|
|
285
|
+
const transportCtx = ctx(extra);
|
|
286
|
+
const run = async () => {
|
|
287
|
+
const bgCtx = {
|
|
288
|
+
requestId: record.requestId ?? `task-${record.taskId}`,
|
|
289
|
+
jsonRpcId: null,
|
|
290
|
+
};
|
|
291
|
+
await runWithRequestContext(bgCtx, async () => {
|
|
292
|
+
try {
|
|
293
|
+
const raw = (await toolHandler({
|
|
294
|
+
...request.params,
|
|
295
|
+
...transportCtx,
|
|
296
|
+
// Standard §8.5 — cancellation is driven by the task's own AbortController.
|
|
297
|
+
signal: record.abort.signal,
|
|
298
|
+
// Standard §8.6 — progress for the long-running task.
|
|
299
|
+
sendProgress,
|
|
300
|
+
}));
|
|
301
|
+
const processed = finalizeToolResponse(tool, raw);
|
|
302
|
+
// Skip if the task was cancelled while the handler was still running.
|
|
303
|
+
if (store.get(record.taskId)?.status === 'working') {
|
|
304
|
+
const updated = store.update(record.taskId, { status: 'completed', result: processed });
|
|
305
|
+
getMetrics()?.tasks.inc({ status: 'completed' });
|
|
306
|
+
if (updated) {
|
|
307
|
+
notifyTaskStatus(updated);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
catch (err) {
|
|
312
|
+
if (store.get(record.taskId)?.status === 'working') {
|
|
313
|
+
const updated = store.update(record.taskId, {
|
|
314
|
+
status: 'failed',
|
|
315
|
+
statusMessage: sanitizeOutwardMessage(err),
|
|
316
|
+
});
|
|
317
|
+
getMetrics()?.tasks.inc({ status: 'failed' });
|
|
318
|
+
if (updated) {
|
|
319
|
+
notifyTaskStatus(updated);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
releaseSlot(subjectKey);
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
};
|
|
328
|
+
void run();
|
|
329
|
+
return { task: toTaskDto(record, store.pollIntervalMs) };
|
|
330
|
+
};
|
|
331
|
+
// Handler for tool execution. The call is wrapped by `withToolTimeout` (standard §14 —
|
|
332
|
+
// `mcp.limits.toolTimeoutMs`) and `truncateToolResponse` (standard §12.2 — oversized
|
|
333
|
+
// results are surfaced with explicit `truncated: true` markers). Arguments are validated
|
|
334
|
+
// against the tool's `inputSchema` (standard §9.3); structuredContent is validated against
|
|
335
|
+
// `outputSchema` (standard §9.4) and mirrored into `content[0]` as JSON text per §12.4.
|
|
336
|
+
server.setRequestHandler(CallToolRequestSchema, withRequestContext(async (request, extra) => {
|
|
337
|
+
const { toolHandler } = getProjectData();
|
|
338
|
+
const toolName = request.params?.name ?? 'unknown';
|
|
339
|
+
const args = request.params?.arguments ?? {};
|
|
340
|
+
const tools = await getTools(ctx(extra));
|
|
341
|
+
const tool = tools.find((t) => t.name === toolName);
|
|
342
|
+
if (!tool) {
|
|
343
|
+
getMetrics()?.toolCalls.inc({ tool: toolName, status: 'invalid_params' });
|
|
344
|
+
throw new McpError(-32602, `Unknown tool: ${toolName}`, { field: 'name', reason: 'unknown_tool' });
|
|
345
|
+
}
|
|
346
|
+
// Standard §17.2 — deprecation warning is emitted at call-time (rate-limited 1/hour).
|
|
347
|
+
warnDeprecatedUsage('tool', toolName, readDeprecation(tool));
|
|
348
|
+
// Standard §7.5 — scope enforcement for tool dispatch.
|
|
349
|
+
const required = (tool._meta?.requiredScopes ??
|
|
350
|
+
tool.requiredScopes ??
|
|
351
|
+
[]);
|
|
352
|
+
if (Array.isArray(required) && required.length > 0) {
|
|
353
|
+
const tokenScopes = String(extra?.authInfo?.payload?.scope ?? '')
|
|
354
|
+
.split(/\s+/)
|
|
355
|
+
.filter(Boolean);
|
|
356
|
+
const missing = required.filter((s) => !tokenScopes.includes(s));
|
|
357
|
+
if (missing.length > 0) {
|
|
358
|
+
getMetrics()?.toolCalls.inc({ tool: toolName, status: 'error' });
|
|
359
|
+
throw new McpError(-32004, `Missing scopes: ${missing.join(',')}`, {
|
|
360
|
+
field: 'scope',
|
|
361
|
+
reason: 'insufficient_scope',
|
|
362
|
+
missing,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const inputCheck = validateToolInput(tool, args);
|
|
367
|
+
if (!inputCheck.valid) {
|
|
368
|
+
getMetrics()?.toolCalls.inc({ tool: toolName, status: 'invalid_params' });
|
|
369
|
+
throw new McpError(-32602, 'Invalid params', { field: inputCheck.field, reason: inputCheck.reason });
|
|
370
|
+
}
|
|
371
|
+
// Standard §14 — per-subject concurrent in-flight cap.
|
|
372
|
+
const maxConcurrent = appConfig.mcp.rateLimit?.maxConcurrentPerSubject ?? 16;
|
|
373
|
+
const subjectKey = subjectKeyFromAuth(extra?.authInfo);
|
|
374
|
+
const progressToken = request.params?._meta?.progressToken;
|
|
375
|
+
const raiseConcurrencyLimit = () => {
|
|
376
|
+
getMetrics()?.toolCalls.inc({ tool: toolName, status: 'rate_limited' });
|
|
377
|
+
getMetrics()?.rateLimitHits.inc({ scope: 'concurrent' });
|
|
378
|
+
throw new RateLimitedError(`Too many concurrent tool calls for subject "${subjectKey}" (limit ${maxConcurrent})`, 1);
|
|
379
|
+
};
|
|
380
|
+
// Standard §8.7 / §9.1 — decide synchronous vs task-augmented execution. Only relevant when
|
|
381
|
+
// the `tasks` capability is enabled; otherwise the `task` param is ignored and the call runs
|
|
382
|
+
// synchronously.
|
|
383
|
+
if (tasksEnabled) {
|
|
384
|
+
const wantsTask = request.params?.task != null;
|
|
385
|
+
const taskSupport = tool?.execution?.taskSupport ?? 'forbidden';
|
|
386
|
+
if (wantsTask && taskSupport === 'forbidden') {
|
|
387
|
+
getMetrics()?.toolCalls.inc({ tool: toolName, status: 'invalid_params' });
|
|
388
|
+
throw new McpError(-32602, 'Tool does not support tasks', { field: 'task', reason: 'task_not_supported' });
|
|
389
|
+
}
|
|
390
|
+
if (!wantsTask && taskSupport === 'required') {
|
|
391
|
+
getMetrics()?.toolCalls.inc({ tool: toolName, status: 'invalid_params' });
|
|
392
|
+
throw new McpError(-32602, 'Tool requires task-augmented execution', {
|
|
393
|
+
field: 'task',
|
|
394
|
+
reason: 'task_required',
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
if (wantsTask) {
|
|
398
|
+
if (!tryAcquireSlot(subjectKey, maxConcurrent)) {
|
|
399
|
+
raiseConcurrencyLimit();
|
|
400
|
+
}
|
|
401
|
+
// The background run releases the slot on its terminal transition.
|
|
402
|
+
return startTask(tool, toolName, request, extra, subjectKey, progressToken);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
// Synchronous path.
|
|
406
|
+
if (!tryAcquireSlot(subjectKey, maxConcurrent)) {
|
|
407
|
+
raiseConcurrencyLimit();
|
|
408
|
+
}
|
|
409
|
+
const stopTimer = getMetrics()?.toolDuration.startTimer({ tool: toolName });
|
|
410
|
+
const sendProgress = buildSendProgress(progressToken);
|
|
411
|
+
let outcome = 'ok';
|
|
412
|
+
try {
|
|
413
|
+
const response = (await withToolTimeout(toolName, () => toolHandler({
|
|
414
|
+
...request.params,
|
|
415
|
+
...ctx(extra),
|
|
416
|
+
// Standard §8.5 — propagate cancellation to user code.
|
|
417
|
+
signal: extra.signal,
|
|
418
|
+
// Standard §8.6 — progress emitter (no-op when no progressToken).
|
|
419
|
+
sendProgress,
|
|
420
|
+
})));
|
|
421
|
+
try {
|
|
422
|
+
return finalizeToolResponse(tool, response);
|
|
423
|
+
}
|
|
424
|
+
catch (finalizeErr) {
|
|
425
|
+
if (finalizeErr?.code === -32603) {
|
|
426
|
+
outcome = 'internal_error';
|
|
427
|
+
}
|
|
428
|
+
throw finalizeErr;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
catch (err) {
|
|
432
|
+
if (outcome === 'ok') {
|
|
433
|
+
const code = err?.code;
|
|
434
|
+
if (code === -32004) {
|
|
435
|
+
outcome = 'timeout';
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
outcome = 'error';
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
throw err;
|
|
442
|
+
}
|
|
443
|
+
finally {
|
|
444
|
+
stopTimer?.();
|
|
445
|
+
getMetrics()?.toolCalls.inc({ tool: toolName, status: outcome });
|
|
446
|
+
releaseSlot(subjectKey);
|
|
447
|
+
}
|
|
448
|
+
}));
|
|
449
|
+
// Standard §8.7 — task lifecycle methods. Registered only when the `tasks` capability is enabled
|
|
450
|
+
// (and advertised), so when off these methods fall through to the SDK's -32601 (method not found).
|
|
451
|
+
if (tasksEnabled && taskStore) {
|
|
452
|
+
const requireOwnedTask = (taskId, subjectKey) => {
|
|
453
|
+
const record = taskStore.get(taskId);
|
|
454
|
+
if (!record || record.subjectKey !== subjectKey) {
|
|
455
|
+
// Do not leak whether the id exists for another subject — a uniform "not found".
|
|
456
|
+
throw new ResourceNotFoundError('Task not found', { reason: 'task_not_found', taskId });
|
|
457
|
+
}
|
|
458
|
+
return record;
|
|
459
|
+
};
|
|
460
|
+
// tasks/get — current task metadata (flat Task shape).
|
|
461
|
+
server.setRequestHandler(GetTaskRequestSchema, withRequestContext(async (request, extra) => {
|
|
462
|
+
const subjectKey = subjectKeyFromAuth(extra?.authInfo);
|
|
463
|
+
const record = requireOwnedTask(request.params.taskId, subjectKey);
|
|
464
|
+
return toTaskDto(record, taskStore.pollIntervalMs);
|
|
465
|
+
}));
|
|
466
|
+
// tasks/result — the underlying tools/call result once completed; status placeholder otherwise.
|
|
467
|
+
server.setRequestHandler(GetTaskPayloadRequestSchema, withRequestContext(async (request, extra) => {
|
|
468
|
+
const subjectKey = subjectKeyFromAuth(extra?.authInfo);
|
|
469
|
+
const record = requireOwnedTask(request.params.taskId, subjectKey);
|
|
470
|
+
if (record.status === 'completed') {
|
|
471
|
+
return record.result;
|
|
472
|
+
}
|
|
473
|
+
if (record.status === 'failed') {
|
|
474
|
+
return {
|
|
475
|
+
isError: true,
|
|
476
|
+
content: [{ type: 'text', text: record.statusMessage ?? 'Task failed' }],
|
|
477
|
+
structuredContent: { taskId: record.taskId, status: record.status },
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
if (record.status === 'cancelled') {
|
|
481
|
+
return {
|
|
482
|
+
isError: true,
|
|
483
|
+
content: [{ type: 'text', text: 'Task was cancelled' }],
|
|
484
|
+
structuredContent: { taskId: record.taskId, status: record.status },
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
// working / input_required — not finished yet; return a status placeholder per §8.7.
|
|
488
|
+
return {
|
|
489
|
+
content: [{ type: 'text', text: `Task ${record.taskId} is ${record.status}` }],
|
|
490
|
+
structuredContent: { taskId: record.taskId, status: record.status },
|
|
491
|
+
};
|
|
492
|
+
}));
|
|
493
|
+
// tasks/list — caller's own tasks, newest first, paginated (§8.4).
|
|
494
|
+
server.setRequestHandler(ListTasksRequestSchema, withRequestContext(async (request, extra) => {
|
|
495
|
+
const subjectKey = subjectKeyFromAuth(extra?.authInfo);
|
|
496
|
+
const records = taskStore.list(subjectKey);
|
|
497
|
+
const cursor = request.params?.cursor;
|
|
498
|
+
// Descending createdAt encoded as a zero-padded sort key so pagination stays stable.
|
|
499
|
+
const { page, nextCursor } = paginate(records, cursor, pageSize, (r) => `${(1e16 - r.createdAt).toString().padStart(17, '0')}-${r.taskId}`);
|
|
500
|
+
const tasks = page.map((r) => toTaskDto(r, taskStore.pollIntervalMs));
|
|
501
|
+
return nextCursor ? { tasks, nextCursor } : { tasks };
|
|
502
|
+
}));
|
|
503
|
+
// tasks/cancel — abort an active task; idempotent on already-finished tasks.
|
|
504
|
+
server.setRequestHandler(CancelTaskRequestSchema, withRequestContext(async (request, extra) => {
|
|
505
|
+
const subjectKey = subjectKeyFromAuth(extra?.authInfo);
|
|
506
|
+
const { taskId } = request.params;
|
|
507
|
+
const existing = requireOwnedTask(taskId, subjectKey);
|
|
508
|
+
const wasActive = !isTerminalTaskStatus(existing.status);
|
|
509
|
+
const updated = taskStore.cancel(taskId) ?? existing;
|
|
510
|
+
if (wasActive && updated.status === 'cancelled') {
|
|
511
|
+
getMetrics()?.tasks.inc({ status: 'cancelled' });
|
|
512
|
+
notifyTaskStatus(updated);
|
|
513
|
+
}
|
|
514
|
+
return toTaskDto(updated, taskStore.pollIntervalMs);
|
|
515
|
+
}));
|
|
516
|
+
}
|
|
517
|
+
// Handlers for prompts are registered only when the server actually has prompts (standard §8.2).
|
|
518
|
+
// When absent, prompts/list and prompts/get fall through to the SDK's -32601 (method not found).
|
|
519
|
+
if (hasPrompts) {
|
|
520
|
+
// Handler for listing available prompts (standard §8.4 — server-side pagination).
|
|
521
|
+
server.setRequestHandler(ListPromptsRequestSchema, withRequestContext(async (request, extra) => {
|
|
522
|
+
const result = await getPromptsList(ctx(extra));
|
|
523
|
+
const prompts = result.prompts.map((p) => {
|
|
524
|
+
const info = readDeprecation(p);
|
|
525
|
+
if (!info) {
|
|
526
|
+
return p;
|
|
527
|
+
}
|
|
528
|
+
assertDeprecationConsistency('prompt', p.name, info);
|
|
529
|
+
return { ...p, description: applyDeprecationToDescription(p.description, info) };
|
|
530
|
+
});
|
|
531
|
+
const cursor = request.params?.cursor;
|
|
532
|
+
const { page, nextCursor } = paginate(prompts, cursor, pageSize, (p) => p.name);
|
|
533
|
+
return nextCursor ? { prompts: page, nextCursor } : { prompts: page };
|
|
534
|
+
}));
|
|
535
|
+
// Handler for getting prompt content
|
|
536
|
+
server.setRequestHandler(GetPromptRequestSchema,
|
|
537
|
+
// @ts-ignore
|
|
538
|
+
withRequestContext(async (request, extra) => {
|
|
539
|
+
const promptName = request.params?.name;
|
|
540
|
+
if (promptName) {
|
|
541
|
+
const { prompts } = await getPromptsList(ctx(extra));
|
|
542
|
+
const prompt = prompts.find((p) => p.name === promptName);
|
|
543
|
+
warnDeprecatedUsage('prompt', promptName, readDeprecation(prompt));
|
|
544
|
+
}
|
|
545
|
+
return await getPrompt(request, ctx(extra));
|
|
546
|
+
}));
|
|
547
|
+
}
|
|
548
|
+
// Handler for listing available resources (standard §8.4 — server-side pagination).
|
|
549
|
+
server.setRequestHandler(ListResourcesRequestSchema, withRequestContext(async (request, extra) => {
|
|
550
|
+
const result = await getResourcesList(ctx(extra));
|
|
551
|
+
const resources = result.resources.map((r) => {
|
|
552
|
+
const info = readDeprecation(r);
|
|
553
|
+
if (!info) {
|
|
554
|
+
return r;
|
|
555
|
+
}
|
|
556
|
+
assertDeprecationConsistency('resource', r.uri, info);
|
|
557
|
+
return { ...r, description: applyDeprecationToDescription(r.description, info) };
|
|
558
|
+
});
|
|
559
|
+
const cursor = request.params?.cursor;
|
|
560
|
+
const { page, nextCursor } = paginate(resources, cursor, pageSize, (r) => r.uri);
|
|
561
|
+
return nextCursor ? { resources: page, nextCursor } : { resources: page };
|
|
562
|
+
}));
|
|
49
563
|
// Handler for reading resource content
|
|
50
|
-
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
51
|
-
|
|
52
|
-
|
|
564
|
+
server.setRequestHandler(ReadResourceRequestSchema, withRequestContext(async (request, extra) => {
|
|
565
|
+
const { uri } = request.params;
|
|
566
|
+
if (uri) {
|
|
567
|
+
const { resources } = await getResourcesList(ctx(extra));
|
|
568
|
+
const resource = resources.find((r) => r.uri === uri);
|
|
569
|
+
warnDeprecatedUsage('resource', uri, readDeprecation(resource));
|
|
570
|
+
}
|
|
571
|
+
return (await getResource(uri, ctx(extra)));
|
|
572
|
+
}));
|
|
573
|
+
// Optional MAY: resources/templates/list — empty list if no templates configured.
|
|
574
|
+
if (templatesEnabled) {
|
|
575
|
+
server.setRequestHandler(ListResourceTemplatesRequestSchema, withRequestContext(async (request, extra) => {
|
|
576
|
+
const templates = await getResourceTemplatesList(ctx(extra));
|
|
577
|
+
const cursor = request.params?.cursor;
|
|
578
|
+
const { page, nextCursor } = paginate(templates, cursor, pageSize, (t) => t.uriTemplate ?? t.name ?? '');
|
|
579
|
+
return nextCursor ? { resourceTemplates: page, nextCursor } : { resourceTemplates: page };
|
|
580
|
+
}));
|
|
581
|
+
}
|
|
582
|
+
// Standard §8.2 (MAY) — completion/complete. Registered only when opt-in config + provider are
|
|
583
|
+
// both present, so the capability advertisement and the handler stay in lock-step.
|
|
584
|
+
if (completionsEnabled) {
|
|
585
|
+
const completionProvider = projectData.completionProvider;
|
|
586
|
+
server.setRequestHandler(CompleteRequestSchema, withRequestContext(async (request) => {
|
|
587
|
+
const params = (request.params ?? {});
|
|
588
|
+
const raw = await completionProvider({
|
|
589
|
+
ref: params.ref,
|
|
590
|
+
argument: params.argument,
|
|
591
|
+
...(params.context ? { context: params.context } : {}),
|
|
592
|
+
});
|
|
593
|
+
const all = Array.isArray(raw) ? raw.map(String) : [];
|
|
594
|
+
// MCP caps completion results at 100 values; `hasMore` flags truncation.
|
|
595
|
+
const values = all.slice(0, 100);
|
|
596
|
+
return { completion: { values, total: all.length, hasMore: all.length > values.length } };
|
|
597
|
+
}));
|
|
598
|
+
}
|
|
599
|
+
// Optional MAY: resources/subscribe + resources/unsubscribe — opt-in via config.
|
|
600
|
+
if (subscribeEnabled) {
|
|
601
|
+
server.setRequestHandler(SubscribeRequestSchema, async (request) => {
|
|
602
|
+
const uri = request.params?.uri;
|
|
603
|
+
subscribeResource(server, uri);
|
|
604
|
+
return {};
|
|
605
|
+
});
|
|
606
|
+
server.setRequestHandler(UnsubscribeRequestSchema, async (request) => {
|
|
607
|
+
const uri = request.params?.uri;
|
|
608
|
+
unsubscribeResource(server, uri);
|
|
609
|
+
return {};
|
|
610
|
+
});
|
|
611
|
+
}
|
|
53
612
|
return server;
|
|
54
613
|
}
|
|
55
614
|
//# sourceMappingURL=create-mcp-server.js.map
|