31
31
CatalogServerStatusResponse ,
32
32
)
33
33
from mcpgateway .services .gateway_service import GatewayService
34
+ from mcpgateway .utils .create_slug import slugify
34
35
35
36
logger = logging .getLogger (__name__ )
36
37
@@ -120,6 +121,9 @@ async def get_catalog_servers(self, request: CatalogListRequest, db) -> CatalogL
120
121
for server_data in servers :
121
122
server = CatalogServer (** server_data )
122
123
server .is_registered = server .url in registered_urls
124
+ # Set availability based on registration status (registered servers are assumed available)
125
+ # Individual health checks can be done via the /status endpoint
126
+ server .is_available = server .is_registered or server_data .get ("is_available" , True )
123
127
catalog_servers .append (server )
124
128
125
129
# Apply filters
@@ -206,18 +210,22 @@ async def register_catalog_server(self, catalog_id: str, request: Optional[Catal
206
210
# First-Party
207
211
from mcpgateway .schemas import GatewayCreate # pylint: disable=import-outside-toplevel
208
212
209
- # Detect transport type from URL or use SSE as default
210
- url = server_data ["url" ].lower ()
211
- # Check for SSE patterns (highest priority)
212
- if url .endswith ("/sse" ) or "/sse/" in url :
213
- transport = "SSE" # SSE endpoints or paths containing /sse/
214
- elif url .startswith ("ws://" ) or url .startswith ("wss://" ):
215
- transport = "SSE" # WebSocket URLs typically use SSE transport
216
- # Then check for HTTP patterns
217
- elif "/mcp" in url or url .endswith ("/" ):
218
- transport = "STREAMABLEHTTP" # Generic MCP endpoints typically use HTTP
219
- else :
220
- transport = "SSE" # Default to SSE for most catalog servers
213
+ # Use explicit transport if provided, otherwise auto-detect from URL
214
+ transport = server_data .get ("transport" )
215
+ if not transport :
216
+ # Detect transport type from URL or use SSE as default
217
+ url = server_data ["url" ].lower ()
218
+ # Check for WebSocket patterns (highest priority)
219
+ if url .startswith ("ws://" ) or url .startswith ("wss://" ):
220
+ transport = "WEBSOCKET" # WebSocket transport for ws:// and wss:// URLs
221
+ # Check for SSE patterns
222
+ elif url .endswith ("/sse" ) or "/sse/" in url :
223
+ transport = "SSE" # SSE endpoints or paths containing /sse/
224
+ # Then check for HTTP patterns
225
+ elif "/mcp" in url or url .endswith ("/" ):
226
+ transport = "STREAMABLEHTTP" # Generic MCP endpoints typically use HTTP
227
+ else :
228
+ transport = "SSE" # Default to SSE for most catalog servers
221
229
222
230
# Check for IPv6 URLs early to provide a clear error message
223
231
url = server_data ["url" ]
@@ -237,6 +245,8 @@ async def register_catalog_server(self, catalog_id: str, request: Optional[Catal
237
245
238
246
# Set authentication based on server requirements
239
247
auth_type = server_data .get ("auth_type" , "Open" )
248
+ skip_initialization = False # Flag to skip connection test for OAuth servers without creds
249
+
240
250
if request and request .api_key and auth_type != "Open" :
241
251
# Handle all possible auth types from the catalog
242
252
if auth_type in ["API Key" , "API" ]:
@@ -248,10 +258,54 @@ async def register_catalog_server(self, catalog_id: str, request: Optional[Catal
248
258
gateway_data ["auth_type" ] = "bearer"
249
259
gateway_data ["auth_token" ] = request .api_key
250
260
else :
251
- # For any other auth types, use custom headers
261
+ # For any other auth types, use custom headers (as list of dicts)
252
262
gateway_data ["auth_type" ] = "authheaders"
253
- gateway_data ["auth_header_key" ] = "X-API-Key"
254
- gateway_data ["auth_header_value" ] = request .api_key
263
+ gateway_data ["auth_headers" ] = [{"key" : "X-API-Key" , "value" : request .api_key }]
264
+ elif auth_type in ["OAuth2.1" , "OAuth" ]:
265
+ # OAuth server without credentials - register but skip initialization
266
+ # User will need to complete OAuth flow later
267
+ skip_initialization = True
268
+ logger .info (f"Registering OAuth server { server_data ['name' ]} without credentials - OAuth flow required later" )
269
+
270
+ # For OAuth servers without credentials, register directly without connection test
271
+ if skip_initialization :
272
+ # Create minimal gateway entry without tool discovery
273
+ # First-Party
274
+ from mcpgateway .db import Gateway as DbGateway # pylint: disable=import-outside-toplevel
275
+
276
+ gateway_create = GatewayCreate (** gateway_data )
277
+ slug_name = slugify (gateway_data ["name" ])
278
+
279
+ db_gateway = DbGateway (
280
+ name = gateway_data ["name" ],
281
+ slug = slug_name ,
282
+ url = gateway_data ["url" ],
283
+ description = gateway_data ["description" ],
284
+ tags = gateway_data .get ("tags" , []),
285
+ transport = gateway_data ["transport" ],
286
+ capabilities = {},
287
+ auth_type = None , # Will be set during OAuth configuration
288
+ enabled = False , # Disabled until OAuth is configured
289
+ created_via = "catalog" ,
290
+ visibility = "public" ,
291
+ version = 1 ,
292
+ )
293
+
294
+ db .add (db_gateway )
295
+ db .commit ()
296
+ db .refresh (db_gateway )
297
+
298
+ # First-Party
299
+ from mcpgateway .schemas import GatewayRead # pylint: disable=import-outside-toplevel
300
+
301
+ gateway_read = GatewayRead .model_validate (db_gateway )
302
+
303
+ return CatalogServerRegisterResponse (
304
+ success = True ,
305
+ server_id = str (gateway_read .id ),
306
+ message = f"Successfully registered { gateway_read .name } - OAuth configuration required before activation" ,
307
+ error = None ,
308
+ )
255
309
256
310
gateway_create = GatewayCreate (** gateway_data )
257
311
@@ -284,9 +338,31 @@ async def register_catalog_server(self, catalog_id: str, request: Optional[Catal
284
338
285
339
except Exception as e :
286
340
logger .error (f"Failed to register catalog server { catalog_id } : { e } " )
341
+
342
+ # Map common exceptions to user-friendly messages
343
+ error_str = str (e )
344
+ user_message = "Registration failed"
345
+
346
+ if "Connection refused" in error_str or "connect" in error_str .lower ():
347
+ user_message = "Server is offline or unreachable"
348
+ elif "SSL" in error_str or "certificate" in error_str .lower ():
349
+ user_message = "SSL certificate verification failed - check server security settings"
350
+ elif "timeout" in error_str .lower () or "timed out" in error_str .lower ():
351
+ user_message = "Server took too long to respond - it may be slow or unavailable"
352
+ elif "401" in error_str or "Unauthorized" in error_str :
353
+ user_message = "Authentication failed - check API key or OAuth credentials"
354
+ elif "403" in error_str or "Forbidden" in error_str :
355
+ user_message = "Access forbidden - check permissions and API key"
356
+ elif "404" in error_str or "Not Found" in error_str :
357
+ user_message = "Server endpoint not found - check URL is correct"
358
+ elif "500" in error_str or "Internal Server Error" in error_str :
359
+ user_message = "Remote server error - the MCP server is experiencing issues"
360
+ elif "IPv6" in error_str :
361
+ user_message = "IPv6 URLs are not supported - please use IPv4 or domain names"
362
+
287
363
# Don't rollback here - let FastAPI handle it
288
364
# db.rollback()
289
- return CatalogServerRegisterResponse (success = False , server_id = "" , message = "Registration failed" , error = str ( e ) )
365
+ return CatalogServerRegisterResponse (success = False , server_id = "" , message = user_message , error = error_str )
290
366
291
367
async def check_server_availability (self , catalog_id : str ) -> CatalogServerStatusResponse :
292
368
"""Check if a catalog server is available.
0 commit comments