@ttoss/http-server-mcp 0.34.5 → 0.36.0

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 CHANGED
@@ -1130,6 +1130,33 @@ Either way the authorization flow works. Driving the official MCP client SDK aga
1130
1130
 
1131
1131
  **Whether the handshake should be public at all is a separate question.** `publicMethods: []` closes both eras, and the authorization flow still works — RFC 9728 discovery is driven by the challenge itself, so a client authenticates from its very first request instead of one step later.
1132
1132
 
1133
+ ### Holding the surface to a budget
1134
+
1135
+ Every tool's name, description and input schema, and the server's instructions, are loaded into every session before the first call. `checkMcpSurface` compares them with a budget and answers the limits they exceed, so a test fails naming the tool that grew:
1136
+
1137
+ ```typescript
1138
+ import {
1139
+ checkMcpSurface,
1140
+ findUnknownToolMentions,
1141
+ } from '@ttoss/http-server-mcp';
1142
+
1143
+ test('the surface stays inside its budget', () => {
1144
+ expect(
1145
+ checkMcpSurface({
1146
+ tools, // { name, description?, inputSchema? }[], e.g. from openApiToToolDefinitions
1147
+ instructions,
1148
+ budget: { total: 70_000, perTool: 10_000 },
1149
+ })
1150
+ ).toEqual([]);
1151
+ });
1152
+
1153
+ test('every tool the instructions name exists', () => {
1154
+ expect(findUnknownToolMentions({ instructions, tools })).toEqual([]);
1155
+ });
1156
+ ```
1157
+
1158
+ Sizes are characters of `JSON.stringify({ name, description, inputSchema })`; `measureMcpSurface` answers them without judging. `total` and `perTool` have no default — they are the product's choice. `description` (each tool's description alone) and `instructions` default to `2048`, where [Claude Code truncates both](https://code.claude.com/docs/en/mcp) without saying so; a schema is never cut, which is why `perTool` cannot stand in for `description`. `findUnknownToolMentions` reads backticked names with a `-` or `_` (`` `get-next` ``); pass `pattern` when the instructions name tools another way.
1159
+
1133
1160
  ## AWS Lambda Deployment
1134
1161
 
1135
1162
  The default **stateless** mode (see [Stateless vs stateful mode](#stateless-vs-stateful-mode)) is built for serverless: a fresh transport is created per request, nothing is kept in memory between invocations, and responses are plain JSON (no SSE) — exactly the request/response shape API Gateway and Lambda Function URLs expect.
package/dist/index.cjs CHANGED
@@ -212,6 +212,148 @@ var createMcpRequestServer = ({
212
212
  };
213
213
  };
214
214
 
215
+ //#endregion
216
+ //#region src/mcpSurface.ts
217
+ /**
218
+ * Claude Code truncates a server's instructions at this many characters
219
+ * without telling anyone, so a rule past it never reaches an agent
220
+ * (https://code.claude.com/docs/en/mcp; a user can change it with
221
+ * `CLAUDE_CODE_MAX_MCP_DESCRIPTION_LENGTH`).
222
+ */
223
+ var DEFAULT_INSTRUCTIONS_LIMIT = 2048;
224
+ /**
225
+ * Claude Code truncates each tool description at the same length as the
226
+ * instructions, from the same documentation, and just as silently.
227
+ */
228
+ var DEFAULT_DESCRIPTION_LIMIT = 2048;
229
+ var sizeOf = tool => {
230
+ return JSON.stringify({
231
+ name: tool.name,
232
+ description: tool.description,
233
+ inputSchema: tool.inputSchema
234
+ }).length;
235
+ };
236
+ /**
237
+ * Measures what an MCP server costs every session that connects to it: each
238
+ * tool's name, description and input schema, and the instructions.
239
+ *
240
+ * It does not judge; {@link checkMcpSurface} compares the same numbers with a
241
+ * budget.
242
+ */
243
+ var measureMcpSurface = ({
244
+ tools,
245
+ instructions
246
+ }) => {
247
+ const sizes = tools.map(tool => {
248
+ return {
249
+ name: tool.name,
250
+ size: sizeOf(tool)
251
+ };
252
+ }).sort((a, b) => {
253
+ return b.size - a.size;
254
+ });
255
+ return {
256
+ total: sizes.reduce((sum, entry) => {
257
+ return sum + entry.size;
258
+ }, 0),
259
+ tools: sizes,
260
+ instructions: instructions?.length ?? 0
261
+ };
262
+ };
263
+ var checkDescriptions = ({
264
+ tools,
265
+ limit
266
+ }) => {
267
+ return tools.flatMap(tool => {
268
+ const size = tool.description?.length ?? 0;
269
+ return size > limit ? [{
270
+ kind: "description",
271
+ name: tool.name,
272
+ size,
273
+ limit
274
+ }] : [];
275
+ });
276
+ };
277
+ /**
278
+ * Every limit an MCP surface exceeds, or `[]` when it fits.
279
+ *
280
+ * Returns rather than throws, so a test's `expect(...).toEqual([])` prints
281
+ * which tool went over and by how much. Meant for a test, not for boot: a
282
+ * server refusing to start over a few hundred bytes is worse than a red test.
283
+ *
284
+ * @example
285
+ * ```typescript
286
+ * expect(
287
+ * checkMcpSurface({
288
+ * tools,
289
+ * instructions,
290
+ * budget: { total: 70_000, perTool: 10_000 },
291
+ * })
292
+ * ).toEqual([]);
293
+ * ```
294
+ */
295
+ var checkMcpSurface = ({
296
+ tools,
297
+ instructions,
298
+ budget = {}
299
+ }) => {
300
+ const report = measureMcpSurface({
301
+ tools,
302
+ instructions
303
+ });
304
+ const violations = [];
305
+ if (budget.total !== void 0 && report.total > budget.total) violations.push({
306
+ kind: "total",
307
+ size: report.total,
308
+ limit: budget.total
309
+ });
310
+ if (budget.perTool !== void 0) {
311
+ for (const {
312
+ name,
313
+ size
314
+ } of report.tools) if (size > budget.perTool) violations.push({
315
+ kind: "per_tool",
316
+ name,
317
+ size,
318
+ limit: budget.perTool
319
+ });
320
+ }
321
+ violations.push(...checkDescriptions({
322
+ tools,
323
+ limit: budget.description ?? 2048
324
+ }));
325
+ const instructionsLimit = budget.instructions ?? 2048;
326
+ if (report.instructions > instructionsLimit) violations.push({
327
+ kind: "instructions",
328
+ size: report.instructions,
329
+ limit: instructionsLimit
330
+ });
331
+ return violations;
332
+ };
333
+ var TOOL_MENTION = /`([a-z][a-z0-9]*(?:[-_][a-z0-9]+)+)`/g;
334
+ /**
335
+ * The tool names the instructions mention that the surface does not have.
336
+ *
337
+ * Instructions are read once, at connect: a renamed tool they still name is
338
+ * one every connected agent is taught to call and then told is "not found".
339
+ * Pass `pattern` (a global RegExp whose first group is the name) when the
340
+ * instructions name tools some other way.
341
+ */
342
+ var findUnknownToolMentions = ({
343
+ instructions,
344
+ tools,
345
+ pattern = TOOL_MENTION
346
+ }) => {
347
+ const known = new Set(tools.map(tool => {
348
+ return tool.name;
349
+ }));
350
+ return [...new Set([...instructions.matchAll(pattern)].map(([, name]) => {
351
+ return name;
352
+ }))].filter(name => {
353
+ return !known.has(name);
354
+ });
355
+ };
356
+
215
357
  //#endregion
216
358
  //#region src/registerAppResource.ts
217
359
  /**
@@ -1571,6 +1713,8 @@ var createMcpRouter = (server, options = {}) => {
1571
1713
  };
1572
1714
 
1573
1715
  //#endregion
1716
+ exports.DEFAULT_DESCRIPTION_LIMIT = DEFAULT_DESCRIPTION_LIMIT;
1717
+ exports.DEFAULT_INSTRUCTIONS_LIMIT = DEFAULT_INSTRUCTIONS_LIMIT;
1574
1718
  Object.defineProperty(exports, 'InMemoryServerEventBus', {
1575
1719
  enumerable: true,
1576
1720
  get: function () {
@@ -1586,14 +1730,17 @@ Object.defineProperty(exports, 'McpServer', {
1586
1730
  exports.UI_EXTENSION_ID = UI_EXTENSION_ID;
1587
1731
  exports.UI_RESOURCE_MIME_TYPE = UI_RESOURCE_MIME_TYPE;
1588
1732
  exports.apiCall = apiCall;
1733
+ exports.checkMcpSurface = checkMcpSurface;
1589
1734
  exports.checkScopes = checkScopes;
1590
1735
  exports.createMcpRouter = createMcpRouter;
1591
1736
  exports.createToolGate = createToolGate;
1592
1737
  exports.defineTool = defineTool;
1593
1738
  exports.errorBodyMessage = errorBodyMessage;
1739
+ exports.findUnknownToolMentions = findUnknownToolMentions;
1594
1740
  exports.getApiHeaders = getApiHeaders;
1595
1741
  exports.getIdentity = getIdentity;
1596
1742
  exports.getRequestLocale = getRequestLocale;
1743
+ exports.measureMcpSurface = measureMcpSurface;
1597
1744
  exports.rankTools = rankTools;
1598
1745
  exports.registerAppResource = registerAppResource;
1599
1746
  exports.registerTools = registerTools;
package/dist/index.d.cts CHANGED
@@ -886,6 +886,142 @@ interface DeferToolsOptions {
886
886
  }) => boolean;
887
887
  }
888
888
  //#endregion
889
+ //#region src/mcpSurface.d.ts
890
+ /**
891
+ * What a client loads for one tool before the first call: the three fields a
892
+ * `tools/list` answers. `openApiToToolDefinitions` returns this shape, and so
893
+ * does a `tools/list` result.
894
+ */
895
+ interface McpSurfaceTool {
896
+ name: string;
897
+ description?: string;
898
+ inputSchema?: unknown;
899
+ }
900
+ /** The size of each part of an MCP surface, in characters of JSON. */
901
+ interface McpSurfaceReport {
902
+ /** Sum of every tool's size. Instructions are not included. */
903
+ total: number;
904
+ /** One entry per tool, largest first. */
905
+ tools: Array<{
906
+ name: string;
907
+ size: number;
908
+ }>;
909
+ /** Length of the server instructions, or `0` when there are none. */
910
+ instructions: number;
911
+ }
912
+ /**
913
+ * The limits a surface is checked against. `description` and `instructions`
914
+ * have defaults, because a client cuts them; `total` and `perTool` are a
915
+ * product's own choice, not a property of any client.
916
+ */
917
+ interface McpSurfaceBudget {
918
+ /** Ceiling on the sum of every tool's size. */
919
+ total?: number;
920
+ /** Ceiling on any one tool's size. */
921
+ perTool?: number;
922
+ /**
923
+ * Ceiling on any one tool's description length. Unlike `perTool`, it leaves
924
+ * out the name and input schema, which no client cuts.
925
+ * @default 2048
926
+ */
927
+ description?: number;
928
+ /**
929
+ * Ceiling on the instructions' length.
930
+ * @default 2048
931
+ */
932
+ instructions?: number;
933
+ }
934
+ /** One limit a surface exceeds. */
935
+ type McpSurfaceViolation = {
936
+ kind: 'total';
937
+ size: number;
938
+ limit: number;
939
+ } | {
940
+ kind: 'per_tool';
941
+ name: string;
942
+ size: number;
943
+ limit: number;
944
+ } | {
945
+ kind: 'description';
946
+ name: string;
947
+ size: number;
948
+ limit: number;
949
+ } | {
950
+ kind: 'instructions';
951
+ size: number;
952
+ limit: number;
953
+ };
954
+ /**
955
+ * Claude Code truncates a server's instructions at this many characters
956
+ * without telling anyone, so a rule past it never reaches an agent
957
+ * (https://code.claude.com/docs/en/mcp; a user can change it with
958
+ * `CLAUDE_CODE_MAX_MCP_DESCRIPTION_LENGTH`).
959
+ */
960
+ declare const DEFAULT_INSTRUCTIONS_LIMIT = 2048;
961
+ /**
962
+ * Claude Code truncates each tool description at the same length as the
963
+ * instructions, from the same documentation, and just as silently.
964
+ */
965
+ declare const DEFAULT_DESCRIPTION_LIMIT = 2048;
966
+ /**
967
+ * Measures what an MCP server costs every session that connects to it: each
968
+ * tool's name, description and input schema, and the instructions.
969
+ *
970
+ * It does not judge; {@link checkMcpSurface} compares the same numbers with a
971
+ * budget.
972
+ */
973
+ declare const measureMcpSurface: ({
974
+ tools,
975
+ instructions
976
+ }: {
977
+ tools: McpSurfaceTool[];
978
+ instructions?: string;
979
+ }) => McpSurfaceReport;
980
+ /**
981
+ * Every limit an MCP surface exceeds, or `[]` when it fits.
982
+ *
983
+ * Returns rather than throws, so a test's `expect(...).toEqual([])` prints
984
+ * which tool went over and by how much. Meant for a test, not for boot: a
985
+ * server refusing to start over a few hundred bytes is worse than a red test.
986
+ *
987
+ * @example
988
+ * ```typescript
989
+ * expect(
990
+ * checkMcpSurface({
991
+ * tools,
992
+ * instructions,
993
+ * budget: { total: 70_000, perTool: 10_000 },
994
+ * })
995
+ * ).toEqual([]);
996
+ * ```
997
+ */
998
+ declare const checkMcpSurface: ({
999
+ tools,
1000
+ instructions,
1001
+ budget
1002
+ }: {
1003
+ tools: McpSurfaceTool[];
1004
+ instructions?: string;
1005
+ budget?: McpSurfaceBudget;
1006
+ }) => McpSurfaceViolation[];
1007
+ /**
1008
+ * The tool names the instructions mention that the surface does not have.
1009
+ *
1010
+ * Instructions are read once, at connect: a renamed tool they still name is
1011
+ * one every connected agent is taught to call and then told is "not found".
1012
+ * Pass `pattern` (a global RegExp whose first group is the name) when the
1013
+ * instructions name tools some other way.
1014
+ */
1015
+ declare const findUnknownToolMentions: ({
1016
+ instructions,
1017
+ tools,
1018
+ pattern
1019
+ }: {
1020
+ instructions: string;
1021
+ tools: Array<Pick<McpSurfaceTool, "name">>;
1022
+ pattern?: RegExp;
1023
+ }) => string[];
1024
+ //#endregion
889
1025
  //#region src/registerAppResource.d.ts
890
1026
  /**
891
1027
  * Identifier of the MCP Apps extension (`io.modelcontextprotocol/ui`), used as
@@ -1711,4 +1847,4 @@ interface McpRouterOptions {
1711
1847
  */
1712
1848
  declare const createMcpRouter: (server: McpServer$1, options?: McpRouterOptions) => McpRouter;
1713
1849
  //#endregion
1714
- export { ApiCallOptions, CognitoUserPoolConfig, type CreateToolGateOptions, type DeferToolsOptions, type DefineToolParams, type GateToolParams, InMemoryServerEventBus, type JsonObjectSchema, McpAuthOptions, McpRouter, McpRouterOptions, McpServer, type RegisterAppResourceParams, type RegisterToolsParams, type RegisteredAppResource, type ServerEvent, type ServerEventBus, type ServerNotifier, type Tool, type ToolCallContext, type ToolCallGate, type ToolGate, type ToolIdentity, type ToolMetaParams, type ToolSchema, type ToolSearchArgs, UI_EXTENSION_ID, UI_RESOURCE_MIME_TYPE, type UiResourceCsp, type UiResourceMeta, type UiResourcePermissions, type UiToolVisibility, apiCall, checkScopes, createMcpRouter, createToolGate, defineTool, errorBodyMessage, getApiHeaders, getIdentity, getRequestLocale, rankTools, registerAppResource, registerTools, z };
1850
+ export { ApiCallOptions, CognitoUserPoolConfig, type CreateToolGateOptions, DEFAULT_DESCRIPTION_LIMIT, DEFAULT_INSTRUCTIONS_LIMIT, type DeferToolsOptions, type DefineToolParams, type GateToolParams, InMemoryServerEventBus, type JsonObjectSchema, McpAuthOptions, McpRouter, McpRouterOptions, McpServer, type McpSurfaceBudget, type McpSurfaceReport, type McpSurfaceTool, type McpSurfaceViolation, type RegisterAppResourceParams, type RegisterToolsParams, type RegisteredAppResource, type ServerEvent, type ServerEventBus, type ServerNotifier, type Tool, type ToolCallContext, type ToolCallGate, type ToolGate, type ToolIdentity, type ToolMetaParams, type ToolSchema, type ToolSearchArgs, UI_EXTENSION_ID, UI_RESOURCE_MIME_TYPE, type UiResourceCsp, type UiResourceMeta, type UiResourcePermissions, type UiToolVisibility, apiCall, checkMcpSurface, checkScopes, createMcpRouter, createToolGate, defineTool, errorBodyMessage, findUnknownToolMentions, getApiHeaders, getIdentity, getRequestLocale, measureMcpSurface, rankTools, registerAppResource, registerTools, z };
package/dist/index.d.mts CHANGED
@@ -887,6 +887,142 @@ interface DeferToolsOptions {
887
887
  }) => boolean;
888
888
  }
889
889
  //#endregion
890
+ //#region src/mcpSurface.d.ts
891
+ /**
892
+ * What a client loads for one tool before the first call: the three fields a
893
+ * `tools/list` answers. `openApiToToolDefinitions` returns this shape, and so
894
+ * does a `tools/list` result.
895
+ */
896
+ interface McpSurfaceTool {
897
+ name: string;
898
+ description?: string;
899
+ inputSchema?: unknown;
900
+ }
901
+ /** The size of each part of an MCP surface, in characters of JSON. */
902
+ interface McpSurfaceReport {
903
+ /** Sum of every tool's size. Instructions are not included. */
904
+ total: number;
905
+ /** One entry per tool, largest first. */
906
+ tools: Array<{
907
+ name: string;
908
+ size: number;
909
+ }>;
910
+ /** Length of the server instructions, or `0` when there are none. */
911
+ instructions: number;
912
+ }
913
+ /**
914
+ * The limits a surface is checked against. `description` and `instructions`
915
+ * have defaults, because a client cuts them; `total` and `perTool` are a
916
+ * product's own choice, not a property of any client.
917
+ */
918
+ interface McpSurfaceBudget {
919
+ /** Ceiling on the sum of every tool's size. */
920
+ total?: number;
921
+ /** Ceiling on any one tool's size. */
922
+ perTool?: number;
923
+ /**
924
+ * Ceiling on any one tool's description length. Unlike `perTool`, it leaves
925
+ * out the name and input schema, which no client cuts.
926
+ * @default 2048
927
+ */
928
+ description?: number;
929
+ /**
930
+ * Ceiling on the instructions' length.
931
+ * @default 2048
932
+ */
933
+ instructions?: number;
934
+ }
935
+ /** One limit a surface exceeds. */
936
+ type McpSurfaceViolation = {
937
+ kind: 'total';
938
+ size: number;
939
+ limit: number;
940
+ } | {
941
+ kind: 'per_tool';
942
+ name: string;
943
+ size: number;
944
+ limit: number;
945
+ } | {
946
+ kind: 'description';
947
+ name: string;
948
+ size: number;
949
+ limit: number;
950
+ } | {
951
+ kind: 'instructions';
952
+ size: number;
953
+ limit: number;
954
+ };
955
+ /**
956
+ * Claude Code truncates a server's instructions at this many characters
957
+ * without telling anyone, so a rule past it never reaches an agent
958
+ * (https://code.claude.com/docs/en/mcp; a user can change it with
959
+ * `CLAUDE_CODE_MAX_MCP_DESCRIPTION_LENGTH`).
960
+ */
961
+ declare const DEFAULT_INSTRUCTIONS_LIMIT = 2048;
962
+ /**
963
+ * Claude Code truncates each tool description at the same length as the
964
+ * instructions, from the same documentation, and just as silently.
965
+ */
966
+ declare const DEFAULT_DESCRIPTION_LIMIT = 2048;
967
+ /**
968
+ * Measures what an MCP server costs every session that connects to it: each
969
+ * tool's name, description and input schema, and the instructions.
970
+ *
971
+ * It does not judge; {@link checkMcpSurface} compares the same numbers with a
972
+ * budget.
973
+ */
974
+ declare const measureMcpSurface: ({
975
+ tools,
976
+ instructions
977
+ }: {
978
+ tools: McpSurfaceTool[];
979
+ instructions?: string;
980
+ }) => McpSurfaceReport;
981
+ /**
982
+ * Every limit an MCP surface exceeds, or `[]` when it fits.
983
+ *
984
+ * Returns rather than throws, so a test's `expect(...).toEqual([])` prints
985
+ * which tool went over and by how much. Meant for a test, not for boot: a
986
+ * server refusing to start over a few hundred bytes is worse than a red test.
987
+ *
988
+ * @example
989
+ * ```typescript
990
+ * expect(
991
+ * checkMcpSurface({
992
+ * tools,
993
+ * instructions,
994
+ * budget: { total: 70_000, perTool: 10_000 },
995
+ * })
996
+ * ).toEqual([]);
997
+ * ```
998
+ */
999
+ declare const checkMcpSurface: ({
1000
+ tools,
1001
+ instructions,
1002
+ budget
1003
+ }: {
1004
+ tools: McpSurfaceTool[];
1005
+ instructions?: string;
1006
+ budget?: McpSurfaceBudget;
1007
+ }) => McpSurfaceViolation[];
1008
+ /**
1009
+ * The tool names the instructions mention that the surface does not have.
1010
+ *
1011
+ * Instructions are read once, at connect: a renamed tool they still name is
1012
+ * one every connected agent is taught to call and then told is "not found".
1013
+ * Pass `pattern` (a global RegExp whose first group is the name) when the
1014
+ * instructions name tools some other way.
1015
+ */
1016
+ declare const findUnknownToolMentions: ({
1017
+ instructions,
1018
+ tools,
1019
+ pattern
1020
+ }: {
1021
+ instructions: string;
1022
+ tools: Array<Pick<McpSurfaceTool, "name">>;
1023
+ pattern?: RegExp;
1024
+ }) => string[];
1025
+ //#endregion
890
1026
  //#region src/registerAppResource.d.ts
891
1027
  /**
892
1028
  * Identifier of the MCP Apps extension (`io.modelcontextprotocol/ui`), used as
@@ -1712,4 +1848,4 @@ interface McpRouterOptions {
1712
1848
  */
1713
1849
  declare const createMcpRouter: (server: McpServer$1, options?: McpRouterOptions) => McpRouter;
1714
1850
  //#endregion
1715
- export { ApiCallOptions, CognitoUserPoolConfig, type CreateToolGateOptions, type DeferToolsOptions, type DefineToolParams, type GateToolParams, InMemoryServerEventBus, type JsonObjectSchema, McpAuthOptions, McpRouter, McpRouterOptions, McpServer, type RegisterAppResourceParams, type RegisterToolsParams, type RegisteredAppResource, type ServerEvent, type ServerEventBus, type ServerNotifier, type Tool, type ToolCallContext, type ToolCallGate, type ToolGate, type ToolIdentity, type ToolMetaParams, type ToolSchema, type ToolSearchArgs, UI_EXTENSION_ID, UI_RESOURCE_MIME_TYPE, type UiResourceCsp, type UiResourceMeta, type UiResourcePermissions, type UiToolVisibility, apiCall, checkScopes, createMcpRouter, createToolGate, defineTool, errorBodyMessage, getApiHeaders, getIdentity, getRequestLocale, rankTools, registerAppResource, registerTools, z };
1851
+ export { ApiCallOptions, CognitoUserPoolConfig, type CreateToolGateOptions, DEFAULT_DESCRIPTION_LIMIT, DEFAULT_INSTRUCTIONS_LIMIT, type DeferToolsOptions, type DefineToolParams, type GateToolParams, InMemoryServerEventBus, type JsonObjectSchema, McpAuthOptions, McpRouter, McpRouterOptions, McpServer, type McpSurfaceBudget, type McpSurfaceReport, type McpSurfaceTool, type McpSurfaceViolation, type RegisterAppResourceParams, type RegisterToolsParams, type RegisteredAppResource, type ServerEvent, type ServerEventBus, type ServerNotifier, type Tool, type ToolCallContext, type ToolCallGate, type ToolGate, type ToolIdentity, type ToolMetaParams, type ToolSchema, type ToolSearchArgs, UI_EXTENSION_ID, UI_RESOURCE_MIME_TYPE, type UiResourceCsp, type UiResourceMeta, type UiResourcePermissions, type UiToolVisibility, apiCall, checkMcpSurface, checkScopes, createMcpRouter, createToolGate, defineTool, errorBodyMessage, findUnknownToolMentions, getApiHeaders, getIdentity, getRequestLocale, measureMcpSurface, rankTools, registerAppResource, registerTools, z };
package/dist/index.mjs CHANGED
@@ -209,6 +209,148 @@ var createMcpRequestServer = ({
209
209
  };
210
210
  };
211
211
 
212
+ //#endregion
213
+ //#region src/mcpSurface.ts
214
+ /**
215
+ * Claude Code truncates a server's instructions at this many characters
216
+ * without telling anyone, so a rule past it never reaches an agent
217
+ * (https://code.claude.com/docs/en/mcp; a user can change it with
218
+ * `CLAUDE_CODE_MAX_MCP_DESCRIPTION_LENGTH`).
219
+ */
220
+ var DEFAULT_INSTRUCTIONS_LIMIT = 2048;
221
+ /**
222
+ * Claude Code truncates each tool description at the same length as the
223
+ * instructions, from the same documentation, and just as silently.
224
+ */
225
+ var DEFAULT_DESCRIPTION_LIMIT = 2048;
226
+ var sizeOf = tool => {
227
+ return JSON.stringify({
228
+ name: tool.name,
229
+ description: tool.description,
230
+ inputSchema: tool.inputSchema
231
+ }).length;
232
+ };
233
+ /**
234
+ * Measures what an MCP server costs every session that connects to it: each
235
+ * tool's name, description and input schema, and the instructions.
236
+ *
237
+ * It does not judge; {@link checkMcpSurface} compares the same numbers with a
238
+ * budget.
239
+ */
240
+ var measureMcpSurface = ({
241
+ tools,
242
+ instructions
243
+ }) => {
244
+ const sizes = tools.map(tool => {
245
+ return {
246
+ name: tool.name,
247
+ size: sizeOf(tool)
248
+ };
249
+ }).sort((a, b) => {
250
+ return b.size - a.size;
251
+ });
252
+ return {
253
+ total: sizes.reduce((sum, entry) => {
254
+ return sum + entry.size;
255
+ }, 0),
256
+ tools: sizes,
257
+ instructions: instructions?.length ?? 0
258
+ };
259
+ };
260
+ var checkDescriptions = ({
261
+ tools,
262
+ limit
263
+ }) => {
264
+ return tools.flatMap(tool => {
265
+ const size = tool.description?.length ?? 0;
266
+ return size > limit ? [{
267
+ kind: "description",
268
+ name: tool.name,
269
+ size,
270
+ limit
271
+ }] : [];
272
+ });
273
+ };
274
+ /**
275
+ * Every limit an MCP surface exceeds, or `[]` when it fits.
276
+ *
277
+ * Returns rather than throws, so a test's `expect(...).toEqual([])` prints
278
+ * which tool went over and by how much. Meant for a test, not for boot: a
279
+ * server refusing to start over a few hundred bytes is worse than a red test.
280
+ *
281
+ * @example
282
+ * ```typescript
283
+ * expect(
284
+ * checkMcpSurface({
285
+ * tools,
286
+ * instructions,
287
+ * budget: { total: 70_000, perTool: 10_000 },
288
+ * })
289
+ * ).toEqual([]);
290
+ * ```
291
+ */
292
+ var checkMcpSurface = ({
293
+ tools,
294
+ instructions,
295
+ budget = {}
296
+ }) => {
297
+ const report = measureMcpSurface({
298
+ tools,
299
+ instructions
300
+ });
301
+ const violations = [];
302
+ if (budget.total !== void 0 && report.total > budget.total) violations.push({
303
+ kind: "total",
304
+ size: report.total,
305
+ limit: budget.total
306
+ });
307
+ if (budget.perTool !== void 0) {
308
+ for (const {
309
+ name,
310
+ size
311
+ } of report.tools) if (size > budget.perTool) violations.push({
312
+ kind: "per_tool",
313
+ name,
314
+ size,
315
+ limit: budget.perTool
316
+ });
317
+ }
318
+ violations.push(...checkDescriptions({
319
+ tools,
320
+ limit: budget.description ?? 2048
321
+ }));
322
+ const instructionsLimit = budget.instructions ?? 2048;
323
+ if (report.instructions > instructionsLimit) violations.push({
324
+ kind: "instructions",
325
+ size: report.instructions,
326
+ limit: instructionsLimit
327
+ });
328
+ return violations;
329
+ };
330
+ var TOOL_MENTION = /`([a-z][a-z0-9]*(?:[-_][a-z0-9]+)+)`/g;
331
+ /**
332
+ * The tool names the instructions mention that the surface does not have.
333
+ *
334
+ * Instructions are read once, at connect: a renamed tool they still name is
335
+ * one every connected agent is taught to call and then told is "not found".
336
+ * Pass `pattern` (a global RegExp whose first group is the name) when the
337
+ * instructions name tools some other way.
338
+ */
339
+ var findUnknownToolMentions = ({
340
+ instructions,
341
+ tools,
342
+ pattern = TOOL_MENTION
343
+ }) => {
344
+ const known = new Set(tools.map(tool => {
345
+ return tool.name;
346
+ }));
347
+ return [...new Set([...instructions.matchAll(pattern)].map(([, name]) => {
348
+ return name;
349
+ }))].filter(name => {
350
+ return !known.has(name);
351
+ });
352
+ };
353
+
212
354
  //#endregion
213
355
  //#region src/registerAppResource.ts
214
356
  /**
@@ -1568,4 +1710,4 @@ var createMcpRouter = (server, options = {}) => {
1568
1710
  };
1569
1711
 
1570
1712
  //#endregion
1571
- export { InMemoryServerEventBus, McpServer, UI_EXTENSION_ID, UI_RESOURCE_MIME_TYPE, apiCall, checkScopes, createMcpRouter, createToolGate, defineTool, errorBodyMessage, getApiHeaders, getIdentity, getRequestLocale, rankTools, registerAppResource, registerTools, z };
1713
+ export { DEFAULT_DESCRIPTION_LIMIT, DEFAULT_INSTRUCTIONS_LIMIT, InMemoryServerEventBus, McpServer, UI_EXTENSION_ID, UI_RESOURCE_MIME_TYPE, apiCall, checkMcpSurface, checkScopes, createMcpRouter, createToolGate, defineTool, errorBodyMessage, findUnknownToolMentions, getApiHeaders, getIdentity, getRequestLocale, measureMcpSurface, rankTools, registerAppResource, registerTools, z };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttoss/http-server-mcp",
3
- "version": "0.34.5",
3
+ "version": "0.36.0",
4
4
  "description": "Model Context Protocol (MCP) server integration for @ttoss/http-server",
5
5
  "keywords": [
6
6
  "ai",
@@ -39,8 +39,8 @@
39
39
  "zod": "^4.4.3",
40
40
  "@ttoss/auth-core": "^0.17.2",
41
41
  "@ttoss/i18n-core": "^0.8.1",
42
- "@ttoss/http-server": "^0.12.2",
43
- "@ttoss/http-server-auth": "^0.8.4"
42
+ "@ttoss/http-server": "^0.13.0",
43
+ "@ttoss/http-server-auth": "^0.8.5"
44
44
  },
45
45
  "devDependencies": {
46
46
  "@modelcontextprotocol/client": "^2.0.0",