@robota-sdk/agent-transport-mcp 3.0.0-beta.82 → 3.0.0-beta.83

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/CHANGELOG.md ADDED
@@ -0,0 +1,244 @@
1
+ # @robota-sdk/agent-transport-mcp
2
+
3
+ ## 3.0.0-beta.83
4
+
5
+ ### Minor Changes
6
+
7
+ - d877de2: External events arrive over HTTP: `POST <public-url>/events/<grant-id>`, on a loopback port behind the owner's own
8
+ HTTPS proxy or tunnel.
9
+
10
+ - `agent-transport` — `./node` exports the resource-server gate every token-admitted HTTP carrier shares:
11
+ `createBearerResourceServer` (Host and Origin checked against the public URL, trusted-proxy `X-Forwarded-For`,
12
+ a failure-only throttle per address, coarse address classes), `describeProtectedResource` (RFC 9728 metadata
13
+ path and body, RFC 6750 challenges), `serveProtectedResourceMetadata`, `refuseBearerToken` and
14
+ `parsePublicHttpsUrl`.
15
+ - `agent-transport-mcp` — the remote MCP gate is composed from that shared gate; its answers are unchanged.
16
+ - `agent-cli` — each grant is its own endpoint and audience (its resource is `<public-url>/events/<grant-id>`).
17
+ An admitted event answers `202 {"turnId"}`; refusals have an empty body: 401 with the `invalid_token` or
18
+ missing-token challenge, 403 `insufficient_scope` or a revoked grant, 400 a malformed event, 404 an unknown
19
+ grant, 413 a body over 16 KiB, 429 over the grant's rate or the address's failure budget, 503 when the issuer
20
+ or the session cannot take it. `--external-event-port <port>` (required with grants) and
21
+ `--external-event-trusted-proxy <ip>` work on the TUI and on `robota session start --background`. A background
22
+ session writes an owner-only, bounded JSONL trail of refusals and settlements under its supervised directory;
23
+ the TUI reports them on one line each.
24
+
25
+ ### Patch Changes
26
+
27
+ - 57280bf: Every published package now declares `"engines": { "node": ">=22.12.0" }`. Before, 27 of the 38
28
+ packages declared no floor (`agent-core`, `agent-tools` and every provider among them),
29
+ `agent-session` and `agent-file-authority` declared `>=20.19.0`, and the other nine declared
30
+ `>=22.0.0`, so a consumer on Node 20 saw at most a warning from a transitive dependency.
31
+
32
+ Why 22.12: `agent-cli` and `agent-ui-terminal` need Node 22 through `ink` 7, and the CommonJS entries
33
+ of `agent-tools` and its dependents, `agent-transport`/`node` and its dependents, and
34
+ `agent-ui-terminal` `require()` ESM-only dependencies (`p-limit`, `jose`, `chalk`), which Node 22
35
+ supports unflagged only from 22.12. `engines` is advisory unless the consumer enables `engine-strict`.
36
+
37
+ No code changes: `tsdown` now reads `node22.12.0` as its build target from the field.
38
+
39
+ - 18c0d5c: Every published package now exports `./package.json`, so `require('<package>/package.json')` and
40
+ `import('<package>/package.json', { with: { type: 'json' } })` work instead of failing with
41
+ `ERR_PACKAGE_PATH_NOT_EXPORTED`, and each tarball now ships the package's `CHANGELOG.md`.
42
+ - Updated dependencies [724fabb]
43
+ - Updated dependencies [d877de2]
44
+ - Updated dependencies [d61e159]
45
+ - Updated dependencies [bfe8ed5]
46
+ - Updated dependencies [7b72344]
47
+ - Updated dependencies [6ae3f28]
48
+ - Updated dependencies [57f57f5]
49
+ - Updated dependencies [ba822c1]
50
+ - Updated dependencies [6e6b06b]
51
+ - Updated dependencies [57280bf]
52
+ - Updated dependencies [18c0d5c]
53
+ - @robota-sdk/agent-interface-transport@3.0.0-beta.83
54
+ - @robota-sdk/agent-transport@3.0.0-beta.83
55
+ - @robota-sdk/agent-interface-session@3.0.0-beta.83
56
+
57
+ ## 3.0.0-beta.82
58
+
59
+ ### Patch Changes
60
+
61
+ - Updated dependencies [c7f9203]
62
+ - @robota-sdk/agent-transport@3.0.0-beta.82
63
+ - @robota-sdk/agent-interface-session@3.0.0-beta.82
64
+ - @robota-sdk/agent-interface-transport@3.0.0-beta.82
65
+
66
+ ## 3.0.0-beta.81
67
+
68
+ ### Patch Changes
69
+
70
+ - Updated dependencies [3038eb7]
71
+ - @robota-sdk/agent-interface-session@3.0.0-beta.81
72
+ - @robota-sdk/agent-interface-transport@3.0.0-beta.81
73
+ - @robota-sdk/agent-transport@3.0.0-beta.81
74
+
75
+ ## 3.0.0-beta.80
76
+
77
+ ### Major Changes
78
+
79
+ - e82215f: **ARCH-011: replace the ambiguous transport lifecycle stub with executable conformance.**
80
+
81
+ `ITransportAdapter` now requires a frozen `service | runner` lifecycle descriptor. `start()` resolves
82
+ at the concrete transport's documented readiness boundary; start before attach and repeated active
83
+ start reject a stable lifecycle error, repeated stop is safe, and stopped adapters can reattach and
84
+ restart.
85
+
86
+ Runner adapters launch separately and expose a typed terminal outcome through
87
+ `waitForCompletion()`. The registry accepts base adapters, rejects duplicate names, keeps
88
+ configuration as an optional capability, returns complete ordered records whose pending slots become
89
+ registry-owned `abandoned` outcomes on stop/rollback, and exposes a real-runner-only first-failure
90
+ wait. It serializes startup/stop, rejects active restart before mutation, and reverses partial startup
91
+ from the currently failing adapter with typed safe rollback details. Runtime host and serve mode
92
+ propagate real nonzero runner results without treating normal shutdown abandonment as failure.
93
+
94
+ HTTP, MCP, both WebSocket adapters, WebRTC, and headless invoke one shared public conformance kit.
95
+ The former `TuiTransport` export is removed because it ignored the attached session; use `renderApp`
96
+ or `TuiInteractionChannel`, which honestly own their session lifecycle.
97
+
98
+ - e13c30c: MCP servers now publish a neutral submission tool by default. Hosts that need the previous
99
+ `robota_submit` tool must pass `submitTool: { name: 'robota_submit', description: '...' }` to
100
+ `createAgentMcpServer`, `createMcpTransport`, or `createMcpHttpHost`. The Robota CLI supplies its
101
+ existing name and description for both stdio and HTTP carriers.
102
+ - 5134b3b: **BREAKING — RUNTIME-003 P2: `submit` hands back the submission's identity, so an answer belongs to
103
+ the caller who asked for it.**
104
+
105
+ `submit` returned nothing, so a caller that needed to know when ITS turn ended had only the
106
+ session-global `complete` / `interrupted` / `error` events — which say that A turn ended and never
107
+ which one. The MCP adapter did exactly that, and the result was measurable: a session runs one turn
108
+ at a time and queues the rest, so two concurrent `submit` calls did not run concurrently. The second
109
+ waited and then took the RUNNING turn's response as its own answer. Both callers were told about one
110
+ turn; neither was told which.
111
+
112
+ `submit` now returns an `ITurnHandle` — `{ turnId, completed }`. The id is minted when the submission
113
+ is ACCEPTED and kept if it waits in the queue, so one submission is one identity from end to end.
114
+
115
+ `completed` always settles, and that is the part that took the work. A queued submission is not
116
+ promised a turn: the co-drive queue coalesces a same-driver input into the one behind it, drops at
117
+ capacity, and discards everything when cleared. A handle that settled only for submissions that ran
118
+ would leave the rest waiting forever — a worse failure than the ambiguity it replaces — so each of
119
+ those rejects with a typed `TurnNotRunError` naming which happened (`coalesced`, `dropped`,
120
+ `cancelled` — shutdown clears the queue through the same path, so it reports as cancelled).
121
+
122
+ **Migration.** A caller that ignores the return value is unaffected: `await session.submit(...)`
123
+ still means what it did, and the direct path still resolves only when the turn is over. An
124
+ IMPLEMENTER of `IInteractiveSession` must now return a handle:
125
+
126
+ ```ts
127
+ // before
128
+ async submit(input: string): Promise<void> {
129
+ await runTurn(input);
130
+ }
131
+
132
+ // after
133
+ async submit(input: string): Promise<ITurnHandle> {
134
+ const turnId = crypto.randomUUID();
135
+ return { turnId, completed: runTurn(input) };
136
+ }
137
+ ```
138
+
139
+ `createTestInteractiveSession` already returns a conforming handle, so a double built on it needs no
140
+ change.
141
+
142
+ One thing this deliberately does NOT do: DAG run advancement (P3) stays with DAG-001.
143
+
144
+ An earlier draft of this note claimed the HTTP route's documented TOCTOU had been measured and was
145
+ not reachable. That was wrong, and it is corrected here rather than left for a reader to trip over.
146
+ The probe behind it used a `submit` stub with no suspension point, so it could not exhibit the race
147
+ it was written to rule out; the real `submit` opens with `await ensureInitialized()`. The race is
148
+ real and is fixed in its own change (#1656), where the route CLAIMS the turn instead of asking
149
+ whether one is running.
150
+
151
+ ### Minor Changes
152
+
153
+ - 3131209: `robota mcp serve` can serve a remote MCP client as an OAuth resource server.
154
+
155
+ `agent-transport-mcp` gains `createMcpRemoteHttpHost`, which admits requests only by an OAuth access
156
+ token checked by an injected `IAccessTokenVerifier`. Its endpoint path and its RFC 9728
157
+ protected-resource metadata path are derived from the `https` public URL, so a proxy prefix works,
158
+ and Host and Origin are checked against that public origin. A refusal has an empty body: `401` with
159
+ `WWW-Authenticate: Bearer resource_metadata="…"` (plus `error="invalid_token"` when a token was
160
+ presented), or `403` with `error="insufficient_scope"`. The token is verified before anything is
161
+ counted, and only failures are counted, per client address, so a valid token is never throttled;
162
+ `X-Forwarded-For` is believed only from a configured trusted proxy. Each refusal goes to an injected
163
+ audit sink as a reason and an address class, never token text. The host stays stateless and issues
164
+ no `Mcp-Session-Id`. The loopback `createMcpHttpHost` is unchanged.
165
+
166
+ `agent-cli` adds `--http-public-url`, `--http-host`, `--oauth-issuer`, `--oauth-scopes`,
167
+ `--oauth-allowed-subjects` and `--trusted-proxy` to `robota mcp serve`. It binds an address other than
168
+ `127.0.0.1` only with the public URL and the OAuth settings, and never with `--http-token-file`.
169
+ Refusals are logged on stderr without token text.
170
+
171
+ - 9db63ee: Add named session capability roles and explicit capability-host queries while preserving the legacy
172
+ `IInteractiveSession` interface shape. HTTP, MCP, protocol, WS, WebRTC, and headless transports now
173
+ declare only the session roles they consume, and the direct aggregate-cast floor is zero.
174
+
175
+ ### Patch Changes
176
+
177
+ - 4f3c075: Assemble complete, verified package generations before switching build output; preserve the previous generation on build failure and pack only verified regular-file images. Include copied CLI web assets in affected-build ordering and artifact transfer. Preserve the CLI version in managed build paths. Public runtime contracts remain compatible (patch).
178
+ - Updated dependencies [37b4bd7]
179
+ - Updated dependencies [50d2c9f]
180
+ - Updated dependencies [a5961c9]
181
+ - Updated dependencies [4c5148e]
182
+ - Updated dependencies [0116a29]
183
+ - Updated dependencies [e82215f]
184
+ - Updated dependencies [52b7346]
185
+ - Updated dependencies [9db63ee]
186
+ - Updated dependencies [b078afa]
187
+ - Updated dependencies [2ebff01]
188
+ - Updated dependencies [9db63ee]
189
+ - Updated dependencies [2ebff01]
190
+ - Updated dependencies [4772067]
191
+ - Updated dependencies [0f98419]
192
+ - Updated dependencies [64ba748]
193
+ - Updated dependencies [d312755]
194
+ - Updated dependencies [3244fb8]
195
+ - Updated dependencies [1e3f91a]
196
+ - Updated dependencies [4f3c075]
197
+ - Updated dependencies [7669851]
198
+ - Updated dependencies [4b76cfa]
199
+ - Updated dependencies [07b627f]
200
+ - Updated dependencies [9665c6e]
201
+ - Updated dependencies [44393be]
202
+ - Updated dependencies [c7fa299]
203
+ - Updated dependencies [5134b3b]
204
+ - Updated dependencies [5c5ff23]
205
+ - Updated dependencies [9814afc]
206
+ - @robota-sdk/agent-interface-session@3.0.0-beta.80
207
+ - @robota-sdk/agent-interface-transport@3.0.0-beta.80
208
+ - @robota-sdk/agent-transport@3.0.0-beta.80
209
+
210
+ ## 3.0.0-beta.79
211
+
212
+ ### Patch Changes
213
+
214
+ - @robota-sdk/agent-interface-transport@3.0.0-beta.79
215
+
216
+ ## 3.0.0-beta.78
217
+
218
+ ### Patch Changes
219
+
220
+ - @robota-sdk/agent-interface-transport@3.0.0-beta.78
221
+
222
+ ## 3.0.0-beta.77
223
+
224
+ ### Patch Changes
225
+
226
+ - @robota-sdk/agent-interface-transport@3.0.0-beta.77
227
+
228
+ ## 3.0.0-beta.76
229
+
230
+ ### Minor Changes
231
+
232
+ - 9df3a88: Split the consolidated `@robota-sdk/agent-transport` package into per-concern transport packages (DQ-AUDIT-005) so unrelated heavy dependencies (React/Ink, ws, Hono, MCP SDK) no longer share one publishable unit and are not dragged into non-TUI consumers' graphs:
233
+
234
+ - `@robota-sdk/agent-transport` — lean core: headless adapter + `TransportRegistry` + scripted-provider testing fixtures (no external runtime deps).
235
+ - `@robota-sdk/agent-transport-tui` — React + Ink terminal UI.
236
+ - `@robota-sdk/agent-transport-ws` — WebSocket transport + protocol (`agent-web-ui` now depends only on this for WS types).
237
+ - `@robota-sdk/agent-transport-http` — Hono HTTP transport.
238
+ - `@robota-sdk/agent-transport-mcp` — MCP server transport.
239
+
240
+ The default transport-registry wiring (pre-registering `WsTransport`) moves to the CLI composition root, removing the core→ws edge.
241
+
242
+ ### Patch Changes
243
+
244
+ - @robota-sdk/agent-interface-transport@3.0.0-beta.76
@@ -1 +1 @@
1
- Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@modelcontextprotocol/sdk/server/index.js"),t=require("@modelcontextprotocol/sdk/types.js"),n=require("@robota-sdk/agent-interface-session"),r=require("node:stream"),i=require("@modelcontextprotocol/sdk/server/stdio.js"),a=require("node:http"),o=require("node:net"),s=require("@modelcontextprotocol/node"),c=require("@modelcontextprotocol/server"),l=require("@robota-sdk/agent-transport/node");const u={name:`agent_submit`,description:`Submit a prompt to the agent and await its turn`};function d(e=u){if(typeof e.name!=`string`||!/^[A-Za-z0-9._-]{1,128}$/.test(e.name))throw Error(`MCP submit tool name must contain 1–128 ASCII letters, digits, dots, hyphens, or underscores`);return t.ToolSchema.parse({name:e.name,description:e.description,inputSchema:{type:`object`,properties:{prompt:{type:`string`,minLength:1}},required:[`prompt`],additionalProperties:!1}})}async function f(e,n){let r=new Set([n]);return(await e.listRuntimeTools()).map(e=>{if(r.has(e.name))throw Error(`Duplicate or reserved MCP tool name: ${e.name}`);return r.add(e.name),t.ToolSchema.parse({name:e.name,description:e.description,inputSchema:e.parameters})})}function p(e){return{content:[{type:`text`,text:e}],isError:!0}}function m(e){return e===null||typeof e==`string`||typeof e==`boolean`?!0:typeof e==`number`?Number.isFinite(e):Array.isArray(e)?e.every(m):typeof e==`object`&&!!e&&Object.values(e).every(m)}function h(e){return Object.values(e).every(m)}async function g(e,t,r,i,a){let o;try{i.throwIfAborted(),o=await f(e,a)}catch(e){return p(e instanceof Error?e.message:String(e))}if(t===a){if(typeof r.prompt!=`string`||r.prompt.length===0||Object.keys(r).some(e=>e!==`prompt`))return p(`${a} requires a non-empty string prompt and no other arguments`);try{return{content:[{type:`text`,text:(await(await e.submit(r.prompt,void 0,void 0,{signal:i})).completed).response}]}}catch(e){if(!(0,n.isTurnNotRunError)(e)&&!i.aborted)throw e;return p(e instanceof Error?e.message:String(e))}}if(!o.some(e=>e.name===t))return p(`Unknown tool: ${t}`);if(!h(r))return p(`Tool arguments must contain JSON values`);try{let n=await e.invokeRuntimeTool(t,r,{signal:i});return{content:[{type:`text`,text:JSON.stringify(n)}],isError:!n.success}}catch(e){return p(e instanceof Error?e.message:String(e))}}async function _(n){let{name:r,version:i,session:a}=n,o=d(n.submitTool);await f(a,o.name);let s=new e.Server({name:r,version:i},{capabilities:{tools:{}}});return s.setRequestHandler(t.ListToolsRequestSchema,async()=>({tools:[...await f(a,o.name),o]})),s.setRequestHandler(t.CallToolRequestSchema,async(e,t)=>g(a,e.params.name,e.params.arguments??{},t.signal,o.name)),s}function v(e){let t=null,n=null,a=null,o=null,s=null,c=null,l=e.stdin??process.stdin,u=e.stdout??process.stdout,d=null,f=null,p=Promise.resolve(),m=()=>{p=new Promise((e,t)=>{d=e,f=t}),p.catch(()=>void 0)},h=()=>{d?.(),c?.(Error(`MCP input closed during startup`))},g=()=>{d?.(),c?.(Error(`MCP output closed during startup`))},v=e=>{f?.(e),c?.(e)},y=()=>{l.off(`end`,h),l.off(`close`,h),u.off(`close`,g),u.off(`error`,v),s&&=(l.unpipe(s),s.destroy(),null)},b=e=>Object.assign(Error(`MCP transport ${e}.`),{name:`TransportLifecycleError`,code:e,transportName:`mcp`});return{name:`mcp`,lifecycle:Object.freeze({kind:`service`}),attach(e){if(n||a||o)throw b(`already-started`);t=e,m()},async start(){if(!t)throw b(`not-attached`);if(n||a||o)throw b(`already-started`);let p=t,m=new Promise((e,t)=>{c=t}),x=new r.PassThrough;s=x,l.once(`end`,h),l.once(`close`,h),u.once(`close`,g),u.on(`error`,v),l.pipe(x),a=(async()=>{let t=null,r=setTimeout(()=>c?.(Error(`MCP carrier startup timed out`)),15e3);try{t=await Promise.race([_({name:e.name,version:e.version,session:p,...e.submitTool===void 0?{}:{submitTool:e.submitTool}}),m]);let r=new i.StdioServerTransport(x,u);r.onclose=()=>d?.(),r.onerror=e=>f?.(e),await t.connect(r),n=t,t=null}catch(e){throw y(),t&&await t.close().catch(()=>void 0),e}finally{clearTimeout(r),c=null}})();try{await a}finally{a=null}},stop(){return o||(o=(async()=>{try{c?.(Error(`MCP carrier startup stopped`)),a&&await a.catch(()=>void 0),n&&await n.close()}finally{y(),n=null,t=null,d?.()}})().finally(()=>{o=null}),o)},getServer(){if(!n)throw Error(`Transport not started. Call start() first.`);return n},waitForClose(){return p}}}const y=6e4,b=4096,x=/^[\x21\x23-\x5b\x5d-\x7e]+$/,S=new o.BlockList;S.addSubnet(`127.0.0.0`,8,`ipv4`),S.addAddress(`::1`,`ipv6`);const C=new o.BlockList;C.addSubnet(`10.0.0.0`,8,`ipv4`),C.addSubnet(`172.16.0.0`,12,`ipv4`),C.addSubnet(`192.168.0.0`,16,`ipv4`),C.addSubnet(`169.254.0.0`,16,`ipv4`),C.addSubnet(`100.64.0.0`,10,`ipv4`),C.addSubnet(`fc00::`,7,`ipv6`),C.addSubnet(`fe80::`,10,`ipv6`);function w(e){if(e===void 0)return;let t=e.trim().toLowerCase(),n=t.startsWith(`::ffff:`)?t.slice(7):void 0;return n!==void 0&&(0,o.isIP)(n)===4?n:(0,o.isIP)(t)===0?void 0:t}function T(e){if(e===void 0)return`unknown`;let t=(0,o.isIP)(e)===4?`ipv4`:`ipv6`;return S.check(e,t)?`loopback`:C.check(e,t)?`private`:`public`}function E(e){let t;try{t=new URL(e)}catch{throw Error(`MCP HTTP public URL must be an absolute https URL`)}if(t.protocol!==`https:`)throw Error(`MCP HTTP public URL must use https`);if(t.username!==``||t.password!==``||t.search!==``||t.hash!==``)throw Error(`MCP HTTP public URL must not carry credentials, a query or a fragment`);return t}function D(e){let t=new Map;return{fail(n){let r=e(),i=t.get(n);if(i===void 0||r-i.startedAt>=y){if(t.delete(n),t.size>=b){for(let[e,n]of t)r-n.startedAt>=y&&t.delete(e);let e=t.keys().next();t.size>=b&&e.done!==!0&&t.delete(e.value)}i={count:0,startedAt:r},t.set(n,i)}i.count+=1;let a=y-(r-i.startedAt);return{throttled:i.count>20,retryAfterSeconds:Math.max(1,Math.ceil(a/1e3))}}}}function O(e,t=()=>performance.now()){let n=E(e.publicUrl),r;try{r=new URL(e.issuer)}catch{throw Error(`MCP HTTP authorization issuer must be an absolute https URL`)}if(r.protocol!==`https:`)throw Error(`MCP HTTP authorization issuer must use https`);if(e.scopes.length===0||!e.scopes.every(e=>x.test(e)))throw Error(`MCP HTTP authorization needs at least one valid scope`);let i=new Set;for(let t of e.trustedProxies??[]){let e=w(t);if(e===void 0)throw Error(`MCP HTTP trusted proxy must be a literal IP address`);i.add(e)}let a=n.pathname,o=`/.well-known/oauth-protected-resource${a===`/`?``:a}`,s=`${n.origin}${o}`,c=new Set([n.host]);n.port===``&&c.add(`${n.hostname}:443`);let u=JSON.stringify({resource:e.publicUrl,authorization_servers:[e.issuer],scopes_supported:[...e.scopes],bearer_methods_supported:[`header`]}),d=e.scopes.join(` `),f={missing:`Bearer resource_metadata="${s}"`,invalid:`Bearer error="invalid_token", resource_metadata="${s}"`,scope:`Bearer error="insufficient_scope", scope="${d}", resource_metadata="${s}"`},p=D(t);function m(e){let t=w(e.socket.remoteAddress);if(t===void 0||!i.has(t))return t;let n=e.headers[`x-forwarded-for`],r=(Array.isArray(n)?n.join(`,`):n??``).split(`,`);for(let e=r.length-1;e>=0;--e){let n=w(r[e]);if(n===void 0)return t;if(!i.has(n))return n}return t}function h(t,n,r){let i=m(t),a=T(i);if(r===`keys-unavailable`){e.audit?.({refusal:r,remote:a,throttled:!1}),n.writeHead(503).end();return}let o=p.fail(i??`unknown`);if(e.audit?.({refusal:r,remote:a,throttled:o.throttled}),o.throttled)n.writeHead(429,{"Retry-After":String(o.retryAfterSeconds)}).end();else if(r===`missing-scope`)n.writeHead(403,{"WWW-Authenticate":f.scope}).end();else{let e=r===`missing-token`?f.missing:f.invalid;n.writeHead(401,{"WWW-Authenticate":e}).end()}}return{endpointPath:a,async admit(t,r){let i=t.headers.host?.toLowerCase(),s=t.headers.origin;if(i===void 0||!c.has(i)||s!==void 0&&s!==n.origin)return r.writeHead(403).end(),!1;if(t.url===o)return t.method===`GET`?(r.writeHead(200,{"Content-Type":`application/json`}).end(u),!1):(r.writeHead(405,{Allow:`GET`}).end(),!1);if(t.url!==a)return r.writeHead(404).end(),!1;let d=(0,l.bearerCredential)(t.headers.authorization);if(d===void 0)return h(t,r,`missing-token`),!1;let f=await e.verifier.verify(d);return f.admitted?!0:(h(t,r,f.refusal),!1)}}}const k=`127.0.0.1`,A=1024*1024;function j(e){if(e!==void 0&&(!Number.isInteger(e)||e<0||e>65535))throw Error(`MCP HTTP port must be an integer in 0..65535`)}function M(e,t,n){let r=d(e.submitTool),i,o,l,u=!1,p=!1,m=0,h,_,v=new Promise((e,t)=>{h=e,_=t});return v.catch(()=>void 0),{async start(){if(u||l)throw Error(`MCP HTTP host already started`);if(u=!0,await f(e.session,r.name),p)throw Error(`MCP HTTP host stopped during startup`);o=(0,c.createMcpHandler)(()=>{let t=new c.Server({name:e.name,version:e.version},{capabilities:{tools:{}}});return t.setRequestHandler(`tools/list`,async()=>({tools:JSON.parse(JSON.stringify([...await f(e.session,r.name),r]))})),t.setRequestHandler(`tools/call`,async(t,n)=>g(e.session,t.params.name,t.params.arguments??{},n.mcpReq.signal,r.name)),t},{maxSubscriptions:0});let d=(0,s.toNodeHandler)(o),v=(e,t)=>{if(m>=16){t.writeHead(503).end();return}m+=1,(async()=>{try{let n;if(e.method===`POST`){let r=Number(e.headers[`content-length`]);if(Number.isFinite(r)&&r>A){t.writeHead(413).end();return}let i=[],a=0;for await(let n of e){let r=Buffer.isBuffer(n)?n:Buffer.from(n);if(a+=r.length,a>A){t.writeHead(413).end(),e.destroy();return}i.push(r)}try{n=JSON.parse(Buffer.concat(i).toString(`utf8`))}catch{t.writeHead(400).end();return}}await d(e,t,n)}catch{t.headersSent?t.destroy():t.writeHead(500).end()}finally{--m}})()},y=(0,a.createServer)({maxHeaderSize:16*1024},(e,t)=>{let r=y.address();if(!r||typeof r==`string`){t.writeHead(503).end();return}let i=n(e,t,r.port);i===!0?v(e,t):i!==!1&&i.then(n=>{n&&v(e,t)},()=>{t.headersSent?t.destroy():t.writeHead(500).end()})});y.maxConnections=32,y.headersTimeout=5e3,y.requestTimeout=15e3,y.on(`error`,e=>_?.(e)),y.on(`close`,()=>h?.());try{await new Promise((n,r)=>{y.once(`error`,r),y.listen(e.port??0,t,()=>{y.off(`error`,r),n()})})}catch(e){throw await o.close(),e}if(p)throw await new Promise(e=>y.close(()=>e())),await o.close(),Error(`MCP HTTP host stopped during startup`);return i=y,y.address()},stop(){return l||(p=!0,l=(async()=>{let e=o,t=i;if(!t){await e?.close(),h?.();return}let n=new Promise((e,n)=>{t.close(t=>t?n(t):e()),t.closeAllConnections()}),r;try{await Promise.race([Promise.all([e?.close(),n]).then(()=>void 0),new Promise((e,t)=>{r=setTimeout(()=>t(Error(`MCP HTTP close timed out`)),5e3)})])}finally{r&&clearTimeout(r)}})(),l)},waitForClose(){return v}}}function N(e){if(e.host!==void 0&&e.host!==k)throw Error(`MCP HTTP host must bind numeric IPv4 loopback`);j(e.port);let t=(0,l.mintTransportToken)(),n=M(e,k,(e,n,r)=>{let i=`${k}:${r}`,a=`http://${i}`;if(e.url!==`/mcp`||e.headers.host!==i||e.headers.origin!==void 0&&e.headers.origin!==a)return n.writeHead(403).end(),!1;let o=e.headers.authorization;return typeof o!=`string`||!(0,l.credentialMatches)(t,(0,l.bearerCredential)(o))?(n.writeHead(401).end(),!1):!0});return{async start(){let e=await n.start();return{url:`http://${k}:${e.port}/mcp`,token:t}},stop:n.stop,waitForClose:n.waitForClose}}function P(e){let t=e.host??k;if((0,o.isIP)(t)===0)throw Error(`MCP HTTP host must bind a literal IP address`);j(e.port);let n=O(e.authorization),r=M(e,t,(e,t)=>n.admit(e,t));return{async start(){let t=await r.start();return{listening:`${t.family===`IPv6`?`[${t.address}]`:t.address}:${t.port}`,url:e.authorization.publicUrl}},stop:r.stop,waitForClose:r.waitForClose}}exports.createAgentMcpServer=_,exports.createMcpHttpHost=N,exports.createMcpRemoteHttpHost=P,exports.createMcpTransport=v;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require("@modelcontextprotocol/sdk/server/index.js"),t=require("@modelcontextprotocol/sdk/types.js"),n=require("@robota-sdk/agent-interface-session"),r=require("node:stream"),i=require("@modelcontextprotocol/sdk/server/stdio.js"),a=require("node:http"),o=require("node:net"),s=require("@modelcontextprotocol/node"),c=require("@modelcontextprotocol/server"),l=require("@robota-sdk/agent-transport/node");const u={name:`agent_submit`,description:`Submit a prompt to the agent and await its turn`};function d(e=u){if(typeof e.name!=`string`||!/^[A-Za-z0-9._-]{1,128}$/.test(e.name))throw Error(`MCP submit tool name must contain 1–128 ASCII letters, digits, dots, hyphens, or underscores`);return t.ToolSchema.parse({name:e.name,description:e.description,inputSchema:{type:`object`,properties:{prompt:{type:`string`,minLength:1}},required:[`prompt`],additionalProperties:!1}})}async function f(e,n){let r=new Set([n]);return(await e.listRuntimeTools()).map(e=>{if(r.has(e.name))throw Error(`Duplicate or reserved MCP tool name: ${e.name}`);return r.add(e.name),t.ToolSchema.parse({name:e.name,description:e.description,inputSchema:e.parameters})})}function p(e){return{content:[{type:`text`,text:e}],isError:!0}}function m(e){return e===null||typeof e==`string`||typeof e==`boolean`?!0:typeof e==`number`?Number.isFinite(e):Array.isArray(e)?e.every(m):typeof e==`object`&&Object.values(e).every(m)}function h(e){return Object.values(e).every(m)}async function g(e,t,r,i,a){let o;try{i.throwIfAborted(),o=await f(e,a)}catch(e){return p(e instanceof Error?e.message:String(e))}if(t===a){if(typeof r.prompt!=`string`||r.prompt.length===0||Object.keys(r).some(e=>e!==`prompt`))return p(`${a} requires a non-empty string prompt and no other arguments`);try{return{content:[{type:`text`,text:(await(await e.submit(r.prompt,void 0,void 0,{signal:i})).completed).response}]}}catch(e){if(!(0,n.isTurnNotRunError)(e)&&!i.aborted)throw e;return p(e instanceof Error?e.message:String(e))}}if(!o.some(e=>e.name===t))return p(`Unknown tool: ${t}`);if(!h(r))return p(`Tool arguments must contain JSON values`);try{let n=await e.invokeRuntimeTool(t,r,{signal:i});return{content:[{type:`text`,text:JSON.stringify(n)}],isError:!n.success}}catch(e){return p(e instanceof Error?e.message:String(e))}}async function _(n){let{name:r,version:i,session:a}=n,o=d(n.submitTool);await f(a,o.name);let s=new e.Server({name:r,version:i},{capabilities:{tools:{}}});return s.setRequestHandler(t.ListToolsRequestSchema,async()=>({tools:[...await f(a,o.name),o]})),s.setRequestHandler(t.CallToolRequestSchema,async(e,t)=>g(a,e.params.name,e.params.arguments??{},t.signal,o.name)),s}function v(e){let t=null,n=null,a=null,o=null,s=null,c=null,l=e.stdin??process.stdin,u=e.stdout??process.stdout,d=null,f=null,p=Promise.resolve(),m=()=>{p=new Promise((e,t)=>{d=e,f=t}),p.catch(()=>void 0)},h=()=>{d?.(),c?.(Error(`MCP input closed during startup`))},g=()=>{d?.(),c?.(Error(`MCP output closed during startup`))},v=e=>{f?.(e),c?.(e)},y=()=>{l.off(`end`,h),l.off(`close`,h),u.off(`close`,g),u.off(`error`,v),s&&=(l.unpipe(s),s.destroy(),null)},b=e=>Object.assign(Error(`MCP transport ${e}.`),{name:`TransportLifecycleError`,code:e,transportName:`mcp`});return{name:`mcp`,lifecycle:Object.freeze({kind:`service`}),attach(e){if(n||a||o)throw b(`already-started`);t=e,m()},async start(){if(!t)throw b(`not-attached`);if(n||a||o)throw b(`already-started`);let p=t,m=new Promise((e,t)=>{c=t}),x=new r.PassThrough;s=x,l.once(`end`,h),l.once(`close`,h),u.once(`close`,g),u.on(`error`,v),l.pipe(x),a=(async()=>{let t=null,r=setTimeout(()=>c?.(Error(`MCP carrier startup timed out`)),15e3);try{t=await Promise.race([_({name:e.name,version:e.version,session:p,...e.submitTool===void 0?{}:{submitTool:e.submitTool}}),m]);let r=new i.StdioServerTransport(x,u);r.onclose=()=>d?.(),r.onerror=e=>f?.(e),await t.connect(r),n=t,t=null}catch(e){throw y(),t&&await t.close().catch(()=>void 0),e}finally{clearTimeout(r),c=null}})();try{await a}finally{a=null}},stop(){return o||(o=(async()=>{try{c?.(Error(`MCP carrier startup stopped`)),a&&await a.catch(()=>void 0),n&&await n.close()}finally{y(),n=null,t=null,d?.()}})().finally(()=>{o=null}),o)},getServer(){if(!n)throw Error(`Transport not started. Call start() first.`);return n},waitForClose(){return p}}}const y=`MCP HTTP`;function b(e,t=()=>performance.now()){let n=(0,l.describeProtectedResource)({resource:e.publicUrl,issuer:e.issuer,scopes:e.scopes,label:y}),r=(0,l.createBearerResourceServer)({publicUrl:e.publicUrl,...e.trustedProxies===void 0?{}:{trustedProxies:e.trustedProxies},label:y,now:t}),i=r.url.pathname;function a(t,i,a){if(a===`keys-unavailable`){e.audit?.({refusal:a,remote:r.remote(t),throttled:!1}),i.writeHead(503).end();return}let o=r.fail(t);e.audit?.({refusal:a,remote:o.remote,throttled:o.throttled}),(0,l.refuseBearerToken)(i,n,a===`missing-token`||a===`missing-scope`?a:`invalid-token`,o)}return{endpointPath:i,async admit(t,o){if(!r.checkNames(t,o))return!1;if(t.url===n.wellKnownPath)return(0,l.serveProtectedResourceMetadata)(t,o,n),!1;if(t.url!==i)return o.writeHead(404).end(),!1;let s=(0,l.bearerCredential)(t.headers.authorization);if(s===void 0)return a(t,o,`missing-token`),!1;let c=await e.verifier.verify(s);return c.admitted?!0:(a(t,o,c.refusal),!1)}}}const x=`127.0.0.1`,S=1024*1024;function C(e){if(e!==void 0&&(!Number.isInteger(e)||e<0||e>65535))throw Error(`MCP HTTP port must be an integer in 0..65535`)}function w(e,t,n){let r=d(e.submitTool),i,o,l,u=!1,p=!1,m=0,h,_,v=new Promise((e,t)=>{h=e,_=t});return v.catch(()=>void 0),{async start(){if(u||l)throw Error(`MCP HTTP host already started`);if(u=!0,await f(e.session,r.name),p)throw Error(`MCP HTTP host stopped during startup`);o=(0,c.createMcpHandler)(()=>{let t=new c.Server({name:e.name,version:e.version},{capabilities:{tools:{}}});return t.setRequestHandler(`tools/list`,async()=>({tools:JSON.parse(JSON.stringify([...await f(e.session,r.name),r]))})),t.setRequestHandler(`tools/call`,async(t,n)=>g(e.session,t.params.name,t.params.arguments??{},n.mcpReq.signal,r.name)),t},{maxSubscriptions:0});let d=(0,s.toNodeHandler)(o),v=(e,t)=>{if(m>=16){t.writeHead(503).end();return}m+=1,(async()=>{try{let n;if(e.method===`POST`){let r=Number(e.headers[`content-length`]);if(Number.isFinite(r)&&r>S){t.writeHead(413).end();return}let i=[],a=0;for await(let n of e){let r=Buffer.isBuffer(n)?n:Buffer.from(n);if(a+=r.length,a>S){t.writeHead(413).end(),e.destroy();return}i.push(r)}try{n=JSON.parse(Buffer.concat(i).toString(`utf8`))}catch{t.writeHead(400).end();return}}await d(e,t,n)}catch{t.headersSent?t.destroy():t.writeHead(500).end()}finally{--m}})()},y=(0,a.createServer)({maxHeaderSize:16*1024},(e,t)=>{let r=y.address();if(!r||typeof r==`string`){t.writeHead(503).end();return}let i=n(e,t,r.port);i===!0?v(e,t):i!==!1&&i.then(n=>{n&&v(e,t)},()=>{t.headersSent?t.destroy():t.writeHead(500).end()})});y.maxConnections=32,y.headersTimeout=5e3,y.requestTimeout=15e3,y.on(`error`,e=>_?.(e)),y.on(`close`,()=>h?.());try{await new Promise((n,r)=>{y.once(`error`,r),y.listen(e.port??0,t,()=>{y.off(`error`,r),n()})})}catch(e){throw await o.close(),e}if(p)throw await new Promise(e=>y.close(()=>e())),await o.close(),Error(`MCP HTTP host stopped during startup`);return i=y,y.address()},stop(){return l||(p=!0,l=(async()=>{let e=o,t=i;if(!t){await e?.close(),h?.();return}let n=new Promise((e,n)=>{t.close(t=>t?n(t):e()),t.closeAllConnections()}),r;try{await Promise.race([Promise.all([e?.close(),n]).then(()=>void 0),new Promise((e,t)=>{r=setTimeout(()=>t(Error(`MCP HTTP close timed out`)),5e3)})])}finally{r&&clearTimeout(r)}})(),l)},waitForClose(){return v}}}function T(e){if(e.host!==void 0&&e.host!==x)throw Error(`MCP HTTP host must bind numeric IPv4 loopback`);C(e.port);let t=(0,l.mintTransportToken)(),n=w(e,x,(e,n,r)=>{let i=`${x}:${r}`,a=`http://${i}`;if(e.url!==`/mcp`||e.headers.host!==i||e.headers.origin!==void 0&&e.headers.origin!==a)return n.writeHead(403).end(),!1;let o=e.headers.authorization;return typeof o!=`string`||!(0,l.credentialMatches)(t,(0,l.bearerCredential)(o))?(n.writeHead(401).end(),!1):!0});return{async start(){let e=await n.start();return{url:`http://${x}:${e.port}/mcp`,token:t}},stop:n.stop,waitForClose:n.waitForClose}}function E(e){let t=e.host??x;if((0,o.isIP)(t)===0)throw Error(`MCP HTTP host must bind a literal IP address`);C(e.port);let n=b(e.authorization),r=w(e,t,(e,t)=>n.admit(e,t));return{async start(){let t=await r.start();return{listening:`${t.family===`IPv6`?`[${t.address}]`:t.address}:${t.port}`,url:e.authorization.publicUrl}},stop:r.stop,waitForClose:r.waitForClose}}exports.createAgentMcpServer=_,exports.createMcpHttpHost=T,exports.createMcpRemoteHttpHost=E,exports.createMcpTransport=v;
@@ -3,6 +3,7 @@ import { ISessionRuntimeTools, ISessionTurnSubmission } from "@robota-sdk/agent-
3
3
  import "@modelcontextprotocol/sdk/types.js";
4
4
  import { IAccessTokenVerifier, ITransportAdapter, TAccessTokenRefusal } from "@robota-sdk/agent-interface-transport";
5
5
  import { Readable, Writable } from "node:stream";
6
+ import { TRemoteAddressClass } from "@robota-sdk/agent-transport/node";
6
7
  //#region src/mcp-session.d.ts
7
8
  /** The exact session capabilities consumed by the MCP transport. */
8
9
  interface IMcpTransportSession extends ISessionTurnSubmission, ISessionRuntimeTools {}
@@ -47,7 +48,7 @@ declare function createMcpTransport(options: IMcpTransportOptions): IMcpTranspor
47
48
  /** Why a remote request was refused: a verifier refusal, or no bearer token at all. */
48
49
  type TMcpRemoteRefusal = TAccessTokenRefusal | 'missing-token';
49
50
  /** A coarse class of the refused peer's address; the address itself is never reported. */
50
- type TMcpRemoteAddressClass = 'loopback' | 'private' | 'public' | 'unknown';
51
+ type TMcpRemoteAddressClass = TRemoteAddressClass;
51
52
  /** One refused request, as the audit sink sees it. Carries no token text and no claim value. */
52
53
  interface IMcpRemoteAuditRecord {
53
54
  readonly refusal: TMcpRemoteRefusal;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/mcp-session.ts","../../src/mcp-tool-surface.ts","../../src/mcp-server.ts","../../src/mcp-transport.ts","../../src/mcp-remote-authorization.ts","../../src/mcp-http-host.ts"],"mappings":";;;;;;;UAMiB,6BAA6B,wBAAwB;;;UCCrD;WACN;WACA;;;;UCAM;EACf;EACA;EACA,SAAS;;EAET,aAAa;;;iBAIO,qBAAqB,SAAS,mBAAmB,QAAQ;;;UCM9D;;EAEf;;EAEA;;EAEA,QAAQ;EACR,SAAS;;EAET,aAAa;;UAGE,sBAAsB,kBAAkB;EACvD,aAAa;;EAEb,gBAAgB;;iBAGF,mBAAmB,SAAS,uBAAuB;;;;KCFvD,oBAAoB;;KAGpB;;UAGK;WACN,SAAS;WACT,QAAQ;;WAER;;;UAIM;;WAEN;;WAEA;;WAEA;;WAEA,UAAU;;WAEV;;WAEA,SAAS,QAAQ;;;;UChCX;EACf;EACA;EACA,SAAS;;EAET;;EAEA;;EAEA,aAAa;;UAGE;EACf,SAAS;IAAU;IAAa;;EAChC,QAAQ;EACR,gBAAgB;;UAGD;EACf;EACA;EACA,SAAS;;EAET;;EAEA;;EAEA,eAAe;;EAEf,aAAa;;UAGE;;EAEf,SAAS;IAAU;IAAmB;;EACtC,QAAQ;EACR,gBAAgB;;iBA6MF,kBAAkB,SAAS,sBAAsB;;;;;iBA2CjD,wBAAwB,SAAS,4BAA4B"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../../src/mcp-session.ts","../../src/mcp-tool-surface.ts","../../src/mcp-server.ts","../../src/mcp-transport.ts","../../src/mcp-remote-authorization.ts","../../src/mcp-http-host.ts"],"mappings":";;;;;;;;UAMiB,6BAA6B,wBAAwB;;;UCCrD;WACN;WACA;;;;UCAM;EACf;EACA;EACA,SAAS;;EAET,aAAa;;;iBAIO,qBAAqB,SAAS,mBAAmB,QAAQ;;;UCM9D;;EAEf;;EAEA;;EAEA,QAAQ;EACR,SAAS;;EAET,aAAa;;UAGE,sBAAsB,kBAAkB;EACvD,aAAa;;EAEb,gBAAgB;;iBAGF,mBAAmB,SAAS,uBAAuB;;;;KCZvD,oBAAoB;;KAGpB,yBAAyB;;UAGpB;WACN,SAAS;WACT,QAAQ;;WAER;;;UAIM;;WAEN;;WAEA;;WAEA;;WAEA,UAAU;;WAEV;;WAEA,SAAS,QAAQ;;;;UCtBX;EACf;EACA;EACA,SAAS;;EAET;;EAEA;;EAEA,aAAa;;UAGE;EACf,SAAS;IAAU;IAAa;;EAChC,QAAQ;EACR,gBAAgB;;UAGD;EACf;EACA;EACA,SAAS;;EAET;;EAEA;;EAEA,eAAe;;EAEf,aAAa;;UAGE;;EAEf,SAAS;IAAU;IAAmB;;EACtC,QAAQ;EACR,gBAAgB;;iBA6MF,kBAAkB,SAAS,sBAAsB;;;;;iBA2CjD,wBAAwB,SAAS,4BAA4B"}
@@ -2,6 +2,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
2
  import "@modelcontextprotocol/sdk/types.js";
3
3
  import { ISessionRuntimeTools, ISessionTurnSubmission } from "@robota-sdk/agent-interface-session";
4
4
  import { Readable, Writable } from "node:stream";
5
+ import { TRemoteAddressClass } from "@robota-sdk/agent-transport/node";
5
6
  import { IAccessTokenVerifier, ITransportAdapter, TAccessTokenRefusal } from "@robota-sdk/agent-interface-transport";
6
7
  //#region src/mcp-session.d.ts
7
8
  /** The exact session capabilities consumed by the MCP transport. */
@@ -47,7 +48,7 @@ declare function createMcpTransport(options: IMcpTransportOptions): IMcpTranspor
47
48
  /** Why a remote request was refused: a verifier refusal, or no bearer token at all. */
48
49
  type TMcpRemoteRefusal = TAccessTokenRefusal | 'missing-token';
49
50
  /** A coarse class of the refused peer's address; the address itself is never reported. */
50
- type TMcpRemoteAddressClass = 'loopback' | 'private' | 'public' | 'unknown';
51
+ type TMcpRemoteAddressClass = TRemoteAddressClass;
51
52
  /** One refused request, as the audit sink sees it. Carries no token text and no claim value. */
52
53
  interface IMcpRemoteAuditRecord {
53
54
  readonly refusal: TMcpRemoteRefusal;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/mcp-session.ts","../../src/mcp-tool-surface.ts","../../src/mcp-server.ts","../../src/mcp-transport.ts","../../src/mcp-remote-authorization.ts","../../src/mcp-http-host.ts"],"mappings":";;;;;;;UAMiB,6BAA6B,wBAAwB;;;UCCrD;WACN;WACA;;;;UCAM;EACf;EACA;EACA,SAAS;;EAET,aAAa;;;iBAIO,qBAAqB,SAAS,mBAAmB,QAAQ;;;UCM9D;;EAEf;;EAEA;;EAEA,QAAQ;EACR,SAAS;;EAET,aAAa;;UAGE,sBAAsB,kBAAkB;EACvD,aAAa;;EAEb,gBAAgB;;iBAGF,mBAAmB,SAAS,uBAAuB;;;;KCFvD,oBAAoB;;KAGpB;;UAGK;WACN,SAAS;WACT,QAAQ;;WAER;;;UAIM;;WAEN;;WAEA;;WAEA;;WAEA,UAAU;;WAEV;;WAEA,SAAS,QAAQ;;;;UChCX;EACf;EACA;EACA,SAAS;;EAET;;EAEA;;EAEA,aAAa;;UAGE;EACf,SAAS;IAAU;IAAa;;EAChC,QAAQ;EACR,gBAAgB;;UAGD;EACf;EACA;EACA,SAAS;;EAET;;EAEA;;EAEA,eAAe;;EAEf,aAAa;;UAGE;;EAEf,SAAS;IAAU;IAAmB;;EACtC,QAAQ;EACR,gBAAgB;;iBA6MF,kBAAkB,SAAS,sBAAsB;;;;;iBA2CjD,wBAAwB,SAAS,4BAA4B"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/mcp-session.ts","../../src/mcp-tool-surface.ts","../../src/mcp-server.ts","../../src/mcp-transport.ts","../../src/mcp-remote-authorization.ts","../../src/mcp-http-host.ts"],"mappings":";;;;;;;;UAMiB,6BAA6B,wBAAwB;;;UCCrD;WACN;WACA;;;;UCAM;EACf;EACA;EACA,SAAS;;EAET,aAAa;;;iBAIO,qBAAqB,SAAS,mBAAmB,QAAQ;;;UCM9D;;EAEf;;EAEA;;EAEA,QAAQ;EACR,SAAS;;EAET,aAAa;;UAGE,sBAAsB,kBAAkB;EACvD,aAAa;;EAEb,gBAAgB;;iBAGF,mBAAmB,SAAS,uBAAuB;;;;KCZvD,oBAAoB;;KAGpB,yBAAyB;;UAGpB;WACN,SAAS;WACT,QAAQ;;WAER;;;UAIM;;WAEN;;WAEA;;WAEA;;WAEA,UAAU;;WAEV;;WAEA,SAAS,QAAQ;;;;UCtBX;EACf;EACA;EACA,SAAS;;EAET;;EAEA;;EAEA,aAAa;;UAGE;EACf,SAAS;IAAU;IAAa;;EAChC,QAAQ;EACR,gBAAgB;;UAGD;EACf;EACA;EACA,SAAS;;EAET;;EAEA;;EAEA,eAAe;;EAEf,aAAa;;UAGE;;EAEf,SAAS;IAAU;IAAmB;;EACtC,QAAQ;EACR,gBAAgB;;iBA6MF,kBAAkB,SAAS,sBAAsB;;;;;iBA2CjD,wBAAwB,SAAS,4BAA4B"}
@@ -1,2 +1,2 @@
1
- import{Server as e}from"@modelcontextprotocol/sdk/server/index.js";import{CallToolRequestSchema as t,ListToolsRequestSchema as n,ToolSchema as r}from"@modelcontextprotocol/sdk/types.js";import{isTurnNotRunError as i}from"@robota-sdk/agent-interface-session";import{PassThrough as a}from"node:stream";import{StdioServerTransport as o}from"@modelcontextprotocol/sdk/server/stdio.js";import{createServer as s}from"node:http";import{BlockList as c,isIP as l}from"node:net";import{toNodeHandler as u}from"@modelcontextprotocol/node";import{Server as d,createMcpHandler as f}from"@modelcontextprotocol/server";import{bearerCredential as p,credentialMatches as m,mintTransportToken as h}from"@robota-sdk/agent-transport/node";const g={name:`agent_submit`,description:`Submit a prompt to the agent and await its turn`};function _(e=g){if(typeof e.name!=`string`||!/^[A-Za-z0-9._-]{1,128}$/.test(e.name))throw Error(`MCP submit tool name must contain 1–128 ASCII letters, digits, dots, hyphens, or underscores`);return r.parse({name:e.name,description:e.description,inputSchema:{type:`object`,properties:{prompt:{type:`string`,minLength:1}},required:[`prompt`],additionalProperties:!1}})}async function v(e,t){let n=new Set([t]);return(await e.listRuntimeTools()).map(e=>{if(n.has(e.name))throw Error(`Duplicate or reserved MCP tool name: ${e.name}`);return n.add(e.name),r.parse({name:e.name,description:e.description,inputSchema:e.parameters})})}function y(e){return{content:[{type:`text`,text:e}],isError:!0}}function b(e){return e===null||typeof e==`string`||typeof e==`boolean`?!0:typeof e==`number`?Number.isFinite(e):Array.isArray(e)?e.every(b):typeof e==`object`&&!!e&&Object.values(e).every(b)}function x(e){return Object.values(e).every(b)}async function S(e,t,n,r,a){let o;try{r.throwIfAborted(),o=await v(e,a)}catch(e){return y(e instanceof Error?e.message:String(e))}if(t===a){if(typeof n.prompt!=`string`||n.prompt.length===0||Object.keys(n).some(e=>e!==`prompt`))return y(`${a} requires a non-empty string prompt and no other arguments`);try{return{content:[{type:`text`,text:(await(await e.submit(n.prompt,void 0,void 0,{signal:r})).completed).response}]}}catch(e){if(!i(e)&&!r.aborted)throw e;return y(e instanceof Error?e.message:String(e))}}if(!o.some(e=>e.name===t))return y(`Unknown tool: ${t}`);if(!x(n))return y(`Tool arguments must contain JSON values`);try{let i=await e.invokeRuntimeTool(t,n,{signal:r});return{content:[{type:`text`,text:JSON.stringify(i)}],isError:!i.success}}catch(e){return y(e instanceof Error?e.message:String(e))}}async function C(r){let{name:i,version:a,session:o}=r,s=_(r.submitTool);await v(o,s.name);let c=new e({name:i,version:a},{capabilities:{tools:{}}});return c.setRequestHandler(n,async()=>({tools:[...await v(o,s.name),s]})),c.setRequestHandler(t,async(e,t)=>S(o,e.params.name,e.params.arguments??{},t.signal,s.name)),c}function w(e){let t=null,n=null,r=null,i=null,s=null,c=null,l=e.stdin??process.stdin,u=e.stdout??process.stdout,d=null,f=null,p=Promise.resolve(),m=()=>{p=new Promise((e,t)=>{d=e,f=t}),p.catch(()=>void 0)},h=()=>{d?.(),c?.(Error(`MCP input closed during startup`))},g=()=>{d?.(),c?.(Error(`MCP output closed during startup`))},_=e=>{f?.(e),c?.(e)},v=()=>{l.off(`end`,h),l.off(`close`,h),u.off(`close`,g),u.off(`error`,_),s&&=(l.unpipe(s),s.destroy(),null)},y=e=>Object.assign(Error(`MCP transport ${e}.`),{name:`TransportLifecycleError`,code:e,transportName:`mcp`});return{name:`mcp`,lifecycle:Object.freeze({kind:`service`}),attach(e){if(n||r||i)throw y(`already-started`);t=e,m()},async start(){if(!t)throw y(`not-attached`);if(n||r||i)throw y(`already-started`);let p=t,m=new Promise((e,t)=>{c=t}),b=new a;s=b,l.once(`end`,h),l.once(`close`,h),u.once(`close`,g),u.on(`error`,_),l.pipe(b),r=(async()=>{let t=null,r=setTimeout(()=>c?.(Error(`MCP carrier startup timed out`)),15e3);try{t=await Promise.race([C({name:e.name,version:e.version,session:p,...e.submitTool===void 0?{}:{submitTool:e.submitTool}}),m]);let r=new o(b,u);r.onclose=()=>d?.(),r.onerror=e=>f?.(e),await t.connect(r),n=t,t=null}catch(e){throw v(),t&&await t.close().catch(()=>void 0),e}finally{clearTimeout(r),c=null}})();try{await r}finally{r=null}},stop(){return i||(i=(async()=>{try{c?.(Error(`MCP carrier startup stopped`)),r&&await r.catch(()=>void 0),n&&await n.close()}finally{v(),n=null,t=null,d?.()}})().finally(()=>{i=null}),i)},getServer(){if(!n)throw Error(`Transport not started. Call start() first.`);return n},waitForClose(){return p}}}const T=6e4,E=4096,D=/^[\x21\x23-\x5b\x5d-\x7e]+$/,O=new c;O.addSubnet(`127.0.0.0`,8,`ipv4`),O.addAddress(`::1`,`ipv6`);const k=new c;k.addSubnet(`10.0.0.0`,8,`ipv4`),k.addSubnet(`172.16.0.0`,12,`ipv4`),k.addSubnet(`192.168.0.0`,16,`ipv4`),k.addSubnet(`169.254.0.0`,16,`ipv4`),k.addSubnet(`100.64.0.0`,10,`ipv4`),k.addSubnet(`fc00::`,7,`ipv6`),k.addSubnet(`fe80::`,10,`ipv6`);function A(e){if(e===void 0)return;let t=e.trim().toLowerCase(),n=t.startsWith(`::ffff:`)?t.slice(7):void 0;return n!==void 0&&l(n)===4?n:l(t)===0?void 0:t}function j(e){if(e===void 0)return`unknown`;let t=l(e)===4?`ipv4`:`ipv6`;return O.check(e,t)?`loopback`:k.check(e,t)?`private`:`public`}function M(e){let t;try{t=new URL(e)}catch{throw Error(`MCP HTTP public URL must be an absolute https URL`)}if(t.protocol!==`https:`)throw Error(`MCP HTTP public URL must use https`);if(t.username!==``||t.password!==``||t.search!==``||t.hash!==``)throw Error(`MCP HTTP public URL must not carry credentials, a query or a fragment`);return t}function N(e){let t=new Map;return{fail(n){let r=e(),i=t.get(n);if(i===void 0||r-i.startedAt>=T){if(t.delete(n),t.size>=E){for(let[e,n]of t)r-n.startedAt>=T&&t.delete(e);let e=t.keys().next();t.size>=E&&e.done!==!0&&t.delete(e.value)}i={count:0,startedAt:r},t.set(n,i)}i.count+=1;let a=T-(r-i.startedAt);return{throttled:i.count>20,retryAfterSeconds:Math.max(1,Math.ceil(a/1e3))}}}}function P(e,t=()=>performance.now()){let n=M(e.publicUrl),r;try{r=new URL(e.issuer)}catch{throw Error(`MCP HTTP authorization issuer must be an absolute https URL`)}if(r.protocol!==`https:`)throw Error(`MCP HTTP authorization issuer must use https`);if(e.scopes.length===0||!e.scopes.every(e=>D.test(e)))throw Error(`MCP HTTP authorization needs at least one valid scope`);let i=new Set;for(let t of e.trustedProxies??[]){let e=A(t);if(e===void 0)throw Error(`MCP HTTP trusted proxy must be a literal IP address`);i.add(e)}let a=n.pathname,o=`/.well-known/oauth-protected-resource${a===`/`?``:a}`,s=`${n.origin}${o}`,c=new Set([n.host]);n.port===``&&c.add(`${n.hostname}:443`);let l=JSON.stringify({resource:e.publicUrl,authorization_servers:[e.issuer],scopes_supported:[...e.scopes],bearer_methods_supported:[`header`]}),u=e.scopes.join(` `),d={missing:`Bearer resource_metadata="${s}"`,invalid:`Bearer error="invalid_token", resource_metadata="${s}"`,scope:`Bearer error="insufficient_scope", scope="${u}", resource_metadata="${s}"`},f=N(t);function m(e){let t=A(e.socket.remoteAddress);if(t===void 0||!i.has(t))return t;let n=e.headers[`x-forwarded-for`],r=(Array.isArray(n)?n.join(`,`):n??``).split(`,`);for(let e=r.length-1;e>=0;--e){let n=A(r[e]);if(n===void 0)return t;if(!i.has(n))return n}return t}function h(t,n,r){let i=m(t),a=j(i);if(r===`keys-unavailable`){e.audit?.({refusal:r,remote:a,throttled:!1}),n.writeHead(503).end();return}let o=f.fail(i??`unknown`);if(e.audit?.({refusal:r,remote:a,throttled:o.throttled}),o.throttled)n.writeHead(429,{"Retry-After":String(o.retryAfterSeconds)}).end();else if(r===`missing-scope`)n.writeHead(403,{"WWW-Authenticate":d.scope}).end();else{let e=r===`missing-token`?d.missing:d.invalid;n.writeHead(401,{"WWW-Authenticate":e}).end()}}return{endpointPath:a,async admit(t,r){let i=t.headers.host?.toLowerCase(),s=t.headers.origin;if(i===void 0||!c.has(i)||s!==void 0&&s!==n.origin)return r.writeHead(403).end(),!1;if(t.url===o)return t.method===`GET`?(r.writeHead(200,{"Content-Type":`application/json`}).end(l),!1):(r.writeHead(405,{Allow:`GET`}).end(),!1);if(t.url!==a)return r.writeHead(404).end(),!1;let u=p(t.headers.authorization);if(u===void 0)return h(t,r,`missing-token`),!1;let d=await e.verifier.verify(u);return d.admitted?!0:(h(t,r,d.refusal),!1)}}}const F=`127.0.0.1`,I=1024*1024;function L(e){if(e!==void 0&&(!Number.isInteger(e)||e<0||e>65535))throw Error(`MCP HTTP port must be an integer in 0..65535`)}function R(e,t,n){let r=_(e.submitTool),i,a,o,c=!1,l=!1,p=0,m,h,g=new Promise((e,t)=>{m=e,h=t});return g.catch(()=>void 0),{async start(){if(c||o)throw Error(`MCP HTTP host already started`);if(c=!0,await v(e.session,r.name),l)throw Error(`MCP HTTP host stopped during startup`);a=f(()=>{let t=new d({name:e.name,version:e.version},{capabilities:{tools:{}}});return t.setRequestHandler(`tools/list`,async()=>({tools:JSON.parse(JSON.stringify([...await v(e.session,r.name),r]))})),t.setRequestHandler(`tools/call`,async(t,n)=>S(e.session,t.params.name,t.params.arguments??{},n.mcpReq.signal,r.name)),t},{maxSubscriptions:0});let g=u(a),_=(e,t)=>{if(p>=16){t.writeHead(503).end();return}p+=1,(async()=>{try{let n;if(e.method===`POST`){let r=Number(e.headers[`content-length`]);if(Number.isFinite(r)&&r>I){t.writeHead(413).end();return}let i=[],a=0;for await(let n of e){let r=Buffer.isBuffer(n)?n:Buffer.from(n);if(a+=r.length,a>I){t.writeHead(413).end(),e.destroy();return}i.push(r)}try{n=JSON.parse(Buffer.concat(i).toString(`utf8`))}catch{t.writeHead(400).end();return}}await g(e,t,n)}catch{t.headersSent?t.destroy():t.writeHead(500).end()}finally{--p}})()},y=s({maxHeaderSize:16*1024},(e,t)=>{let r=y.address();if(!r||typeof r==`string`){t.writeHead(503).end();return}let i=n(e,t,r.port);i===!0?_(e,t):i!==!1&&i.then(n=>{n&&_(e,t)},()=>{t.headersSent?t.destroy():t.writeHead(500).end()})});y.maxConnections=32,y.headersTimeout=5e3,y.requestTimeout=15e3,y.on(`error`,e=>h?.(e)),y.on(`close`,()=>m?.());try{await new Promise((n,r)=>{y.once(`error`,r),y.listen(e.port??0,t,()=>{y.off(`error`,r),n()})})}catch(e){throw await a.close(),e}if(l)throw await new Promise(e=>y.close(()=>e())),await a.close(),Error(`MCP HTTP host stopped during startup`);return i=y,y.address()},stop(){return o||(l=!0,o=(async()=>{let e=a,t=i;if(!t){await e?.close(),m?.();return}let n=new Promise((e,n)=>{t.close(t=>t?n(t):e()),t.closeAllConnections()}),r;try{await Promise.race([Promise.all([e?.close(),n]).then(()=>void 0),new Promise((e,t)=>{r=setTimeout(()=>t(Error(`MCP HTTP close timed out`)),5e3)})])}finally{r&&clearTimeout(r)}})(),o)},waitForClose(){return g}}}function z(e){if(e.host!==void 0&&e.host!==F)throw Error(`MCP HTTP host must bind numeric IPv4 loopback`);L(e.port);let t=h(),n=R(e,F,(e,n,r)=>{let i=`${F}:${r}`,a=`http://${i}`;if(e.url!==`/mcp`||e.headers.host!==i||e.headers.origin!==void 0&&e.headers.origin!==a)return n.writeHead(403).end(),!1;let o=e.headers.authorization;return typeof o!=`string`||!m(t,p(o))?(n.writeHead(401).end(),!1):!0});return{async start(){let e=await n.start();return{url:`http://${F}:${e.port}/mcp`,token:t}},stop:n.stop,waitForClose:n.waitForClose}}function B(e){let t=e.host??F;if(l(t)===0)throw Error(`MCP HTTP host must bind a literal IP address`);L(e.port);let n=P(e.authorization),r=R(e,t,(e,t)=>n.admit(e,t));return{async start(){let t=await r.start();return{listening:`${t.family===`IPv6`?`[${t.address}]`:t.address}:${t.port}`,url:e.authorization.publicUrl}},stop:r.stop,waitForClose:r.waitForClose}}export{C as createAgentMcpServer,z as createMcpHttpHost,B as createMcpRemoteHttpHost,w as createMcpTransport};
1
+ import{Server as e}from"@modelcontextprotocol/sdk/server/index.js";import{CallToolRequestSchema as t,ListToolsRequestSchema as n,ToolSchema as r}from"@modelcontextprotocol/sdk/types.js";import{isTurnNotRunError as i}from"@robota-sdk/agent-interface-session";import{PassThrough as a}from"node:stream";import{StdioServerTransport as o}from"@modelcontextprotocol/sdk/server/stdio.js";import{createServer as s}from"node:http";import{isIP as c}from"node:net";import{toNodeHandler as l}from"@modelcontextprotocol/node";import{Server as u,createMcpHandler as d}from"@modelcontextprotocol/server";import{bearerCredential as f,createBearerResourceServer as p,credentialMatches as m,describeProtectedResource as h,mintTransportToken as g,refuseBearerToken as _,serveProtectedResourceMetadata as v}from"@robota-sdk/agent-transport/node";const y={name:`agent_submit`,description:`Submit a prompt to the agent and await its turn`};function b(e=y){if(typeof e.name!=`string`||!/^[A-Za-z0-9._-]{1,128}$/.test(e.name))throw Error(`MCP submit tool name must contain 1–128 ASCII letters, digits, dots, hyphens, or underscores`);return r.parse({name:e.name,description:e.description,inputSchema:{type:`object`,properties:{prompt:{type:`string`,minLength:1}},required:[`prompt`],additionalProperties:!1}})}async function x(e,t){let n=new Set([t]);return(await e.listRuntimeTools()).map(e=>{if(n.has(e.name))throw Error(`Duplicate or reserved MCP tool name: ${e.name}`);return n.add(e.name),r.parse({name:e.name,description:e.description,inputSchema:e.parameters})})}function S(e){return{content:[{type:`text`,text:e}],isError:!0}}function C(e){return e===null||typeof e==`string`||typeof e==`boolean`?!0:typeof e==`number`?Number.isFinite(e):Array.isArray(e)?e.every(C):typeof e==`object`&&Object.values(e).every(C)}function w(e){return Object.values(e).every(C)}async function T(e,t,n,r,a){let o;try{r.throwIfAborted(),o=await x(e,a)}catch(e){return S(e instanceof Error?e.message:String(e))}if(t===a){if(typeof n.prompt!=`string`||n.prompt.length===0||Object.keys(n).some(e=>e!==`prompt`))return S(`${a} requires a non-empty string prompt and no other arguments`);try{return{content:[{type:`text`,text:(await(await e.submit(n.prompt,void 0,void 0,{signal:r})).completed).response}]}}catch(e){if(!i(e)&&!r.aborted)throw e;return S(e instanceof Error?e.message:String(e))}}if(!o.some(e=>e.name===t))return S(`Unknown tool: ${t}`);if(!w(n))return S(`Tool arguments must contain JSON values`);try{let i=await e.invokeRuntimeTool(t,n,{signal:r});return{content:[{type:`text`,text:JSON.stringify(i)}],isError:!i.success}}catch(e){return S(e instanceof Error?e.message:String(e))}}async function E(r){let{name:i,version:a,session:o}=r,s=b(r.submitTool);await x(o,s.name);let c=new e({name:i,version:a},{capabilities:{tools:{}}});return c.setRequestHandler(n,async()=>({tools:[...await x(o,s.name),s]})),c.setRequestHandler(t,async(e,t)=>T(o,e.params.name,e.params.arguments??{},t.signal,s.name)),c}function D(e){let t=null,n=null,r=null,i=null,s=null,c=null,l=e.stdin??process.stdin,u=e.stdout??process.stdout,d=null,f=null,p=Promise.resolve(),m=()=>{p=new Promise((e,t)=>{d=e,f=t}),p.catch(()=>void 0)},h=()=>{d?.(),c?.(Error(`MCP input closed during startup`))},g=()=>{d?.(),c?.(Error(`MCP output closed during startup`))},_=e=>{f?.(e),c?.(e)},v=()=>{l.off(`end`,h),l.off(`close`,h),u.off(`close`,g),u.off(`error`,_),s&&=(l.unpipe(s),s.destroy(),null)},y=e=>Object.assign(Error(`MCP transport ${e}.`),{name:`TransportLifecycleError`,code:e,transportName:`mcp`});return{name:`mcp`,lifecycle:Object.freeze({kind:`service`}),attach(e){if(n||r||i)throw y(`already-started`);t=e,m()},async start(){if(!t)throw y(`not-attached`);if(n||r||i)throw y(`already-started`);let p=t,m=new Promise((e,t)=>{c=t}),b=new a;s=b,l.once(`end`,h),l.once(`close`,h),u.once(`close`,g),u.on(`error`,_),l.pipe(b),r=(async()=>{let t=null,r=setTimeout(()=>c?.(Error(`MCP carrier startup timed out`)),15e3);try{t=await Promise.race([E({name:e.name,version:e.version,session:p,...e.submitTool===void 0?{}:{submitTool:e.submitTool}}),m]);let r=new o(b,u);r.onclose=()=>d?.(),r.onerror=e=>f?.(e),await t.connect(r),n=t,t=null}catch(e){throw v(),t&&await t.close().catch(()=>void 0),e}finally{clearTimeout(r),c=null}})();try{await r}finally{r=null}},stop(){return i||(i=(async()=>{try{c?.(Error(`MCP carrier startup stopped`)),r&&await r.catch(()=>void 0),n&&await n.close()}finally{v(),n=null,t=null,d?.()}})().finally(()=>{i=null}),i)},getServer(){if(!n)throw Error(`Transport not started. Call start() first.`);return n},waitForClose(){return p}}}const O=`MCP HTTP`;function k(e,t=()=>performance.now()){let n=h({resource:e.publicUrl,issuer:e.issuer,scopes:e.scopes,label:O}),r=p({publicUrl:e.publicUrl,...e.trustedProxies===void 0?{}:{trustedProxies:e.trustedProxies},label:O,now:t}),i=r.url.pathname;function a(t,i,a){if(a===`keys-unavailable`){e.audit?.({refusal:a,remote:r.remote(t),throttled:!1}),i.writeHead(503).end();return}let o=r.fail(t);e.audit?.({refusal:a,remote:o.remote,throttled:o.throttled}),_(i,n,a===`missing-token`||a===`missing-scope`?a:`invalid-token`,o)}return{endpointPath:i,async admit(t,o){if(!r.checkNames(t,o))return!1;if(t.url===n.wellKnownPath)return v(t,o,n),!1;if(t.url!==i)return o.writeHead(404).end(),!1;let s=f(t.headers.authorization);if(s===void 0)return a(t,o,`missing-token`),!1;let c=await e.verifier.verify(s);return c.admitted?!0:(a(t,o,c.refusal),!1)}}}const A=`127.0.0.1`,j=1024*1024;function M(e){if(e!==void 0&&(!Number.isInteger(e)||e<0||e>65535))throw Error(`MCP HTTP port must be an integer in 0..65535`)}function N(e,t,n){let r=b(e.submitTool),i,a,o,c=!1,f=!1,p=0,m,h,g=new Promise((e,t)=>{m=e,h=t});return g.catch(()=>void 0),{async start(){if(c||o)throw Error(`MCP HTTP host already started`);if(c=!0,await x(e.session,r.name),f)throw Error(`MCP HTTP host stopped during startup`);a=d(()=>{let t=new u({name:e.name,version:e.version},{capabilities:{tools:{}}});return t.setRequestHandler(`tools/list`,async()=>({tools:JSON.parse(JSON.stringify([...await x(e.session,r.name),r]))})),t.setRequestHandler(`tools/call`,async(t,n)=>T(e.session,t.params.name,t.params.arguments??{},n.mcpReq.signal,r.name)),t},{maxSubscriptions:0});let g=l(a),_=(e,t)=>{if(p>=16){t.writeHead(503).end();return}p+=1,(async()=>{try{let n;if(e.method===`POST`){let r=Number(e.headers[`content-length`]);if(Number.isFinite(r)&&r>j){t.writeHead(413).end();return}let i=[],a=0;for await(let n of e){let r=Buffer.isBuffer(n)?n:Buffer.from(n);if(a+=r.length,a>j){t.writeHead(413).end(),e.destroy();return}i.push(r)}try{n=JSON.parse(Buffer.concat(i).toString(`utf8`))}catch{t.writeHead(400).end();return}}await g(e,t,n)}catch{t.headersSent?t.destroy():t.writeHead(500).end()}finally{--p}})()},v=s({maxHeaderSize:16*1024},(e,t)=>{let r=v.address();if(!r||typeof r==`string`){t.writeHead(503).end();return}let i=n(e,t,r.port);i===!0?_(e,t):i!==!1&&i.then(n=>{n&&_(e,t)},()=>{t.headersSent?t.destroy():t.writeHead(500).end()})});v.maxConnections=32,v.headersTimeout=5e3,v.requestTimeout=15e3,v.on(`error`,e=>h?.(e)),v.on(`close`,()=>m?.());try{await new Promise((n,r)=>{v.once(`error`,r),v.listen(e.port??0,t,()=>{v.off(`error`,r),n()})})}catch(e){throw await a.close(),e}if(f)throw await new Promise(e=>v.close(()=>e())),await a.close(),Error(`MCP HTTP host stopped during startup`);return i=v,v.address()},stop(){return o||(f=!0,o=(async()=>{let e=a,t=i;if(!t){await e?.close(),m?.();return}let n=new Promise((e,n)=>{t.close(t=>t?n(t):e()),t.closeAllConnections()}),r;try{await Promise.race([Promise.all([e?.close(),n]).then(()=>void 0),new Promise((e,t)=>{r=setTimeout(()=>t(Error(`MCP HTTP close timed out`)),5e3)})])}finally{r&&clearTimeout(r)}})(),o)},waitForClose(){return g}}}function P(e){if(e.host!==void 0&&e.host!==A)throw Error(`MCP HTTP host must bind numeric IPv4 loopback`);M(e.port);let t=g(),n=N(e,A,(e,n,r)=>{let i=`${A}:${r}`,a=`http://${i}`;if(e.url!==`/mcp`||e.headers.host!==i||e.headers.origin!==void 0&&e.headers.origin!==a)return n.writeHead(403).end(),!1;let o=e.headers.authorization;return typeof o!=`string`||!m(t,f(o))?(n.writeHead(401).end(),!1):!0});return{async start(){let e=await n.start();return{url:`http://${A}:${e.port}/mcp`,token:t}},stop:n.stop,waitForClose:n.waitForClose}}function F(e){let t=e.host??A;if(c(t)===0)throw Error(`MCP HTTP host must bind a literal IP address`);M(e.port);let n=k(e.authorization),r=N(e,t,(e,t)=>n.admit(e,t));return{async start(){let t=await r.start();return{listening:`${t.family===`IPv6`?`[${t.address}]`:t.address}:${t.port}`,url:e.authorization.publicUrl}},stop:r.stop,waitForClose:r.waitForClose}}export{E as createAgentMcpServer,P as createMcpHttpHost,F as createMcpRemoteHttpHost,D as createMcpTransport};
2
2
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["Server"],"sources":["../../src/mcp-tool-surface.ts","../../src/mcp-server.ts","../../src/mcp-transport.ts","../../src/mcp-remote-authorization.ts","../../src/mcp-http-host.ts"],"sourcesContent":["/** One canonical tool catalog and invocation path shared by stdio and HTTP MCP carriers. */\nimport { ToolSchema } from '@modelcontextprotocol/sdk/types.js';\nimport { isTurnNotRunError } from '@robota-sdk/agent-interface-session';\n\nimport type { IMcpTransportSession } from './mcp-session.js';\nimport type { CallToolResult, Tool } from '@modelcontextprotocol/sdk/types.js';\n\nexport interface IMcpSubmitToolIdentity {\n readonly name: string;\n readonly description: string;\n}\n\nconst DEFAULT_SUBMIT_TOOL: IMcpSubmitToolIdentity = {\n name: 'agent_submit',\n description: 'Submit a prompt to the agent and await its turn',\n};\n\nexport function resolveSubmitTool(identity: IMcpSubmitToolIdentity = DEFAULT_SUBMIT_TOOL): Tool {\n if (typeof identity.name !== 'string' || !/^[A-Za-z0-9._-]{1,128}$/.test(identity.name)) {\n throw new Error(\n 'MCP submit tool name must contain 1–128 ASCII letters, digits, dots, hyphens, or underscores',\n );\n }\n return ToolSchema.parse({\n name: identity.name,\n description: identity.description,\n inputSchema: {\n type: 'object' as const,\n properties: { prompt: { type: 'string', minLength: 1 } },\n required: ['prompt'],\n additionalProperties: false,\n },\n });\n}\n\nexport async function readCatalog(\n session: IMcpTransportSession,\n submitToolName: string,\n): Promise<Tool[]> {\n const names = new Set([submitToolName]);\n return (await session.listRuntimeTools()).map((schema) => {\n if (names.has(schema.name)) {\n throw new Error(`Duplicate or reserved MCP tool name: ${schema.name}`);\n }\n names.add(schema.name);\n return ToolSchema.parse({\n name: schema.name,\n description: schema.description,\n inputSchema: schema.parameters,\n });\n });\n}\n\nfunction toolError(message: string): CallToolResult {\n return { content: [{ type: 'text' as const, text: message }], isError: true };\n}\n\nfunction isJsonValue(value: unknown): boolean {\n if (value === null || typeof value === 'string' || typeof value === 'boolean') return true;\n if (typeof value === 'number') return Number.isFinite(value);\n if (Array.isArray(value)) return value.every(isJsonValue);\n return typeof value === 'object' && value !== null && Object.values(value).every(isJsonValue);\n}\n\nfunction isRuntimeParameters(\n value: Record<string, unknown>,\n): value is Parameters<IMcpTransportSession['invokeRuntimeTool']>[1] {\n return Object.values(value).every(isJsonValue);\n}\n\nexport async function invokeMcpTool(\n session: IMcpTransportSession,\n toolName: string,\n parameters: Record<string, unknown>,\n signal: AbortSignal,\n submitToolName: string,\n): Promise<CallToolResult> {\n let catalog: Tool[];\n try {\n signal.throwIfAborted();\n catalog = await readCatalog(session, submitToolName);\n } catch (error) {\n return toolError(error instanceof Error ? error.message : String(error));\n }\n if (toolName === submitToolName) {\n if (\n typeof parameters.prompt !== 'string' ||\n parameters.prompt.length === 0 ||\n Object.keys(parameters).some((key) => key !== 'prompt')\n ) {\n return toolError(\n `${submitToolName} requires a non-empty string prompt and no other arguments`,\n );\n }\n try {\n const handle = await session.submit(parameters.prompt, undefined, undefined, { signal });\n const result = await handle.completed;\n return { content: [{ type: 'text', text: result.response }] };\n } catch (error) {\n if (!isTurnNotRunError(error) && !signal.aborted) throw error;\n return toolError(error instanceof Error ? error.message : String(error));\n }\n }\n if (!catalog.some((tool) => tool.name === toolName))\n return toolError(`Unknown tool: ${toolName}`);\n if (!isRuntimeParameters(parameters)) return toolError('Tool arguments must contain JSON values');\n try {\n const result = await session.invokeRuntimeTool(toolName, parameters, { signal });\n return {\n content: [{ type: 'text', text: JSON.stringify(result) }],\n isError: !result.success,\n };\n } catch (error) {\n return toolError(error instanceof Error ? error.message : String(error));\n }\n}\n","/** MCP translates the canonical session tool port; execution policy stays in the session. */\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';\n\nimport { invokeMcpTool, readCatalog, resolveSubmitTool } from './mcp-tool-surface.js';\n\nimport type { IMcpTransportSession } from './mcp-session.js';\nimport type { IMcpSubmitToolIdentity } from './mcp-tool-surface.js';\n\nexport interface IAgentMcpOptions {\n name: string;\n version: string;\n session: IMcpTransportSession;\n /** Host-owned identity for the submission extension. */\n submitTool?: IMcpSubmitToolIdentity;\n}\n\n/** Validate the catalog before the caller connects a carrier. */\nexport async function createAgentMcpServer(options: IAgentMcpOptions): Promise<Server> {\n const { name, version, session } = options;\n const submitTool = resolveSubmitTool(options.submitTool);\n await readCatalog(session, submitTool.name);\n const server = new Server({ name, version }, { capabilities: { tools: {} } });\n server.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: [...(await readCatalog(session, submitTool.name)), submitTool],\n }));\n server.setRequestHandler(CallToolRequestSchema, async (request, extra) =>\n invokeMcpTool(\n session,\n request.params.name,\n request.params.arguments ?? {},\n extra.signal,\n submitTool.name,\n ),\n );\n return server;\n}\n","/**\n * ITransportAdapter implementation for MCP transport.\n *\n * Connects the official SDK stdio carrier to the canonical session MCP server.\n * The adapter owns carrier readiness and teardown; the product shell owns process policy.\n */\n\nimport { PassThrough } from 'node:stream';\n\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\n\nimport { createAgentMcpServer } from './mcp-server.js';\n\nimport type { IMcpTransportSession } from './mcp-session.js';\nimport type { IMcpSubmitToolIdentity } from './mcp-tool-surface.js';\nimport type { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport type {\n ITransportAdapter,\n ITransportLifecycleError,\n} from '@robota-sdk/agent-interface-transport';\nimport type { Readable, Writable } from 'node:stream';\n\nconst STARTUP_TIMEOUT_MS = 15000;\n\nexport interface IMcpTransportOptions {\n /** Name for the MCP server. */\n name: string;\n /** Version string. */\n version: string;\n /** Process-owned streams; defaults to the current process stdio. */\n stdin?: Readable;\n stdout?: Writable;\n /** Host-owned identity for the submission extension. */\n submitTool?: IMcpSubmitToolIdentity;\n}\n\nexport interface IMcpTransport extends ITransportAdapter<IMcpTransportSession> {\n getServer(): Server;\n /** Settles when the carrier closes or its input ends; rejects on carrier failure. */\n waitForClose(): Promise<void>;\n}\n\nexport function createMcpTransport(options: IMcpTransportOptions): IMcpTransport {\n let session: IMcpTransportSession | null = null;\n let server: Server | null = null;\n let starting: Promise<void> | null = null;\n let stopping: Promise<void> | null = null;\n let inputBridge: PassThrough | null = null;\n let cancelStart: ((error: Error) => void) | null = null;\n const stdin = options.stdin ?? process.stdin;\n const stdout = options.stdout ?? process.stdout;\n let resolveClose: (() => void) | null = null;\n let rejectClose: ((error: Error) => void) | null = null;\n let closePromise: Promise<void> = Promise.resolve();\n const resetClose = (): void => {\n closePromise = new Promise<void>((resolve, reject) => {\n resolveClose = resolve;\n rejectClose = reject;\n });\n // A carrier may fail between connect and the process owner awaiting this promise.\n // allow-fallback: waitForClose retains the rejection for the process owner; this observer only prevents an early unhandled-rejection warning.\n void closePromise.catch(() => undefined);\n };\n const onInputClose = (): void => {\n resolveClose?.();\n cancelStart?.(new Error('MCP input closed during startup'));\n };\n const onOutputClose = (): void => {\n resolveClose?.();\n cancelStart?.(new Error('MCP output closed during startup'));\n };\n const onOutputError = (error: Error): void => {\n rejectClose?.(error);\n cancelStart?.(error);\n };\n const removeInputListeners = (): void => {\n stdin.off('end', onInputClose);\n stdin.off('close', onInputClose);\n stdout.off('close', onOutputClose);\n stdout.off('error', onOutputError);\n if (inputBridge) {\n stdin.unpipe(inputBridge);\n inputBridge.destroy();\n inputBridge = null;\n }\n };\n const lifecycleError = (code: ITransportLifecycleError['code']): ITransportLifecycleError =>\n Object.assign(new Error(`MCP transport ${code}.`), {\n name: 'TransportLifecycleError' as const,\n code,\n transportName: 'mcp',\n });\n\n return {\n name: 'mcp',\n lifecycle: Object.freeze({ kind: 'service' }),\n attach(s: IMcpTransportSession) {\n if (server || starting || stopping) throw lifecycleError('already-started');\n session = s;\n resetClose();\n },\n async start() {\n if (!session) throw lifecycleError('not-attached');\n if (server || starting || stopping) throw lifecycleError('already-started');\n const attachedSession = session;\n const cancelled = new Promise<never>((_resolve, reject) => {\n cancelStart = reject;\n });\n const bridge = new PassThrough();\n inputBridge = bridge;\n stdin.once('end', onInputClose);\n stdin.once('close', onInputClose);\n stdout.once('close', onOutputClose);\n stdout.on('error', onOutputError);\n // Preserve early frames with backpressure while the canonical tool catalog is validated.\n // Piping also exposes EOF before the SDK begins reading.\n stdin.pipe(bridge);\n starting = (async () => {\n let candidate: Server | null = null;\n const timeout = setTimeout(\n () => cancelStart?.(new Error('MCP carrier startup timed out')),\n STARTUP_TIMEOUT_MS,\n );\n try {\n candidate = await Promise.race([\n createAgentMcpServer({\n name: options.name,\n version: options.version,\n session: attachedSession,\n ...(options.submitTool !== undefined ? { submitTool: options.submitTool } : {}),\n }),\n cancelled,\n ]);\n const nextCarrier = new StdioServerTransport(bridge, stdout);\n nextCarrier.onclose = () => resolveClose?.();\n nextCarrier.onerror = (error) => rejectClose?.(error);\n await candidate.connect(nextCarrier);\n server = candidate;\n candidate = null;\n } catch (error) {\n removeInputListeners();\n // allow-fallback: the startup error is already being thrown; a failed best-effort close must not replace it.\n if (candidate) await candidate.close().catch(() => undefined);\n throw error;\n } finally {\n clearTimeout(timeout);\n cancelStart = null;\n }\n })();\n try {\n await starting;\n } finally {\n starting = null;\n }\n },\n stop() {\n if (stopping) return stopping;\n stopping = (async () => {\n try {\n cancelStart?.(new Error('MCP carrier startup stopped'));\n // allow-fallback: stop explicitly cancels startup, so its expected rejection is consumed before carrier teardown.\n if (starting) await starting.catch(() => undefined);\n if (server) await server.close();\n } finally {\n removeInputListeners();\n server = null;\n session = null;\n resolveClose?.();\n }\n })().finally(() => {\n stopping = null;\n });\n return stopping;\n },\n getServer() {\n if (!server) throw new Error('Transport not started. Call start() first.');\n return server;\n },\n waitForClose() {\n return closePromise;\n },\n };\n}\n","/**\n * The admission gate of the remote MCP HTTP host: an OAuth resource server in the sense of\n * RFC 6750 (bearer challenges) and RFC 9728 (protected-resource metadata).\n *\n * The host is reached through its PUBLIC URL, usually behind a reverse proxy, so every name the gate\n * checks comes from that URL rather than from the listener: the endpoint path and the metadata path\n * (a proxy prefix therefore works when the proxy forwards the path unchanged), and the Host and\n * Origin a request must carry (the proxy must preserve Host). The listener's own address is never a\n * name a client uses.\n *\n * Refusals answer with an empty body and, for a token problem, only the standard challenge: an error\n * code from RFC 6750 and where the metadata lives. Nothing describes WHY in prose, so a refusal\n * cannot leak which check failed beyond the code a client is entitled to.\n *\n * The token is verified before anything is counted, and only failures are counted, so a peer holding\n * a valid token is never throttled — however many failures share its address. Each failure is also\n * handed to an audit sink as a closed-set reason and a coarse address class, never token text.\n */\n\nimport { BlockList, isIP } from 'node:net';\n\nimport { bearerCredential } from '@robota-sdk/agent-transport/node';\n\nimport type {\n IAccessTokenVerifier,\n TAccessTokenRefusal,\n} from '@robota-sdk/agent-interface-transport';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\n\nconst WELL_KNOWN_PREFIX = '/.well-known/oauth-protected-resource';\n/** Window over which one address's failures are counted. */\nconst FAILURE_WINDOW_MS = 60_000;\n/** Failures one address may have in a window before its further failures answer 429. */\nconst MAX_FAILURES_PER_WINDOW = 20;\n/** Addresses tracked at once; the oldest is forgotten first, so a spray cannot grow memory. */\nconst MAX_TRACKED_ADDRESSES = 4096;\n/** RFC 6749 scope-token characters: printable ASCII except space, `\"` and `\\`. */\nconst SCOPE_TOKEN = /^[\\x21\\x23-\\x5b\\x5d-\\x7e]+$/;\n\n/** Why a remote request was refused: a verifier refusal, or no bearer token at all. */\nexport type TMcpRemoteRefusal = TAccessTokenRefusal | 'missing-token';\n\n/** A coarse class of the refused peer's address; the address itself is never reported. */\nexport type TMcpRemoteAddressClass = 'loopback' | 'private' | 'public' | 'unknown';\n\n/** One refused request, as the audit sink sees it. Carries no token text and no claim value. */\nexport interface IMcpRemoteAuditRecord {\n readonly refusal: TMcpRemoteRefusal;\n readonly remote: TMcpRemoteAddressClass;\n /** True when the refusal was answered 429 because the address exceeded its failure budget. */\n readonly throttled: boolean;\n}\n\n/** How the remote host decides who may reach the session. */\nexport interface IMcpRemoteAuthorization {\n /** The `https` URL clients use for the MCP endpoint; also the resource identifier (`aud`). */\n readonly publicUrl: string;\n /** The authorization server named in the protected-resource metadata. */\n readonly issuer: string;\n /** Scopes a token must carry; advertised as `scopes_supported` and in `insufficient_scope`. */\n readonly scopes: readonly string[];\n /** Decides each presented token. Built for the same issuer, resource and scopes. */\n readonly verifier: IAccessTokenVerifier;\n /** Proxy addresses whose `X-Forwarded-For` is believed. Absent: the header is ignored. */\n readonly trustedProxies?: readonly string[];\n /** Receives one content-free record per refused request. */\n readonly audit?: (record: IMcpRemoteAuditRecord) => void;\n}\n\nexport interface IMcpRemoteGate {\n readonly endpointPath: string;\n /** Resolves true when the request may reach MCP; otherwise the response has been written. */\n admit(req: IncomingMessage, res: ServerResponse): Promise<boolean>;\n}\n\nconst LOOPBACK_RANGES = new BlockList();\nLOOPBACK_RANGES.addSubnet('127.0.0.0', 8, 'ipv4');\nLOOPBACK_RANGES.addAddress('::1', 'ipv6');\nconst PRIVATE_RANGES = new BlockList();\nPRIVATE_RANGES.addSubnet('10.0.0.0', 8, 'ipv4');\nPRIVATE_RANGES.addSubnet('172.16.0.0', 12, 'ipv4');\nPRIVATE_RANGES.addSubnet('192.168.0.0', 16, 'ipv4');\nPRIVATE_RANGES.addSubnet('169.254.0.0', 16, 'ipv4');\nPRIVATE_RANGES.addSubnet('100.64.0.0', 10, 'ipv4');\nPRIVATE_RANGES.addSubnet('fc00::', 7, 'ipv6');\nPRIVATE_RANGES.addSubnet('fe80::', 10, 'ipv6');\n\n/** A literal address in one spelling: IPv4-mapped IPv6 folded to IPv4, IPv6 lowercased. */\nfunction normalizeAddress(value: string | undefined): string | undefined {\n if (value === undefined) return undefined;\n const text = value.trim().toLowerCase();\n const mapped = text.startsWith('::ffff:') ? text.slice('::ffff:'.length) : undefined;\n if (mapped !== undefined && isIP(mapped) === 4) return mapped;\n return isIP(text) === 0 ? undefined : text;\n}\n\nfunction classifyAddress(address: string | undefined): TMcpRemoteAddressClass {\n if (address === undefined) return 'unknown';\n const family = isIP(address) === 4 ? 'ipv4' : 'ipv6';\n if (LOOPBACK_RANGES.check(address, family)) return 'loopback';\n if (PRIVATE_RANGES.check(address, family)) return 'private';\n return 'public';\n}\n\nfunction parsePublicUrl(publicUrl: string): URL {\n let url: URL;\n try {\n url = new URL(publicUrl);\n } catch {\n throw new Error('MCP HTTP public URL must be an absolute https URL');\n }\n if (url.protocol !== 'https:') throw new Error('MCP HTTP public URL must use https');\n if (url.username !== '' || url.password !== '' || url.search !== '' || url.hash !== '') {\n throw new Error('MCP HTTP public URL must not carry credentials, a query or a fragment');\n }\n return url;\n}\n\ninterface IFailureWindow {\n count: number;\n startedAt: number;\n}\n\n/** Counts failures per address in fixed windows. Only failures are ever recorded here. */\nfunction createFailureCounter(now: () => number) {\n const windows = new Map<string, IFailureWindow>();\n return {\n /** Records one failure; answers whether the address is now over budget, and for how long. */\n fail(address: string): { throttled: boolean; retryAfterSeconds: number } {\n const at = now();\n let entry = windows.get(address);\n if (entry === undefined || at - entry.startedAt >= FAILURE_WINDOW_MS) {\n windows.delete(address);\n if (windows.size >= MAX_TRACKED_ADDRESSES) {\n for (const [key, value] of windows) {\n if (at - value.startedAt >= FAILURE_WINDOW_MS) windows.delete(key);\n }\n const oldest = windows.keys().next();\n if (windows.size >= MAX_TRACKED_ADDRESSES && oldest.done !== true) {\n windows.delete(oldest.value);\n }\n }\n entry = { count: 0, startedAt: at };\n windows.set(address, entry);\n }\n entry.count += 1;\n const remainingMs = FAILURE_WINDOW_MS - (at - entry.startedAt);\n return {\n throttled: entry.count > MAX_FAILURES_PER_WINDOW,\n retryAfterSeconds: Math.max(1, Math.ceil(remainingMs / 1000)),\n };\n },\n };\n}\n\n/**\n * Builds the gate. Throws when the configuration cannot describe a safe resource server: a public\n * URL that is not plain `https`, a non-`https` issuer, no scope or a scope a challenge cannot quote,\n * or a trusted proxy that is not a literal address.\n */\nexport function createMcpRemoteGate(\n authorization: IMcpRemoteAuthorization,\n now: () => number = () => performance.now(),\n): IMcpRemoteGate {\n const url = parsePublicUrl(authorization.publicUrl);\n let issuer: URL;\n try {\n issuer = new URL(authorization.issuer);\n } catch {\n throw new Error('MCP HTTP authorization issuer must be an absolute https URL');\n }\n if (issuer.protocol !== 'https:') throw new Error('MCP HTTP authorization issuer must use https');\n if (\n authorization.scopes.length === 0 ||\n !authorization.scopes.every((scope) => SCOPE_TOKEN.test(scope))\n ) {\n throw new Error('MCP HTTP authorization needs at least one valid scope');\n }\n const trustedProxies = new Set<string>();\n for (const proxy of authorization.trustedProxies ?? []) {\n const address = normalizeAddress(proxy);\n if (address === undefined)\n throw new Error('MCP HTTP trusted proxy must be a literal IP address');\n trustedProxies.add(address);\n }\n\n const endpointPath = url.pathname;\n // RFC 9728 §3.1: the well-known segment goes between the host and the resource's path, with a\n // lone terminating slash dropped.\n const wellKnownPath = `${WELL_KNOWN_PREFIX}${endpointPath === '/' ? '' : endpointPath}`;\n const resourceMetadataUrl = `${url.origin}${wellKnownPath}`;\n const expectedHosts = new Set([url.host]);\n if (url.port === '') expectedHosts.add(`${url.hostname}:443`);\n const metadata = JSON.stringify({\n resource: authorization.publicUrl,\n authorization_servers: [authorization.issuer],\n scopes_supported: [...authorization.scopes],\n bearer_methods_supported: ['header'],\n });\n const scopeList = authorization.scopes.join(' ');\n const challenges = {\n missing: `Bearer resource_metadata=\"${resourceMetadataUrl}\"`,\n invalid: `Bearer error=\"invalid_token\", resource_metadata=\"${resourceMetadataUrl}\"`,\n scope: `Bearer error=\"insufficient_scope\", scope=\"${scopeList}\", resource_metadata=\"${resourceMetadataUrl}\"`,\n };\n const failures = createFailureCounter(now);\n\n function remoteAddress(req: IncomingMessage): string | undefined {\n const peer = normalizeAddress(req.socket.remoteAddress);\n if (peer === undefined || !trustedProxies.has(peer)) return peer;\n const header = req.headers['x-forwarded-for'];\n const hops = (Array.isArray(header) ? header.join(',') : (header ?? '')).split(',');\n // Walk from the proxy inward: the nearest hop not itself a trusted proxy is the client, as the\n // trusted proxy saw it. Hops further left were written by the client and are not believed.\n for (let index = hops.length - 1; index >= 0; index -= 1) {\n const hop = normalizeAddress(hops[index]);\n if (hop === undefined) return peer;\n if (!trustedProxies.has(hop)) return hop;\n }\n return peer;\n }\n\n function refuse(req: IncomingMessage, res: ServerResponse, refusal: TMcpRemoteRefusal): void {\n const address = remoteAddress(req);\n const remote = classifyAddress(address);\n if (refusal === 'keys-unavailable') {\n // The issuer, not the peer, failed: not counted against the peer, and not a token verdict.\n authorization.audit?.({ refusal, remote, throttled: false });\n res.writeHead(503).end();\n return;\n }\n const budget = failures.fail(address ?? 'unknown');\n authorization.audit?.({ refusal, remote, throttled: budget.throttled });\n if (budget.throttled) {\n res.writeHead(429, { 'Retry-After': String(budget.retryAfterSeconds) }).end();\n } else if (refusal === 'missing-scope') {\n res.writeHead(403, { 'WWW-Authenticate': challenges.scope }).end();\n } else {\n const challenge = refusal === 'missing-token' ? challenges.missing : challenges.invalid;\n res.writeHead(401, { 'WWW-Authenticate': challenge }).end();\n }\n }\n\n return {\n endpointPath,\n async admit(req, res) {\n const host = req.headers.host?.toLowerCase();\n const origin = req.headers.origin;\n if (\n host === undefined ||\n !expectedHosts.has(host) ||\n (origin !== undefined && origin !== url.origin)\n ) {\n res.writeHead(403).end();\n return false;\n }\n if (req.url === wellKnownPath) {\n if (req.method !== 'GET') {\n res.writeHead(405, { Allow: 'GET' }).end();\n return false;\n }\n res.writeHead(200, { 'Content-Type': 'application/json' }).end(metadata);\n return false;\n }\n if (req.url !== endpointPath) {\n res.writeHead(404).end();\n return false;\n }\n const token = bearerCredential(req.headers.authorization);\n if (token === undefined) {\n refuse(req, res, 'missing-token');\n return false;\n }\n const verdict = await authorization.verifier.verify(token);\n if (verdict.admitted) return true;\n refuse(req, res, verdict.refusal);\n return false;\n },\n };\n}\n","/**\n * Streamable HTTP carriers for one process-owned MCP session: an authenticated loopback host\n * admitted by a minted bearer, and a remote host admitted by OAuth access tokens. Both share one\n * listener lifecycle and one stateless MCP handler; they differ only in the gate in front of it.\n */\nimport { createServer } from 'node:http';\nimport { isIP } from 'node:net';\n\nimport { toNodeHandler } from '@modelcontextprotocol/node';\nimport { createMcpHandler, Server } from '@modelcontextprotocol/server';\nimport {\n bearerCredential,\n credentialMatches,\n mintTransportToken,\n} from '@robota-sdk/agent-transport/node';\n\nimport { createMcpRemoteGate } from './mcp-remote-authorization.js';\nimport { invokeMcpTool, readCatalog, resolveSubmitTool } from './mcp-tool-surface.js';\n\nimport type { IMcpRemoteAuthorization } from './mcp-remote-authorization.js';\nimport type { IMcpTransportSession } from './mcp-session.js';\nimport type { IMcpSubmitToolIdentity } from './mcp-tool-surface.js';\nimport type { Tool as ModernTool } from '@modelcontextprotocol/server';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { AddressInfo } from 'node:net';\n\nconst LOOPBACK = '127.0.0.1';\nconst MAX_BODY_BYTES = 1024 * 1024;\nconst MAX_ACTIVE_REQUESTS = 16;\nconst MAX_CONNECTIONS = 32;\nconst HEADERS_TIMEOUT_MS = 5000;\nconst REQUEST_TIMEOUT_MS = 15000;\nconst CLOSE_TIMEOUT_MS = 5000;\n\nexport interface IMcpHttpHostOptions {\n name: string;\n version: string;\n session: IMcpTransportSession;\n /** Defaults to an OS-assigned port. */\n port?: number;\n /** Accepted only for numeric IPv4 loopback; provided for explicit bind-refusal checks. */\n host?: string;\n /** Host-owned identity for the submission extension. */\n submitTool?: IMcpSubmitToolIdentity;\n}\n\nexport interface IMcpHttpHost {\n start(): Promise<{ url: string; token: string }>;\n stop(): Promise<void>;\n waitForClose(): Promise<void>;\n}\n\nexport interface IMcpRemoteHttpHostOptions {\n name: string;\n version: string;\n session: IMcpTransportSession;\n /** Defaults to an OS-assigned port. */\n port?: number;\n /** Literal IP address to bind. Defaults to `127.0.0.1`, for a proxy on the same machine. */\n host?: string;\n /** Who may reach the session, decided from OAuth access tokens. Required: there is no open mode. */\n authorization: IMcpRemoteAuthorization;\n /** Host-owned identity for the submission extension. */\n submitTool?: IMcpSubmitToolIdentity;\n}\n\nexport interface IMcpRemoteHttpHost {\n /** Resolves with the address actually bound and the public URL clients are told to use. */\n start(): Promise<{ listening: string; url: string }>;\n stop(): Promise<void>;\n waitForClose(): Promise<void>;\n}\n\n/** Decides one request before its body is read. False means the response has been written. */\ntype TRequestGate = (\n req: IncomingMessage,\n res: ServerResponse,\n port: number,\n) => boolean | Promise<boolean>;\n\ninterface IHostCore {\n start(): Promise<AddressInfo>;\n stop(): Promise<void>;\n waitForClose(): Promise<void>;\n}\n\nfunction validatePort(port: number | undefined): void {\n if (port !== undefined && (!Number.isInteger(port) || port < 0 || port > 65535)) {\n throw new Error('MCP HTTP port must be an integer in 0..65535');\n }\n}\n\nfunction createHostCore(\n options: Omit<IMcpHttpHostOptions, 'host'>,\n bindHost: string,\n gate: TRequestGate,\n): IHostCore {\n const submitTool = resolveSubmitTool(options.submitTool);\n let listener: ReturnType<typeof createServer> | undefined;\n let handler: ReturnType<typeof createMcpHandler> | undefined;\n let stopPromise: Promise<void> | undefined;\n let startCalled = false;\n let stoppingRequested = false;\n let active = 0;\n let resolveClose: (() => void) | undefined;\n let rejectClose: ((error: Error) => void) | undefined;\n const closed = new Promise<void>((resolve, reject) => {\n resolveClose = resolve;\n rejectClose = reject;\n });\n void closed.catch(() => undefined);\n\n return {\n async start() {\n if (startCalled || stopPromise) throw new Error('MCP HTTP host already started');\n startCalled = true;\n await readCatalog(options.session, submitTool.name);\n if (stoppingRequested) throw new Error('MCP HTTP host stopped during startup');\n handler = createMcpHandler(\n () => {\n const server = new Server(\n { name: options.name, version: options.version },\n { capabilities: { tools: {} } },\n );\n server.setRequestHandler('tools/list', async () => ({\n // The v1 catalog validator returns a wider schema type than SDK v2's JSON-only Tool.\n // Normalize at this SDK boundary while keeping one canonical runtime catalog owner.\n tools: JSON.parse(\n JSON.stringify([\n ...(await readCatalog(options.session, submitTool.name)),\n submitTool,\n ]),\n ) as ModernTool[],\n }));\n server.setRequestHandler('tools/call', async (request, ctx) =>\n invokeMcpTool(\n options.session,\n request.params.name,\n request.params.arguments ?? {},\n ctx.mcpReq.signal,\n submitTool.name,\n ),\n );\n return server;\n },\n { maxSubscriptions: 0 },\n );\n const nodeHandler = toNodeHandler(handler);\n const serve = (req: IncomingMessage, res: ServerResponse): void => {\n if (active >= MAX_ACTIVE_REQUESTS) {\n res.writeHead(503).end();\n return;\n }\n active += 1;\n void (async () => {\n try {\n let parsedBody: unknown;\n if (req.method === 'POST') {\n const declared = Number(req.headers['content-length']);\n if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {\n res.writeHead(413).end();\n return;\n }\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of req) {\n const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array);\n size += bytes.length;\n if (size > MAX_BODY_BYTES) {\n res.writeHead(413).end();\n req.destroy();\n return;\n }\n chunks.push(bytes);\n }\n try {\n parsedBody = JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown;\n } catch {\n res.writeHead(400).end();\n return;\n }\n }\n await nodeHandler(req, res, parsedBody);\n } catch {\n if (!res.headersSent) res.writeHead(500).end();\n else res.destroy();\n } finally {\n active -= 1;\n }\n })();\n };\n const server = createServer({ maxHeaderSize: 16 * 1024 }, (req, res) => {\n const address = server.address();\n if (!address || typeof address === 'string') {\n res.writeHead(503).end();\n return;\n }\n const verdict = gate(req, res, address.port);\n if (verdict === true) {\n serve(req, res);\n } else if (verdict !== false) {\n verdict.then(\n (admitted) => {\n if (admitted) serve(req, res);\n },\n () => {\n if (!res.headersSent) res.writeHead(500).end();\n else res.destroy();\n },\n );\n }\n });\n server.maxConnections = MAX_CONNECTIONS;\n server.headersTimeout = HEADERS_TIMEOUT_MS;\n server.requestTimeout = REQUEST_TIMEOUT_MS;\n server.on('error', (error) => rejectClose?.(error));\n server.on('close', () => resolveClose?.());\n try {\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(options.port ?? 0, bindHost, () => {\n server.off('error', reject);\n resolve();\n });\n });\n } catch (error) {\n await handler.close();\n throw error;\n }\n if (stoppingRequested) {\n await new Promise<void>((resolve) => server.close(() => resolve()));\n await handler.close();\n throw new Error('MCP HTTP host stopped during startup');\n }\n listener = server;\n return server.address() as AddressInfo;\n },\n stop() {\n if (stopPromise) return stopPromise;\n stoppingRequested = true;\n stopPromise = (async () => {\n const currentHandler = handler;\n const currentListener = listener;\n if (!currentListener) {\n await currentHandler?.close();\n resolveClose?.();\n return;\n }\n const close = new Promise<void>((resolve, reject) => {\n currentListener.close((error) => (error ? reject(error) : resolve()));\n currentListener.closeAllConnections();\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n await Promise.race([\n Promise.all([currentHandler?.close(), close]).then(() => undefined),\n new Promise<never>((_resolve, reject) => {\n timer = setTimeout(\n () => reject(new Error('MCP HTTP close timed out')),\n CLOSE_TIMEOUT_MS,\n );\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n })();\n return stopPromise;\n },\n waitForClose() {\n return closed;\n },\n };\n}\n\nexport function createMcpHttpHost(options: IMcpHttpHostOptions): IMcpHttpHost {\n if (options.host !== undefined && options.host !== LOOPBACK) {\n throw new Error('MCP HTTP host must bind numeric IPv4 loopback');\n }\n validatePort(options.port);\n\n const token = mintTransportToken();\n const core = createHostCore(options, LOOPBACK, (req, res, port) => {\n const expectedHost = `${LOOPBACK}:${port}`;\n const expectedOrigin = `http://${expectedHost}`;\n if (\n req.url !== '/mcp' ||\n req.headers.host !== expectedHost ||\n (req.headers.origin !== undefined && req.headers.origin !== expectedOrigin)\n ) {\n res.writeHead(403).end();\n return false;\n }\n const authorization = req.headers.authorization;\n if (\n typeof authorization !== 'string' ||\n !credentialMatches(token, bearerCredential(authorization))\n ) {\n res.writeHead(401).end();\n return false;\n }\n return true;\n });\n\n return {\n async start() {\n const address = await core.start();\n return { url: `http://${LOOPBACK}:${address.port}/mcp`, token };\n },\n stop: core.stop,\n waitForClose: core.waitForClose,\n };\n}\n\n/**\n * A host reachable beyond loopback. It never mints or accepts the loopback bearer: every request\n * is admitted by an OAuth access token checked against the injected verifier.\n */\nexport function createMcpRemoteHttpHost(options: IMcpRemoteHttpHostOptions): IMcpRemoteHttpHost {\n const bindHost = options.host ?? LOOPBACK;\n if (isIP(bindHost) === 0) throw new Error('MCP HTTP host must bind a literal IP address');\n validatePort(options.port);\n const gate = createMcpRemoteGate(options.authorization);\n const core = createHostCore(options, bindHost, (req, res) => gate.admit(req, res));\n\n return {\n async start() {\n const address = await core.start();\n const host = address.family === 'IPv6' ? `[${address.address}]` : address.address;\n return { listening: `${host}:${address.port}`, url: options.authorization.publicUrl };\n },\n stop: core.stop,\n waitForClose: core.waitForClose,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["Server"],"sources":["../../src/mcp-tool-surface.ts","../../src/mcp-server.ts","../../src/mcp-transport.ts","../../src/mcp-remote-authorization.ts","../../src/mcp-http-host.ts"],"sourcesContent":["/** One canonical tool catalog and invocation path shared by stdio and HTTP MCP carriers. */\nimport { ToolSchema } from '@modelcontextprotocol/sdk/types.js';\nimport { isTurnNotRunError } from '@robota-sdk/agent-interface-session';\n\nimport type { IMcpTransportSession } from './mcp-session.js';\nimport type { CallToolResult, Tool } from '@modelcontextprotocol/sdk/types.js';\n\nexport interface IMcpSubmitToolIdentity {\n readonly name: string;\n readonly description: string;\n}\n\nconst DEFAULT_SUBMIT_TOOL: IMcpSubmitToolIdentity = {\n name: 'agent_submit',\n description: 'Submit a prompt to the agent and await its turn',\n};\n\nexport function resolveSubmitTool(identity: IMcpSubmitToolIdentity = DEFAULT_SUBMIT_TOOL): Tool {\n if (typeof identity.name !== 'string' || !/^[A-Za-z0-9._-]{1,128}$/.test(identity.name)) {\n throw new Error(\n 'MCP submit tool name must contain 1–128 ASCII letters, digits, dots, hyphens, or underscores',\n );\n }\n return ToolSchema.parse({\n name: identity.name,\n description: identity.description,\n inputSchema: {\n type: 'object' as const,\n properties: { prompt: { type: 'string', minLength: 1 } },\n required: ['prompt'],\n additionalProperties: false,\n },\n });\n}\n\nexport async function readCatalog(\n session: IMcpTransportSession,\n submitToolName: string,\n): Promise<Tool[]> {\n const names = new Set([submitToolName]);\n return (await session.listRuntimeTools()).map((schema) => {\n if (names.has(schema.name)) {\n throw new Error(`Duplicate or reserved MCP tool name: ${schema.name}`);\n }\n names.add(schema.name);\n return ToolSchema.parse({\n name: schema.name,\n description: schema.description,\n inputSchema: schema.parameters,\n });\n });\n}\n\nfunction toolError(message: string): CallToolResult {\n return { content: [{ type: 'text' as const, text: message }], isError: true };\n}\n\nfunction isJsonValue(value: unknown): boolean {\n if (value === null || typeof value === 'string' || typeof value === 'boolean') return true;\n if (typeof value === 'number') return Number.isFinite(value);\n if (Array.isArray(value)) return value.every(isJsonValue);\n return typeof value === 'object' && Object.values(value).every(isJsonValue);\n}\n\nfunction isRuntimeParameters(\n value: Record<string, unknown>,\n): value is Parameters<IMcpTransportSession['invokeRuntimeTool']>[1] {\n return Object.values(value).every(isJsonValue);\n}\n\nexport async function invokeMcpTool(\n session: IMcpTransportSession,\n toolName: string,\n parameters: Record<string, unknown>,\n signal: AbortSignal,\n submitToolName: string,\n): Promise<CallToolResult> {\n let catalog: Tool[];\n try {\n signal.throwIfAborted();\n catalog = await readCatalog(session, submitToolName);\n } catch (error) {\n return toolError(error instanceof Error ? error.message : String(error));\n }\n if (toolName === submitToolName) {\n if (\n typeof parameters.prompt !== 'string' ||\n parameters.prompt.length === 0 ||\n Object.keys(parameters).some((key) => key !== 'prompt')\n ) {\n return toolError(\n `${submitToolName} requires a non-empty string prompt and no other arguments`,\n );\n }\n try {\n const handle = await session.submit(parameters.prompt, undefined, undefined, { signal });\n const result = await handle.completed;\n return { content: [{ type: 'text', text: result.response }] };\n } catch (error) {\n if (!isTurnNotRunError(error) && !signal.aborted) throw error;\n return toolError(error instanceof Error ? error.message : String(error));\n }\n }\n if (!catalog.some((tool) => tool.name === toolName))\n return toolError(`Unknown tool: ${toolName}`);\n if (!isRuntimeParameters(parameters)) return toolError('Tool arguments must contain JSON values');\n try {\n const result = await session.invokeRuntimeTool(toolName, parameters, { signal });\n return {\n content: [{ type: 'text', text: JSON.stringify(result) }],\n isError: !result.success,\n };\n } catch (error) {\n return toolError(error instanceof Error ? error.message : String(error));\n }\n}\n","/** MCP translates the canonical session tool port; execution policy stays in the session. */\nimport { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';\n\nimport { invokeMcpTool, readCatalog, resolveSubmitTool } from './mcp-tool-surface.js';\n\nimport type { IMcpTransportSession } from './mcp-session.js';\nimport type { IMcpSubmitToolIdentity } from './mcp-tool-surface.js';\n\nexport interface IAgentMcpOptions {\n name: string;\n version: string;\n session: IMcpTransportSession;\n /** Host-owned identity for the submission extension. */\n submitTool?: IMcpSubmitToolIdentity;\n}\n\n/** Validate the catalog before the caller connects a carrier. */\nexport async function createAgentMcpServer(options: IAgentMcpOptions): Promise<Server> {\n const { name, version, session } = options;\n const submitTool = resolveSubmitTool(options.submitTool);\n await readCatalog(session, submitTool.name);\n const server = new Server({ name, version }, { capabilities: { tools: {} } });\n server.setRequestHandler(ListToolsRequestSchema, async () => ({\n tools: [...(await readCatalog(session, submitTool.name)), submitTool],\n }));\n server.setRequestHandler(CallToolRequestSchema, async (request, extra) =>\n invokeMcpTool(\n session,\n request.params.name,\n request.params.arguments ?? {},\n extra.signal,\n submitTool.name,\n ),\n );\n return server;\n}\n","/**\n * ITransportAdapter implementation for MCP transport.\n *\n * Connects the official SDK stdio carrier to the canonical session MCP server.\n * The adapter owns carrier readiness and teardown; the product shell owns process policy.\n */\n\nimport { PassThrough } from 'node:stream';\n\nimport { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';\n\nimport { createAgentMcpServer } from './mcp-server.js';\n\nimport type { IMcpTransportSession } from './mcp-session.js';\nimport type { IMcpSubmitToolIdentity } from './mcp-tool-surface.js';\nimport type { Server } from '@modelcontextprotocol/sdk/server/index.js';\nimport type {\n ITransportAdapter,\n ITransportLifecycleError,\n} from '@robota-sdk/agent-interface-transport';\nimport type { Readable, Writable } from 'node:stream';\n\nconst STARTUP_TIMEOUT_MS = 15000;\n\nexport interface IMcpTransportOptions {\n /** Name for the MCP server. */\n name: string;\n /** Version string. */\n version: string;\n /** Process-owned streams; defaults to the current process stdio. */\n stdin?: Readable;\n stdout?: Writable;\n /** Host-owned identity for the submission extension. */\n submitTool?: IMcpSubmitToolIdentity;\n}\n\nexport interface IMcpTransport extends ITransportAdapter<IMcpTransportSession> {\n getServer(): Server;\n /** Settles when the carrier closes or its input ends; rejects on carrier failure. */\n waitForClose(): Promise<void>;\n}\n\nexport function createMcpTransport(options: IMcpTransportOptions): IMcpTransport {\n let session: IMcpTransportSession | null = null;\n let server: Server | null = null;\n let starting: Promise<void> | null = null;\n let stopping: Promise<void> | null = null;\n let inputBridge: PassThrough | null = null;\n let cancelStart: ((error: Error) => void) | null = null;\n const stdin = options.stdin ?? process.stdin;\n const stdout = options.stdout ?? process.stdout;\n let resolveClose: (() => void) | null = null;\n let rejectClose: ((error: Error) => void) | null = null;\n let closePromise: Promise<void> = Promise.resolve();\n const resetClose = (): void => {\n closePromise = new Promise<void>((resolve, reject) => {\n resolveClose = resolve;\n rejectClose = reject;\n });\n // A carrier may fail between connect and the process owner awaiting this promise.\n // allow-fallback: waitForClose retains the rejection for the process owner; this observer only prevents an early unhandled-rejection warning.\n void closePromise.catch(() => undefined);\n };\n const onInputClose = (): void => {\n resolveClose?.();\n cancelStart?.(new Error('MCP input closed during startup'));\n };\n const onOutputClose = (): void => {\n resolveClose?.();\n cancelStart?.(new Error('MCP output closed during startup'));\n };\n const onOutputError = (error: Error): void => {\n rejectClose?.(error);\n cancelStart?.(error);\n };\n const removeInputListeners = (): void => {\n stdin.off('end', onInputClose);\n stdin.off('close', onInputClose);\n stdout.off('close', onOutputClose);\n stdout.off('error', onOutputError);\n if (inputBridge) {\n stdin.unpipe(inputBridge);\n inputBridge.destroy();\n inputBridge = null;\n }\n };\n const lifecycleError = (code: ITransportLifecycleError['code']): ITransportLifecycleError =>\n Object.assign(new Error(`MCP transport ${code}.`), {\n name: 'TransportLifecycleError' as const,\n code,\n transportName: 'mcp',\n });\n\n return {\n name: 'mcp',\n lifecycle: Object.freeze({ kind: 'service' }),\n attach(s: IMcpTransportSession) {\n if (server || starting || stopping) throw lifecycleError('already-started');\n session = s;\n resetClose();\n },\n async start() {\n if (!session) throw lifecycleError('not-attached');\n if (server || starting || stopping) throw lifecycleError('already-started');\n const attachedSession = session;\n const cancelled = new Promise<never>((_resolve, reject) => {\n cancelStart = reject;\n });\n const bridge = new PassThrough();\n inputBridge = bridge;\n stdin.once('end', onInputClose);\n stdin.once('close', onInputClose);\n stdout.once('close', onOutputClose);\n stdout.on('error', onOutputError);\n // Preserve early frames with backpressure while the canonical tool catalog is validated.\n // Piping also exposes EOF before the SDK begins reading.\n stdin.pipe(bridge);\n starting = (async () => {\n let candidate: Server | null = null;\n const timeout = setTimeout(\n () => cancelStart?.(new Error('MCP carrier startup timed out')),\n STARTUP_TIMEOUT_MS,\n );\n try {\n candidate = await Promise.race([\n createAgentMcpServer({\n name: options.name,\n version: options.version,\n session: attachedSession,\n ...(options.submitTool !== undefined ? { submitTool: options.submitTool } : {}),\n }),\n cancelled,\n ]);\n const nextCarrier = new StdioServerTransport(bridge, stdout);\n nextCarrier.onclose = () => resolveClose?.();\n nextCarrier.onerror = (error) => rejectClose?.(error);\n await candidate.connect(nextCarrier);\n server = candidate;\n candidate = null;\n } catch (error) {\n removeInputListeners();\n // allow-fallback: the startup error is already being thrown; a failed best-effort close must not replace it.\n if (candidate) await candidate.close().catch(() => undefined);\n throw error;\n } finally {\n clearTimeout(timeout);\n cancelStart = null;\n }\n })();\n try {\n await starting;\n } finally {\n starting = null;\n }\n },\n stop() {\n if (stopping) return stopping;\n stopping = (async () => {\n try {\n cancelStart?.(new Error('MCP carrier startup stopped'));\n // allow-fallback: stop explicitly cancels startup, so its expected rejection is consumed before carrier teardown.\n if (starting) await starting.catch(() => undefined);\n if (server) await server.close();\n } finally {\n removeInputListeners();\n server = null;\n session = null;\n resolveClose?.();\n }\n })().finally(() => {\n stopping = null;\n });\n return stopping;\n },\n getServer() {\n if (!server) throw new Error('Transport not started. Call start() first.');\n return server;\n },\n waitForClose() {\n return closePromise;\n },\n };\n}\n","/**\n * The admission gate of the remote MCP HTTP host: an OAuth resource server in the sense of\n * RFC 6750 (bearer challenges) and RFC 9728 (protected-resource metadata).\n *\n * The name checks, challenges, metadata and failure throttle are the shared resource-server gate of\n * `@robota-sdk/agent-transport/node`; this host adds only its one endpoint and its verifier. The MCP\n * endpoint is the public URL's own path, so the public URL is also the resource identifier (`aud`).\n *\n * The token is verified before anything is counted, and only failures are counted. Each failure is\n * handed to an audit sink as a closed-set reason and a coarse address class, never token text.\n */\n\nimport {\n bearerCredential,\n createBearerResourceServer,\n describeProtectedResource,\n refuseBearerToken,\n serveProtectedResourceMetadata,\n} from '@robota-sdk/agent-transport/node';\n\nimport type {\n IAccessTokenVerifier,\n TAccessTokenRefusal,\n} from '@robota-sdk/agent-interface-transport';\nimport type { TRemoteAddressClass } from '@robota-sdk/agent-transport/node';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\n\nconst LABEL = 'MCP HTTP';\n\n/** Why a remote request was refused: a verifier refusal, or no bearer token at all. */\nexport type TMcpRemoteRefusal = TAccessTokenRefusal | 'missing-token';\n\n/** A coarse class of the refused peer's address; the address itself is never reported. */\nexport type TMcpRemoteAddressClass = TRemoteAddressClass;\n\n/** One refused request, as the audit sink sees it. Carries no token text and no claim value. */\nexport interface IMcpRemoteAuditRecord {\n readonly refusal: TMcpRemoteRefusal;\n readonly remote: TMcpRemoteAddressClass;\n /** True when the refusal was answered 429 because the address exceeded its failure budget. */\n readonly throttled: boolean;\n}\n\n/** How the remote host decides who may reach the session. */\nexport interface IMcpRemoteAuthorization {\n /** The `https` URL clients use for the MCP endpoint; also the resource identifier (`aud`). */\n readonly publicUrl: string;\n /** The authorization server named in the protected-resource metadata. */\n readonly issuer: string;\n /** Scopes a token must carry; advertised as `scopes_supported` and in `insufficient_scope`. */\n readonly scopes: readonly string[];\n /** Decides each presented token. Built for the same issuer, resource and scopes. */\n readonly verifier: IAccessTokenVerifier;\n /** Proxy addresses whose `X-Forwarded-For` is believed. Absent: the header is ignored. */\n readonly trustedProxies?: readonly string[];\n /** Receives one content-free record per refused request. */\n readonly audit?: (record: IMcpRemoteAuditRecord) => void;\n}\n\nexport interface IMcpRemoteGate {\n readonly endpointPath: string;\n /** Resolves true when the request may reach MCP; otherwise the response has been written. */\n admit(req: IncomingMessage, res: ServerResponse): Promise<boolean>;\n}\n\n/**\n * Builds the gate. Throws when the configuration cannot describe a safe resource server: a public\n * URL that is not plain `https`, a non-`https` issuer, no scope or a scope a challenge cannot quote,\n * or a trusted proxy that is not a literal address.\n */\nexport function createMcpRemoteGate(\n authorization: IMcpRemoteAuthorization,\n now: () => number = () => performance.now(),\n): IMcpRemoteGate {\n const resource = describeProtectedResource({\n resource: authorization.publicUrl,\n issuer: authorization.issuer,\n scopes: authorization.scopes,\n label: LABEL,\n });\n const server = createBearerResourceServer({\n publicUrl: authorization.publicUrl,\n ...(authorization.trustedProxies !== undefined\n ? { trustedProxies: authorization.trustedProxies }\n : {}),\n label: LABEL,\n now,\n });\n const endpointPath = server.url.pathname;\n\n function refuse(req: IncomingMessage, res: ServerResponse, refusal: TMcpRemoteRefusal): void {\n if (refusal === 'keys-unavailable') {\n // The issuer, not the peer, failed: not counted against the peer, and not a token verdict.\n authorization.audit?.({ refusal, remote: server.remote(req), throttled: false });\n res.writeHead(503).end();\n return;\n }\n const failure = server.fail(req);\n authorization.audit?.({ refusal, remote: failure.remote, throttled: failure.throttled });\n refuseBearerToken(\n res,\n resource,\n refusal === 'missing-token' || refusal === 'missing-scope' ? refusal : 'invalid-token',\n failure,\n );\n }\n\n return {\n endpointPath,\n async admit(req, res) {\n if (!server.checkNames(req, res)) return false;\n if (req.url === resource.wellKnownPath) {\n serveProtectedResourceMetadata(req, res, resource);\n return false;\n }\n if (req.url !== endpointPath) {\n res.writeHead(404).end();\n return false;\n }\n const token = bearerCredential(req.headers.authorization);\n if (token === undefined) {\n refuse(req, res, 'missing-token');\n return false;\n }\n const verdict = await authorization.verifier.verify(token);\n if (verdict.admitted) return true;\n refuse(req, res, verdict.refusal);\n return false;\n },\n };\n}\n","/**\n * Streamable HTTP carriers for one process-owned MCP session: an authenticated loopback host\n * admitted by a minted bearer, and a remote host admitted by OAuth access tokens. Both share one\n * listener lifecycle and one stateless MCP handler; they differ only in the gate in front of it.\n */\nimport { createServer } from 'node:http';\nimport { isIP } from 'node:net';\n\nimport { toNodeHandler } from '@modelcontextprotocol/node';\nimport { createMcpHandler, Server } from '@modelcontextprotocol/server';\nimport {\n bearerCredential,\n credentialMatches,\n mintTransportToken,\n} from '@robota-sdk/agent-transport/node';\n\nimport { createMcpRemoteGate } from './mcp-remote-authorization.js';\nimport { invokeMcpTool, readCatalog, resolveSubmitTool } from './mcp-tool-surface.js';\n\nimport type { IMcpRemoteAuthorization } from './mcp-remote-authorization.js';\nimport type { IMcpTransportSession } from './mcp-session.js';\nimport type { IMcpSubmitToolIdentity } from './mcp-tool-surface.js';\nimport type { Tool as ModernTool } from '@modelcontextprotocol/server';\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { AddressInfo } from 'node:net';\n\nconst LOOPBACK = '127.0.0.1';\nconst MAX_BODY_BYTES = 1024 * 1024;\nconst MAX_ACTIVE_REQUESTS = 16;\nconst MAX_CONNECTIONS = 32;\nconst HEADERS_TIMEOUT_MS = 5000;\nconst REQUEST_TIMEOUT_MS = 15000;\nconst CLOSE_TIMEOUT_MS = 5000;\n\nexport interface IMcpHttpHostOptions {\n name: string;\n version: string;\n session: IMcpTransportSession;\n /** Defaults to an OS-assigned port. */\n port?: number;\n /** Accepted only for numeric IPv4 loopback; provided for explicit bind-refusal checks. */\n host?: string;\n /** Host-owned identity for the submission extension. */\n submitTool?: IMcpSubmitToolIdentity;\n}\n\nexport interface IMcpHttpHost {\n start(): Promise<{ url: string; token: string }>;\n stop(): Promise<void>;\n waitForClose(): Promise<void>;\n}\n\nexport interface IMcpRemoteHttpHostOptions {\n name: string;\n version: string;\n session: IMcpTransportSession;\n /** Defaults to an OS-assigned port. */\n port?: number;\n /** Literal IP address to bind. Defaults to `127.0.0.1`, for a proxy on the same machine. */\n host?: string;\n /** Who may reach the session, decided from OAuth access tokens. Required: there is no open mode. */\n authorization: IMcpRemoteAuthorization;\n /** Host-owned identity for the submission extension. */\n submitTool?: IMcpSubmitToolIdentity;\n}\n\nexport interface IMcpRemoteHttpHost {\n /** Resolves with the address actually bound and the public URL clients are told to use. */\n start(): Promise<{ listening: string; url: string }>;\n stop(): Promise<void>;\n waitForClose(): Promise<void>;\n}\n\n/** Decides one request before its body is read. False means the response has been written. */\ntype TRequestGate = (\n req: IncomingMessage,\n res: ServerResponse,\n port: number,\n) => boolean | Promise<boolean>;\n\ninterface IHostCore {\n start(): Promise<AddressInfo>;\n stop(): Promise<void>;\n waitForClose(): Promise<void>;\n}\n\nfunction validatePort(port: number | undefined): void {\n if (port !== undefined && (!Number.isInteger(port) || port < 0 || port > 65535)) {\n throw new Error('MCP HTTP port must be an integer in 0..65535');\n }\n}\n\nfunction createHostCore(\n options: Omit<IMcpHttpHostOptions, 'host'>,\n bindHost: string,\n gate: TRequestGate,\n): IHostCore {\n const submitTool = resolveSubmitTool(options.submitTool);\n let listener: ReturnType<typeof createServer> | undefined;\n let handler: ReturnType<typeof createMcpHandler> | undefined;\n let stopPromise: Promise<void> | undefined;\n let startCalled = false;\n let stoppingRequested = false;\n let active = 0;\n let resolveClose: (() => void) | undefined;\n let rejectClose: ((error: Error) => void) | undefined;\n const closed = new Promise<void>((resolve, reject) => {\n resolveClose = resolve;\n rejectClose = reject;\n });\n void closed.catch(() => undefined);\n\n return {\n async start() {\n if (startCalled || stopPromise) throw new Error('MCP HTTP host already started');\n startCalled = true;\n await readCatalog(options.session, submitTool.name);\n if (stoppingRequested) throw new Error('MCP HTTP host stopped during startup');\n handler = createMcpHandler(\n () => {\n const server = new Server(\n { name: options.name, version: options.version },\n { capabilities: { tools: {} } },\n );\n server.setRequestHandler('tools/list', async () => ({\n // The v1 catalog validator returns a wider schema type than SDK v2's JSON-only Tool.\n // Normalize at this SDK boundary while keeping one canonical runtime catalog owner.\n tools: JSON.parse(\n JSON.stringify([\n ...(await readCatalog(options.session, submitTool.name)),\n submitTool,\n ]),\n ) as ModernTool[],\n }));\n server.setRequestHandler('tools/call', async (request, ctx) =>\n invokeMcpTool(\n options.session,\n request.params.name,\n request.params.arguments ?? {},\n ctx.mcpReq.signal,\n submitTool.name,\n ),\n );\n return server;\n },\n { maxSubscriptions: 0 },\n );\n const nodeHandler = toNodeHandler(handler);\n const serve = (req: IncomingMessage, res: ServerResponse): void => {\n if (active >= MAX_ACTIVE_REQUESTS) {\n res.writeHead(503).end();\n return;\n }\n active += 1;\n void (async () => {\n try {\n let parsedBody: unknown;\n if (req.method === 'POST') {\n const declared = Number(req.headers['content-length']);\n if (Number.isFinite(declared) && declared > MAX_BODY_BYTES) {\n res.writeHead(413).end();\n return;\n }\n const chunks: Buffer[] = [];\n let size = 0;\n for await (const chunk of req) {\n const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as Uint8Array);\n size += bytes.length;\n if (size > MAX_BODY_BYTES) {\n res.writeHead(413).end();\n req.destroy();\n return;\n }\n chunks.push(bytes);\n }\n try {\n parsedBody = JSON.parse(Buffer.concat(chunks).toString('utf8')) as unknown;\n } catch {\n res.writeHead(400).end();\n return;\n }\n }\n await nodeHandler(req, res, parsedBody);\n } catch {\n if (!res.headersSent) res.writeHead(500).end();\n else res.destroy();\n } finally {\n active -= 1;\n }\n })();\n };\n const server = createServer({ maxHeaderSize: 16 * 1024 }, (req, res) => {\n const address = server.address();\n if (!address || typeof address === 'string') {\n res.writeHead(503).end();\n return;\n }\n const verdict = gate(req, res, address.port);\n if (verdict === true) {\n serve(req, res);\n } else if (verdict !== false) {\n verdict.then(\n (admitted) => {\n if (admitted) serve(req, res);\n },\n () => {\n if (!res.headersSent) res.writeHead(500).end();\n else res.destroy();\n },\n );\n }\n });\n server.maxConnections = MAX_CONNECTIONS;\n server.headersTimeout = HEADERS_TIMEOUT_MS;\n server.requestTimeout = REQUEST_TIMEOUT_MS;\n server.on('error', (error) => rejectClose?.(error));\n server.on('close', () => resolveClose?.());\n try {\n await new Promise<void>((resolve, reject) => {\n server.once('error', reject);\n server.listen(options.port ?? 0, bindHost, () => {\n server.off('error', reject);\n resolve();\n });\n });\n } catch (error) {\n await handler.close();\n throw error;\n }\n if (stoppingRequested) {\n await new Promise<void>((resolve) => server.close(() => resolve()));\n await handler.close();\n throw new Error('MCP HTTP host stopped during startup');\n }\n listener = server;\n return server.address() as AddressInfo;\n },\n stop() {\n if (stopPromise) return stopPromise;\n stoppingRequested = true;\n stopPromise = (async () => {\n const currentHandler = handler;\n const currentListener = listener;\n if (!currentListener) {\n await currentHandler?.close();\n resolveClose?.();\n return;\n }\n const close = new Promise<void>((resolve, reject) => {\n currentListener.close((error) => (error ? reject(error) : resolve()));\n currentListener.closeAllConnections();\n });\n let timer: ReturnType<typeof setTimeout> | undefined;\n try {\n await Promise.race([\n Promise.all([currentHandler?.close(), close]).then(() => undefined),\n new Promise<never>((_resolve, reject) => {\n timer = setTimeout(\n () => reject(new Error('MCP HTTP close timed out')),\n CLOSE_TIMEOUT_MS,\n );\n }),\n ]);\n } finally {\n if (timer) clearTimeout(timer);\n }\n })();\n return stopPromise;\n },\n waitForClose() {\n return closed;\n },\n };\n}\n\nexport function createMcpHttpHost(options: IMcpHttpHostOptions): IMcpHttpHost {\n if (options.host !== undefined && options.host !== LOOPBACK) {\n throw new Error('MCP HTTP host must bind numeric IPv4 loopback');\n }\n validatePort(options.port);\n\n const token = mintTransportToken();\n const core = createHostCore(options, LOOPBACK, (req, res, port) => {\n const expectedHost = `${LOOPBACK}:${port}`;\n const expectedOrigin = `http://${expectedHost}`;\n if (\n req.url !== '/mcp' ||\n req.headers.host !== expectedHost ||\n (req.headers.origin !== undefined && req.headers.origin !== expectedOrigin)\n ) {\n res.writeHead(403).end();\n return false;\n }\n const authorization = req.headers.authorization;\n if (\n typeof authorization !== 'string' ||\n !credentialMatches(token, bearerCredential(authorization))\n ) {\n res.writeHead(401).end();\n return false;\n }\n return true;\n });\n\n return {\n async start() {\n const address = await core.start();\n return { url: `http://${LOOPBACK}:${address.port}/mcp`, token };\n },\n stop: core.stop,\n waitForClose: core.waitForClose,\n };\n}\n\n/**\n * A host reachable beyond loopback. It never mints or accepts the loopback bearer: every request\n * is admitted by an OAuth access token checked against the injected verifier.\n */\nexport function createMcpRemoteHttpHost(options: IMcpRemoteHttpHostOptions): IMcpRemoteHttpHost {\n const bindHost = options.host ?? LOOPBACK;\n if (isIP(bindHost) === 0) throw new Error('MCP HTTP host must bind a literal IP address');\n validatePort(options.port);\n const gate = createMcpRemoteGate(options.authorization);\n const core = createHostCore(options, bindHost, (req, res) => gate.admit(req, res));\n\n return {\n async start() {\n const address = await core.start();\n const host = address.family === 'IPv6' ? `[${address.address}]` : address.address;\n return { listening: `${host}:${address.port}`, url: options.authorization.publicUrl };\n },\n stop: core.stop,\n waitForClose: core.waitForClose,\n };\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@robota-sdk/agent-transport-mcp",
3
- "version": "3.0.0-beta.82",
3
+ "version": "3.0.0-beta.83",
4
4
  "description": "Model Context Protocol (MCP) server transport for the Robota SDK",
5
5
  "type": "module",
6
6
  "main": "dist/node/index.js",
@@ -28,7 +28,8 @@
28
28
  "default": "./dist/node/index.cjs"
29
29
  }
30
30
  }
31
- }
31
+ },
32
+ "./package.json": "./package.json"
32
33
  },
33
34
  "repository": {
34
35
  "type": "git",
@@ -39,19 +40,20 @@
39
40
  "url": "https://github.com/woojubb/robota/issues"
40
41
  },
41
42
  "engines": {
42
- "node": ">=22.0.0"
43
+ "node": ">=22.12.0"
43
44
  },
44
45
  "files": [
45
46
  "dist",
46
- "src"
47
+ "src",
48
+ "CHANGELOG.md"
47
49
  ],
48
50
  "dependencies": {
49
51
  "@modelcontextprotocol/node": "^2.0.0",
50
52
  "@modelcontextprotocol/sdk": "^1.29.0",
51
53
  "@modelcontextprotocol/server": "^2.0.0",
52
- "@robota-sdk/agent-interface-session": "3.0.0-beta.82",
53
- "@robota-sdk/agent-interface-transport": "3.0.0-beta.82",
54
- "@robota-sdk/agent-transport": "3.0.0-beta.82"
54
+ "@robota-sdk/agent-interface-session": "3.0.0-beta.83",
55
+ "@robota-sdk/agent-interface-transport": "3.0.0-beta.83",
56
+ "@robota-sdk/agent-transport": "3.0.0-beta.83"
55
57
  },
56
58
  "devDependencies": {
57
59
  "@modelcontextprotocol/client": "^2.0.0",
@@ -2,46 +2,36 @@
2
2
  * The admission gate of the remote MCP HTTP host: an OAuth resource server in the sense of
3
3
  * RFC 6750 (bearer challenges) and RFC 9728 (protected-resource metadata).
4
4
  *
5
- * The host is reached through its PUBLIC URL, usually behind a reverse proxy, so every name the gate
6
- * checks comes from that URL rather than from the listener: the endpoint path and the metadata path
7
- * (a proxy prefix therefore works when the proxy forwards the path unchanged), and the Host and
8
- * Origin a request must carry (the proxy must preserve Host). The listener's own address is never a
9
- * name a client uses.
5
+ * The name checks, challenges, metadata and failure throttle are the shared resource-server gate of
6
+ * `@robota-sdk/agent-transport/node`; this host adds only its one endpoint and its verifier. The MCP
7
+ * endpoint is the public URL's own path, so the public URL is also the resource identifier (`aud`).
10
8
  *
11
- * Refusals answer with an empty body and, for a token problem, only the standard challenge: an error
12
- * code from RFC 6750 and where the metadata lives. Nothing describes WHY in prose, so a refusal
13
- * cannot leak which check failed beyond the code a client is entitled to.
14
- *
15
- * The token is verified before anything is counted, and only failures are counted, so a peer holding
16
- * a valid token is never throttled — however many failures share its address. Each failure is also
9
+ * The token is verified before anything is counted, and only failures are counted. Each failure is
17
10
  * handed to an audit sink as a closed-set reason and a coarse address class, never token text.
18
11
  */
19
12
 
20
- import { BlockList, isIP } from 'node:net';
21
-
22
- import { bearerCredential } from '@robota-sdk/agent-transport/node';
13
+ import {
14
+ bearerCredential,
15
+ createBearerResourceServer,
16
+ describeProtectedResource,
17
+ refuseBearerToken,
18
+ serveProtectedResourceMetadata,
19
+ } from '@robota-sdk/agent-transport/node';
23
20
 
24
21
  import type {
25
22
  IAccessTokenVerifier,
26
23
  TAccessTokenRefusal,
27
24
  } from '@robota-sdk/agent-interface-transport';
25
+ import type { TRemoteAddressClass } from '@robota-sdk/agent-transport/node';
28
26
  import type { IncomingMessage, ServerResponse } from 'node:http';
29
27
 
30
- const WELL_KNOWN_PREFIX = '/.well-known/oauth-protected-resource';
31
- /** Window over which one address's failures are counted. */
32
- const FAILURE_WINDOW_MS = 60_000;
33
- /** Failures one address may have in a window before its further failures answer 429. */
34
- const MAX_FAILURES_PER_WINDOW = 20;
35
- /** Addresses tracked at once; the oldest is forgotten first, so a spray cannot grow memory. */
36
- const MAX_TRACKED_ADDRESSES = 4096;
37
- /** RFC 6749 scope-token characters: printable ASCII except space, `"` and `\`. */
38
- const SCOPE_TOKEN = /^[\x21\x23-\x5b\x5d-\x7e]+$/;
28
+ const LABEL = 'MCP HTTP';
39
29
 
40
30
  /** Why a remote request was refused: a verifier refusal, or no bearer token at all. */
41
31
  export type TMcpRemoteRefusal = TAccessTokenRefusal | 'missing-token';
42
32
 
43
33
  /** A coarse class of the refused peer's address; the address itself is never reported. */
44
- export type TMcpRemoteAddressClass = 'loopback' | 'private' | 'public' | 'unknown';
34
+ export type TMcpRemoteAddressClass = TRemoteAddressClass;
45
35
 
46
36
  /** One refused request, as the audit sink sees it. Carries no token text and no claim value. */
47
37
  export interface IMcpRemoteAuditRecord {
@@ -73,86 +63,6 @@ export interface IMcpRemoteGate {
73
63
  admit(req: IncomingMessage, res: ServerResponse): Promise<boolean>;
74
64
  }
75
65
 
76
- const LOOPBACK_RANGES = new BlockList();
77
- LOOPBACK_RANGES.addSubnet('127.0.0.0', 8, 'ipv4');
78
- LOOPBACK_RANGES.addAddress('::1', 'ipv6');
79
- const PRIVATE_RANGES = new BlockList();
80
- PRIVATE_RANGES.addSubnet('10.0.0.0', 8, 'ipv4');
81
- PRIVATE_RANGES.addSubnet('172.16.0.0', 12, 'ipv4');
82
- PRIVATE_RANGES.addSubnet('192.168.0.0', 16, 'ipv4');
83
- PRIVATE_RANGES.addSubnet('169.254.0.0', 16, 'ipv4');
84
- PRIVATE_RANGES.addSubnet('100.64.0.0', 10, 'ipv4');
85
- PRIVATE_RANGES.addSubnet('fc00::', 7, 'ipv6');
86
- PRIVATE_RANGES.addSubnet('fe80::', 10, 'ipv6');
87
-
88
- /** A literal address in one spelling: IPv4-mapped IPv6 folded to IPv4, IPv6 lowercased. */
89
- function normalizeAddress(value: string | undefined): string | undefined {
90
- if (value === undefined) return undefined;
91
- const text = value.trim().toLowerCase();
92
- const mapped = text.startsWith('::ffff:') ? text.slice('::ffff:'.length) : undefined;
93
- if (mapped !== undefined && isIP(mapped) === 4) return mapped;
94
- return isIP(text) === 0 ? undefined : text;
95
- }
96
-
97
- function classifyAddress(address: string | undefined): TMcpRemoteAddressClass {
98
- if (address === undefined) return 'unknown';
99
- const family = isIP(address) === 4 ? 'ipv4' : 'ipv6';
100
- if (LOOPBACK_RANGES.check(address, family)) return 'loopback';
101
- if (PRIVATE_RANGES.check(address, family)) return 'private';
102
- return 'public';
103
- }
104
-
105
- function parsePublicUrl(publicUrl: string): URL {
106
- let url: URL;
107
- try {
108
- url = new URL(publicUrl);
109
- } catch {
110
- throw new Error('MCP HTTP public URL must be an absolute https URL');
111
- }
112
- if (url.protocol !== 'https:') throw new Error('MCP HTTP public URL must use https');
113
- if (url.username !== '' || url.password !== '' || url.search !== '' || url.hash !== '') {
114
- throw new Error('MCP HTTP public URL must not carry credentials, a query or a fragment');
115
- }
116
- return url;
117
- }
118
-
119
- interface IFailureWindow {
120
- count: number;
121
- startedAt: number;
122
- }
123
-
124
- /** Counts failures per address in fixed windows. Only failures are ever recorded here. */
125
- function createFailureCounter(now: () => number) {
126
- const windows = new Map<string, IFailureWindow>();
127
- return {
128
- /** Records one failure; answers whether the address is now over budget, and for how long. */
129
- fail(address: string): { throttled: boolean; retryAfterSeconds: number } {
130
- const at = now();
131
- let entry = windows.get(address);
132
- if (entry === undefined || at - entry.startedAt >= FAILURE_WINDOW_MS) {
133
- windows.delete(address);
134
- if (windows.size >= MAX_TRACKED_ADDRESSES) {
135
- for (const [key, value] of windows) {
136
- if (at - value.startedAt >= FAILURE_WINDOW_MS) windows.delete(key);
137
- }
138
- const oldest = windows.keys().next();
139
- if (windows.size >= MAX_TRACKED_ADDRESSES && oldest.done !== true) {
140
- windows.delete(oldest.value);
141
- }
142
- }
143
- entry = { count: 0, startedAt: at };
144
- windows.set(address, entry);
145
- }
146
- entry.count += 1;
147
- const remainingMs = FAILURE_WINDOW_MS - (at - entry.startedAt);
148
- return {
149
- throttled: entry.count > MAX_FAILURES_PER_WINDOW,
150
- retryAfterSeconds: Math.max(1, Math.ceil(remainingMs / 1000)),
151
- };
152
- },
153
- };
154
- }
155
-
156
66
  /**
157
67
  * Builds the gate. Throws when the configuration cannot describe a safe resource server: a public
158
68
  * URL that is not plain `https`, a non-`https` issuer, no scope or a scope a challenge cannot quote,
@@ -162,104 +72,45 @@ export function createMcpRemoteGate(
162
72
  authorization: IMcpRemoteAuthorization,
163
73
  now: () => number = () => performance.now(),
164
74
  ): IMcpRemoteGate {
165
- const url = parsePublicUrl(authorization.publicUrl);
166
- let issuer: URL;
167
- try {
168
- issuer = new URL(authorization.issuer);
169
- } catch {
170
- throw new Error('MCP HTTP authorization issuer must be an absolute https URL');
171
- }
172
- if (issuer.protocol !== 'https:') throw new Error('MCP HTTP authorization issuer must use https');
173
- if (
174
- authorization.scopes.length === 0 ||
175
- !authorization.scopes.every((scope) => SCOPE_TOKEN.test(scope))
176
- ) {
177
- throw new Error('MCP HTTP authorization needs at least one valid scope');
178
- }
179
- const trustedProxies = new Set<string>();
180
- for (const proxy of authorization.trustedProxies ?? []) {
181
- const address = normalizeAddress(proxy);
182
- if (address === undefined)
183
- throw new Error('MCP HTTP trusted proxy must be a literal IP address');
184
- trustedProxies.add(address);
185
- }
186
-
187
- const endpointPath = url.pathname;
188
- // RFC 9728 §3.1: the well-known segment goes between the host and the resource's path, with a
189
- // lone terminating slash dropped.
190
- const wellKnownPath = `${WELL_KNOWN_PREFIX}${endpointPath === '/' ? '' : endpointPath}`;
191
- const resourceMetadataUrl = `${url.origin}${wellKnownPath}`;
192
- const expectedHosts = new Set([url.host]);
193
- if (url.port === '') expectedHosts.add(`${url.hostname}:443`);
194
- const metadata = JSON.stringify({
75
+ const resource = describeProtectedResource({
195
76
  resource: authorization.publicUrl,
196
- authorization_servers: [authorization.issuer],
197
- scopes_supported: [...authorization.scopes],
198
- bearer_methods_supported: ['header'],
77
+ issuer: authorization.issuer,
78
+ scopes: authorization.scopes,
79
+ label: LABEL,
199
80
  });
200
- const scopeList = authorization.scopes.join(' ');
201
- const challenges = {
202
- missing: `Bearer resource_metadata="${resourceMetadataUrl}"`,
203
- invalid: `Bearer error="invalid_token", resource_metadata="${resourceMetadataUrl}"`,
204
- scope: `Bearer error="insufficient_scope", scope="${scopeList}", resource_metadata="${resourceMetadataUrl}"`,
205
- };
206
- const failures = createFailureCounter(now);
207
-
208
- function remoteAddress(req: IncomingMessage): string | undefined {
209
- const peer = normalizeAddress(req.socket.remoteAddress);
210
- if (peer === undefined || !trustedProxies.has(peer)) return peer;
211
- const header = req.headers['x-forwarded-for'];
212
- const hops = (Array.isArray(header) ? header.join(',') : (header ?? '')).split(',');
213
- // Walk from the proxy inward: the nearest hop not itself a trusted proxy is the client, as the
214
- // trusted proxy saw it. Hops further left were written by the client and are not believed.
215
- for (let index = hops.length - 1; index >= 0; index -= 1) {
216
- const hop = normalizeAddress(hops[index]);
217
- if (hop === undefined) return peer;
218
- if (!trustedProxies.has(hop)) return hop;
219
- }
220
- return peer;
221
- }
81
+ const server = createBearerResourceServer({
82
+ publicUrl: authorization.publicUrl,
83
+ ...(authorization.trustedProxies !== undefined
84
+ ? { trustedProxies: authorization.trustedProxies }
85
+ : {}),
86
+ label: LABEL,
87
+ now,
88
+ });
89
+ const endpointPath = server.url.pathname;
222
90
 
223
91
  function refuse(req: IncomingMessage, res: ServerResponse, refusal: TMcpRemoteRefusal): void {
224
- const address = remoteAddress(req);
225
- const remote = classifyAddress(address);
226
92
  if (refusal === 'keys-unavailable') {
227
93
  // The issuer, not the peer, failed: not counted against the peer, and not a token verdict.
228
- authorization.audit?.({ refusal, remote, throttled: false });
94
+ authorization.audit?.({ refusal, remote: server.remote(req), throttled: false });
229
95
  res.writeHead(503).end();
230
96
  return;
231
97
  }
232
- const budget = failures.fail(address ?? 'unknown');
233
- authorization.audit?.({ refusal, remote, throttled: budget.throttled });
234
- if (budget.throttled) {
235
- res.writeHead(429, { 'Retry-After': String(budget.retryAfterSeconds) }).end();
236
- } else if (refusal === 'missing-scope') {
237
- res.writeHead(403, { 'WWW-Authenticate': challenges.scope }).end();
238
- } else {
239
- const challenge = refusal === 'missing-token' ? challenges.missing : challenges.invalid;
240
- res.writeHead(401, { 'WWW-Authenticate': challenge }).end();
241
- }
98
+ const failure = server.fail(req);
99
+ authorization.audit?.({ refusal, remote: failure.remote, throttled: failure.throttled });
100
+ refuseBearerToken(
101
+ res,
102
+ resource,
103
+ refusal === 'missing-token' || refusal === 'missing-scope' ? refusal : 'invalid-token',
104
+ failure,
105
+ );
242
106
  }
243
107
 
244
108
  return {
245
109
  endpointPath,
246
110
  async admit(req, res) {
247
- const host = req.headers.host?.toLowerCase();
248
- const origin = req.headers.origin;
249
- if (
250
- host === undefined ||
251
- !expectedHosts.has(host) ||
252
- (origin !== undefined && origin !== url.origin)
253
- ) {
254
- res.writeHead(403).end();
255
- return false;
256
- }
257
- if (req.url === wellKnownPath) {
258
- if (req.method !== 'GET') {
259
- res.writeHead(405, { Allow: 'GET' }).end();
260
- return false;
261
- }
262
- res.writeHead(200, { 'Content-Type': 'application/json' }).end(metadata);
111
+ if (!server.checkNames(req, res)) return false;
112
+ if (req.url === resource.wellKnownPath) {
113
+ serveProtectedResourceMetadata(req, res, resource);
263
114
  return false;
264
115
  }
265
116
  if (req.url !== endpointPath) {
@@ -59,7 +59,7 @@ function isJsonValue(value: unknown): boolean {
59
59
  if (value === null || typeof value === 'string' || typeof value === 'boolean') return true;
60
60
  if (typeof value === 'number') return Number.isFinite(value);
61
61
  if (Array.isArray(value)) return value.every(isJsonValue);
62
- return typeof value === 'object' && value !== null && Object.values(value).every(isJsonValue);
62
+ return typeof value === 'object' && Object.values(value).every(isJsonValue);
63
63
  }
64
64
 
65
65
  function isRuntimeParameters(