Skip to content

fix(mcp): strip URL credentials from server names in raised errors - #4000

Open
dimaosipa wants to merge 6 commits into
openai:mainfrom
dimaosipa:fix/redact-mcp-url-credentials
Open

fix(mcp): strip URL credentials from server names in raised errors#4000
dimaosipa wants to merge 6 commits into
openai:mainfrom
dimaosipa:fix/redact-mcp-url-credentials

Conversation

@dimaosipa

Copy link
Copy Markdown
Contributor

Summary

  • MCPServerSse / MCPServerStreamableHttp default their name to the connection url (sse: {url}), which commonly embeds credentials (user:password@host or ?api_key=...). every raised connection and tool-call error interpolated that raw name, so any transient failure surfaced the credential to the caller.
  • unlike the log paths, raised errors are not gated by DONT_LOG_TOOL_DATA, so there was no way to suppress this even on the default redact-by-default setting.
  • the redaction helper already existed and is unit-tested (get_mcp_server_log_name), and server.py already used it for logs. the raised errors just never got it.

fix:

  • add MCPServer._error_name, a thin wrapper over that helper. credentials, query, and fragment are stripped; host and path stay so the error still tells you which server failed.
  • apply it to the 16 raised-error sites in mcp/server.py, plus the two raised errors in mcp/util.py that sit directly next to already-redacted log calls.
  • functional uses of server.name (tool namespacing, span identity) are deliberately unchanged.

before:

UserError: Failed to connect to MCP server 'sse: https://user:s3cr3t_pw@mcp.example.com/sse?api_key=SECRET_QS_KEY': HTTP error 503

after:

UserError: Failed to connect to MCP server 'sse: https://mcp.example.com/sse': HTTP error 503

Test plan

  • 6 new tests in tests/mcp/test_server_errors.py: the _error_name property (default url name redacted, explicit name untouched), the connect / list_tools / call_tool raised errors, and the invoke_mcp_tool AgentsException. each asserts the password and api key are gone and the host is kept.
  • make format and make lint: clean.
  • make tests: full suite green (5770 + 28 passed, 0 failed). tests/mcp/ specifically: 239 passed.
  • make typecheck: adds 0 new errors.
  • ran .agents/skills/code-change-verification/scripts/run.sh: format and lint pass, then it stops at typecheck on 31 pre-existing errors unrelated to this change (vercel extension name-collision, py3.12-only apis on a 3.11 checkout, boto3 not installed, redis type: ignore skew).

Issue number

Checks

  • I've added new tests, if relevant
  • I've run .agents/skills/code-change-verification/scripts/run.sh
  • I've confirmed all verification steps pass

MCPServerSse and MCPServerStreamableHttp default their name to the connection
url ("sse: {url}"), which commonly embeds credentials (user:password@ or
?api_key=). every raised connection/tool-call error interpolated that raw name,
so a transient network failure surfaced the credential to the caller. unlike the
log paths, raised errors are not governed by DONT_LOG_TOOL_DATA, so there was no
way to suppress it.

- add MCPServer._error_name, which runs the existing get_mcp_server_log_name
  helper. credentials, query, and fragment are stripped; host and path stay so
  the error still says which server failed.
- use it for the 16 raised-error sites in server.py, and strip the server name
  in the two raised errors in mcp/util.py that sit next to already-redacted
  log calls.
- functional uses of server.name (tool namespacing, span identity) are
  unchanged.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8a36d63d0a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/mcp/server.py Outdated
Comment thread src/agents/mcp/util.py Outdated

@seratch seratch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for the contribution. The underlying credential-exposure issue is valid, and reusing the existing MCP URL sanitizer is directionally correct. Before we can merge this, please make the redaction cover the complete caller-visible exception graph, not only str() of the outer exception.

The current HTTP wrappers still chain the credential-bearing httpx exception, while the generic MCPUtil wrapper copies and chains the raw exception. Please extract only safe diagnostic fields and raise the sanitized SDK exception without retaining sensitive __cause__ or __context__ data. When tool-data redaction is enabled, generic exception details must also remain hidden.

Please add regression tests asserting that the credentials are absent from the outer message, recursive cause/context chain, and rendered traceback. Once those focused changes are covered, this should be ready for another review.

the first pass only sanitized str() of the outer exception. the httpx error was
still chained, so e.request.url (with credentials) stayed reachable through
__cause__/__context__, and the generic MCPUtil wrapper copied the raw exception
text into the message.

- extract only safe diagnostic fields (status code, reason phrase, exception
  class name) and never retain the raw transport exception while tool-data
  redaction is on. _raise_user_error_for_http_error becomes
  _build_user_error_for_http_error so the raise happens outside the except
  block; setting __context__ before raising does not work, python re-sets it.
- cover connect(), cleanup(), list_tools(), call_tool() and the streamable-http
  override, plus invoke_mcp_tool in mcp/util.py.
- hide generic exception details under DONT_LOG_TOOL_DATA. the four tests that
  assert the failure reason reaches the model now enable tool-data logging
  explicitly, which is the mode that behavior belongs to.
- regression tests assert the credentials are absent from the outer message,
  the recursive cause/context chain, and the rendered traceback.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 075f3bf384

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/mcp/server.py Outdated
Comment thread src/agents/mcp/util.py Outdated
Comment thread src/agents/mcp/util.py Outdated
the previous pass dropped __cause__/__context__ but still kept the raw exception
alive in the frame locals of the raising frames, and still read the exception
type name while redacting.

- decide redaction where the exception is caught (_transport_error_cause), so
  nothing holds the httpx error while tool data is redacted. python already
  clears the "except ... as e" binding, so the only remaining references were
  the explicit locals, which are now dropped too (connect, cleanup, and the
  streamable-http exception-group branch).
- invoke_mcp_tool no longer inspects the caught exception at all while
  redacting: a custom MCPServer controls the message, the attribute access, and
  the type name, so the redacted message is fixed text.
- tests assert no SDK traceback frame local holds the credentialed request URL,
  and that an exception whose type name is a secret and whose __str__/__repr__
  raise never reaches the caller-visible message.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 79638d0032

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/mcp/util.py
Comment thread src/agents/mcp/server.py
Comment thread src/agents/mcp/util.py Outdated
…cting

three remaining retention paths from the latest review:

- invoke_mcp_tool kept call_task and finished_task in the raising frame. a
  completed task holds its exception and repr(task) renders it, so the payload
  was still reachable from frame locals. both references are dropped before the
  raise, and the frame-locals test now walks task objects too (it fails without
  the fix).
- cleanup kept the exception-group loop target exc after the loop, which
  outlives it like the named transport locals; cleared alongside them.
- the redacted invoke_mcp_tool message no longer reads server.name. an explicit
  name is caller supplied and a URL-derived one keeps its path, and this message
  reaches the model through the default failure formatter. this matches
  get_mcp_server_log_message, which also drops the name while redacting.

the transport errors raised from server.py still carry the sanitized server
name, since those surface to the developer and need to say which server failed.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f43e9119b2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/agents/mcp/server.py
Comment thread src/agents/mcp/util.py Outdated
Comment thread src/agents/mcp/util.py
dimaosipa and others added 2 commits July 28, 2026 17:09
these errors are not developer-only: invoke_mcp_tool re-raises UserError and
MCPToolCancellationError unchanged, and default_tool_error_function puts
str(error) into the model-visible tool result. so every identifier they carry
is model-visible under the default policy.

- _error_name returns a fixed "<redacted>" label while tool data is redacted,
  and the sanitized name (credentials, query, and fragment stripped) only when
  tool-data logging is enabled. an explicit name is caller supplied and a
  URL-derived one keeps its path, so neither is safe by default.
- the redacted invoke_mcp_tool message and the server-initiated cancellation
  error drop the tool and server names too. this mirrors the "Invoking MCP tool"
  debug log in the same function, which already omits both while redacting.
- safe diagnostics still survive: status code, reason phrase, and the failure
  kind stay in the message.
- tests cover both modes for default and explicit names.

@seratch seratch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Thanks for addressing the exception-chain feedback. The latest revision now goes substantially beyond the URL-credential issue: it turns OPENAI_AGENTS_DONT_LOG_TOOL_DATA from a documented logging control into runtime exception policy, hides otherwise safe server and tool identifiers, and adds machinery for task and traceback-frame locals plus hostile exception class names.

Please reset this PR to the narrower contract: always sanitize URL-derived server names independently of the logging flag; ensure auth credentials are absent from the outer message, normal rendered traceback, __cause__, and __context__; and preserve existing non-secret names and diagnostics. Do not attempt to guarantee redaction of arbitrary custom names, exception type names, or third-party frame-local capture.
Please replace the current broad machinery and combinatorial tests with focused regression coverage for that contract.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants