Coverage for mcpgateway / routers / server_well_known.py: 97%

31 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-09 03:05 +0000

1# -*- coding: utf-8 -*- 

2"""Location: ./mcpgateway/routers/server_well_known.py 

3Copyright 2025 

4SPDX-License-Identifier: Apache-2.0 

5 

6Virtual Server Well-Known URI Handler Router. 

7 

8This module implements well-known URI endpoints for virtual servers at 

9/servers/{server_id}/.well-known/* paths. It supports: 

10- oauth-protected-resource (RFC 9728 OAuth Protected Resource Metadata) 

11- robots.txt, security.txt, ai.txt, dnt-policy.txt (shared with root endpoints) 

12 

13These endpoints allow MCP clients to discover OAuth configuration and other 

14metadata specific to individual virtual servers. 

15""" 

16 

17# Third-Party 

18from fastapi import APIRouter, Depends, HTTPException, Request 

19from fastapi.responses import PlainTextResponse 

20from sqlalchemy.orm import Session 

21 

22# First-Party 

23from mcpgateway.config import settings 

24from mcpgateway.db import get_db 

25from mcpgateway.db import Server as DbServer 

26from mcpgateway.routers.well_known import get_base_url_with_protocol, get_well_known_file_content 

27from mcpgateway.services.logging_service import LoggingService 

28 

29# Get logger instance 

30logging_service = LoggingService() 

31logger = logging_service.get_logger(__name__) 

32 

33# Router without prefix - will be mounted at /servers in main.py 

34router = APIRouter(tags=["Servers"]) 

35 

36 

37@router.get("/{server_id}/.well-known/oauth-protected-resource") 

38async def server_oauth_protected_resource( 

39 request: Request, 

40 server_id: str, 

41): 

42 """ 

43 DEPRECATED: OAuth 2.0 Protected Resource Metadata endpoint (server-scoped, non-compliant). 

44 

45 This endpoint is deprecated and non-compliant with RFC 9728. It returns a 301 redirect. 

46 

47 RFC 9728 Section 3.1 requires the well-known path to be constructed by inserting 

48 /.well-known/oauth-protected-resource/ into the resource URL, not appending it. 

49 

50 Old (non-compliant): /servers/{server_id}/.well-known/oauth-protected-resource 

51 New (RFC 9728): /.well-known/oauth-protected-resource/servers/{server_id}/mcp 

52 

53 Args: 

54 request: FastAPI request object for building redirect URL. 

55 server_id: The ID of the server. 

56 

57 Raises: 

58 HTTPException: 404 if well-known disabled, 301 redirect to compliant endpoint. 

59 """ 

60 if not settings.well_known_enabled: 

61 raise HTTPException(status_code=404, detail="Not found") 

62 

63 # Build RFC 9728 compliant redirect URL 

64 base_url = get_base_url_with_protocol(request) 

65 compliant_url = f"{base_url}/.well-known/oauth-protected-resource/servers/{server_id}/mcp" 

66 

67 logger.warning(f"Deprecated server-scoped OAuth metadata endpoint called for server {server_id}. " f"Redirecting to RFC 9728 compliant endpoint: {compliant_url}") 

68 

69 # Return 301 Permanent Redirect 

70 raise HTTPException(status_code=301, detail="Moved Permanently", headers={"Location": compliant_url}) 

71 

72 

73@router.get("/{server_id}/.well-known/{filename:path}", include_in_schema=False) 

74async def server_well_known_file( 

75 server_id: str, 

76 filename: str, 

77 db: Session = Depends(get_db), 

78) -> PlainTextResponse: 

79 """ 

80 Serve well-known URI files for a specific virtual server. 

81 

82 Returns the same well-known files as the root endpoint (robots.txt, security.txt, 

83 ai.txt, dnt-policy.txt) but scoped to a virtual server path. This allows MCP clients 

84 to discover these files at the virtual server level. 

85 

86 The endpoint validates that the server exists and is publicly accessible before 

87 serving the file. This avoids leaking information about private/team servers. 

88 

89 Args: 

90 server_id: The ID of the virtual server. 

91 filename: The well-known filename requested (e.g., "robots.txt"). 

92 db: Database session dependency. 

93 

94 Returns: 

95 PlainTextResponse with the file content. 

96 

97 Raises: 

98 HTTPException: 404 if server not found, disabled, non-public, or file not configured. 

99 """ 

100 # Check global well-known toggle first to avoid leaking server existence 

101 if not settings.well_known_enabled: 

102 raise HTTPException(status_code=404, detail="Not found") 

103 

104 # Validate server exists and is publicly accessible 

105 server = db.get(DbServer, server_id) 

106 

107 if not server: 

108 raise HTTPException(status_code=404, detail="Server not found") 

109 

110 if not server.enabled: 

111 raise HTTPException(status_code=404, detail="Server not found") 

112 

113 if getattr(server, "visibility", "public") != "public": 

114 raise HTTPException(status_code=404, detail="Server not found") 

115 

116 # Use shared helper to get the file content 

117 return get_well_known_file_content(filename)