@try-works/dsh-model-as-tool 0.1.3 → 0.3.6

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 (257) hide show
  1. package/bin/dsh-tool-mode-credentials.mjs +20 -0
  2. package/bin/dsh-tool-mode.mjs +202 -0
  3. package/catalog/desert-ant-models.json +693 -693
  4. package/client/client.model-tools.js +592 -0
  5. package/client/index.js +1679 -0
  6. package/lib/adapters/index.d.ts.map +1 -1
  7. package/lib/adapters/index.js +4 -4
  8. package/lib/bindings.d.ts.map +1 -1
  9. package/lib/bindings.js +26 -25
  10. package/lib/capability.d.ts.map +1 -1
  11. package/lib/capability.js +17 -0
  12. package/lib/capture-path.d.ts +97 -0
  13. package/lib/capture-path.d.ts.map +1 -0
  14. package/lib/capture-path.js +152 -0
  15. package/lib/diagnostics.d.ts +166 -0
  16. package/lib/diagnostics.d.ts.map +1 -0
  17. package/lib/diagnostics.js +135 -0
  18. package/lib/extension-dependencies.d.ts +197 -0
  19. package/lib/extension-dependencies.d.ts.map +1 -0
  20. package/lib/extension-dependencies.js +369 -0
  21. package/lib/extension-dispatch.d.ts +117 -0
  22. package/lib/extension-dispatch.d.ts.map +1 -0
  23. package/lib/extension-dispatch.js +124 -0
  24. package/lib/extension-registrar.d.ts +100 -0
  25. package/lib/extension-registrar.d.ts.map +1 -0
  26. package/lib/extension-registrar.js +134 -0
  27. package/lib/extensions-api.d.ts +217 -0
  28. package/lib/extensions-api.d.ts.map +1 -0
  29. package/lib/extensions-api.js +17 -0
  30. package/lib/extensions-remote.d.ts +54 -0
  31. package/lib/extensions-remote.d.ts.map +1 -0
  32. package/lib/extensions-remote.js +66 -0
  33. package/lib/extensions-service.d.ts +334 -0
  34. package/lib/extensions-service.d.ts.map +1 -0
  35. package/lib/extensions-service.js +1351 -0
  36. package/lib/harness-llm.d.ts +138 -0
  37. package/lib/harness-llm.d.ts.map +1 -0
  38. package/lib/harness-llm.js +218 -0
  39. package/lib/harness-serving.d.ts +95 -0
  40. package/lib/harness-serving.d.ts.map +1 -0
  41. package/lib/harness-serving.js +157 -0
  42. package/lib/headless.d.ts +97 -0
  43. package/lib/headless.d.ts.map +1 -0
  44. package/lib/headless.js +349 -0
  45. package/lib/http-acquisition.d.ts +27 -0
  46. package/lib/http-acquisition.d.ts.map +1 -0
  47. package/lib/http-acquisition.js +136 -0
  48. package/lib/index.d.ts +250 -7
  49. package/lib/index.d.ts.map +1 -1
  50. package/lib/index.js +2625 -70
  51. package/lib/invocation-accounting.d.ts +169 -0
  52. package/lib/invocation-accounting.d.ts.map +1 -0
  53. package/lib/invocation-accounting.js +148 -0
  54. package/lib/legacy-migration.d.ts +95 -0
  55. package/lib/legacy-migration.d.ts.map +1 -0
  56. package/lib/legacy-migration.js +429 -0
  57. package/lib/managed-workers.d.ts +151 -0
  58. package/lib/managed-workers.d.ts.map +1 -0
  59. package/lib/managed-workers.js +249 -0
  60. package/lib/mcp/client-grant.d.ts +74 -0
  61. package/lib/mcp/client-grant.d.ts.map +1 -0
  62. package/lib/mcp/client-grant.js +60 -0
  63. package/lib/mcp/credential-cli.d.ts +38 -0
  64. package/lib/mcp/credential-cli.d.ts.map +1 -0
  65. package/lib/mcp/credential-cli.js +148 -0
  66. package/lib/mcp/credential-store.d.ts +143 -0
  67. package/lib/mcp/credential-store.d.ts.map +1 -0
  68. package/lib/mcp/credential-store.js +454 -0
  69. package/lib/mcp/host-binding.d.ts +25 -0
  70. package/lib/mcp/host-binding.d.ts.map +1 -1
  71. package/lib/mcp/host-binding.js +107 -14
  72. package/lib/mcp/http.d.ts +68 -18
  73. package/lib/mcp/http.d.ts.map +1 -1
  74. package/lib/mcp/http.js +390 -62
  75. package/lib/mcp/oauth.d.ts +149 -0
  76. package/lib/mcp/oauth.d.ts.map +1 -0
  77. package/lib/mcp/oauth.js +396 -0
  78. package/lib/mcp/protocol.d.ts +46 -7
  79. package/lib/mcp/protocol.d.ts.map +1 -1
  80. package/lib/mcp/protocol.js +63 -16
  81. package/lib/mcp/rate-limit.d.ts +68 -0
  82. package/lib/mcp/rate-limit.d.ts.map +1 -0
  83. package/lib/mcp/rate-limit.js +114 -0
  84. package/lib/mcp/sdk-v2.d.ts +17 -0
  85. package/lib/mcp/sdk-v2.d.ts.map +1 -0
  86. package/lib/mcp/sdk-v2.js +181 -0
  87. package/lib/mcp/server.d.ts +11 -1
  88. package/lib/mcp/server.d.ts.map +1 -1
  89. package/lib/mcp/server.js +80 -13
  90. package/lib/mcp/stdio.d.ts.map +1 -1
  91. package/lib/mcp/stdio.js +9 -1
  92. package/lib/mcp/subscriptions.d.ts +43 -1
  93. package/lib/mcp/subscriptions.d.ts.map +1 -1
  94. package/lib/mcp/subscriptions.js +90 -9
  95. package/lib/model-qualification.d.ts +192 -0
  96. package/lib/model-qualification.d.ts.map +1 -0
  97. package/lib/model-qualification.js +178 -0
  98. package/lib/planning.d.ts +246 -0
  99. package/lib/planning.d.ts.map +1 -0
  100. package/lib/planning.js +457 -0
  101. package/lib/prepared-assets.d.ts +178 -0
  102. package/lib/prepared-assets.d.ts.map +1 -0
  103. package/lib/prepared-assets.js +286 -0
  104. package/lib/provider-bridge.d.ts +37 -0
  105. package/lib/provider-bridge.d.ts.map +1 -1
  106. package/lib/provider-bridge.js +189 -12
  107. package/lib/ptc-authority.d.ts +140 -0
  108. package/lib/ptc-authority.d.ts.map +1 -0
  109. package/lib/ptc-authority.js +216 -0
  110. package/lib/ptc-runtime.d.ts +156 -0
  111. package/lib/ptc-runtime.d.ts.map +1 -0
  112. package/lib/ptc-runtime.js +191 -0
  113. package/lib/publication.d.ts +2 -1
  114. package/lib/publication.d.ts.map +1 -1
  115. package/lib/publication.js +13 -5
  116. package/lib/receipt.d.ts.map +1 -1
  117. package/lib/receipt.js +8 -0
  118. package/lib/title-proposal.d.ts +194 -0
  119. package/lib/title-proposal.d.ts.map +1 -0
  120. package/lib/title-proposal.js +244 -0
  121. package/lib/tool-mode-run.d.ts +235 -0
  122. package/lib/tool-mode-run.d.ts.map +1 -0
  123. package/lib/tool-mode-run.js +283 -0
  124. package/lib/types.d.ts +3 -1
  125. package/lib/types.d.ts.map +1 -1
  126. package/lib/update-surface.d.ts +79 -0
  127. package/lib/update-surface.d.ts.map +1 -0
  128. package/lib/update-surface.js +224 -0
  129. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts +38 -0
  130. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.d.ts.map +1 -0
  131. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/standard.js +445 -0
  132. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts +14 -0
  133. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.d.ts.map +1 -0
  134. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-embed.js +63 -0
  135. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts +14 -0
  136. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.d.ts.map +1 -0
  137. package/node_modules/@try-works/tool-mode-contracts/lib/contracts/text-rerank.js +76 -0
  138. package/node_modules/@try-works/tool-mode-contracts/lib/dsh.d.ts.map +1 -1
  139. package/node_modules/@try-works/tool-mode-contracts/lib/dsh.js +69 -55
  140. package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts +4 -1
  141. package/node_modules/@try-works/tool-mode-contracts/lib/index.d.ts.map +1 -1
  142. package/node_modules/@try-works/tool-mode-contracts/lib/index.js +26 -2
  143. package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts +28 -2
  144. package/node_modules/@try-works/tool-mode-contracts/lib/registry.d.ts.map +1 -1
  145. package/node_modules/@try-works/tool-mode-contracts/lib/registry.js +93 -10
  146. package/node_modules/@try-works/tool-mode-contracts/package.json +0 -1
  147. package/node_modules/@try-works/tool-mode-extension-sdk/bin/dsh-tool-mode.mjs +20 -0
  148. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts +80 -0
  149. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.d.ts.map +1 -0
  150. package/node_modules/@try-works/tool-mode-extension-sdk/lib/cli.js +533 -0
  151. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts +83 -0
  152. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.d.ts.map +1 -0
  153. package/node_modules/@try-works/tool-mode-extension-sdk/lib/declarative-recipes.js +125 -0
  154. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts +70 -0
  155. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.d.ts.map +1 -0
  156. package/node_modules/@try-works/tool-mode-extension-sdk/lib/discovery.js +201 -0
  157. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts +57 -0
  158. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.d.ts.map +1 -0
  159. package/node_modules/@try-works/tool-mode-extension-sdk/lib/errors.js +83 -0
  160. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts +31 -0
  161. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.d.ts.map +1 -0
  162. package/node_modules/@try-works/tool-mode-extension-sdk/lib/index.js +31 -0
  163. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts +86 -0
  164. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.d.ts.map +1 -0
  165. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/archive.js +185 -0
  166. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts +126 -0
  167. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.d.ts.map +1 -0
  168. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/dependencies.js +297 -0
  169. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts +216 -0
  170. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.d.ts.map +1 -0
  171. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/lifecycle.js +800 -0
  172. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts +210 -0
  173. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.d.ts.map +1 -0
  174. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/migration.js +368 -0
  175. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts +63 -0
  176. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.d.ts.map +1 -0
  177. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/runtime-link.js +149 -0
  178. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts +35 -0
  179. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.d.ts.map +1 -0
  180. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/source-directory.js +179 -0
  181. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts +179 -0
  182. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.d.ts.map +1 -0
  183. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/staged.js +235 -0
  184. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts +367 -0
  185. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.d.ts.map +1 -0
  186. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/store.js +807 -0
  187. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts +185 -0
  188. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.d.ts.map +1 -0
  189. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/update-flow.js +504 -0
  190. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts +17 -0
  191. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.d.ts.map +1 -0
  192. package/node_modules/@try-works/tool-mode-extension-sdk/lib/install/zip-source.js +80 -0
  193. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts +52 -0
  194. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.d.ts.map +1 -0
  195. package/node_modules/@try-works/tool-mode-extension-sdk/lib/load.js +102 -0
  196. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts +171 -0
  197. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.d.ts.map +1 -0
  198. package/node_modules/@try-works/tool-mode-extension-sdk/lib/manifest.js +271 -0
  199. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts +242 -0
  200. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.d.ts.map +1 -0
  201. package/node_modules/@try-works/tool-mode-extension-sdk/lib/runtime.js +58 -0
  202. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts +118 -0
  203. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.d.ts.map +1 -0
  204. package/node_modules/@try-works/tool-mode-extension-sdk/lib/state.js +113 -0
  205. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts +2 -0
  206. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.d.ts.map +1 -0
  207. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker/worker-child.js +351 -0
  208. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts +150 -0
  209. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.d.ts.map +1 -0
  210. package/node_modules/@try-works/tool-mode-extension-sdk/lib/worker.js +670 -0
  211. package/node_modules/@try-works/tool-mode-extension-sdk/package.json +53 -0
  212. package/node_modules/@try-works/tool-mode-extension-sdk/schema/extension-manifest-1.1.0.schema.json +253 -0
  213. package/node_modules/@try-works/tool-mode-recipes/bin/tool-mode-recipe.mjs +15 -0
  214. package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts +427 -0
  215. package/node_modules/@try-works/tool-mode-recipes/lib/broker.d.ts.map +1 -0
  216. package/node_modules/@try-works/tool-mode-recipes/lib/broker.js +592 -0
  217. package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts +60 -0
  218. package/node_modules/@try-works/tool-mode-recipes/lib/cli.d.ts.map +1 -0
  219. package/node_modules/@try-works/tool-mode-recipes/lib/cli.js +936 -0
  220. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts +237 -0
  221. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.d.ts.map +1 -0
  222. package/node_modules/@try-works/tool-mode-recipes/lib/dsh-adapter.js +200 -0
  223. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts +85 -0
  224. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.d.ts.map +1 -0
  225. package/node_modules/@try-works/tool-mode-recipes/lib/fixture-provider.js +176 -0
  226. package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts +105 -0
  227. package/node_modules/@try-works/tool-mode-recipes/lib/images.d.ts.map +1 -0
  228. package/node_modules/@try-works/tool-mode-recipes/lib/images.js +129 -0
  229. package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts +25 -0
  230. package/node_modules/@try-works/tool-mode-recipes/lib/index.d.ts.map +1 -0
  231. package/node_modules/@try-works/tool-mode-recipes/lib/index.js +25 -0
  232. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts +325 -0
  233. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.d.ts.map +1 -0
  234. package/node_modules/@try-works/tool-mode-recipes/lib/recipe-format.js +1310 -0
  235. package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts +51 -0
  236. package/node_modules/@try-works/tool-mode-recipes/lib/templates.d.ts.map +1 -0
  237. package/node_modules/@try-works/tool-mode-recipes/lib/templates.js +310 -0
  238. package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts +227 -0
  239. package/node_modules/@try-works/tool-mode-recipes/lib/titles.d.ts.map +1 -0
  240. package/node_modules/@try-works/tool-mode-recipes/lib/titles.js +407 -0
  241. package/node_modules/@try-works/tool-mode-recipes/package.json +56 -0
  242. package/node_modules/@try-works/tool-mode-recipes/recipes/describe-image.json +38 -0
  243. package/node_modules/@try-works/tool-mode-recipes/recipes/text-title.json +45 -0
  244. package/node_modules/@try-works/tool-mode-recipes/schema/harness-model-recipe.schema.json +273 -0
  245. package/package.json +43 -15
  246. package/remote/spec.mjs +439 -0
  247. package/remote/typert.host.mjs +14 -0
  248. package/remote/typert.remote-client.mjs +16 -0
  249. package/lib/adapters/tongue.d.ts +0 -31
  250. package/lib/adapters/tongue.d.ts.map +0 -1
  251. package/lib/adapters/tongue.js +0 -93
  252. package/lib/adapters/voz.d.ts +0 -28
  253. package/lib/adapters/voz.d.ts.map +0 -1
  254. package/lib/adapters/voz.js +0 -102
  255. /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts +0 -0
  256. /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.d.ts.map +0 -0
  257. /package/{lib → node_modules/@try-works/tool-mode-extension-sdk/lib}/licensing.js +0 -0
package/lib/mcp/http.js CHANGED
@@ -17,15 +17,21 @@
17
17
  * @module @try-works/dsh-model-as-tool/mcp/http
18
18
  */
19
19
  import { timingSafeEqual } from 'node:crypto';
20
- import { assertHeadersMatch, ERROR_CODES, errorResponse, } from "./protocol.js";
21
- import { dispatch } from "./server.js";
22
- /** A conservative default: loopback, the illustrative port, a small body cap. */
20
+ import { toWebRequest } from '@modelcontextprotocol/node';
21
+ import { assertHeadersMatch, decodeHeaderValue, ERROR_CODES, errorResponse, negotiateVersion, parseRequestMetadata, unsupportedRequiredCapabilities, WireError, } from "./protocol.js";
22
+ import { serveSdkV2Request } from "./sdk-v2.js";
23
+ import { oauthChallenge, oauthErrorStatus } from "./oauth.js";
24
+ import { createRateLimiter, DEFAULT_RATE_LIMIT_POLICY, } from "./rate-limit.js";
25
+ import { resolveClientGrant } from "./client-grant.js";
26
+ /** A conservative default: loopback, the illustrative port, a small body cap,
27
+ * and the default per-client rate limit (AT-12). */
23
28
  export const DEFAULT_HTTP_CONFIG = {
24
29
  path: '/mcp',
25
30
  host: '127.0.0.1',
26
31
  port: 3765,
27
32
  maxBodyBytes: 4 * 1024 * 1024,
28
33
  allowedOrigins: [],
34
+ rateLimit: DEFAULT_RATE_LIMIT_POLICY,
29
35
  };
30
36
  /** Whether an address is loopback. */
31
37
  export function isLoopback(address) {
@@ -60,33 +66,75 @@ function tokenMatches(presented, accepted) {
60
66
  return false;
61
67
  return timingSafeEqual(left, right);
62
68
  }
63
- /**
64
- * Build the request handler.
65
- *
66
- * Ordering is deliberate and matches P26-16's list: transport identity (loopback,
67
- * size, method, path) is settled before authentication, and authentication before
68
- * any protocol parsing. A request that is not from the loopback is refused without
69
- * its token being examined at all.
70
- * @param config - the transport configuration.
71
- * @param host - the MCP surface.
72
- * @returns the handler.
73
- */
74
- export function createHttpHandler(config, host) {
69
+ /** Validate the Host header when the client supplies one. */
70
+ function hostHeaderAllowed(header, config) {
71
+ if (header === undefined || header.length === 0)
72
+ return true;
73
+ const value = header.toLowerCase();
74
+ const authority = value.startsWith('[')
75
+ ? value.slice(1, value.indexOf(']'))
76
+ : (value.split(':')[0] ?? '');
77
+ const configured = config.host.toLowerCase();
78
+ const allowed = new Set([configured]);
79
+ if (isLoopback(configured)) {
80
+ allowed.add('localhost');
81
+ allowed.add('127.0.0.1');
82
+ allowed.add('::1');
83
+ }
84
+ return allowed.has(authority);
85
+ }
86
+ function createRequestGate(config, host) {
87
+ /**
88
+ * THE LIMITER IS PER-HANDLER STATE, in memory and unpersisted (AT-12).
89
+ *
90
+ * One limiter per bound endpoint, keyed by the authenticated client identity.
91
+ * Absent configuration means the DEFAULT policy applies, so the endpoint is
92
+ * bounded even when the operator configured nothing; `false` disables it; a
93
+ * policy overrides it. Construction validates the policy, so a broken
94
+ * configuration fails when the transport starts rather than mid-request.
95
+ */
96
+ const effectivePolicy = config.rateLimit === false
97
+ ? DEFAULT_RATE_LIMIT_POLICY
98
+ : (config.rateLimit ?? DEFAULT_RATE_LIMIT_POLICY);
99
+ const limiter = config.rateLimit === false
100
+ ? undefined
101
+ : createRateLimiter(effectivePolicy);
75
102
  return async (request) => {
103
+ // 0. DISCOVERY METADATA (RFC 8414/9728), when the OAuth scheme serves it.
104
+ // Metadata endpoints are the ONE thing served unauthenticated: a client must
105
+ // learn the issuer, audience and token endpoint BEFORE it can hold a token.
106
+ if (config.auth.scheme === 'oauth-resource-server') {
107
+ if (request.method === 'GET' && request.path === '/.well-known/oauth-protected-resource') {
108
+ return respond(200, config.auth.server.protectedResourceMetadata());
109
+ }
110
+ if (request.method === 'GET' && request.path === '/.well-known/oauth-authorization-server') {
111
+ return respond(200, config.auth.server.authorizationServerMetadata());
112
+ }
113
+ }
76
114
  // 1. Loopback only (MCP-02, SEC-02). Checked against the SOCKET address, not a
77
115
  // header: a header is caller-supplied, and trusting one would make the binding
78
116
  // decorative.
79
117
  if (!isLoopback(request.remoteAddress)) {
80
118
  return respond(403, { error: 'this endpoint serves loopback clients only' });
81
119
  }
120
+ if (!hostHeaderAllowed(request.headers.host, config)) {
121
+ return respond(403, { error: 'the Host header is not approved for this endpoint' });
122
+ }
82
123
  // 2. Size limit (SEC-02), measured on the received bytes.
83
124
  const bodyBytes = Buffer.byteLength(request.body, 'utf8');
84
125
  if (bodyBytes > config.maxBodyBytes) {
85
126
  // 413 rather than 400: the request may be perfectly well formed and merely
86
127
  // too large, and a client needs to tell those apart.
128
+ /**
129
+ * NO `id` — the pinned schema neither permits nor requires one here.
130
+ *
131
+ * This wrote `id: null`, which `RequestId` (`type: ["string","integer"]`) REJECTS:
132
+ * `null` is not a string and not an integer. `JSONRPCErrorResponse` requires only
133
+ * `["error","jsonrpc"]`, so the id is optional — and when the body was too large to
134
+ * read there is no id to echo anyway. Omitting it is both valid and honest.
135
+ */
87
136
  return respond(413, {
88
137
  jsonrpc: '2.0',
89
- id: null,
90
138
  error: { code: ERROR_CODES.invalidRequest, message: `the request body exceeds ${config.maxBodyBytes} bytes` },
91
139
  });
92
140
  }
@@ -115,15 +163,82 @@ export function createHttpHandler(config, host) {
115
163
  message: 'authentication is required',
116
164
  }), { 'www-authenticate': 'Bearer' });
117
165
  }
118
- if (!tokenMatches(presented, config.auth.token)) {
119
- // The message does not distinguish "unknown" from "expired": with a static
120
- // token there is nothing to expire, and a more specific message would only
121
- // help an attacker.
122
- return respond(401, errorResponse(null, {
123
- code: ERROR_CODES.invalidRequest,
124
- message: 'the presented credential is not accepted',
125
- }), { 'www-authenticate': 'Bearer' });
166
+ /** The AUTHENTICATED client identity, resolved once and used for rate limiting. */
167
+ let clientId;
168
+ if (config.auth.scheme === 'static-token') {
169
+ let matched;
170
+ if (tokenMatches(presented, config.auth.token))
171
+ matched = config.auth.clientId;
172
+ // SEC-01: a GENERATED static-token credential authenticates through the
173
+ // durable store, carrying its OWN client identity into grants and receipts.
174
+ if (matched === undefined && config.auth.registry !== undefined) {
175
+ matched = config.auth.registry.bySecret(presented)?.clientId;
176
+ }
177
+ if (matched === undefined) {
178
+ // The message does not distinguish "unknown" from "expired": with a static
179
+ // token there is nothing to expire, and a more specific message would only
180
+ // help an attacker.
181
+ return respond(401, errorResponse(null, {
182
+ code: ERROR_CODES.invalidRequest,
183
+ message: 'the presented credential is not accepted',
184
+ }), { 'www-authenticate': 'Bearer' });
185
+ }
186
+ clientId = matched;
187
+ }
188
+ else {
189
+ const verdict = await config.auth.server.verify(presented, config.auth.requiredScope);
190
+ if (!verdict.ok) {
191
+ // An RFC 6750 challenge names WHY: invalid_token vs insufficient_scope are
192
+ // different client instructions, and flattening them would misdirect a
193
+ // client that could otherwise self-correct. The STATUS follows §3.1 the
194
+ // same way: invalid_request 400, invalid_token 401, insufficient_scope
195
+ // 403 (the F4 gap was one flat 401 for all three).
196
+ return respond(oauthErrorStatus(verdict.error), errorResponse(null, {
197
+ code: ERROR_CODES.invalidRequest,
198
+ message: verdict.error.description,
199
+ }), { 'www-authenticate': oauthChallenge(verdict.error) });
200
+ }
201
+ clientId = verdict.token.clientId;
202
+ }
203
+ /**
204
+ * 5.5. Per-client rate limiting (AT-12), AFTER authentication and BEFORE the
205
+ * body is examined.
206
+ *
207
+ * A refused request is answered here without parsing, so a rate-limited caller
208
+ * cannot make the transport pay body-processing cost per attempt, and the
209
+ * counter is keyed on the identity the transport just proved — never the
210
+ * remote address, which every loopback client shares. Unauthenticated and
211
+ * malformed-credential requests were already refused at step 5 WITHOUT moving
212
+ * a counter: counting them would turn the limiter into an authentication
213
+ * oracle, where a 429 after N attempts tells a caller whether a credential
214
+ * exists.
215
+ *
216
+ * The `id` is OMITTED, exactly as the oversize-body refusal omits it: the body
217
+ * was never examined, so there is no id to echo, and the pinned `RequestId`
218
+ * does not admit `null`. The envelope is still a conformant
219
+ * `JSONRPCErrorResponse`, and `Retry-After` names when the window opens.
220
+ */
221
+ if (limiter !== undefined) {
222
+ const decision = limiter.admit(clientId);
223
+ if (!decision.admitted) {
224
+ return respond(429, errorResponse(null, {
225
+ code: ERROR_CODES.rateLimited,
226
+ message: `rate limit exceeded: at most ${effectivePolicy.maxRequests} `
227
+ + `requests per client per ${Math.round(effectivePolicy.windowMs / 1000)}s window (AT-12)`,
228
+ data: { retryAfterMs: decision.retryAfterMs },
229
+ }), { 'retry-after': String(Math.ceil(decision.retryAfterMs / 1000)) });
230
+ }
126
231
  }
232
+ /**
233
+ * 5.5. The per-client external grant (SEC-04), DERIVED here because the
234
+ * authentication verdict just happened: the grant is built from the verified
235
+ * client identity, the operator's grant configuration and the LIVE published
236
+ * surface — never from anything in the request body, which is what BASE-05
237
+ * forbids. The derived identity travels with the dispatch as CALLER identity,
238
+ * so the surface can authorize against the CLIENT's grant instead of the
239
+ * host's own interactive grant.
240
+ */
241
+ const caller = resolveClientGrant(clientId, config.grants, host.listTools().map(tool => tool.name));
127
242
  // 6. Parse, then check headers against the body (P26-05).
128
243
  let parsed;
129
244
  try {
@@ -136,6 +251,13 @@ export function createHttpHandler(config, host) {
136
251
  return respond(400, errorResponse(null, { code: ERROR_CODES.invalidRequest, message: 'the body is not a JSON-RPC object' }));
137
252
  }
138
253
  const wire = parsed;
254
+ if (wire.jsonrpc !== '2.0' || typeof wire.method !== 'string'
255
+ || (wire.id !== undefined && typeof wire.id !== 'string'
256
+ && !(typeof wire.id === 'number' && Number.isInteger(wire.id)))) {
257
+ return respond(400, errorResponse(null, {
258
+ code: ERROR_CODES.invalidRequest, message: 'the body is not a JSON-RPC 2.0 request or notification',
259
+ }));
260
+ }
139
261
  // Built conditionally rather than with explicit `undefined` values, because
140
262
  // `exactOptionalPropertyTypes` distinguishes "absent" from "present and
141
263
  // undefined" — and so does the header check, which treats a missing header
@@ -155,14 +277,102 @@ export function createHttpHandler(config, host) {
155
277
  // P26-05 names HTTP 400 and HeaderMismatch together.
156
278
  return respond(400, errorResponse(wire.id ?? null, { code: ERROR_CODES.headerMismatch, message }));
157
279
  }
158
- // 7. Dispatch. A notification carries no id and is acknowledged with 202.
159
- const outcome = await dispatch(wire, host, { signal: new AbortController().signal });
280
+ // Pinned metadata failures are transport rejections (HTTP 400), not SDK
281
+ // dispatch errors under HTTP 200. Keep the shared wire validation, not a
282
+ // second business dispatcher. Optional clientInfo remains optional.
283
+ if (wire.id !== undefined && wire.method !== 'initialize') {
284
+ try {
285
+ const metadata = parseRequestMetadata(wire.params);
286
+ negotiateVersion(metadata);
287
+ const declared = metadata.clientCapabilities.required;
288
+ const required = Array.isArray(declared)
289
+ ? declared.filter((name) => typeof name === 'string') : [];
290
+ const unsupported = unsupportedRequiredCapabilities(required);
291
+ if (unsupported.length > 0) {
292
+ throw new WireError(ERROR_CODES.unsupportedClientCapability, `the client requires unsupported capability: ${unsupported.join(', ')}`, { requiredCapabilities: Object.fromEntries(unsupported.map(name => [name, {}])) });
293
+ }
294
+ }
295
+ catch (error) {
296
+ if (!(error instanceof WireError))
297
+ throw error;
298
+ return respond(400, errorResponse(wire.id, error));
299
+ }
300
+ }
301
+ // 7. The OFFICIAL v2 server handles the protocol, not our handwritten dispatch.
302
+ // This finite response-description interface cannot carry open subscriptions.
303
+ // Refuse before creating a subscription rather than swallowing notifications.
304
+ if (wire.id !== undefined && wire.method === 'subscriptions/listen') {
305
+ return respond(404, errorResponse(wire.id ?? null, {
306
+ code: ERROR_CODES.methodNotFound,
307
+ message: 'this HTTP transport does not offer open subscription streams',
308
+ }));
309
+ }
310
+ // Pinned Streamable HTTP defines NO client-to-server notifications;
311
+ // notifications/cancelled is stdio-only. 202/no body is mandatory only IF a
312
+ // notification is accepted. We implement no extension notification handlers,
313
+ // so explicitly refuse rather than silently dropping unsupported messages.
160
314
  if (wire.id === undefined) {
161
- return { status: 202, headers: {}, body: '' };
315
+ return respond(400, errorResponse(null, {
316
+ code: ERROR_CODES.invalidRequest,
317
+ message: 'this stateless endpoint does not accept standalone notifications',
318
+ }));
319
+ }
320
+ const headers = new Headers();
321
+ for (const [name, value] of Object.entries(request.headers)) {
322
+ if (value !== undefined)
323
+ headers.set(name, value);
162
324
  }
163
- // The dispatch `httpStatus` already distinguishes invalid params (400) from a
164
- // protocol-level failure that still produced a well-formed response (200).
165
- return respond(outcome.httpStatus, outcome.response);
325
+ // Normalize sentinel values only AFTER shared validation. The pinned
326
+ // transport requires Mcp-Method; do not synthesize a missing header here or
327
+ // bypass assertHeadersMatch's required-header/mismatch rejection.
328
+ if (headerInput.method !== undefined)
329
+ headers.set('mcp-method', decodeHeaderValue(headerInput.method));
330
+ if (headerInput.protocolVersion !== undefined) {
331
+ headers.set('mcp-protocol-version', decodeHeaderValue(headerInput.protocolVersion));
332
+ }
333
+ // The SDK already decodes Mcp-Name correctly (including non-ASCII and
334
+ // literal sentinels), so preserve its original wire encoding here.
335
+ return { caller, token: presented, headers };
336
+ };
337
+ }
338
+ /**
339
+ * Shared, guarded Fetch/stream core. The Node adapter's toNodeHandler is NOT
340
+ * used directly: its early 413 has a different wire envelope, it does not watch
341
+ * IncomingMessage.aborted, and its close/drain listeners are not removed.
342
+ * Use its official bounded Request conversion only AFTER our unchanged gate.
343
+ */
344
+ export function createHttpStreamHandler(config, host) {
345
+ const gate = createRequestGate(config, host);
346
+ return async (request, signal) => {
347
+ const verdict = await gate(request);
348
+ if ('status' in verdict) {
349
+ return new Response(verdict.body, { status: verdict.status, headers: verdict.headers });
350
+ }
351
+ signal?.throwIfAborted();
352
+ const webRequest = await toWebRequest({
353
+ method: request.method,
354
+ url: config.path,
355
+ // Host was validated already; never let an untrusted authority affect URL
356
+ // construction. Keep the actual normalized headers on the Request below.
357
+ headers: { host: 'localhost' },
358
+ async *[Symbol.asyncIterator]() { yield Buffer.from(request.body, 'utf8'); },
359
+ }, undefined, {
360
+ maxRequestBodySize: config.maxBodyBytes,
361
+ ...signal === undefined ? {} : { signal },
362
+ });
363
+ return serveSdkV2Request(host, verdict.caller, new Request(webRequest, { headers: verdict.headers }), verdict.token);
364
+ };
365
+ }
366
+ /** Finite, materializing test facade; production sockets use the stream core. */
367
+ export function createHttpHandler(config, host) {
368
+ const stream = createHttpStreamHandler(config, host);
369
+ return async (request) => {
370
+ const response = await stream(request);
371
+ return {
372
+ status: response.status,
373
+ headers: Object.fromEntries(response.headers.entries()),
374
+ body: await response.text(),
375
+ };
166
376
  };
167
377
  }
168
378
  /** Extract a bearer token from an Authorization header. */
@@ -175,13 +385,16 @@ function bearerOf(header) {
175
385
  /**
176
386
  * Whether this transport may claim OAuth conformance.
177
387
  *
178
- * It may not. `P26-15` permits a static-token mode while forbidding the claim, and
179
- * this returns false so a status report or a compatibility matrix cannot assert
180
- * otherwise by accident.
181
- * @returns always false, for the static-token scheme.
388
+ * The static-token scheme may not; the oauth-resource-server scheme may, because
389
+ * it verifies HS256 credentials, audience, expiry and scope against the embedded
390
+ * server and answers RFC 6750 challenges — a conformant subset, not an invention.
391
+ * The claim is per-DEPLOYMENT, which is what a status report needs: a host using
392
+ * static tokens must still report false.
393
+ * @param auth - the live auth configuration.
394
+ * @returns whether the live scheme may claim the OAuth 2.1 resource-server profile.
182
395
  */
183
- export function claimsOAuthConformance() {
184
- return false;
396
+ export function claimsOAuthConformance(auth) {
397
+ return auth.scheme === 'oauth-resource-server';
185
398
  }
186
399
  /**
187
400
  * The authentication profile this transport reports.
@@ -189,17 +402,79 @@ export function claimsOAuthConformance() {
189
402
  * A machine-readable statement of what is actually implemented, so a client or an
190
403
  * operator can tell custom authentication from the OAuth profile without reading
191
404
  * the source.
405
+ * @param auth - the live auth configuration.
192
406
  * @returns the auth profile description.
193
407
  */
194
- export function authProfile() {
408
+ export function authProfile(auth) {
409
+ if (auth.scheme === 'static-token') {
410
+ return {
411
+ scheme: 'static-token',
412
+ // Stated explicitly rather than left to inference.
413
+ oauth21ResourceServer: false,
414
+ perClientRevocable: true,
415
+ note: 'Custom authentication. The OAuth 2.1 resource-server profile is not used and is not claimed.',
416
+ };
417
+ }
195
418
  return {
196
- scheme: 'static-token',
197
- // Stated explicitly rather than left to inference.
198
- oauth21ResourceServer: false,
419
+ scheme: 'oauth-resource-server',
420
+ oauth21ResourceServer: true,
199
421
  perClientRevocable: true,
200
- note: 'Custom authentication. The OAuth 2.1 resource-server profile is not implemented and is not claimed.',
422
+ issuer: auth.server.authorizationServerMetadata().issuer,
423
+ audience: auth.server.protectedResourceMetadata().resource,
424
+ note: 'OAuth 2.1 resource-server profile via the embedded local authorization server: '
425
+ + 'HS256 credentials, RFC 6750 challenges, audience/expiry/scope validation, per-client revocation.',
201
426
  };
202
427
  }
428
+ /** Wait for capacity without stranding a promise on a disconnected socket. */
429
+ function waitForDrain(outgoing, signal) {
430
+ return new Promise((resolve, reject) => {
431
+ const cleanup = () => {
432
+ outgoing.off('drain', onDrain);
433
+ outgoing.off('error', onError);
434
+ outgoing.off('close', onAbort);
435
+ signal.removeEventListener('abort', onAbort);
436
+ };
437
+ const onDrain = () => { cleanup(); resolve(); };
438
+ const onError = (error) => { cleanup(); reject(error); };
439
+ const onAbort = () => { cleanup(); reject(signal.reason ?? new Error('HTTP response closed')); };
440
+ outgoing.once('drain', onDrain);
441
+ outgoing.once('error', onError);
442
+ outgoing.once('close', onAbort);
443
+ signal.addEventListener('abort', onAbort, { once: true });
444
+ if (signal.aborted || outgoing.destroyed)
445
+ onAbort();
446
+ });
447
+ }
448
+ /** Pump bytes, never text-materialize SSE, and cancel even a blocked read. */
449
+ async function writeStreamResponse(response, outgoing, signal) {
450
+ const reader = response.body?.getReader();
451
+ const onAbort = () => { void reader?.cancel(signal.reason).catch(() => { }); };
452
+ signal.addEventListener('abort', onAbort, { once: true });
453
+ let drained = false;
454
+ try {
455
+ signal.throwIfAborted();
456
+ outgoing.writeHead(response.status, Object.fromEntries(response.headers.entries()));
457
+ outgoing.flushHeaders();
458
+ if (reader !== undefined) {
459
+ while (true) {
460
+ const next = await reader.read();
461
+ signal.throwIfAborted();
462
+ if (next.done)
463
+ break;
464
+ if (!outgoing.write(next.value))
465
+ await waitForDrain(outgoing, signal);
466
+ }
467
+ }
468
+ drained = true;
469
+ outgoing.end();
470
+ }
471
+ finally {
472
+ signal.removeEventListener('abort', onAbort);
473
+ if (!drained)
474
+ await reader?.cancel(signal.reason).catch(() => { });
475
+ reader?.releaseLock();
476
+ }
477
+ }
203
478
  /**
204
479
  * Bind the endpoint on loopback.
205
480
  *
@@ -220,38 +495,87 @@ export async function startHttpServer(config, host) {
220
495
  + 'A routable bind address would expose inference to the network.');
221
496
  }
222
497
  const { createServer } = await import('node:http');
223
- const handler = createHttpHandler(config, host);
498
+ const handler = createHttpStreamHandler(config, host);
499
+ const active = new Set();
224
500
  const server = createServer((incoming, outgoing) => {
501
+ const abort = new AbortController();
502
+ active.add(abort);
503
+ const onAborted = () => { abort.abort(new Error('HTTP client disconnected')); };
504
+ const onClose = () => { if (!outgoing.writableFinished)
505
+ onAborted(); };
506
+ const cleanup = () => {
507
+ incoming.off('aborted', onAborted);
508
+ incoming.off('error', onAborted);
509
+ outgoing.off('close', onClose);
510
+ outgoing.off('error', onAborted);
511
+ active.delete(abort);
512
+ };
513
+ incoming.once('aborted', onAborted);
514
+ incoming.once('error', onAborted);
515
+ outgoing.once('close', onClose);
516
+ outgoing.once('error', onAborted);
225
517
  const chunks = [];
226
518
  let received = 0;
519
+ let refused = false;
227
520
  incoming.on('data', (chunk) => {
521
+ if (refused || abort.signal.aborted)
522
+ return;
228
523
  received += chunk.byteLength;
229
- // The limit is enforced DURING the read, not after it: buffering an
230
- // unbounded body in order to measure it defeats the limit.
524
+ // Bound DURING receipt, and free already buffered chunks on refusal.
231
525
  if (received > config.maxBodyBytes) {
232
- outgoing.writeHead(413, { 'content-type': 'application/json' });
233
- outgoing.end(JSON.stringify({ error: 'request body too large' }));
234
- incoming.destroy();
526
+ refused = true;
527
+ chunks.length = 0;
528
+ outgoing.writeHead(413, { 'content-type': 'application/json', connection: 'close' });
529
+ outgoing.end(JSON.stringify({
530
+ jsonrpc: '2.0',
531
+ error: {
532
+ code: ERROR_CODES.invalidRequest,
533
+ message: `the request body exceeds ${config.maxBodyBytes} bytes`,
534
+ },
535
+ }));
536
+ cleanup();
235
537
  return;
236
538
  }
237
539
  chunks.push(chunk);
238
540
  });
239
- incoming.on('end', () => {
240
- void handler({
241
- method: incoming.method ?? 'POST',
242
- path: (incoming.url ?? '/').split('?')[0],
243
- headers: incoming.headers,
244
- body: Buffer.concat(chunks).toString('utf8'),
245
- remoteAddress: incoming.socket.remoteAddress ?? '',
246
- }).then(response => {
247
- outgoing.writeHead(response.status, response.headers);
248
- outgoing.end(response.body);
249
- }).catch((error) => {
250
- // A handler that throws must not hang the connection.
251
- outgoing.writeHead(500, { 'content-type': 'application/json' });
252
- outgoing.end(JSON.stringify({ error: error instanceof Error ? error.message : 'internal error' }));
253
- });
541
+ incoming.once('end', () => {
542
+ if (refused || abort.signal.aborted) {
543
+ cleanup();
544
+ return;
545
+ }
546
+ const body = Buffer.concat(chunks).toString('utf8');
547
+ chunks.length = 0;
548
+ void (async () => {
549
+ try {
550
+ const response = await handler({
551
+ method: incoming.method ?? 'POST',
552
+ path: (incoming.url ?? '/').split('?')[0],
553
+ headers: incoming.headers,
554
+ body,
555
+ remoteAddress: incoming.socket.remoteAddress ?? '',
556
+ }, abort.signal);
557
+ await writeStreamResponse(response, outgoing, abort.signal);
558
+ }
559
+ catch (error) {
560
+ if (abort.signal.aborted || outgoing.destroyed)
561
+ return;
562
+ if (outgoing.headersSent) {
563
+ // A partial SSE response cannot be replaced by a JSON error.
564
+ outgoing.destroy(error instanceof Error ? error : new Error(String(error)));
565
+ }
566
+ else {
567
+ outgoing.writeHead(500, { 'content-type': 'application/json' });
568
+ outgoing.end(JSON.stringify({ error: error instanceof Error ? error.message : 'internal error' }));
569
+ }
570
+ }
571
+ finally {
572
+ cleanup();
573
+ }
574
+ })();
254
575
  });
576
+ // No 'end' event is guaranteed on a truncated upload.
577
+ incoming.once('close', () => { if (!incoming.complete)
578
+ cleanup(); });
255
579
  });
256
580
  await new Promise((resolve, reject) => {
257
581
  server.once('error', reject);
@@ -265,8 +589,12 @@ export async function startHttpServer(config, host) {
265
589
  port,
266
590
  url: `http://${config.host}:${port}${config.path}`,
267
591
  async close() {
592
+ for (const abort of active)
593
+ abort.abort(new Error('HTTP endpoint closed'));
268
594
  await new Promise((resolve, reject) => {
269
595
  server.close(error => error === undefined ? resolve() : reject(error));
596
+ // server.close alone waits indefinitely for active/keep-alive sockets.
597
+ server.closeAllConnections();
270
598
  });
271
599
  },
272
600
  };