Skip to content

Breaking: reject a callable tool_filter on the deprecated OpenAI Agents MCP providers' servers - #291

Open
campoy wants to merge 4 commits into
mainfrom
fix/openai-agents-python-mcp-dynamic-tool-filter-fails-discovery
Open

campoy wants to merge 4 commits into
mainfrom
fix/openai-agents-python-mcp-dynamic-tool-filter-fails-discovery

Conversation

@campoy

@campoy campoy commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Stacked on #275. This PR's base is #275's branch; merge it after #275.

What was changed

StatelessMCPServerProvider and StatefulMCPServerProvider now reject a callable tool_filter on
the server that the Worker's factory returns, with the same non-retryable ApplicationError that
OpenAIAgentsPlugin(mcp_servers=...) already raises for it:

MCP server 'tools-filtered-stateless' sets a callable tool_filter on its worker-side MCPServer, which cannot be applied there because the run context and agent it receives exist only in the workflow; pass the filter to temporal_mcp_server() instead

Breaking change: a Workflow that uses such a server only for direct call_tool() or
list_prompts() calls never lists tools, so it completes today; natively, too, the filter applies
only to tool listing. With this change its first MCP Activity fails with the error above. Runs where
an agent lists the tools already fail today, with a retryable UserError; they now fail at once.

There is no version gate. The check runs only in Activities, so recorded Histories replay as before.
A run still open when its Worker is upgraded fails at its next MCP Activity for that server. Before,
removing the filter from the factory let a run that was retrying the list Activity continue; now it
has to be started again.

Upgrade: pass the filter to temporal_mcp_server(tool_filter=...), with
OpenAIAgentsPlugin(mcp_servers=...), or use a static filter (create_static_tool_filter(...)) on
the factory's server.

Why?

The Agents SDK applies a callable tool_filter when an agent lists tools, and raises
UserError("run_context and agent are required for dynamic tool filtering") when it gets no run
context or agent. The deprecated providers' list Activity calls list_tools() with neither, so
every attempt raised that retryable error. Under the default config the Activity retried forever
and the run never reached the model. temporal_mcp_server() already rejects such a filter, on every
MCP Activity, with a clear error. This change gives the deprecated routes the same rule, and the same
place as #275's require_approval check.

Checklist

  1. Closes [Bug] openai_agents: a callable tool_filter on a deprecated MCP provider's server makes tool listing retry forever #290

  2. How was this tested:

    • test_mcp_server_factory_require_approval_is_rejected from Breaking: reject a require_approval policy on the OpenAI Agents MCP factory's server #275 becomes
      test_mcp_server_factory_settings_are_rejected, with a callable tool_filter case next to the
      require_approval one, stateless and stateful. It also checks that the error names the
      rejected setting. On Breaking: reject a require_approval policy on the OpenAI Agents MCP factory's server #275's branch the 2 tool_filter cases fail with DID NOT RAISE WorkflowFailureError; with the change all 4 cases pass, and they
      pass again in each of 5 runs in a row.
    • The issue's reproduction exits 1 on 1.1.0, on main and on Breaking: reject a require_approval policy on the OpenAI Agents MCP factory's server #275's branch (both deprecated
      routes still retrying UserError after 30 seconds) and 0 with this change (both fail at once
      with the error above). Its controls are the same on all of them.
    • A probe that calls call_tool() and list_prompts() directly through both deprecated routes
      shows the breaking change: on main and on Breaking: reject a require_approval policy on the OpenAI Agents MCP factory's server #275 all four calls complete, as they do natively; with
      this change all four fail with the error above.
    • Histories recorded on main with a callable and a static filter (stateless, maximum attempts
      1. replay with this change.
    • Locally, with uv 0.12.24, make sync and make lint (ruff, pyright, mypy, basedpyright,
      pydocstyle) pass. make test ran on a loaded machine: 430 passed and 4 failed. One was the new
      stateless tool_filter case, which hit the test's 10-second execution timeout with
      Error reporting WFT to server in the log; the other three are tracing and tool-error tests this
      change does not touch. Run alone, all 4 pass. CI is the full run.
  3. Any docs updates needed?
    The README's paragraph on the deprecated APIs is updated here.

Not covered:

Draft while #275 and the breaking change are open.

campoy added a commit that referenced this pull request Oct 31, 2026
The rejection test keeps #291's setting parameter, with an instance_evaluator case from #275.
Base automatically changed from fix/openai-agents-python-factory-require-approval-ignored to main October 31, 2026 08:49
campoy and others added 4 commits October 31, 2026 02:39
- Move both factory-server checks to _mcp_server_checks.py, which needs
  no mcp extra; split the approval check into a test and a raising wrapper
  and evaluate it once per stateful session.
- Name the registered server and the route's option in the error, and
  state the mechanism without claiming the run's approval behavior.
- Reject a server that overrides how the SDK evaluates the policy.
- Heartbeat the stateful session with auto_heartbeater(interval=30).
- Give the same rule in the README and docstrings; list which settings
  on the factory's server are applied, rejected or ignored, and describe
  a run that already holds its cached tool list.
- Tests: record cleanup() before raising, add a cleanup-error session
  mode, use 30 s deadlines, inline the stateful ActivityConfig, and reuse
  InProcessMCPServer in the v2 tests.
…oviders' servers

StatelessMCPServerProvider and StatefulMCPServerProvider listed tools with no
run context or agent, so a callable tool_filter on the factory's server made
the Agents SDK raise UserError on every attempt, and the list Activity retried
forever under the default config. Reject such a filter where the require_approval
check rejects a policy, so every MCP Activity for that server fails with the
non-retryable error that mcp_servers already raises.
Run the tool_filter check from #275's new _mcp_server_checks.py, with the registered server name, in the same places as the require_approval check.
@campoy
campoy force-pushed the fix/openai-agents-python-mcp-dynamic-tool-filter-fails-discovery branch from a8c8da0 to bf1e422 Compare October 31, 2026 09:49
@campoy
campoy marked this pull request as ready for review October 31, 2026 17:25
@campoy
campoy requested a review from a team as a code owner October 31, 2026 17:25

@DABH DABH left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review at head bf1e422. Main has nothing newer under python/openai_agents, and the two 'Address review findings' commits from #275's round net to zero against it, so the diff is the fix alone. It closes the gap the #275 review listed as out of scope: the legacy routes lacked the v2 route's callable tool_filter guard. In a worktree at the head: make lint clean, make test 447 passed, and the six test_mcp_server_factory_settings_are_rejected cases passed three more times in a row; with the head's test file on main, the two tool_filter cases fail with DID NOT RAISE WorkflowFailureError and the other four pass. End-to-end probes on a local dev server used a real MCPServerStdio, so the Agents SDK applies the filter itself. The issue's reproduction exits 1 on main (both deprecated routes at list-tools attempt 6 with the retryable UserError after 30 s, controls as in the issue) and 0 at the head (both fail at once with the non-retryable message, and the Workflow fails instead of looping on Workflow Tasks). A static filter still completes on both routes at the head. A Workflow that only makes direct call_tool() and list_prompts() calls completes on main and fails at its first call at the head. A run left retrying list-tools on main (attempt 4) fails at its next attempt once a head Worker picks it up. The body discloses both. A rejected stateful server is never connected. Replay: five histories recorded on main (a static filter on each route, direct calls on each route, and a bounded-retry UserError failure) replay on the head, and the five recorded at the head replay on main. The check runs only inside Activities, and no command, Activity type or input changes, so no version gate is needed. Dropped after checking: the cached-tool-list path from the #275 review (a callable filter on these routes could never list on main, stateless caching is off by default and the stateful proxy has no cache); a non-callable, non-dict tool_filter passing the predicate (invalid natively too, where the SDK raises ValueError); and the session Activity outliving a failed Workflow (the same 'Activity not found on completion' warning appears for healthy stateful runs on main). Nothing material was left unverified.

Severity: P1 = wrong result or stuck run in realistic use; P2 = correctness or robustness in an edge case; P3 = docs, tests, cleanup.

Where the problems cluster: both comments are about what a user of the deprecated routes is told, in the error's remedy and in the API docs. Approving: the fix is correct at all three call sites, its tests fail on main and pass here, replay is unaffected in both directions and the break is disclosed; both comments are non-blocking.

PR body: it still describes the stacked state. 'Stacked on #275 ... merge it after #275': #275 merged as aad7a9d and GitHub retargeted this PR to main. 'The check, _reject_dynamic_tool_filter, moves from _mcp_backend.py to _mcp.py': #275's round already moved it to _mcp_server_checks.py; this PR adds _has_dynamic_tool_filter there and calls the check from _mcp.py. 'all 4 cases pass': there are six now, counting #275's instance_evaluator case. 'Draft while #275 and the breaking change are open': it is ready for review. 'StatefulMCPServerProvider before its session connects' reads as if the session Activity fails. It does not: it skips connect() and keeps the dedicated Worker running so that each operation fails, as the comment at _mcp.py:545-549 says. GitHub also lists no closing issue for this PR (closingIssuesReferences is empty, probably because the PR was opened against #275's branch), so as things stand merging will not close #290. Editing the body or linking the issue under Development should restore the link.

Parity: the TypeScript providers on main already reject a callable toolFilter on the factory's server in every operation (mcp-provider.ts:14-25, stateful-mcp-provider.ts:76), so this brings Python in line. Two differences remain. Python's deprecated routes lack the workflow-side option (your 'Not covered'). TypeScript also tags the failure MCPToolFilterUnsupported, while Python's error has no type; that predates this PR on the v2 route.

)
else:
server = cast(Callable[[], MCPServer], self._server_factory)()
_reject_dynamic_tool_filter(self._server_name, server)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P3 (non-blocking): on the deprecated routes, the error's only remedy, 'pass the filter to temporal_mcp_server() instead', is incomplete, and following it as written trades one endless retry for another. temporal_mcp_server() reaches only factories registered with OpenAIAgentsPlugin(mcp_servers=...). Probe at the head: a Worker that keeps StatelessMCPServerProvider("tools-filtered", factory), with the Workflow changed to temporal_mcp_server("tools-filtered", tool_filter=drop_danger), is still running after 20 s. temporalio.mcp.tools-filtered.list-tools is at attempt 6, and its last failure is the Worker's retryable NotFoundError ('is not registered on this worker'). A stateful user also loses the persistent session on that route (your 'Not covered' says so). The message never mentions the in-place fix that works on both deprecated routes: a static filter on the factory's server. The body's Upgrade paragraph has the complete answer. The #275 review made the same point about the approval message, and #275 fixed it by passing the route (line 197). Do the same here. Give _reject_dynamic_tool_filter an optional route or remedy argument that _mcp_backend.py:163 leaves at today's text, and have lines 195 and 500 say something like '...; use a static filter (create_static_tool_filter) on the factory's server, or register the factory with OpenAIAgentsPlugin(mcp_servers=...) and pass the filter to temporal_mcp_server(tool_filter=...)'. The assertion at tests/test_openai.py:3147 would need the new text.

`require_approval` to `stateless_mcp_server()` or `stateful_mcp_server()`;
as with `temporal_mcp_server()`, a policy on the factory's server is rejected
unless it never asks for approval.
unless it never asks for approval. So is a callable `tool_filter` there; a

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

P3 (non-blocking): this sentence is the only place the new rule is documented, and it leaves out the part that breaks code that works today. The API docs for these routes do not mention it. stateless_mcp_server() and stateful_mcp_server() document only the require_approval rule (workflow.py:387-391, :450-454). The providers' server_factory arguments (_mcp.py:167-169, :471-473) and mcp_server_providers (_temporal_openai_agents.py:372-374) document neither rule, although the mcp_servers entry just below states both rejections for the v2 route. Release notes carry only the commit subject, so this paragraph is also where an upgrader learns that the check runs in every operation. Probes: a Workflow that only calls call_tool() and list_prompts() on such a server completes on main but fails at its first MCP Activity at the head, on both routes. A run left retrying list-tools on main fails at its next attempt once a head Worker picks it up. Suggest one sentence in both server_factory docstrings and in mcp_server_providers (a callable tool_filter on the returned server is rejected in every MCP operation; a static one is applied). Here, something like: 'So is a callable tool_filter there, in every MCP operation, direct call_tool() and prompt calls included; a static filter still works. Remove such a filter from the factory in the same deploy, or let open runs finish first.'

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants