Skip to content

Commit 61c5622

Browse files
fix: expose OAuth metadata for private servers
Signed-off-by: darrenhalden537-lang <291019754+darrenhalden537-lang@users.noreply.github.com>
1 parent 83f340d commit 61c5622

2 files changed

Lines changed: 37 additions & 7 deletions

File tree

mcpgateway/services/server_service.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1970,19 +1970,23 @@ def get_oauth_protected_resource_metadata(self, db: Session, server_id: str, res
19701970
"""
19711971
server = db.get(DbServer, server_id)
19721972

1973-
# Return not found for non-existent, disabled, or non-public servers
1974-
# (avoids leaking information about private/team servers)
1973+
# Return not found for non-existent or disabled servers.
19751974
if not server:
19761975
raise ServerNotFoundError(f"Server not found: {server_id}")
19771976

19781977
if not server.enabled:
19791978
raise ServerNotFoundError(f"Server not found: {server_id}")
19801979

1981-
if getattr(server, "visibility", "public") != "public":
1980+
# RFC 9728 metadata is intentionally unauthenticated discovery material.
1981+
# OAuth-enabled private/team servers advertise this URL from /servers/{id}/mcp;
1982+
# let those clients fetch only the non-secret authorization metadata while
1983+
# keeping non-OAuth private/team servers hidden.
1984+
oauth_enabled = getattr(server, "oauth_enabled", False)
1985+
if getattr(server, "visibility", "public") != "public" and not oauth_enabled:
19821986
raise ServerNotFoundError(f"Server not found: {server_id}")
19831987

19841988
# Check OAuth configuration
1985-
if not getattr(server, "oauth_enabled", False):
1989+
if not oauth_enabled:
19861990
raise ServerError(f"OAuth not enabled for server: {server_id}")
19871991

19881992
oauth_config = getattr(server, "oauth_config", None)

tests/e2e/test_oauth_protected_resource.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -306,8 +306,8 @@ async def test_server_with_oauth_disabled_returns_404(self, client: AsyncClient)
306306
assert response.status_code == 404
307307
assert "OAuth not enabled" in response.json()["detail"]
308308

309-
async def test_private_server_returns_404(self, client: AsyncClient):
310-
"""Scenario 4: Server with visibility="private" (non-public) returns 404."""
309+
async def test_private_server_with_oauth_enabled_returns_metadata(self, client: AsyncClient):
310+
"""Scenario 4: OAuth-enabled private servers expose only RFC 9728 metadata."""
311311
server_id = await self._create_server(
312312
client,
313313
{
@@ -318,15 +318,41 @@ async def test_private_server_returns_404(self, client: AsyncClient):
318318
"oauth_enabled": True,
319319
"oauth_config": {
320320
"authorization_server": "https://idp.example.com",
321+
"token_endpoint": "https://idp.example.com/oauth/token",
322+
"client_secret": "must-not-be-exposed",
321323
},
322324
},
323325
"team_id": None,
324326
},
325327
)
326328

329+
response = await client.get(f"/.well-known/oauth-protected-resource/servers/{server_id}/mcp")
330+
assert response.status_code == 200
331+
332+
data = response.json()
333+
assert data["resource"] == f"http://test/servers/{server_id}/mcp"
334+
assert data["authorization_servers"] == ["https://idp.example.com"]
335+
assert data["bearer_methods_supported"] == ["header"]
336+
# Only discovery metadata should be public; OAuth credentials/endpoints are not exposed.
337+
assert "client_secret" not in data
338+
assert "token_endpoint" not in data
339+
340+
async def test_private_server_without_oauth_still_returns_404(self, client: AsyncClient):
341+
"""Non-public servers without OAuth metadata remain hidden."""
342+
server_id = await self._create_server(
343+
client,
344+
{
345+
"server": {
346+
"name": "server_private_no_oauth",
347+
"description": "Private server without OAuth",
348+
"visibility": "private",
349+
},
350+
"team_id": None,
351+
},
352+
)
353+
327354
response = await client.get(f"/.well-known/oauth-protected-resource/servers/{server_id}/mcp")
328355
assert response.status_code == 404
329-
# Should not leak that the server exists (just "not found")
330356
assert "not found" in response.json()["detail"].lower()
331357

332358
async def test_disabled_server_returns_404(self, client: AsyncClient):

0 commit comments

Comments
 (0)