Repository navigation
MCP auto-exposure: single module + opt-in flag - #2059
Conversation
…e + opt-in flag) Consolidates the exploration in PRs #2002 and #2053 into one tracked module. A resources server sets expose_tools_over_mcp = True and its existing FastAPI POST /<tool> routes are served over an MCP /mcp endpoint — no decorators, no handler changes. run_webserver calls maybe_auto_expose(server, app) after the app is built, so exposure is automatic (no user-side install call). nemo_gym/mcp_auto_exposure.py (new, one file): the detector (route-signature bind + server middleware audit), direct dispatch (run the frozen handler once via a fabricated Request whose .session is materialized directly — no second app pass, public API only), a replay fallback for shapes/servers direct dispatch can't prove equivalent, route harvest, signed session token, the /seed_session wrap, and the /mcp mount. Merges the two prototype files, drops all dead code (hybrid_call, the duplicate route walk, the route.body_field read — schema now comes from the same signature resolution that decides dispatch). Framework hooks: +1 line (the opt-in flag on SimpleResourcesServer) and +3 lines (the maybe_auto_expose call in run_webserver). No tool files changed: git diff origin/main -- resources_servers/ is empty. Verified: acceptance suite 44/44 against pristine origin/main handlers; all in-tree tool routes dispatch direct, zero replay, live-verified for finance, workplace, aviary, newton_bench, openenv, and ns_tools (incl. the factory- __signature__ and dict-body and raw-body/PlainTextResponse shapes). Signed-off-by: Codex <adasif@nvidia.com> Signed-off-by: Codex <codex@openai.com>
Every in-tree tool route dispatches direct (verified across all 97 servers), so the replay fallback had zero runtime users. For an opt-in flag the consistent design is to fail loud: if a server installs custom middleware or a handler uses a shape direct dispatch cannot reproduce (FastAPI DI, multiple body models, ...), exposure now raises at startup naming the route and reason, rather than silently degrading to a second-pass HTTP replay. Deletes _replay, mint_session_cookie, the replay branch + caches, and the MCPTool replay fields (mode/replay_path/raw_path/reasons). ~90 lines removed; module 692 -> 600 lines. Verified: acceptance suite 44/44; custom-middleware and Depends handlers refuse loudly at startup; finance/workplace/aviary/ newton/openenv still expose direct. Signed-off-by: Codex <adasif@nvidia.com> Signed-off-by: Codex <codex@openai.com>
Self-contained pytest (tests/unit_tests/test_mcp_auto_exposure.py, 11 tests, ~1.5s, no external deps): flag gate + /mcp mount, tools/list schema harvest, direct dispatch with HTTP-door/MCP session sharing, dict-body and raw-body/ PlainTextResponse dispatch, allowed_tools filter/gate, error mapping (unknown/missing-token/invalid-token/422), the refusal paths (custom middleware, Depends), and the factory-__signature__ resolution regression. gitignore prototypes/ — the live-uvicorn demo harness is kept locally for manual runs but is not part of the PR (the pytest is the CI test story). Signed-off-by: Codex <adasif@nvidia.com> Signed-off-by: Codex <codex@openai.com>
…ignored) The live-uvicorn acceptance harness was useful for exploration; the CI test story is now tests/unit_tests/test_mcp_auto_exposure.py. Files remain on disk for local manual runs but are untracked + gitignored. Signed-off-by: Codex <adasif@nvidia.com> Signed-off-by: Codex <codex@openai.com>
MCP-native agents (e.g. Claude Code) record trajectory tool calls as mcp__<server>__<tool>, while verifiers compare names against their bare tool vocabulary — so MCP-driven rollouts scored 0.0 on otherwise perfect trajectories. The /verify route now strips this server's own MCP prefix from function_call items before the subclass verify runs, so rollouts score identically on both transports with no per-server changes. Found by a live gym eval run of claude_code_agent against the auto-exposed workplace_assistant (5/5 reward 1.0 after; trajectories identical). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Codex <adasif@nvidia.com>
…eserve provenance Addresses three review findings on the verify-side normalization added in the prior commit: - Gate the normalization on expose_tools_over_mcp (was unconditional for every SimpleResourcesServer). HTTP-only benchmarks now get byte-identical verify and keep valid baselines; only servers that actually expose tools over MCP — the only ones that can receive mcp__<server>__ names — normalize. (F52, F51) - Stop mutating the parsed verify request in place. Score against a deep copy, then restore the names the model emitted in the echoed response (matched by call_id), so persisted rollout artifacts keep the real tool names and transport provenance instead of laundered bare names. (F3, F35) Verified: unit 13/13 (adds a flag-off no-op test and a provenance-preserved assertion); base_resources_server + server_utils suites 26/26; gym-native e2e — MCP door 5/5 reward 1.0 with persisted names still namespaced, HTTP door unaffected (bare-name normalization is identity). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Codex <adasif@nvidia.com>
Review pass (7 specialized reviewers + devil's-advocate convergence, 59 confirmed findings) followed by a fix workflow with adversarial verification. Highlights: Detector/dispatch correctness (silently-wrong shapes now correct or refused): - Optional[Model] bodies bind as body params instead of dropping arguments - dict[str, Any] accepted like dict; ambiguous unions refuse loudly - handlers reading await request.json() alongside a body model get the real bytes - sync (def) handlers run via run_in_threadpool, matching FastAPI (no event-loop stall) - response_model (decorator kwarg) wins for return filtering; results are always dump-then-validate filtered, matching the plain HTTP route - fabricated Request carries a synthesized session cookie - unexpected handler exceptions log a server-side traceback and map to the same repr(e) text as the HTTP exception middleware Refuse-loudly guards (were silent misconfigurations): - dispatcher opting in without mcp_tool_inventory(): startup ValueError (was a warning) - multiple candidate catch-alls: inventory items must name their route - inventory names checked against RESERVED_MCP_TOOL_NAMES; tool names must match ^[A-Za-z0-9_-]+$ (nested routes refuse) - app already serving /mcp (MCPResourcesServer overlap): refuse instead of shadowing - missing /seed_session: clear error instead of StopIteration Tokens/session: - claims cache deleted (unbounded growth); token verified per call - URLSafeTimedSerializer with 24h max_age; constants imported from base_resources_server (single source of truth); tokenless tools/list respects the install-time allowed_tools floor Contracts/plumbing: - mcp_tool_inventory() / mcp_toolless_catchall_paths declared on SimpleResourcesServer - mint_metadata builds MCPServerMetadata; run_webserver imports the MCP module only for opted-in servers - docstring accuracy audit: 77 reviewed, 3 stale corrected Tests: 13 -> 43 (all refusals, parity shapes, token paths, provenance). E2E re-verified: both doors 5/5 reward 1.0, MCP trajectories keep namespaced names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Codex <adasif@nvidia.com>
Remove or tighten 21 comments added during the hardening pass that restated adjacent code, narrated the change, or argued correctness to a reviewer (classified and unanimously approved by a cold-eyes verification pass; 76 load-bearing constraint/why comments kept). Also wraps the surviving trimmed comments to the 119-column limit and applies ruff format. No logic changes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Codex <adasif@nvidia.com>
…iddleware guard Three engine-side changes from the review follow-up queue: - Verify-name normalization now wraps the /verify route's current endpoint at install time (same in-place swap as the seed_session wrap), so a flag-on server that strips and re-registers /verify cannot bypass it; a flag-on app without /verify refuses at install. Flag-off servers register plain self.verify — byte-identical to main. - mcp_excluded_paths ClassVar: routes listed there are never advertised, callable, or shape-checked over MCP while staying untouched over HTTP; unknown declarations refuse at install. - setup_session_middleware is idempotent; double-install previously stacked SessionMiddleware twice with no test able to catch it. Tests 43 -> 75 across the three suites; e2e both doors re-verified 5/5 reward 1.0 (one HTTP task flaked to 0.0 on first run and re-ran 1.0 — policy variance, 12-call trajectory). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Codex <adasif@nvidia.com>
…d-tools hook Folds the enablement (previously planned as a stacked PR) into this branch so everything is e2e-testable from the tree with yaml-only configs: - gymnasium/base.py builds its app from super().setup_webserver(), gaining /seed_session and /verify like every other resources server (flag stays off for the gymnasium family; /verify moves from 404 to 422/500 if probed) - mcp_allowed_tools_for_session(seed_body) on SimpleResourcesServer: servers can restrict a rollout's MCP token to the task's equipped tools; the seed wrap mints the claim, intersecting any install-time floor; a raising hook fails the seed request - flag + mcp_tool_inventory(): workplace_assistant (shared TOOLKITS constant, 27 tools), ns_tools (schemas captured from ToolManager at startup), math_advanced_calculations (_function_map), indirect_prompt_injection (TOOL_HANDLERS + per-task restriction via the new hook) - flag + toolless declaration: finance_sec_search - flag only: browsecomp_advanced_harness, circle_click, example_multi_step, example_session_state_mgmt, example_single_tool_call, google_search, litmus_agent, math_with_code, newton_bench, openenv, tavily_search (math_with_code and newton_bench exclude /end_session) - aviary and genrm_compare deliberately not enabled: aviary's /step//close carry a cross-rollout env_id; genrm's only route is a harness-side batch API Tests 75 -> 79 (hook coverage); ruff + py_compile clean on all touched files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Codex <adasif@nvidia.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Codex <adasif@nvidia.com>
Enablement is now operator-side: a server opts into MCP by setting expose_tools_over_mcp: true in its (YAML) config, not by a class attribute. - BaseResourcesServerConfig gains expose_tools_over_mcp: bool = False; the ClassVar is removed from SimpleResourcesServer. Both gates (run_webserver, maybe_auto_expose) read it from server.config defensively, so agents and model servers (which lack the field) resolve to False. - The 9 flag-only servers revert to byte-identical origin/main — a typed-route server needs no code change at all to be MCP-exposable. Servers whose tools live behind one catch-all keep only their mcp_tool_inventory() (irreducibly code); math_with_code/newton_bench keep mcp_excluded_paths; finance keeps its toolless declaration; IPI keeps its per-task allowed-tools hook. Proven end to end: example_single_tool_call (unchanged .py) and workplace_assistant (inventory only) both expose over MCP and pass claude_code_agent rollouts driven purely by a compose yaml. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Codex <adasif@nvidia.com>
Per review (@bxyu-nvidia): we can't properly test the individual envs, so this PR ships only the auto-exposure engine. Reverted all resources_servers/ changes and the .gitignore additions to origin/main; enablement is documented for users instead of shipped. Also added the reason (server_utils.py:681) that the MCP SDK import is gated to resources servers — agents/models never expose tools over MCP. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Codex <adasif@nvidia.com>
Per review scope discipline: the token expiry (URLSafeTimedSerializer + 24h
max_age) was added for a security review finding, not driven by any env. The
signing secret is derived from public names anyway, so a bounded lifetime on a
forgeable token buys little; hardening the secret is the real (separate) fix.
Token now uses URLSafeSerializer. The {sid, tools} payload stays — the
per-rollout tool restriction it carries is validated by indirect_prompt_injection
(a benchmark whose per-task toolset must not leak across domains over MCP's
static tools/list).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Codex <adasif@nvidia.com>
…) override Per review (@bxyu-nvidia): rather than mcp_tool_inventory() + mcp_toolless_catchall_paths + mcp_excluded_paths — which grow core infra per env — one overridable function returns the tools to register: def mcp_tools(self, harvested, catchall) -> list[MCPTool] | None: return harvested # default; override to filter / add / disable harvested = auto-harvested typed POST routes; catchall (if the server has one parameterized catch-all) builds tools that dispatch through it. Exclusion = filter harvested; inventory = harvested + [catchall.tool(...)]; toolless = ignore the catchall (now a soft warning, not a hard refuse). Reserved-name, tool-name charset, and duplicate checks stay as loud raises on the final list. Also: Optional[Model] bodies now refuse at startup instead of carrying speculative unwrap support — no in-tree tool uses that shape, and refusing keeps the never-silently-wrong contract. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Codex <adasif@nvidia.com>
…nners, flatten installer The module was too dense to safely modify. Readability-only, no behavior change: - A 'How to read this file' block up top makes the two timelines explicit (STARTUP: harvest+bind+wrap+mount; PER-CALL: session_claims -> call_direct) and gives a reading order. - Section banners relabeled by timeline + action; one-line role docstrings on DirectBinding / BindOutcome / MCPTool / _CatchAll. - install_auto_exposure flattened: its nested closures (mint_metadata, session-token decode, effective_allowed, _to_result) lifted to module-level helpers taking explicit args, so the installer reads top-to-bottom. Behaviour-equivalence verified three ways: 75 unit tests pass; a cold-eyes diff review confirmed each lifted helper is byte-identical and receives exactly what its closure captured; and a live claude_code_agent + workplace_assistant rollout over MCP scored 5/5 reward 1.0 with tool-name provenance intact. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Codex <adasif@nvidia.com>
…list, dead field, re-bind Four simplifications, each adversarially verified to have no consumer or be pure dedup before cutting (978 -> 912 lines): - _session_cookie_header and its call_direct wiring: no tool handler in the repo forwards request.cookies; handlers get their session via scope["session"], which is untouched. - the install-time allowed_tools floor (install_auto_exposure param, allowed_floor, _effective_allowed): the production path never passes it, so it was dead at runtime. The per-session restriction via mcp_allowed_tools_for_session — which a real benchmark needs — is intact and its tests unchanged. - DirectBinding.defaulted_params: written, never read; Python applies query defaults itself. The behavior test for defaulted params stays. - _validate_tools no longer re-calls bind_route to regenerate refusal reasons; they are stashed on MCPTool at harvest (route field dropped, path/reasons added). Same refusal messages. Verified: 72 unit tests pass; per-cut cold-eyes equivalence review; live claude_code_agent + workplace rollout over MCP at 5/5 reward 1.0. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Codex <adasif@nvidia.com>
Eleven changes from a reader-round + verification-round hunt, each
adversarially confirmed no-consumer or behavior-identical:
Structural: verify wrapper is kwargs-only (FastAPI invokes endpoints with
keywords); the duplicated find/swap route plumbing became _take_route/_swap_route;
BindOutcome dataclass replaced by a plain tuple with the tool schema derived
from the binding; _make_receive, audit_middleware, _schema_for, and BASIC_PATHS
inlined at their single call sites; _to_result collapsed; oversized comments
trimmed (reading guide untouched).
Behavioral (all consumer-checked): defaulted query params now refuse at
startup instead of silently dispatching with defaults (no in-tree handler has
one); the seed_session injected Request param uses a fixed non-colliding name;
a raising mcp_allowed_tools_for_session propagates (still fails the seed
request, fail-closed); the two union-body refusal messages merged; the session
token is always {"sid", "tools"} (tools None = unrestricted).
Verified: 71 unit tests pass; per-cluster cold-eyes equivalence review; live
both-doors workplace e2e 5/5 reward 1.0 each (MCP provenance namespaced, HTTP
bare).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Codex <adasif@nvidia.com>
|
Function map of flowchart TB
subgraph STARTUP["STARTUP — runs once, when a flag-on server boots (read 1 to 6)"]
MAE["1. maybe_auto_expose<br/>gate: no-op unless the server config opts in<br/><i>why: exposure is an operator choice, never implicit</i>"]
IAE["2. install_auto_exposure<br/>orchestrator: refuse if /mcp is already served, then signer, harvest, endpoint wraps, mcp mount<br/><i>why: runs after the app is fully built, so every route is present</i>"]
HT["3. harvest_tools<br/>walk POST routes, bind each, ask server.mcp_tools for the final list<br/><i>why: tools come from routes authors already wrote</i>"]
BR["3a. bind_route — per route<br/>detector: handler shape to a DirectBinding, or refusal reasons<br/><i>why: dispatch must be provably HTTP-equivalent or refuse at boot</i>"]
CA["3b. _CatchAll.tool — dispatcher servers only<br/>build a named tool that dispatches through the one catch-all route<br/><i>why: dispatcher servers keep per-tool schemas in data, not routes</i>"]
VT["3c. _validate_tools<br/>final checks: name charset, reserved names, duplicates, bindability<br/><i>why: a bad advertisement corrupts rollouts silently — fail at boot</i>"]
WSS["4. _wrap_seed_session<br/>swap the seed_session endpoint so its response gains the mcp key<br/><i>why: hands each rollout its mcp URL plus a signed session token</i>"]
WV["5. _wrap_verify<br/>swap the verify endpoint: normalize mcp-namespaced tool names, scoring only<br/><i>why: MCP agents record namespaced names; verifiers know bare ones</i>"]
TR2["4+5 use: _take_route and _swap_route<br/>find-or-refuse a route, swap its endpoint in place keeping order<br/><i>why: order matters — a catch-all would otherwise shadow the swap</i>"]
MOUNT["6. mcp mount<br/>SDK low-level Server plus StreamableHTTPSessionManager, Route + Mount pair at the front of the route table<br/><i>why: official protocol layer; front position so catch-alls cannot shadow it</i>"]
end
subgraph PERCALL["PER-CALL — seed flow (S1) then MCP flow (M1 to M4) per request"]
MSM["S1. _mint_session_metadata — on each POST seed_session<br/>sign sid plus allowed tools for this rollout<br/><i>why: the mcp endpoint is stateless — the token is the rollout identity</i>"]
LT["M1. list_tools — on tools/list<br/>advertise harvested tools, filtered by the token claim"]
CT["M1. call_tool — on tools/call<br/>resolve the session from the token, dispatch, map failures to isError"]
PST["M2. _parse_session_token<br/>verify the signature, yield sid and allowed tools"]
CD["M3. call_direct<br/>run the route's own handler once via a fabricated Request with the session in scope<br/><i>why: no second app pass; handlers stay byte-identical</i>"]
DDE["M3-fail. DirectDispatchError<br/>handler failure carrying the same text the HTTP route returns"]
TOR["M4. _to_result<br/>shape handler output into SDK content"]
end
MAE --> IAE
IAE --> HT
HT -->|per route| BR
HT -->|catch-all servers| CA
CA -->|first tool: bind once, cache| BR
HT --> VT
IAE --> WSS
IAE --> WV
WSS --> TR2
WV --> TR2
IAE --> MOUNT
WSS -. per seed_session .-> MSM
MOUNT -. registers .-> LT
MOUNT -. registers .-> CT
LT -->|via session_claims| PST
CT -->|via session_claims| PST
CT --> CD
CD -->|failure| DDE
CT --> TOR
Data carriers not shown as nodes: |
…to-exposure The defaulted-param refusal from the simplification pass had silently not survived the implementing agent's session — an independent verification pass caught the acceptance branch still in place. Re-applied: a non-DI defaulted param now refuses at startup naming the route; the two tests flipped to refusal assertions. Docs: the MCP tutorial page gains an auto-exposure section — yaml-only opt-in, the mcp_tools(harvested, catchall) override with workplace_assistant as the worked example (user-side app_mcp.py subclass; resources_servers ships unchanged), per-rollout restriction via mcp_allowed_tools_for_session, and a complete run-with-Claude-Code example (Anthropic API by default, key redacted via env.yaml placeholder). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Codex <adasif@nvidia.com>
|
🌿 Preview your docs: https://nvidia-preview-mcp-auto-exposure.docs.buildwithfern.com/nemo/gym Here are the markdown pages you've updated: |
- defaulted handler-signature params refuse at startup (doc said they were accepted; contradicted the code and tests in the same commit) - 'required' qualifier dropped from the non-body-param refusal bullet - mcp_tools() returning None/[] exposes no tools but does not disable exposure: /mcp stays mounted and /seed_session still carries the mcp key - three-ways table: the Gym-owned row's 'When' now states the real discriminator (MCP-first server, no HTTP routes to reuse) instead of a criterion equally true of auto-exposure Signed-off-by: Codex <adasif@nvidia.com>
…CP mechanism PR #2059 supersedes the decorator-based MCP API from #1682: tools are the plain POST routes authors already write, exposed over MCP by the expose_tools_over_mcp config flag, with no decorators and no signature changes. Removed from the user-facing API: - @gym_tool, MCPResourcesServer, MCPSessionError, the token contextvar, and the header middleware (base_resources_server.py, 357 -> 151 lines) - their unit tests (test_base_resources_server.py) Kept: the wire contract the engine reuses (MCPServerMetadata, the X-NeMo-Gym-Session-Token header, the token salt, RESERVED_MCP_TOOL_NAMES). example_mcp_weather is rewritten as a plain SimpleResourcesServer with a typed POST /get_weather route and expose_tools_over_mcp: true in its yaml - the in-tree example of the new mechanism. Its tests now cover both doors: plain HTTP with the session cookie, and MCP tools/call through the real engine mount (plus a missing-token rejection test). Docs: the tutorial page now teaches auto-exposure as the Gym-owned flow (two integration shapes, not three); the external-server section and the workplace_assistant catch-all example are unchanged. Signed-off-by: Codex <adasif@nvidia.com>
- DNS-rebinding note (docs + engine comment): the loopback-only allowlist is FastMCP auto-config; the low-level manager path Gym uses has no such default, and an enabled default allowlist accepts no hosts at all - tutorial listing: verify body param was unannotated (FastAPI would read it as a query param if copied); the verify request model is now shown - accepted-shapes paragraph: dict bodies are supported and now listed - workplace app_mcp.py snippet now says the reader must create the file Signed-off-by: Codex <adasif@nvidia.com>
The agent-wiring section now leads into the complete workplace_assistant run (catch-all override, app_mcp.py, compose yaml, eval command); the weather server keeps the implementation tutorial and gets a short same-wiring run note. Verified live: the example's exact commands score 5/5 reward 1.0 with mcp__workplace_assistant__* tool names. Signed-off-by: Codex <adasif@nvidia.com>
- the weather run now shows the concrete gym eval command (the --agent name was discoverable only by opening the config yaml) - the generated-config JSON's workplace_assistant key is introduced in its lead-in instead of appearing before the example is named - the weather cross-reference uses an explicit anchor link (the old 'end of this section' pointed at its own last line) - 'hidden MCP metadata' -> 'MCP metadata' (nothing is hidden; it is a plain key in the seed response) - 'never required to be dispatchable' -> plain wording tied to the startup shape checks the page already introduced - missing --- rule restored before the workplace section - 27-tool count no longer attributed to the five toolkits alone (the company-directory lookup is always included) Signed-off-by: Codex <adasif@nvidia.com>
…crets
The literal {"secret": "leak"} exists to prove undeclared fields are
stripped by response_model on the MCP dispatch path; detect-secrets'
Secret Keyword plugin flagged it. Verified with CI's exact scan command
that the full PR diff is now clean.
Signed-off-by: Codex <adasif@nvidia.com>
Reviewer UX ask: state roughly which function shapes are supported. The refusal bullets required knowing FastAPI's parameter classification (that a bare scalar like my_string: str is a query param); the section now leads with a signature-by-signature table — every row verified against bind_route — and names the query-string pattern behind the refusals explicitly. Signed-off-by: Codex <adasif@nvidia.com>
- the pattern sentence claimed the query string explains all five refuse rows; it explains exactly the two query-parameter rows (union bodies, multiple bodies, and Depends are unrelated) - scoped accordingly - the old 'non-body parameter' umbrella covered Header/Cookie markers, *args/**kwargs, and non-str path params, which the rewrite left uncategorized - restored as an explicit clause (each verified to refuse at startup with a named reason) Signed-off-by: Codex <adasif@nvidia.com>
| MCP-side engine: the official SDK's public low-level ``mcp.server.lowlevel.Server`` + | ||
| ``StreamableHTTPSessionManager`` — no private-attribute access. | ||
|
|
||
| How to read this file |
There was a problem hiding this comment.
no need to change now, but if things grow/mature we may want to move this into the docs explicitly 🤣
| self.detail = detail | ||
|
|
||
|
|
||
| async def call_direct( |
There was a problem hiding this comment.
i think there might be a better way to handle this since it looks like we're replicating a lot of Middleware functionality.
| app.router.routes[idx] = app.router.routes.pop() # in-place swap keeps ordering vs catch-all routes | ||
|
|
||
|
|
||
| def _wrap_seed_session(app: FastAPI, mint_metadata: Callable[[Request, dict], dict]) -> None: |
There was a problem hiding this comment.
i think we might be able to do this cleaner in a middleware as well
- the TOKEN_HEADER alias comment still justified itself by 'the two MCP mechanisms' — the second mechanism was removed by this PR; the real reason is that the header is wire contract shared with claude_code_agent - the reading guide now mentions install_auto_exposure's first action (refusing a pre-existing /mcp), matching the diagram fix - copyright year 2026 on the two files new to this PR Signed-off-by: Codex <adasif@nvidia.com>
mcp_tools is now list["MCPTool"] -> Optional[list["MCPTool"]] via a TYPE_CHECKING-guarded import (a runtime import would be circular and would pull the mcp SDK into agent/model processes); seed_body is dict[str, Any]; _validate_tools takes Optional[list[MCPTool]]. Verified at import time that base still loads without the SDK. Signed-off-by: Codex <adasif@nvidia.com>
Replaces the (binding, reasons, body_model) 3-tuple with a frozen dataclass so the fields are named at both call sites — in particular body_model, whose survives-a-failed-bind role is now self-documenting. Signed-off-by: Codex <adasif@nvidia.com>
> **Draft / RFC.** Consolidates the exploration in NVIDIA-NeMo#2002 and NVIDIA-NeMo#2053 into one tracked module + a small framework hook, **removes NVIDIA-NeMo#1682's decorator API** (`@gym_tool` / `MCPResourcesServer`) so there is exactly one user-facing MCP mechanism, then hardened by a full review pass (details below). Branched off `main`. ## What this is Serve a resources server's **existing** FastAPI tool routes over MCP with **zero handler changes** and **one opt-in config field** — no code change at all for a typed-route server: ```yaml resources_servers: my_server: entrypoint: app.py expose_tools_over_mcp: true # the only addition — set per instance in the config ``` `run_webserver` installs the `/mcp` endpoint automatically after the app is built (and imports the MCP SDK only for servers that opt in). Handlers keep their `request: Request` param and `request.session[...]` reads exactly as written. The only in-tree code a server ever needs is for shapes the config can't express, and it is one override: `mcp_tools(harvested, catchall)` — return the harvested typed routes (the default), filter one out to exclude a harness-only route (e.g. `/end_session`), or append catch-all-backed tools (`harvested + [catchall.tool(name, input_schema, description)]`) for dispatcher servers. A dropped route is never advertised, never callable over MCP, never shape-checked, and unchanged over HTTP. This PR also **removes the decorator-based MCP API** that NVIDIA-NeMo#1682 added (`@gym_tool`, `MCPResourcesServer`, `MCPSessionError`, the token contextvar and header middleware): tools no longer need decorators or MCP-specific signatures, so the old mechanism is superseded rather than parallel. The wire contract survives (`MCPServerMetadata`, the `X-NeMo-Gym-Session-Token` header, the token salt, the reserved-name set) — `claude_code_agent` on `main` consumes it unchanged. `example_mcp_weather` is rewritten onto auto-exposure: a plain typed `POST /get_weather` route plus the yaml flag, with tests driving the MCP door (and the wrapped `/seed_session` + `/verify`) through the real engine mount, and the plain-HTTP tool route on the stock app. ## How to review this (~30 min reading order) 1. `nemo_gym/mcp_auto_exposure.py` module docstring — the contract in one screen. 2. `bind_route` — the detector. The accept/refuse table below is what it enforces. 3. `call_direct` — dispatch parity with the plain HTTP route (threadpool for sync handlers, body bytes, session cookie, `response_model` filtering, error-text parity). 4. `harvest_tools` → `install_auto_exposure` — tool map, startup guards, session token, `/seed_session` wrap, `/mcp` mount. 5. `base_resources_server.py` diff — the `expose_tools_over_mcp` config field, the two overridables (`mcp_tools(harvested, catchall)` — what to expose; `mcp_allowed_tools_for_session(seed_body)` — per-rollout narrowing), and the removal of the NVIDIA-NeMo#1682 decorator API (the file drops from 315 lines on `main` to 150; it had grown to 356 on this branch before the removal commit). 6. `server_utils.py` — the 6-line activation point (plus a 4-line session-middleware idempotency guard). 7. `tests/unit_tests/test_mcp_auto_exposure.py` — 45 tests double as the spec; each guard and parity shape has a named test. ## What the detector accepts vs refuses **Accepts (dispatches identically to the plain HTTP route):** async and sync handlers (sync runs via `run_in_threadpool`, exactly like FastAPI); `body: Model`; `body: dict` / `dict[str, Any]`; raw-body catch-alls reading `await request.json()`; handlers taking both a body model *and* a `request` they read raw; `str` path params; `response_model=` filtering (decorator kwarg or return annotation). **Refuses loudly at startup (`ValueError` naming the route and reason):** non-Gym middleware; `Depends`/`Security`; `*args/**kwargs`; multiple body models; union/optional body params; defaulted query params; unresolvable annotations; tool names outside `^[A-Za-z0-9_-]+$` (e.g. nested routes); tool names (from `mcp_tools()`) colliding with `verify`/`seed_session`/`aggregate_metrics`/`mcp`; duplicate tool names; multiple parameterized catch-all routes (auto-exposure cannot tell which backs the tools); a server that already serves `/mcp` (a hand-rolled MCP mount would be shadowed); a missing `/seed_session` or `/verify`. These rules apply to route-handler signatures only — inner tool functions may use `**kwargs` freely, and no in-tree server trips any refusal. No silent fallback exists: a shape either dispatches provably like HTTP or the server does not start. One soft case: a dispatcher whose `mcp_tools()` override ignores its catch-all logs a warning (the catch-all-backed tools stay HTTP-only). ## Session & token `/seed_session` gains an additive `"mcp"` key (`MCPServerMetadata` — the same shape `claude_code_agent` on `main` already consumes): the `/mcp` URL plus a signed per-rollout token in `X-NeMo-Gym-Session-Token`. The token payload is always `{"sid", "tools"}` — the session id plus this rollout's allow-list from `mcp_allowed_tools_for_session(seed_body)` (`null` = unrestricted) — signed with `URLSafeSerializer` (untimed; expiry was cut as consumer-less scope). The header/salt/metadata constants live in `base_resources_server` — the wire contract retained from NVIDIA-NeMo#1682's scheme after its decorator API was removed. The token is verified on every call (no cache). `tools/list` works tokenless and advertises the full exposed set; a token carrying an allow-list narrows both `tools/list` and `tools/call` for that session. Known limitation, deliberately out of scope: the signing secret derivation (`class___config-name`) is pre-existing `main` behavior (the session-middleware secret) — hardening it is a separate change. ## Verify-time tool-name normalization (install-time, gated) MCP-native agents record trajectory tool calls namespaced (`mcp__<server>__<tool>`); verifiers know bare names — found live when MCP rollouts scored 0.0 on perfect trajectories. For **flag-on servers only**, installation wraps the `/verify` route's current endpoint (whatever handler it holds — servers that re-register `/verify` are covered by construction): names are normalized for scoring on a deep copy, then the model's original names are restored in the echoed response (matched by `call_id`). Flag-off servers — every existing benchmark — keep verify byte-for-byte, so no baselines move; persisted rollout artifacts always keep the names the model actually emitted. ## Diff shape (+2,136 / −631, 11 files) | Area | Change | |---|---| | `nemo_gym/mcp_auto_exposure.py` | **new, one file, 841 lines** — detector, direct dispatcher, harvest + guards, token, `/seed_session` + `/verify` wraps, `/mcp` mount | | `nemo_gym/base_resources_server.py` | +48/−207: adds the `expose_tools_over_mcp` config field, the `normalize_tool_name` helper, and the `mcp_tools()` / `mcp_allowed_tools_for_session()` overridables; **removes** `@gym_tool`, `MCPResourcesServer`, `MCPSessionError`, and the header middleware (their unit tests go too: `test_base_resources_server.py` −277) | | `nemo_gym/server_utils.py` | +10: `run_webserver` activates auto-exposure for opted-in servers (lazy MCP import); `setup_session_middleware` is now idempotent | | `tests/unit_tests/test_mcp_auto_exposure.py` | **new — 45 self-contained tests** (TestClient, synthetic servers, ~3 s); +37 in `test_server_utils.py`; docs section in `fern/.../mcp-resources-server.mdx` | | `resources_servers/` | `example_mcp_weather` rewritten onto auto-exposure (plain typed route + yaml flag; 6 tests: the MCP door — plus the wrapped `/seed_session` and `/verify` — through the real engine mount, and the plain-HTTP tool route on the stock app). **Every other server: byte-identical to `main`.** Enablement stays per-server follow-up: pure config for typed-route servers; a small `mcp_tools()` override for catch-all dispatchers. | MCP engine: the official SDK's **public low-level** `Server` — no private-attr access. ## Review provenance This branch already went through a 7-reviewer + devil's-advocate review pass (59 confirmed findings) followed by a fix pass with adversarial re-verification: all 55 code findings are fixed here with tests (silently-wrong detector shapes, refuse-loudly gaps, an unbounded token cache — the token is now verified on every call with no cache, and expiry was later cut as consumer-less scope — event-loop blocking for sync handlers, provenance-preserving verify normalization), and a docstring-accuracy audit (77 reviewed, 3 corrected). The remaining follow-ups are listed under "For discussion". ## Verified - **Reproducible from this PR:** `pytest tests/unit_tests/test_mcp_auto_exposure.py` → **45/45** (~3 s; covers every refusal, parity shape, token path, exclusion semantics, and the normalization gating/provenance — including a re-registered-`/verify` regression test). - **Live e2e at this HEAD (`3891dd65`):** `workplace_assistant` MCP door (`claude_code_agent`) — **5/5 `reward: 1.0`**, all 8 distinct tools namespaced; HTTP door (`simple_agent`) — **5/5** on the second run, 4/5 on the first (the failing rollout omitted the `status: "Lead"` search filter — a trajectory-verified policy flake, not an engine failure; the HTTP door runs the stock route code). `example_mcp_weather` (`claude_code_agent`) — **5/5 `reward: 1.0`** at the removal commit `4d6d569c`; the one commit since is docs plus a comment reword. Earlier demo (below): both doors 5/5 tasks `reward: 1.0`, ~30 s wall for 5 concurrent rollouts, MCP-door trajectories persist the namespaced names the model emitted. n=5 single runs of a stochastic policy — a smoke demonstration that both transports work end to end, not a statistical parity claim. - **Development-time only (local, gitignored — not reproducible from this PR):** a 44-check live acceptance harness exercising six representative in-tree servers (finance, workplace, aviary, newton_bench, openenv, ns_tools) against pristine `origin/main` handlers, covering the factory-`__signature__`, `dict`-body, and raw-body/`PlainTextResponse` shapes; 44/44 as of 2026-07-17 against a pre-refactor revision of the engine (development-time evidence; the harness predates the `mcp_tools()` API). Other in-tree servers were covered by a static route sweep, not this live harness. ## Runnable e2e — both doors through `gym eval run` The full 5-task `workplace_assistant` example dataset, driven end to end by Gym. Because `resources_servers/` ships unchanged, the demo uses a small user-side entrypoint (`app_mcp.py`, shown in the docs added by this PR) that subclasses the stock server and overrides `mcp_tools()` to append the 27 catch-all-backed tools; the flag itself is plain yaml: Compose config (`workplace_2059.yaml`) — MCP door via `claude_code_agent` (already on `main`; it reads the `mcp` key `/seed_session` returns and wires the Claude Code binary itself), HTTP door via `simple_agent`: ```yaml workplace_assistant: resources_servers: workplace_assistant: entrypoint: app_mcp.py # thin subclass overriding mcp_tools(); see the docs page domain: agent expose_tools_over_mcp: true workplace_claude: responses_api_agents: claude_code_agent: entrypoint: app.py resources_server: {type: resources_servers, name: workplace_assistant} model: ${anthropic_model_name} anthropic_api_key: ${anthropic_api_key} # env.yaml; any Anthropic-format endpoint anthropic_base_url: ${anthropic_base_url} # optional gateway; CLI appends /v1/messages policy_model: responses_api_models: inference_provider: entrypoint: app.py base_url: ${policy_base_url} # any OpenAI-format endpoint api_key: ${policy_api_key} model: ${policy_model_name} workplace_simple: responses_api_agents: simple_agent: entrypoint: app.py max_steps: 12 resources_server: {type: resources_servers, name: workplace_assistant} model_server: {type: responses_api_models, name: policy_model} ``` ```bash gym env start --config workplace_2059.yaml # (a) MCP door — Claude Code discovers and calls the tools over /mcp with the per-rollout token gym eval run --no-serve --agent workplace_claude \ --input resources_servers/workplace_assistant/data/example.jsonl \ --output results/workplace_claude_rollouts.jsonl # (b) HTTP door — the same tools as plain POST /<tool_name> gym eval run --no-serve --agent workplace_simple \ --input resources_servers/workplace_assistant/data/example.jsonl \ --output results/workplace_simple_rollouts.jsonl ``` Tip: if your provider's OpenAI *Responses-API* emulation is incomplete (a common gateway gap on multi-turn tool exchanges), use `inference_provider` (chat-completions + Gym's converter) rather than `openai_model`. ## Enablement (deliberately none in this PR) This PR ships the engine plus the one in-tree example (`example_mcp_weather`) — every other server under `resources_servers/` is byte-identical to `main` (we cannot properly test each env here, per review). Enabling a server is per-server follow-up work: set `expose_tools_over_mcp: true` in its instance config (typed-route servers need no code at all), plus a small `mcp_tools()` override for catch-all dispatcher servers — the docs added in this PR walk through workplace_assistant end to end. During development, a fleet exercise ran `claude_code_agent` over `/mcp` against 12 servers enabled this way (workplace_assistant 27 tools, math_advanced_calculations, finance_sec_search, indirect_prompt_injection with per-task narrowing, the examples, and others), all dispatching correctly; those results are development-time evidence, not shipped code. **Deliberately not exposed even as follow-up (each needs design work first):** aviary (its `/step`/`/close` bodies carry an `env_id` that can address other rollouts' environments — needs session-derived lookup), genrm_compare (its only route is a harness-side batch API), and the gymnasium family (reward accrues from `/reset`/`/step`, which a model could drive to reroll its own episode). The 74 verify-only benchmarks have no tool routes to expose. ## For discussion - Flag is **opt-in (default off)** — auto-exposing every route isn't always wanted (aviary's `/step` carries `env_id`; exposing it lets a model address other rollouts' envs). - Follow-ups deliberately not in this PR: session-binding aviary's `env_id` so it can be exposed safely; an MCP-safe reward design for the gymnasium family; and hardening the pre-existing token-signing secret derivation shared with NVIDIA-NeMo#1682. - **NVIDIA-NeMo#1682's decorator API is removed in this PR** rather than left as a parallel mechanism: auto-exposure reads the routes authors already wrote, so `@gym_tool` had no remaining role, and keeping both would have forced every author to choose. `example_mcp_weather` (the only in-tree consumer) is migrated here; NVIDIA-NeMo#2002's dual-registration approach is superseded. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Signed-off-by: Codex <adasif@nvidia.com> Signed-off-by: Codex <codex@openai.com> Co-authored-by: Codex <codex@openai.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
What this is
Serve a resources server's existing FastAPI tool routes over MCP with zero handler changes and one opt-in config field — no code change at all for a typed-route server:
run_webserverinstalls the/mcpendpoint automatically after the app is built (and imports the MCP SDK only for servers that opt in). Handlers keep theirrequest: Requestparam andrequest.session[...]reads exactly as written. The only in-tree code a server ever needs is for shapes the config can't express, and it is one override:mcp_tools(harvested, catchall)— return the harvested typed routes (the default), filter one out to exclude a harness-only route (e.g./end_session), or append catch-all-backed tools (harvested + [catchall.tool(name, input_schema, description)]) for dispatcher servers. A dropped route is never advertised, never callable over MCP, never shape-checked, and unchanged over HTTP.This PR also removes the decorator-based MCP API that #1682 added (
@gym_tool,MCPResourcesServer,MCPSessionError, the token contextvar and header middleware): tools no longer need decorators or MCP-specific signatures, so the old mechanism is superseded rather than parallel. The wire contract survives (MCPServerMetadata, theX-NeMo-Gym-Session-Tokenheader, the token salt, the reserved-name set) —claude_code_agentonmainconsumes it unchanged.example_mcp_weatheris rewritten onto auto-exposure: a plain typedPOST /get_weatherroute plus the yaml flag, with tests driving the MCP door (and the wrapped/seed_session+/verify) through the real engine mount, and the plain-HTTP tool route on the stock app.How to review this (~30 min reading order)
nemo_gym/mcp_auto_exposure.pymodule docstring — the contract in one screen.bind_route— the detector. The accept/refuse table below is what it enforces.call_direct— dispatch parity with the plain HTTP route (threadpool for sync handlers, body bytes, session cookie,response_modelfiltering, error-text parity).harvest_tools→install_auto_exposure— tool map, startup guards, session token,/seed_sessionwrap,/mcpmount.base_resources_server.pydiff — theexpose_tools_over_mcpconfig field, the two overridables (mcp_tools(harvested, catchall)— what to expose;mcp_allowed_tools_for_session(seed_body)— per-rollout narrowing), and the removal of the feat: Add Gym-owned MCP resources server support #1682 decorator API (the file drops from 315 lines onmainto 150; it had grown to 356 on this branch before the removal commit).server_utils.py— the 6-line activation point (plus a 4-line session-middleware idempotency guard).tests/unit_tests/test_mcp_auto_exposure.py— 45 tests double as the spec; each guard and parity shape has a named test.What the detector accepts vs refuses
Accepts (dispatches identically to the plain HTTP route): async and sync handlers (sync runs via
run_in_threadpool, exactly like FastAPI);body: Model;body: dict/dict[str, Any]; raw-body catch-alls readingawait request.json(); handlers taking both a body model and arequestthey read raw;strpath params;response_model=filtering (decorator kwarg or return annotation).Refuses loudly at startup (
ValueErrornaming the route and reason): non-Gym middleware;Depends/Security;*args/**kwargs; multiple body models; union/optional body params; defaulted query params; unresolvable annotations; tool names outside^[A-Za-z0-9_-]+$(e.g. nested routes); tool names (frommcp_tools()) colliding withverify/seed_session/aggregate_metrics/mcp; duplicate tool names; multiple parameterized catch-all routes (auto-exposure cannot tell which backs the tools); a server that already serves/mcp(a hand-rolled MCP mount would be shadowed); a missing/seed_sessionor/verify. These rules apply to route-handler signatures only — inner tool functions may use**kwargsfreely, and no in-tree server trips any refusal.No silent fallback exists: a shape either dispatches provably like HTTP or the server does not start. One soft case: a dispatcher whose
mcp_tools()override ignores its catch-all logs a warning (the catch-all-backed tools stay HTTP-only).Session & token
/seed_sessiongains an additive"mcp"key (MCPServerMetadata— the same shapeclaude_code_agentonmainalready consumes): the/mcpURL plus a signed per-rollout token inX-NeMo-Gym-Session-Token. The token payload is always{"sid", "tools"}— the session id plus this rollout's allow-list frommcp_allowed_tools_for_session(seed_body)(null= unrestricted) — signed withURLSafeSerializer(untimed; expiry was cut as consumer-less scope). The header/salt/metadata constants live inbase_resources_server— the wire contract retained from #1682's scheme after its decorator API was removed. The token is verified on every call (no cache).tools/listworks tokenless and advertises the full exposed set; a token carrying an allow-list narrows bothtools/listandtools/callfor that session. Known limitation, deliberately out of scope: the signing secret derivation (class___config-name) is pre-existingmainbehavior (the session-middleware secret) — hardening it is a separate change.Verify-time tool-name normalization (install-time, gated)
MCP-native agents record trajectory tool calls namespaced (
mcp__<server>__<tool>); verifiers know bare names — found live when MCP rollouts scored 0.0 on perfect trajectories. For flag-on servers only, installation wraps the/verifyroute's current endpoint (whatever handler it holds — servers that re-register/verifyare covered by construction): names are normalized for scoring on a deep copy, then the model's original names are restored in the echoed response (matched bycall_id). Flag-off servers — every existing benchmark — keep verify byte-for-byte, so no baselines move; persisted rollout artifacts always keep the names the model actually emitted.Diff shape (+2,136 / −631, 11 files)
nemo_gym/mcp_auto_exposure.py/seed_session+/verifywraps,/mcpmountnemo_gym/base_resources_server.pyexpose_tools_over_mcpconfig field, thenormalize_tool_namehelper, and themcp_tools()/mcp_allowed_tools_for_session()overridables; removes@gym_tool,MCPResourcesServer,MCPSessionError, and the header middleware (their unit tests go too:test_base_resources_server.py−277)nemo_gym/server_utils.pyrun_webserveractivates auto-exposure for opted-in servers (lazy MCP import);setup_session_middlewareis now idempotenttests/unit_tests/test_mcp_auto_exposure.pytest_server_utils.py; docs section infern/.../mcp-resources-server.mdxresources_servers/example_mcp_weatherrewritten onto auto-exposure (plain typed route + yaml flag; 6 tests: the MCP door — plus the wrapped/seed_sessionand/verify— through the real engine mount, and the plain-HTTP tool route on the stock app). Every other server: byte-identical tomain. Enablement stays per-server follow-up: pure config for typed-route servers; a smallmcp_tools()override for catch-all dispatchers.MCP engine: the official SDK's public low-level
Server— no private-attr access.Review provenance
This branch already went through a 7-reviewer + devil's-advocate review pass (59 confirmed findings) followed by a fix pass with adversarial re-verification: all 55 code findings are fixed here with tests (silently-wrong detector shapes, refuse-loudly gaps, an unbounded token cache — the token is now verified on every call with no cache, and expiry was later cut as consumer-less scope — event-loop blocking for sync handlers, provenance-preserving verify normalization), and a docstring-accuracy audit (77 reviewed, 3 corrected). The remaining follow-ups are listed under "For discussion".
Verified
pytest tests/unit_tests/test_mcp_auto_exposure.py→ 45/45 (~3 s; covers every refusal, parity shape, token path, exclusion semantics, and the normalization gating/provenance — including a re-registered-/verifyregression test).3891dd65):workplace_assistantMCP door (claude_code_agent) — 5/5reward: 1.0, all 8 distinct tools namespaced; HTTP door (simple_agent) — 5/5 on the second run, 4/5 on the first (the failing rollout omitted thestatus: "Lead"search filter — a trajectory-verified policy flake, not an engine failure; the HTTP door runs the stock route code).example_mcp_weather(claude_code_agent) — 5/5reward: 1.0at the removal commit4d6d569c; the one commit since is docs plus a comment reword. Earlier demo (below): both doors 5/5 tasksreward: 1.0, ~30 s wall for 5 concurrent rollouts, MCP-door trajectories persist the namespaced names the model emitted. n=5 single runs of a stochastic policy — a smoke demonstration that both transports work end to end, not a statistical parity claim.origin/mainhandlers, covering the factory-__signature__,dict-body, and raw-body/PlainTextResponseshapes; 44/44 as of 2026-07-17 against a pre-refactor revision of the engine (development-time evidence; the harness predates themcp_tools()API). Other in-tree servers were covered by a static route sweep, not this live harness.Runnable e2e — both doors through
gym eval runThe full 5-task
workplace_assistantexample dataset, driven end to end by Gym. Becauseresources_servers/ships unchanged, the demo uses a small user-side entrypoint (app_mcp.py, shown in the docs added by this PR) that subclasses the stock server and overridesmcp_tools()to append the 27 catch-all-backed tools; the flag itself is plain yaml:Compose config (
workplace_2059.yaml) — MCP door viaclaude_code_agent(already onmain; it reads themcpkey/seed_sessionreturns and wires the Claude Code binary itself), HTTP door viasimple_agent:Tip: if your provider's OpenAI Responses-API emulation is incomplete (a common gateway gap on multi-turn tool exchanges), use
inference_provider(chat-completions + Gym's converter) rather thanopenai_model.Enablement (deliberately none in this PR)
This PR ships the engine plus the one in-tree example (
example_mcp_weather) — every other server underresources_servers/is byte-identical tomain(we cannot properly test each env here, per review). Enabling a server is per-server follow-up work: setexpose_tools_over_mcp: truein its instance config (typed-route servers need no code at all), plus a smallmcp_tools()override for catch-all dispatcher servers — the docs added in this PR walk through workplace_assistant end to end. During development, a fleet exercise ranclaude_code_agentover/mcpagainst 12 servers enabled this way (workplace_assistant 27 tools, math_advanced_calculations, finance_sec_search, indirect_prompt_injection with per-task narrowing, the examples, and others), all dispatching correctly; those results are development-time evidence, not shipped code.Deliberately not exposed even as follow-up (each needs design work first): aviary (its
/step//closebodies carry anenv_idthat can address other rollouts' environments — needs session-derived lookup), genrm_compare (its only route is a harness-side batch API), and the gymnasium family (reward accrues from/reset//step, which a model could drive to reroll its own episode). The 74 verify-only benchmarks have no tool routes to expose.For discussion
/stepcarriesenv_id; exposing it lets a model address other rollouts' envs).env_idso it can be exposed safely; an MCP-safe reward design for the gymnasium family; and hardening the pre-existing token-signing secret derivation shared with feat: Add Gym-owned MCP resources server support #1682.@gym_toolhad no remaining role, and keeping both would have forced every author to choose.example_mcp_weather(the only in-tree consumer) is migrated here; feat:gym_tooldual registration — every tool served over both HTTP and MCP from one declaration #2002's dual-registration approach is superseded.🤖 Generated with Claude Code