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.
Files changed (200) hide show
  1. package/README.md +5 -0
  2. package/cli-template/.dockerignore +16 -0
  3. package/cli-template/.gitlab-ci.yml +135 -0
  4. package/cli-template/AGENTS.md +1 -0
  5. package/cli-template/CHANGELOG.md +64 -0
  6. package/cli-template/FA-MCP-SDK-DOC/00-FA-MCP-SDK-index.md +27 -4
  7. package/cli-template/FA-MCP-SDK-DOC/02-1-tools-and-api.md +195 -0
  8. package/cli-template/FA-MCP-SDK-DOC/02-2-prompts-and-resources.md +172 -9
  9. package/cli-template/FA-MCP-SDK-DOC/03-configuration.md +170 -12
  10. package/cli-template/FA-MCP-SDK-DOC/04-authentication.md +158 -8
  11. package/cli-template/FA-MCP-SDK-DOC/06-utilities.md +67 -6
  12. package/cli-template/FA-MCP-SDK-DOC/07-testing-and-operations.md +31 -15
  13. package/cli-template/FA-MCP-SDK-DOC/10-mcp-apps.md +1 -1
  14. package/cli-template/FA-MCP-SDK-DOC/11-public-contract.md +342 -0
  15. package/cli-template/README.md +37 -0
  16. package/cli-template/deploy/docker/.env.example +10 -0
  17. package/cli-template/deploy/docker/Dockerfile +44 -0
  18. package/cli-template/deploy/docker/Dockerfile.local +29 -0
  19. package/cli-template/deploy/docker/README.md +94 -0
  20. package/cli-template/deploy/docker/config/local.docker.yaml +14 -0
  21. package/cli-template/deploy/docker/docker-compose.yml +31 -0
  22. package/cli-template/deploy/gitlab-runner/.env.example +16 -0
  23. package/cli-template/deploy/gitlab-runner/README.md +65 -0
  24. package/cli-template/deploy/gitlab-runner/config/config.toml.template +26 -0
  25. package/cli-template/deploy/gitlab-runner/docker-compose.yml +39 -0
  26. package/cli-template/deploy/gitlab-runner/entrypoint.sh +27 -0
  27. package/cli-template/deploy/gitlab-runner/start.sh +47 -0
  28. package/cli-template/gitignore +96 -95
  29. package/cli-template/package.json +1 -1
  30. package/config/_local.yaml +73 -11
  31. package/config/custom-environment-variables.yaml +102 -0
  32. package/config/default.yaml +164 -11
  33. package/config/local.yaml +20 -19
  34. package/dist/core/_types_/config.d.ts +119 -0
  35. package/dist/core/_types_/config.d.ts.map +1 -1
  36. package/dist/core/_types_/types.d.ts +137 -4
  37. package/dist/core/_types_/types.d.ts.map +1 -1
  38. package/dist/core/agent-tester/agent-tester-router.d.ts.map +1 -1
  39. package/dist/core/agent-tester/agent-tester-router.js +25 -11
  40. package/dist/core/agent-tester/agent-tester-router.js.map +1 -1
  41. package/dist/core/agent-tester/services/TesterMcpClientService.d.ts.map +1 -1
  42. package/dist/core/agent-tester/services/TesterMcpClientService.js +6 -4
  43. package/dist/core/agent-tester/services/TesterMcpClientService.js.map +1 -1
  44. package/dist/core/auth/admin-auth.js +4 -4
  45. package/dist/core/auth/admin-auth.js.map +1 -1
  46. package/dist/core/auth/agent-tester-auth.d.ts +1 -1
  47. package/dist/core/auth/agent-tester-auth.d.ts.map +1 -1
  48. package/dist/core/auth/agent-tester-auth.js +8 -4
  49. package/dist/core/auth/agent-tester-auth.js.map +1 -1
  50. package/dist/core/auth/auth-profile.d.ts +38 -0
  51. package/dist/core/auth/auth-profile.d.ts.map +1 -0
  52. package/dist/core/auth/auth-profile.js +101 -0
  53. package/dist/core/auth/auth-profile.js.map +1 -0
  54. package/dist/core/auth/jwt-v2.d.ts +27 -0
  55. package/dist/core/auth/jwt-v2.d.ts.map +1 -0
  56. package/dist/core/auth/jwt-v2.js +180 -0
  57. package/dist/core/auth/jwt-v2.js.map +1 -0
  58. package/dist/core/auth/jwt.d.ts +27 -13
  59. package/dist/core/auth/jwt.d.ts.map +1 -1
  60. package/dist/core/auth/jwt.js +36 -13
  61. package/dist/core/auth/jwt.js.map +1 -1
  62. package/dist/core/auth/key-resolver.d.ts +74 -0
  63. package/dist/core/auth/key-resolver.d.ts.map +1 -0
  64. package/dist/core/auth/key-resolver.js +330 -0
  65. package/dist/core/auth/key-resolver.js.map +1 -0
  66. package/dist/core/auth/middleware.d.ts.map +1 -1
  67. package/dist/core/auth/middleware.js +66 -0
  68. package/dist/core/auth/middleware.js.map +1 -1
  69. package/dist/core/auth/multi-auth.d.ts +1 -1
  70. package/dist/core/auth/multi-auth.d.ts.map +1 -1
  71. package/dist/core/auth/multi-auth.js +7 -7
  72. package/dist/core/auth/multi-auth.js.map +1 -1
  73. package/dist/core/auth/token-generator/server.js +4 -4
  74. package/dist/core/auth/token-generator/server.js.map +1 -1
  75. package/dist/core/auth/types.d.ts +5 -0
  76. package/dist/core/auth/types.d.ts.map +1 -1
  77. package/dist/core/db/pg-db.d.ts +7 -0
  78. package/dist/core/db/pg-db.d.ts.map +1 -1
  79. package/dist/core/db/pg-db.js +54 -3
  80. package/dist/core/db/pg-db.js.map +1 -1
  81. package/dist/core/errors/BaseMcpError.d.ts +21 -1
  82. package/dist/core/errors/BaseMcpError.d.ts.map +1 -1
  83. package/dist/core/errors/BaseMcpError.js +20 -1
  84. package/dist/core/errors/BaseMcpError.js.map +1 -1
  85. package/dist/core/errors/ValidationError.d.ts +5 -0
  86. package/dist/core/errors/ValidationError.d.ts.map +1 -1
  87. package/dist/core/errors/ValidationError.js +6 -1
  88. package/dist/core/errors/ValidationError.js.map +1 -1
  89. package/dist/core/errors/errors.d.ts +31 -3
  90. package/dist/core/errors/errors.d.ts.map +1 -1
  91. package/dist/core/errors/errors.js +86 -6
  92. package/dist/core/errors/errors.js.map +1 -1
  93. package/dist/core/errors/specific-errors.d.ts +54 -0
  94. package/dist/core/errors/specific-errors.d.ts.map +1 -0
  95. package/dist/core/errors/specific-errors.js +82 -0
  96. package/dist/core/errors/specific-errors.js.map +1 -0
  97. package/dist/core/index.d.ts +10 -2
  98. package/dist/core/index.d.ts.map +1 -1
  99. package/dist/core/index.js +9 -1
  100. package/dist/core/index.js.map +1 -1
  101. package/dist/core/init-mcp-server.d.ts.map +1 -1
  102. package/dist/core/init-mcp-server.js +39 -0
  103. package/dist/core/init-mcp-server.js.map +1 -1
  104. package/dist/core/mcp/create-mcp-server.d.ts +12 -6
  105. package/dist/core/mcp/create-mcp-server.d.ts.map +1 -1
  106. package/dist/core/mcp/create-mcp-server.js +592 -33
  107. package/dist/core/mcp/create-mcp-server.js.map +1 -1
  108. package/dist/core/mcp/debug-trace.d.ts +3 -1
  109. package/dist/core/mcp/debug-trace.d.ts.map +1 -1
  110. package/dist/core/mcp/debug-trace.js +17 -2
  111. package/dist/core/mcp/debug-trace.js.map +1 -1
  112. package/dist/core/mcp/deprecation.d.ts +31 -0
  113. package/dist/core/mcp/deprecation.d.ts.map +1 -0
  114. package/dist/core/mcp/deprecation.js +96 -0
  115. package/dist/core/mcp/deprecation.js.map +1 -0
  116. package/dist/core/mcp/mcp-logging.d.ts +32 -0
  117. package/dist/core/mcp/mcp-logging.d.ts.map +1 -0
  118. package/dist/core/mcp/mcp-logging.js +97 -0
  119. package/dist/core/mcp/mcp-logging.js.map +1 -0
  120. package/dist/core/mcp/pagination.d.ts +13 -0
  121. package/dist/core/mcp/pagination.d.ts.map +1 -0
  122. package/dist/core/mcp/pagination.js +50 -0
  123. package/dist/core/mcp/pagination.js.map +1 -0
  124. package/dist/core/mcp/prompts.d.ts +5 -1
  125. package/dist/core/mcp/prompts.d.ts.map +1 -1
  126. package/dist/core/mcp/prompts.js +3 -1
  127. package/dist/core/mcp/prompts.js.map +1 -1
  128. package/dist/core/mcp/resources.d.ts +9 -0
  129. package/dist/core/mcp/resources.d.ts.map +1 -1
  130. package/dist/core/mcp/resources.js +158 -11
  131. package/dist/core/mcp/resources.js.map +1 -1
  132. package/dist/core/mcp/server-stdio.d.ts +7 -1
  133. package/dist/core/mcp/server-stdio.d.ts.map +1 -1
  134. package/dist/core/mcp/server-stdio.js +8 -3
  135. package/dist/core/mcp/server-stdio.js.map +1 -1
  136. package/dist/core/mcp/task-store.d.ts +97 -0
  137. package/dist/core/mcp/task-store.d.ts.map +1 -0
  138. package/dist/core/mcp/task-store.js +175 -0
  139. package/dist/core/mcp/task-store.js.map +1 -0
  140. package/dist/core/mcp/tool-limits.d.ts +22 -0
  141. package/dist/core/mcp/tool-limits.d.ts.map +1 -0
  142. package/dist/core/mcp/tool-limits.js +115 -0
  143. package/dist/core/mcp/tool-limits.js.map +1 -0
  144. package/dist/core/mcp/validate-tool-args.d.ts +16 -0
  145. package/dist/core/mcp/validate-tool-args.d.ts.map +1 -0
  146. package/dist/core/mcp/validate-tool-args.js +67 -0
  147. package/dist/core/mcp/validate-tool-args.js.map +1 -0
  148. package/dist/core/mcp/validate-tool-names.d.ts +11 -0
  149. package/dist/core/mcp/validate-tool-names.d.ts.map +1 -0
  150. package/dist/core/mcp/validate-tool-names.js +23 -0
  151. package/dist/core/mcp/validate-tool-names.js.map +1 -0
  152. package/dist/core/metrics/metrics.d.ts +45 -0
  153. package/dist/core/metrics/metrics.d.ts.map +1 -0
  154. package/dist/core/metrics/metrics.js +119 -0
  155. package/dist/core/metrics/metrics.js.map +1 -0
  156. package/dist/core/utils/mask-sensitive.d.ts +44 -0
  157. package/dist/core/utils/mask-sensitive.d.ts.map +1 -0
  158. package/dist/core/utils/mask-sensitive.js +64 -0
  159. package/dist/core/utils/mask-sensitive.js.map +1 -0
  160. package/dist/core/utils/testing/McpHttpClient.d.ts +8 -33
  161. package/dist/core/utils/testing/McpHttpClient.d.ts.map +1 -1
  162. package/dist/core/utils/testing/McpHttpClient.js +8 -74
  163. package/dist/core/utils/testing/McpHttpClient.js.map +1 -1
  164. package/dist/core/utils/testing/McpStreamableHttpClient.d.ts +24 -30
  165. package/dist/core/utils/testing/McpStreamableHttpClient.d.ts.map +1 -1
  166. package/dist/core/utils/testing/McpStreamableHttpClient.js +36 -198
  167. package/dist/core/utils/testing/McpStreamableHttpClient.js.map +1 -1
  168. package/dist/core/utils/utils.d.ts.map +1 -1
  169. package/dist/core/utils/utils.js +2 -0
  170. package/dist/core/utils/utils.js.map +1 -1
  171. package/dist/core/web/admin-router.js +3 -3
  172. package/dist/core/web/admin-router.js.map +1 -1
  173. package/dist/core/web/cors.d.ts +9 -1
  174. package/dist/core/web/cors.d.ts.map +1 -1
  175. package/dist/core/web/cors.js +26 -5
  176. package/dist/core/web/cors.js.map +1 -1
  177. package/dist/core/web/event-store.d.ts +33 -0
  178. package/dist/core/web/event-store.d.ts.map +1 -0
  179. package/dist/core/web/event-store.js +65 -0
  180. package/dist/core/web/event-store.js.map +1 -0
  181. package/dist/core/web/oauth-router.d.ts +37 -0
  182. package/dist/core/web/oauth-router.d.ts.map +1 -0
  183. package/dist/core/web/oauth-router.js +207 -0
  184. package/dist/core/web/oauth-router.js.map +1 -0
  185. package/dist/core/web/request-id.d.ts +44 -0
  186. package/dist/core/web/request-id.d.ts.map +1 -0
  187. package/dist/core/web/request-id.js +82 -0
  188. package/dist/core/web/request-id.js.map +1 -0
  189. package/dist/core/web/server-http.d.ts.map +1 -1
  190. package/dist/core/web/server-http.js +322 -182
  191. package/dist/core/web/server-http.js.map +1 -1
  192. package/package.json +15 -2
  193. package/scripts/claude-2-agents-symlink.js +10 -1
  194. package/scripts/generate-jwt.js +129 -51
  195. package/src/template/custom-resources.ts +14 -0
  196. package/src/template/prompts/custom-prompts.ts +4 -0
  197. package/src/template/tools/handle-tool-call.ts +59 -3
  198. package/src/template/tools/tools.ts +92 -31
  199. package/src/tests/mcp/test-http.js +1 -1
  200. 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 { getTools } from '../utils/utils.js';
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
- * Tool/list/read handlers below all read `server.getClientCapabilities()` on
11
- * each call so they always pass the **current** capabilities to user code
12
- * by the time `tools/call` arrives, the initialize handshake has completed
13
- * and the call returns the host's reported capabilities (including any
14
- * `extensions["io.modelcontextprotocol/ui"]` payload for MCP Apps).
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
- const ctx = () => {
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 caps ? { transport: 'stdio', clientCapabilities: caps } : { transport: 'stdio' };
131
+ return {
132
+ transport: transportType,
133
+ ...(headers ? { headers } : {}),
134
+ ...(payload ? { payload } : {}),
135
+ ...(caps ? { clientCapabilities: caps } : {}),
136
+ };
30
137
  };
31
- // Handler for listing available tools
32
- server.setRequestHandler(ListToolsRequestSchema, async () => {
33
- const tools = await getTools(ctx());
34
- return { tools };
35
- });
36
- // Handler for tool execution
37
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
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
- return (await toolHandler({ ...request.params, ...ctx() }));
40
- });
41
- // Handler for listing available prompts
42
- server.setRequestHandler(ListPromptsRequestSchema, async () => getPromptsList(ctx()));
43
- // Handler for getting prompt content
44
- server.setRequestHandler(GetPromptRequestSchema,
45
- // @ts-ignore
46
- async (request) => await getPrompt(request, ctx()));
47
- // Handler for listing available resources
48
- server.setRequestHandler(ListResourcesRequestSchema, async () => getResourcesList(ctx()));
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
- return (await getResource(request.params.uri, ctx()));
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