fix(mcp): preserve scopes through admin server edits

This commit is contained in:
Yucheng He 2026-09-18 11:29:02 -07:00
parent 9cff026bae
commit f59cd303c6
4 changed files with 38 additions and 12 deletions

View file

@ -7088,6 +7088,7 @@ class MCPServerManager:
spec_path=server.spec_path,
transport=server.transport,
auth_type=server.auth_type,
credentials={"scopes": server.scopes} if server.scopes else None,
created_at=server.created_at,
updated_at=server.updated_at,
teams=[],

View file

@ -849,7 +849,9 @@ if MCP_AVAILABLE:
if not credentials:
return False
as_dict: Final[dict[str, object]] = dict(credentials)
return any(value for key, value in as_dict.items() if key not in MCP_ADMIN_CONFIG_CREDENTIAL_KEYS)
return any(
value for key, value in as_dict.items() if key not in MCP_ADMIN_CONFIG_CREDENTIAL_KEYS and key != "scopes"
)
def _inherit_credentials_from_existing_server(
payload: NewMCPServerRequest,

View file

@ -10424,11 +10424,15 @@ def test_build_mcp_server_table_carries_oauth2_flow():
transport=MCPTransport.http,
auth_type=MCPAuth.oauth2,
oauth2_flow="client_credentials",
client_id="client-123",
client_secret="secret-xyz",
scopes=["scope:a", "scope:b"],
)
table = manager._build_mcp_server_table(server)
assert table.oauth2_flow == "client_credentials"
assert table.credentials == {"scopes": ["scope:a", "scope:b"]}
def test_build_mcp_server_table_carries_null_oauth2_flow():

View file

@ -1704,14 +1704,26 @@ class TestTemporaryMCPSessionEndpoints:
return _inherit_credentials_from_existing_server(payload)
def test_admin_config_alone_does_not_suppress_credential_inheritance(self):
"""The edit form round-trips upstream_resource, which is admin config rather than a credential.
Treating the blob as "credentials supplied" left the Authorize session with no declared app on
the exact path where this knob is configured."""
updated = self._inherit_with({"upstream_resource": "api://audience"})
@pytest.mark.parametrize(
"credentials",
[
{"upstream_resource": "api://audience"},
{"scopes": ["scope:a", "scope:b"]},
{"scopes": ["scope:edited"], "upstream_resource": "api://audience"},
{"scopes": ["scope:edited"], "upstream_token_header": "esb-oauth"},
{"scopes": []},
{"scopes": None},
],
)
def test_admin_config_alone_does_not_suppress_credential_inheritance(self, credentials: MCPCredentials):
updated = self._inherit_with(credentials, scopes=["scope:stored"])
assert updated.credentials["client_id"] == "client-123"
assert updated.credentials["client_secret"] == "secret-xyz"
assert updated.credentials == {
"client_id": "client-123",
"client_secret": "secret-xyz",
"scopes": ["scope:stored"],
**credentials,
}
def test_upstream_token_header_is_inherited_like_other_admin_config(self):
"""It is admin config rather than a credential, so a session server derived from an existing
@ -1730,11 +1742,18 @@ class TestTemporaryMCPSessionEndpoints:
assert updated.credentials["client_secret"] == "secret-xyz"
assert updated.credentials["upstream_token_header"] == "esb-oauth"
def test_supplied_credential_still_wins_over_inheritance(self):
"""A caller that supplies a real credential keeps it; inheritance must not overwrite it."""
updated = self._inherit_with({"auth_value": "caller-token"})
@pytest.mark.parametrize(
"credentials",
[
{"auth_value": "caller-token"},
{"client_id": "caller-client", "scopes": ["scope:edited"]},
{"client_secret": "caller-secret", "scopes": ["scope:edited"]},
],
)
def test_supplied_credential_still_wins_over_inheritance(self, credentials: MCPCredentials):
updated = self._inherit_with(credentials)
assert updated.credentials == {"auth_value": "caller-token"}
assert updated.credentials == credentials
def test_inheritance_carries_upstream_resource_to_the_session_server(self):
"""Without this the temporary server omits the resource indicator and the Authorize leg it