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

39 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-02-11 07:10 +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 JSONResponse, 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 

28from mcpgateway.services.server_service import ServerError, ServerNotFoundError, ServerService 

29 

30# Get logger instance 

31logging_service = LoggingService() 

32logger = logging_service.get_logger(__name__) 

33 

34# Initialize services 

35server_service = ServerService() 

36 

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

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

39 

40 

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

42async def server_oauth_protected_resource( 

43 request: Request, 

44 server_id: str, 

45 db: Session = Depends(get_db), 

46) -> JSONResponse: 

47 """ 

48 RFC 9728 OAuth 2.0 Protected Resource Metadata endpoint for a specific server. 

49 

50 Returns OAuth configuration for the server per RFC 9728, enabling MCP clients 

51 to discover OAuth authorization servers and authenticate using browser-based SSO. 

52 This endpoint does not require authentication per RFC 9728 requirements. 

53 

54 Args: 

55 request: FastAPI request object for building resource URL. 

56 server_id: The ID of the server to get OAuth configuration for. 

57 db: Database session dependency. 

58 

59 Returns: 

60 JSONResponse with RFC 9728 Protected Resource Metadata. 

61 

62 Raises: 

63 HTTPException: 404 if server not found, disabled, non-public, OAuth not enabled, or not configured. 

64 """ 

65 # Check global well-known toggle first to respect admin configuration 

66 if not settings.well_known_enabled: 

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

68 

69 # Build resource URL using proper protocol detection for proxies 

70 # Note: get_base_url_with_protocol uses request.base_url which already includes root_path 

71 base_url = get_base_url_with_protocol(request) 

72 resource_url = f"{base_url}/servers/{server_id}" 

73 

74 try: 

75 response_data = server_service.get_oauth_protected_resource_metadata(db, server_id, resource_url) 

76 except ServerNotFoundError: 

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

78 except ServerError as e: 

79 raise HTTPException(status_code=404, detail=str(e)) 

80 

81 # Add cache headers 

82 headers = {"Cache-Control": f"public, max-age={settings.well_known_cache_max_age}"} 

83 

84 return JSONResponse(content=response_data, headers=headers) 

85 

86 

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

88async def server_well_known_file( 

89 server_id: str, 

90 filename: str, 

91 db: Session = Depends(get_db), 

92) -> PlainTextResponse: 

93 """ 

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

95 

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

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

98 to discover these files at the virtual server level. 

99 

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

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

102 

103 Args: 

104 server_id: The ID of the virtual server. 

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

106 db: Database session dependency. 

107 

108 Returns: 

109 PlainTextResponse with the file content. 

110 

111 Raises: 

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

113 """ 

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

115 if not settings.well_known_enabled: 

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

117 

118 # Validate server exists and is publicly accessible 

119 server = db.get(DbServer, server_id) 

120 

121 if not server: 

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

123 

124 if not server.enabled: 

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

126 

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

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

129 

130 # Use shared helper to get the file content 

131 return get_well_known_file_content(filename)