Skip to content

feat(pi): use MCP tool grants in sandbox sessions - #4587

Merged
athitten merged 8 commits into
mainfrom
athitten/pi-session-mcp-tools
Oct 21, 2026
Merged

athitten merged 8 commits into
mainfrom
athitten/pi-session-mcp-tools

Conversation

@athitten

@athitten athitten commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Pi's sandbox sessions can now use the Resources server's tools through MCP tool grants. Before this PR, a session refused any required tool grant with a 422. So Pi could not run a benchmark whose task tools live on the Resources server, such as GDPVal since #4178, where the agent hands in its files by calling the Resources server's finish tool.

  • Tool grants. With expose_tools_over_mcp: true on the Resources server and resources_tool_transports: [mcp] on the Environment Server, each MCP grant reaches Pi through gym_mcp.mjs, the extension the local path already uses for mcp_servers. Pi names each tool <grant name>_<tool name>, at most 64 characters. Discovery waits up to 30 seconds per request, and each tool call up to the new sandbox_mcp_timeout_seconds (default 300).

  • Grants Pi cannot use. Seeding refuses with a 422:

    • required direct_http grants, as before;
    • required MCP grants whose name is not 1 to 62 letters, digits, - and _;
    • two required MCP grants whose names differ by a _<suffix> (such as web and web_v2), because Pi could not tell their tools apart.

    An optional MCP grant with either problem is left out and recorded as an mcp_server_skipped observation gap.

  • Setup failures. A required grant that Pi cannot set up stops Pi before the first model call. The error and an mcp_setup_failed observation gap give the cause, naming the grant and the server's scheme, host and port when a grant failed. An optional grant that fails is skipped and recorded as mcp_server_skipped. Error text leaves out response bodies, header values and the URL's path and query.

  • Finish tools. In a GDPVal smoke run, one model called finish 573 times in a row until its context overflowed. The new finish_tool_names (like Stirrup's setting of the same name) lists granted tools whose successful call ends the run: gym_mcp.mjs returns Pi's terminate: true for them (Pi 0.69.0 or later), Pi stops when every tool call of the turn is one of them, and the response and observations count the run as completed. Setup fails if no granted server offers a listed tool.

    • A finish call from a reply cut off at the output limit is not run, because Pi would run it with whatever arguments it parsed from the cut-off reply, and the run would end on a partial submission. The model gets an error and calls it again. A reply counts as cut off when the server reports length, or when it used all of its max_tokens: vLLM reports tool_calls for a reply it parsed a tool call from, even when the reply was cut off.
  • Tool names in responses. Responses name MCP calls mcp__<grant>__<tool>, the form a Resources server's /verify strips, as Hermes does. Observations and model calls keep the names the model saw.

  • Event delivery. Pi's JSON mode sends console.log and process.stdout.write to stderr, and the runner records only stdout. gym_mcp.mjs writes its events with the stdout stream's own write, which still reaches stdout and keeps each line whole.

Most of the gym_mcp.mjs diff is re-indentation: per-server discovery moved into its own function. Hide whitespace changes to review it.

No issue. Needed by #4589 (Pi on GDPVal), which is still stacked on #4178's pre-merge branch and will be rebased onto main. Related:

Validation

At 4e2f867, on main d96b337 (main has moved since without touching these files; the branch merges cleanly):

  • pytest responses_api_agents/pi_agent/tests with real Pi 0.80.2 on PATH: 296 passed, 2 skipped (they need Python 3.8).
  • node --test responses_api_agents/pi_agent/tests/test_gym_mcp.mjs: 17 passed on Node 24.21.0 and on Node 22.19.0 (the sandbox's Node).
  • pytest responses_api_agents/pi_sandboxed_agent/tests, which shares gym_mcp.mjs: 49 passed.
  • New tests cover seed refusals and left-out optional grants; setup failures and their gaps, also after an interrupted run; finish tools (mixed or failed turns, cut-off replies by either signal, unknown names); response tool names; timeouts; error text without secrets; and event delivery past Pi's stdout redirect, also at exit 78.
  • Real Pi 0.80.2 through the real runner and supervisor, against local MCP and model servers (passes on Node 24 and 22.19.0): a finish run ends after one model call; a finish call cut off at the output limit, reported as length or as vLLM does, never reaches the server, and the model calls it again; a required server that is down stops Pi before any model call, with the cause. The test skips when the pinned Pi is not installed. In CI it runs only in the non-blocking harness conformance job (Pi job, step "Validate probes, checker, and adapter").
  • Mutation check: 32 one-at-a-time changes to the new code; 22 make a test fail. One of the rest (dropping the toolUse check in _ends_on_finish_tool) changes nothing, because a reply cut off at the output limit is reported incomplete anyway. The other nine are rare cases left without tests on purpose: name-length and overlap boundaries, malformed tool results, several finish calls in one turn, finish tools listed on another server, the exit wait with a full stdout pipe, escaping of hostile tool names, and tools without a name.
  • End to end with real Pi 0.80.2, gym_mcp.mjs at 4e2f867 and main's GDPVal resources server (expose_tools_over_mcp: true, stub model), on Node 24 and 22.19.0: the model saw the four GDPVal tools, finish was recorded for the session with its arguments, and the run ended after that call with one model request.
  • pre-commit run --all-files: passed, with no changes.
  • Three rounds of independent review (correctness, tests and CI, compatibility and docs, Pi internals); the later commits fix what they found.

Rollouts

The rollouts used earlier revisions of this PR. Later commits changed the seed checks, error text, observation gaps and event delivery, and added the refusal of a finish call from a cut-off reply. Every session in these runs set up its tools without errors.

Smoke on #4178 at bf9f8db with this PR (then 6730336), #4588, #4589 and #4586, 3 tasks: all 3 runs ended on finish with no follow-up model call, and the files were saved. Details are in #4589.

220 GDPVal tasks with Pi on #4178 merged with main, run with #4589, the HOME change from #3902 and an earlier revision of this PR:

  • Model: an internal checkpoint served with vLLM, 32,768 output tokens per call.
  • Every session reached the four GDPVal tools over MCP.
  • 204 runs ended on finish, with no follow-up model call and no finish loops. In 4 of them, Pi's argument check rejected the first finish call, and the model called it again correctly in the next turn.
  • None of the 208 finish calls came from a reply that used its whole output budget (22 of the 6,449 model calls did, none with a finish call), so the refusal added since would not have changed these runs.
  • 16 runs ended on a model call that hit the output limit (11 in the earlier Pi run).
  • Scores with the official judge panel: ELO 1182.8 and rubric 78.4 over 217 tasks, against 1150.6 and 79.3 for the earlier Pi run counted the same way. Details are in feat(gdpval): run Pi in place of Stirrup #4589.

Compatibility and benchmark impact

  • No change for sessions without MCP grants (resources_tool_transports: [] or [direct_http]), which covers every Pi config in the repo. Required direct_http grants are still refused.
  • Required MCP grants used to be refused at seed with a 422. They are now used. A server Pi cannot use (unreachable from the sandbox, or not a stateless JSON MCP endpoint) fails each episode when Pi starts, after the runtime install, as a retryable agent failure.
  • Optional MCP grants used to be ignored. Pi now connects to them, or leaves them out with an mcp_server_skipped gap. Today only an agent config's tool_accesses (none in the repo) or docs(harbor): add a SWE-Interact run recipe on the Harbor task shape #4728 would send them.
  • Stored rollouts name MCP calls mcp__<grant>__<tool> in the response; observations keep <grant>_<tool>.
  • New observation gaps mcp_server_skipped and mcp_setup_failed.
  • A finish call is refused only when the reply reports length or uses all of the request's max_tokens. A lower limit that the model server applies itself, such as the room left in the context window, is not detected.
  • The local mcp_servers path and pi_sandboxed_agent share gym_mcp.mjs. Their entries have no required field, so they stay required. On a setup failure, stderr now carries the cause with the server's scheme, host and port, and stdout gets an ng_gym_mcp_setup_failed line.
  • The grant's headers (for a Resources server, the episode's session token) are in a sandbox file the model's tools can read until close removes it.
  • finish_tool_names needs Pi 0.69.0 or later; the default pin is 0.80.2.
  • Benchmark impact: none for existing configs. Pi on GDPVal (feat(gdpval): run Pi in place of Stirrup #4589) depends on this PR.

Checklist

  • I have read the contributing guidelines.
  • The change is focused; unrelated "drive-by" edits are tracked as separate issues/PRs.
  • Tests added or updated and pass locally, or N/A for docs-only / non-code changes (so CI unit/server checks pass when applicable).
  • Documentation added or updated, or N/A with justification.
  • Pre-commit checks pass locally (pre-commit run --all-files) (so CI lint/format checks pass).
  • New source files include the required Apache-2.0 SPDX header. N/A: no new files.
  • All commits have DCO sign-off (git commit -s) (so the DCO check passes).

@athitten athitten added feature New capabilities, enhancements, or enablement work area:agent Agent harnesses and Responses API agent behavior labels Oct 14, 2026
@copy-pr-bot

copy-pr-bot Bot commented Oct 14, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

Signed-off-by: Abhishree <athittenaman@nvidia.com>
Signed-off-by: Abhishree <athittenaman@nvidia.com>
Signed-off-by: Abhishree <athittenaman@nvidia.com>
@athitten
athitten force-pushed the athitten/pi-session-mcp-tools branch from 6730336 to e7445d0 Compare October 20, 2026 11:26
Signed-off-by: Abhishree <athittenaman@nvidia.com>
Signed-off-by: Abhishree <athittenaman@nvidia.com>
Signed-off-by: Abhishree <athittenaman@nvidia.com>
Signed-off-by: Abhishree <athittenaman@nvidia.com>
Signed-off-by: Abhishree <athittenaman@nvidia.com>
@athitten

Copy link
Copy Markdown
Contributor Author

/ok to test 4e2f867

@athitten
athitten marked this pull request as ready for review October 20, 2026 16:15
@svcnvidia-nemo-ci svcnvidia-nemo-ci added the needs-review PR is ready for code review and waiting on a reviewer label Oct 20, 2026

@yaoyu-33 yaoyu-33 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @athitten, this looks good. Reviewed at 4e2f867.

The seed checks, the MCP setup in the sandbox, ending the run on finish (including the cut-off refusal), and the mcp__<grant>__<tool> naming for /verify all look correct. We checked the termination, message_end ordering and stdout assumptions against Pi 0.80.2's source. Locally, the pi_agent tests, test_gym_mcp.mjs and the pi_sandboxed_agent tests pass; the real-Pi tests need Linux, so they were not run here.

Non-blocking, for a follow-up if useful:

  • Tests for the grant-overlap rule beyond the both-required case (required plus optional, two optional, the 62-character boundary).
  • finish_tool_names matches bare tool names on every grant; scoping it per grant would matter once a second grant can offer a tool with the same name.

@athitten

Copy link
Copy Markdown
Contributor Author

Thanks @athitten, this looks good. Reviewed at 4e2f867.

The seed checks, the MCP setup in the sandbox, ending the run on finish (including the cut-off refusal), and the mcp__<grant>__<tool> naming for /verify all look correct. We checked the termination, message_end ordering and stdout assumptions against Pi 0.80.2's source. Locally, the pi_agent tests, test_gym_mcp.mjs and the pi_sandboxed_agent tests pass; the real-Pi tests need Linux, so they were not run here.

Non-blocking, for a follow-up if useful:

  • Tests for the grant-overlap rule beyond the both-required case (required plus optional, two optional, the 62-character boundary).
  • finish_tool_names matches bare tool names on every grant; scoping it per grant would matter once a second grant can offer a tool with the same name.

Thanks @yaoyu-33 ! Will add in a separate small Pi PR

@athitten
athitten added this pull request to the merge queue Oct 21, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔄 Merge queue validation started! You can track the progress here: https://github.com/NVIDIA-NeMo/Gym/actions/runs/39536523049

Merged via the queue into main with commit cbc7288 Oct 21, 2026
49 of 54 checks passed
@athitten
athitten deleted the athitten/pi-session-mcp-tools branch October 21, 2026 03:24

This branch was previously deployed

1 inactive deployment
public — 4e2f867f Deployed Oct 20, 2026 by copy-pr-bot[bot] via release / finalize / notify #5991
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:agent Agent harnesses and Responses API agent behavior feature New capabilities, enhancements, or enablement work needs-review PR is ready for code review and waiting on a reviewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants