Coverage for mcpgateway/schemas.py: 74%
476 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-09 11:03 +0100
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-09 11:03 +0100
1# -*- coding: utf-8 -*-
2"""MCP Gateway Schema Definitions.
4Copyright 2025
5SPDX-License-Identifier: Apache-2.0
6Authors: Mihai Criveti
8This module provides Pydantic models for request/response validation in the MCP Gateway.
9It implements schemas for:
10- Tool registration and invocation
11- Resource management and subscriptions
12- Prompt templates and arguments
13- Gateway federation
14- RPC message formats
15- Event messages
16- Admin interface
18The schemas ensure proper validation according to the MCP specification while adding
19gateway-specific extensions for federation support.
20"""
22# Standard
23import base64
24from datetime import datetime, timezone
25import json
26import logging
27from typing import Any, Dict, List, Literal, Optional, Union
29# Third-Party
30from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field, field_serializer, field_validator, model_validator, ValidationInfo
32# First-Party
33from mcpgateway.models import ImageContent
34from mcpgateway.models import Prompt as MCPPrompt
35from mcpgateway.models import Resource as MCPResource
36from mcpgateway.models import ResourceContent, TextContent
37from mcpgateway.models import Tool as MCPTool
38from mcpgateway.utils.services_auth import decode_auth, encode_auth
40logger = logging.getLogger(__name__)
43def to_camel_case(s: str) -> str:
44 """
45 Convert a string from snake_case to camelCase.
47 Args:
48 s (str): The string to be converted, which is assumed to be in snake_case.
50 Returns:
51 str: The string converted to camelCase.
53 Example:
54 >>> to_camel_case("hello_world_example")
55 'helloWorldExample'
56 """
57 return "".join(word.capitalize() if i else word for i, word in enumerate(s.split("_")))
60def encode_datetime(v: datetime) -> str:
61 """
62 Convert a datetime object to an ISO 8601 formatted string.
64 Args:
65 v (datetime): The datetime object to be encoded.
67 Returns:
68 str: The ISO 8601 formatted string representation of the datetime object.
70 Example:
71 >>> encode_datetime(datetime(2023, 5, 22, 14, 30, 0))
72 '2023-05-22T14:30:00'
73 """
74 return v.isoformat()
77# --- Base Model ---
78class BaseModelWithConfigDict(BaseModel):
79 """Base model with common configuration.
81 Provides:
82 - ORM mode for SQLAlchemy integration
83 - JSON encoders for datetime handling
84 - Automatic conversion from snake_case to camelCase for output
85 """
87 model_config = ConfigDict(
88 from_attributes=True,
89 alias_generator=to_camel_case,
90 populate_by_name=True,
91 use_enum_values=True,
92 extra="ignore",
93 json_schema_extra={"nullable": True},
94 )
96 def to_dict(self, use_alias: bool = False) -> Dict[str, Any]:
97 """
98 Converts the model instance into a dictionary representation.
100 Args:
101 use_alias (bool): Whether to use aliases for field names (default is False). If True,
102 field names will be converted using the alias generator function.
104 Returns:
105 Dict[str, Any]: A dictionary where keys are field names and values are corresponding field values,
106 with any nested models recursively converted to dictionaries.
107 """
108 output = {}
109 for key, value in self.dict(by_alias=use_alias).items():
110 output[key] = value if not isinstance(value, BaseModel) else value.to_dict(use_alias)
111 return output
114# --- Metrics Schemas ---
117class ToolMetrics(BaseModelWithConfigDict):
118 """
119 Represents the performance and execution statistics for a tool.
121 Attributes:
122 total_executions (int): Total number of tool invocations.
123 successful_executions (int): Number of successful tool invocations.
124 failed_executions (int): Number of failed tool invocations.
125 failure_rate (float): Failure rate (failed invocations / total invocations).
126 min_response_time (Optional[float]): Minimum response time in seconds.
127 max_response_time (Optional[float]): Maximum response time in seconds.
128 avg_response_time (Optional[float]): Average response time in seconds.
129 last_execution_time (Optional[datetime]): Timestamp of the most recent invocation.
130 """
132 total_executions: int = Field(..., description="Total number of tool invocations")
133 successful_executions: int = Field(..., description="Number of successful tool invocations")
134 failed_executions: int = Field(..., description="Number of failed tool invocations")
135 failure_rate: float = Field(..., description="Failure rate (failed invocations / total invocations)")
136 min_response_time: Optional[float] = Field(None, description="Minimum response time in seconds")
137 max_response_time: Optional[float] = Field(None, description="Maximum response time in seconds")
138 avg_response_time: Optional[float] = Field(None, description="Average response time in seconds")
139 last_execution_time: Optional[datetime] = Field(None, description="Timestamp of the most recent invocation")
142class ResourceMetrics(BaseModelWithConfigDict):
143 """
144 Represents the performance and execution statistics for a resource.
146 Attributes:
147 total_executions (int): Total number of resource invocations.
148 successful_executions (int): Number of successful resource invocations.
149 failed_executions (int): Number of failed resource invocations.
150 failure_rate (float): Failure rate (failed invocations / total invocations).
151 min_response_time (Optional[float]): Minimum response time in seconds.
152 max_response_time (Optional[float]): Maximum response time in seconds.
153 avg_response_time (Optional[float]): Average response time in seconds.
154 last_execution_time (Optional[datetime]): Timestamp of the most recent invocation.
155 """
157 total_executions: int = Field(..., description="Total number of resource invocations")
158 successful_executions: int = Field(..., description="Number of successful resource invocations")
159 failed_executions: int = Field(..., description="Number of failed resource invocations")
160 failure_rate: float = Field(..., description="Failure rate (failed invocations / total invocations)")
161 min_response_time: Optional[float] = Field(None, description="Minimum response time in seconds")
162 max_response_time: Optional[float] = Field(None, description="Maximum response time in seconds")
163 avg_response_time: Optional[float] = Field(None, description="Average response time in seconds")
164 last_execution_time: Optional[datetime] = Field(None, description="Timestamp of the most recent invocation")
167class ServerMetrics(BaseModelWithConfigDict):
168 """
169 Represents the performance and execution statistics for a server.
171 Attributes:
172 total_executions (int): Total number of server invocations.
173 successful_executions (int): Number of successful server invocations.
174 failed_executions (int): Number of failed server invocations.
175 failure_rate (float): Failure rate (failed invocations / total invocations).
176 min_response_time (Optional[float]): Minimum response time in seconds.
177 max_response_time (Optional[float]): Maximum response time in seconds.
178 avg_response_time (Optional[float]): Average response time in seconds.
179 last_execution_time (Optional[datetime]): Timestamp of the most recent invocation.
180 """
182 total_executions: int = Field(..., description="Total number of server invocations")
183 successful_executions: int = Field(..., description="Number of successful server invocations")
184 failed_executions: int = Field(..., description="Number of failed server invocations")
185 failure_rate: float = Field(..., description="Failure rate (failed invocations / total invocations)")
186 min_response_time: Optional[float] = Field(None, description="Minimum response time in seconds")
187 max_response_time: Optional[float] = Field(None, description="Maximum response time in seconds")
188 avg_response_time: Optional[float] = Field(None, description="Average response time in seconds")
189 last_execution_time: Optional[datetime] = Field(None, description="Timestamp of the most recent invocation")
192class PromptMetrics(BaseModelWithConfigDict):
193 """
194 Represents the performance and execution statistics for a prompt.
196 Attributes:
197 total_executions (int): Total number of prompt invocations.
198 successful_executions (int): Number of successful prompt invocations.
199 failed_executions (int): Number of failed prompt invocations.
200 failure_rate (float): Failure rate (failed invocations / total invocations).
201 min_response_time (Optional[float]): Minimum response time in seconds.
202 max_response_time (Optional[float]): Maximum response time in seconds.
203 avg_response_time (Optional[float]): Average response time in seconds.
204 last_execution_time (Optional[datetime]): Timestamp of the most recent invocation.
205 """
207 total_executions: int = Field(..., description="Total number of prompt invocations")
208 successful_executions: int = Field(..., description="Number of successful prompt invocations")
209 failed_executions: int = Field(..., description="Number of failed prompt invocations")
210 failure_rate: float = Field(..., description="Failure rate (failed invocations / total invocations)")
211 min_response_time: Optional[float] = Field(None, description="Minimum response time in seconds")
212 max_response_time: Optional[float] = Field(None, description="Maximum response time in seconds")
213 avg_response_time: Optional[float] = Field(None, description="Average response time in seconds")
214 last_execution_time: Optional[datetime] = Field(None, description="Timestamp of the most recent invocation")
217# --- JSON Path API modifier Schema
220class JsonPathModifier(BaseModelWithConfigDict):
221 """Schema for JSONPath queries.
223 Provides the structure for parsing JSONPath queries and optional mapping.
224 """
226 jsonpath: Optional[str] = Field(None, description="JSONPath expression for querying JSON data.")
227 mapping: Optional[Dict[str, str]] = Field(None, description="Mapping of fields from original data to output.")
230# --- Tool Schemas ---
231# Authentication model
232class AuthenticationValues(BaseModelWithConfigDict):
233 """Schema for all Authentications.
234 Provides the authentication values for different types of authentication.
235 """
237 auth_type: Optional[str] = Field(None, description="Type of authentication: basic, bearer, headers or None")
238 auth_value: Optional[str] = Field(None, description="Encoded Authentication values")
240 # Only For tool read and view tool
241 username: str = Field("", description="Username for basic authentication")
242 password: str = Field("", description="Password for basic authentication")
243 token: str = Field("", description="Bearer token for authentication")
244 auth_header_key: str = Field("", description="Key for custom headers authentication")
245 auth_header_value: str = Field("", description="Value for custom headers authentication")
248class ToolCreate(BaseModelWithConfigDict):
249 """Schema for creating a new tool.
251 Supports both MCP-compliant tools and REST integrations. Validates:
252 - Unique tool name
253 - Valid endpoint URL
254 - Valid JSON Schema for input validation
255 - Integration type: 'MCP' for MCP-compliant tools or 'REST' for REST integrations
256 - Request type (For REST-GET,POST,PUT,DELETE and for MCP-SSE,STDIO,STREAMABLEHTTP)
257 - Optional authentication credentials: BasicAuth or BearerTokenAuth or HeadersAuth.
258 """
260 name: str = Field(..., description="Unique name for the tool")
261 url: Union[str, AnyHttpUrl] = Field(None, description="Tool endpoint URL")
262 description: Optional[str] = Field(None, description="Tool description")
263 request_type: Literal["GET", "POST", "PUT", "DELETE", "SSE", "STDIO", "STREAMABLEHTTP"] = Field("SSE", description="HTTP method to be used for invoking the tool")
264 integration_type: Literal["MCP", "REST"] = Field("MCP", description="Tool integration type: 'MCP' for MCP-compliant tools, 'REST' for REST integrations")
265 headers: Optional[Dict[str, str]] = Field(None, description="Additional headers to send when invoking the tool")
266 input_schema: Optional[Dict[str, Any]] = Field(
267 default_factory=lambda: {"type": "object", "properties": {}},
268 description="JSON Schema for validating tool parameters",
269 )
270 annotations: Optional[Dict[str, Any]] = Field(
271 default_factory=dict,
272 description="Tool annotations for behavior hints (title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint)",
273 )
274 jsonpath_filter: Optional[str] = Field(default="", description="JSON modification filter")
275 auth: Optional[AuthenticationValues] = Field(None, description="Authentication credentials (Basic or Bearer Token or custom headers) if required")
276 gateway_id: Optional[str] = Field(None, description="id of gateway for the tool")
278 @model_validator(mode="before")
279 def assemble_auth(cls, values: Dict[str, Any]) -> Dict[str, Any]:
280 """
281 Assemble authentication information from separate keys if provided.
283 Looks for keys "auth_type", "auth_username", "auth_password", "auth_token", "auth_header_key" and "auth_header_value".
284 Constructs the "auth" field as a dictionary suitable for BasicAuth or BearerTokenAuth or HeadersAuth.
286 Args:
287 values: Dict with authentication information
289 Returns:
290 Dict: Reformatedd values dict
291 """
292 logger.debug(
293 "Assembling auth in ToolCreate with raw values",
294 extra={
295 "auth_type": values.get("auth_type"),
296 "auth_username": values.get("auth_username"),
297 "auth_password": values.get("auth_password"),
298 "auth_token": values.get("auth_token"),
299 "auth_header_key": values.get("auth_header_key"),
300 "auth_header_value": values.get("auth_header_value"),
301 },
302 )
304 auth_type = values.get("auth_type")
305 if auth_type: 305 ↛ 306line 305 didn't jump to line 306 because the condition on line 305 was never true
306 if auth_type.lower() == "basic":
307 creds = base64.b64encode(f"{values.get('auth_username', '')}:{values.get('auth_password', '')}".encode("utf-8")).decode()
308 encoded_auth = encode_auth({"Authorization": f"Basic {creds}"})
309 values["auth"] = {"auth_type": "basic", "auth_value": encoded_auth}
310 elif auth_type.lower() == "bearer":
311 encoded_auth = encode_auth({"Authorization": f"Bearer {values.get('auth_token', '')}"})
312 values["auth"] = {"auth_type": "bearer", "auth_value": encoded_auth}
313 elif auth_type.lower() == "authheaders":
314 encoded_auth = encode_auth({values.get("auth_header_key", ""): values.get("auth_header_value", "")})
315 values["auth"] = {"auth_type": "authheaders", "auth_value": encoded_auth}
316 return values
319class ToolUpdate(BaseModelWithConfigDict):
320 """Schema for updating an existing tool.
322 Similar to ToolCreate but all fields are optional to allow partial updates.
323 """
325 name: Optional[str] = Field(None, description="Unique name for the tool")
326 url: Optional[Union[str, AnyHttpUrl]] = Field(None, description="Tool endpoint URL")
327 description: Optional[str] = Field(None, description="Tool description")
328 request_type: Optional[Literal["GET", "POST", "PUT", "DELETE", "SSE", "STDIO", "STREAMABLEHTTP"]] = Field(None, description="HTTP method to be used for invoking the tool")
329 integration_type: Optional[Literal["MCP", "REST"]] = Field(None, description="Tool integration type")
330 headers: Optional[Dict[str, str]] = Field(None, description="Additional headers to send when invoking the tool")
331 input_schema: Optional[Dict[str, Any]] = Field(None, description="JSON Schema for validating tool parameters")
332 annotations: Optional[Dict[str, Any]] = Field(None, description="Tool annotations for behavior hints")
333 jsonpath_filter: Optional[str] = Field(None, description="JSON path filter for rpc tool calls")
334 auth: Optional[AuthenticationValues] = Field(None, description="Authentication credentials (Basic or Bearer Token or custom headers) if required")
335 gateway_id: Optional[str] = Field(None, description="id of gateway for the tool")
337 @model_validator(mode="before")
338 def assemble_auth(cls, values: Dict[str, Any]) -> Dict[str, Any]:
339 """
340 Assemble authentication information from separate keys if provided.
342 Looks for keys "auth_type", "auth_username", "auth_password", "auth_token", "auth_header_key" and "auth_header_value".
343 Constructs the "auth" field as a dictionary suitable for BasicAuth or BearerTokenAuth or HeadersAuth.
345 Args:
346 values: Dict with authentication information
348 Returns:
349 Dict: Reformatedd values dict
350 """
352 logger.debug(
353 "Assembling auth in ToolCreate with raw values",
354 extra={
355 "auth_type": values.get("auth_type"),
356 "auth_username": values.get("auth_username"),
357 "auth_password": values.get("auth_password"),
358 "auth_token": values.get("auth_token"),
359 "auth_header_key": values.get("auth_header_key"),
360 "auth_header_value": values.get("auth_header_value"),
361 },
362 )
364 auth_type = values.get("auth_type")
365 if auth_type: 365 ↛ 366line 365 didn't jump to line 366 because the condition on line 365 was never true
366 if auth_type.lower() == "basic":
367 creds = base64.b64encode(f"{values.get('auth_username', '')}:{values.get('auth_password', '')}".encode("utf-8")).decode()
368 encoded_auth = encode_auth({"Authorization": f"Basic {creds}"})
369 values["auth"] = {"auth_type": "basic", "auth_value": encoded_auth}
370 elif auth_type.lower() == "bearer":
371 encoded_auth = encode_auth({"Authorization": f"Bearer {values.get('auth_token', '')}"})
372 values["auth"] = {"auth_type": "bearer", "auth_value": encoded_auth}
373 elif auth_type.lower() == "authheaders":
374 encoded_auth = encode_auth({values.get("auth_header_key", ""): values.get("auth_header_value", "")})
375 values["auth"] = {"auth_type": "authheaders", "auth_value": encoded_auth}
376 return values
379class ToolRead(BaseModelWithConfigDict):
380 """Schema for reading tool information.
382 Includes all tool fields plus:
383 - Database ID
384 - Creation/update timestamps
385 - enabled: If Tool is enabled or disabled.
386 - reachable: If Tool is reachable or not.
387 - Gateway ID for federation
388 - Execution count indicating the number of times the tool has been executed.
389 - Metrics: Aggregated metrics for the tool invocations.
390 - Request type and authentication settings.
391 """
393 id: str
394 original_name: str
395 url: Optional[str]
396 description: Optional[str]
397 request_type: str
398 integration_type: str
399 headers: Optional[Dict[str, str]]
400 input_schema: Dict[str, Any]
401 annotations: Optional[Dict[str, Any]]
402 jsonpath_filter: Optional[str]
403 auth: Optional[AuthenticationValues]
404 created_at: datetime
405 updated_at: datetime
406 enabled: bool
407 reachable: bool
408 gateway_id: Optional[str]
409 execution_count: int
410 metrics: ToolMetrics
411 name: str
412 gateway_slug: str
413 original_name_slug: str
416class ToolInvocation(BaseModelWithConfigDict):
417 """Schema for tool invocation requests.
419 Captures:
420 - Tool name to invoke
421 - Arguments matching tool's input schema
422 """
424 name: str = Field(..., description="Name of tool to invoke")
425 arguments: Dict[str, Any] = Field(default_factory=dict, description="Arguments matching tool's input schema")
428class ToolResult(BaseModelWithConfigDict):
429 """Schema for tool invocation results.
431 Supports:
432 - Multiple content types (text/image)
433 - Error reporting
434 - Optional error messages
435 """
437 content: List[Union[TextContent, ImageContent]]
438 is_error: bool = False
439 error_message: Optional[str] = None
442class ResourceCreate(BaseModelWithConfigDict):
443 """Schema for creating a new resource.
445 Supports:
446 - Static resources with URI
447 - Template resources with parameters
448 - Both text and binary content
449 """
451 uri: str = Field(..., description="Unique URI for the resource")
452 name: str = Field(..., description="Human-readable resource name")
453 description: Optional[str] = Field(None, description="Resource description")
454 mime_type: Optional[str] = Field(None, description="Resource MIME type")
455 template: Optional[str] = Field(None, description="URI template for parameterized resources")
456 content: Union[str, bytes] = Field(..., description="Resource content (text or binary)")
459class ResourceUpdate(BaseModelWithConfigDict):
460 """Schema for updating an existing resource.
462 Similar to ResourceCreate but URI is not required and all fields are optional.
463 """
465 name: Optional[str] = Field(None, description="Human-readable resource name")
466 description: Optional[str] = Field(None, description="Resource description")
467 mime_type: Optional[str] = Field(None, description="Resource MIME type")
468 template: Optional[str] = Field(None, description="URI template for parameterized resources")
469 content: Optional[Union[str, bytes]] = Field(None, description="Resource content (text or binary)")
472class ResourceRead(BaseModelWithConfigDict):
473 """Schema for reading resource information.
475 Includes all resource fields plus:
476 - Database ID
477 - Content size
478 - Creation/update timestamps
479 - Active status
480 - Metrics: Aggregated metrics for the resource invocations.
481 """
483 id: int
484 uri: str
485 name: str
486 description: Optional[str]
487 mime_type: Optional[str]
488 size: Optional[int]
489 created_at: datetime
490 updated_at: datetime
491 is_active: bool
492 metrics: ResourceMetrics
495class ResourceSubscription(BaseModelWithConfigDict):
496 """Schema for resource subscriptions.
498 Tracks:
499 - Resource URI being subscribed to
500 - Unique subscriber identifier
501 """
503 uri: str = Field(..., description="URI of resource to subscribe to")
504 subscriber_id: str = Field(..., description="Unique subscriber identifier")
507class ResourceNotification(BaseModelWithConfigDict):
508 """Schema for resource update notifications.
510 Contains:
511 - Resource URI
512 - Updated content
513 - Update timestamp
514 """
516 uri: str
517 content: ResourceContent
518 timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
520 @field_serializer("timestamp")
521 def serialize_timestamp(self, dt: datetime) -> str:
522 """Serialize the `timestamp` field as an ISO 8601 string with UTC timezone.
524 Converts the given datetime to UTC and returns it in ISO 8601 format,
525 replacing the "+00:00" suffix with "Z" to indicate UTC explicitly.
527 Args:
528 dt (datetime): The datetime object to serialize.
530 Returns:
531 str: ISO 8601 formatted string in UTC, ending with 'Z'.
532 """
533 return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
536# --- Prompt Schemas ---
539class PromptArgument(BaseModelWithConfigDict):
540 """Schema for prompt template arguments.
542 Defines:
543 - Argument name
544 - Optional description
545 - Required flag
546 """
548 name: str = Field(..., description="Argument name")
549 description: Optional[str] = Field(None, description="Argument description")
550 required: bool = Field(default=False, description="Whether argument is required")
552 model_config: ConfigDict = ConfigDict(
553 **{
554 # start with every key from the base
555 **BaseModelWithConfigDict.model_config,
556 # override only json_schema_extra by merging the two dicts:
557 "json_schema_extra": {
558 **BaseModelWithConfigDict.model_config.get("json_schema_extra", {}),
559 "example": {
560 "name": "language",
561 "description": "Programming language",
562 "required": True,
563 },
564 },
565 }
566 )
569class PromptCreate(BaseModelWithConfigDict):
570 """Schema for creating a new prompt template.
572 Includes:
573 - Template name and description
574 - Template text
575 - Expected arguments
576 """
578 name: str = Field(..., description="Unique name for the prompt")
579 description: Optional[str] = Field(None, description="Prompt description")
580 template: str = Field(..., description="Prompt template text")
581 arguments: List[PromptArgument] = Field(default_factory=list, description="List of arguments for the template")
584class PromptUpdate(BaseModelWithConfigDict):
585 """Schema for updating an existing prompt.
587 Similar to PromptCreate but all fields are optional to allow partial updates.
588 """
590 name: Optional[str] = Field(None, description="Unique name for the prompt")
591 description: Optional[str] = Field(None, description="Prompt description")
592 template: Optional[str] = Field(None, description="Prompt template text")
593 arguments: Optional[List[PromptArgument]] = Field(None, description="List of arguments for the template")
596class PromptRead(BaseModelWithConfigDict):
597 """Schema for reading prompt information.
599 Includes all prompt fields plus:
600 - Database ID
601 - Creation/update timestamps
602 - Active status
603 - Metrics: Aggregated metrics for the prompt invocations.
604 """
606 id: int
607 name: str
608 description: Optional[str]
609 template: str
610 arguments: List[PromptArgument]
611 created_at: datetime
612 updated_at: datetime
613 is_active: bool
614 metrics: PromptMetrics
617class PromptInvocation(BaseModelWithConfigDict):
618 """Schema for prompt invocation requests.
620 Contains:
621 - Prompt name to use
622 - Arguments for template rendering
623 """
625 name: str = Field(..., description="Name of prompt to use")
626 arguments: Dict[str, str] = Field(default_factory=dict, description="Arguments for template rendering")
629# --- Gateway Schemas ---
632class GatewayCreate(BaseModelWithConfigDict):
633 """Schema for registering a new federation gateway.
635 Captures:
636 - Gateway name
637 - Endpoint URL
638 - Optional description
639 - Authentication type: basic, bearer, headers
640 - Authentication credentials: username/password or token or custom headers
641 """
643 name: str = Field(..., description="Unique name for the gateway")
644 url: Union[str, AnyHttpUrl] = Field(..., description="Gateway endpoint URL")
645 description: Optional[str] = Field(None, description="Gateway description")
646 transport: str = Field(default="SSE", description="Transport used by MCP server: SSE or STREAMABLEHTTP")
648 # Authorizations
649 auth_type: Optional[str] = Field(None, description="Type of authentication: basic, bearer, headers, or none")
650 # Fields for various types of authentication
651 auth_username: Optional[str] = Field(None, description="Username for basic authentication")
652 auth_password: Optional[str] = Field(None, description="Password for basic authentication")
653 auth_token: Optional[str] = Field(None, description="Token for bearer authentication")
654 auth_header_key: Optional[str] = Field(None, description="Key for custom headers authentication")
655 auth_header_value: Optional[str] = Field(None, description="Value for custom headers authentication")
657 # Adding `auth_value` as an alias for better access post-validation
658 auth_value: Optional[str] = Field(None, validate_default=True)
660 @field_validator("url", mode="before")
661 def ensure_url_scheme(cls, v: str) -> str:
662 """
663 Ensure URL has an http/https scheme.
665 Args:
666 v: Input url
668 Returns:
669 str: URL with correct schema
671 """
672 if isinstance(v, str) and not (v.startswith("http://") or v.startswith("https://")): 672 ↛ 673line 672 didn't jump to line 673 because the condition on line 672 was never true
673 return f"http://{v}"
674 return v
676 @field_validator("auth_value", mode="before")
677 def create_auth_value(cls, v, info):
678 """
679 This validator will run before the model is fully instantiated (mode="before")
680 It will process the auth fields based on auth_type and generate auth_value.
682 Args:
683 v: Input url
684 info: ValidationInfo containing auth_type
686 Returns:
687 str: Auth value
688 """
689 data = info.data
690 auth_type = data.get("auth_type")
692 if (auth_type is None) or (auth_type == ""): 692 ↛ 696line 692 didn't jump to line 696 because the condition on line 692 was always true
693 return v # If no auth_type is provided, no need to create auth_value
695 # Process the auth fields and generate auth_value based on auth_type
696 auth_value = cls._process_auth_fields(info)
698 return auth_value
700 @staticmethod
701 def _process_auth_fields(info: ValidationInfo) -> Optional[Dict[str, Any]]:
702 """
703 Processes the input authentication fields and returns the correct auth_value.
704 This method is called based on the selected auth_type.
706 Args:
707 info: ValidationInfo containing auth fields
709 Returns:
710 Dict with encoded auth
712 Raises:
713 ValueError: If auth_type is invalid
714 """
715 data = info.data
716 auth_type = data.get("auth_type")
718 if auth_type == "basic":
719 # For basic authentication, both username and password must be present
720 username = data.get("auth_username")
721 password = data.get("auth_password")
723 if not username or not password:
724 raise ValueError("For 'basic' auth, both 'auth_username' and 'auth_password' must be provided.")
726 creds = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode()
727 return encode_auth({"Authorization": f"Basic {creds}"})
729 if auth_type == "bearer":
730 # For bearer authentication, only token is required
731 token = data.get("auth_token")
733 if not token:
734 raise ValueError("For 'bearer' auth, 'auth_token' must be provided.")
736 return encode_auth({"Authorization": f"Bearer {token}"})
738 if auth_type == "authheaders":
739 # For headers authentication, both key and value must be present
740 header_key = data.get("auth_header_key")
741 header_value = data.get("auth_header_value")
743 if not header_key or not header_value:
744 raise ValueError("For 'headers' auth, both 'auth_header_key' and 'auth_header_value' must be provided.")
746 return encode_auth({header_key: header_value})
748 raise ValueError("Invalid 'auth_type'. Must be one of: basic, bearer, or headers.")
751class GatewayUpdate(BaseModelWithConfigDict):
752 """Schema for updating an existing federation gateway.
754 Similar to GatewayCreate but all fields are optional to allow partial updates.
755 """
757 name: Optional[str] = Field(None, description="Unique name for the gateway")
758 url: Optional[Union[str, AnyHttpUrl]] = Field(None, description="Gateway endpoint URL")
759 description: Optional[str] = Field(None, description="Gateway description")
760 transport: str = Field(default="SSE", description="Transport used by MCP server: SSE or STREAMABLEHTTP")
762 name: Optional[str] = Field(None, description="Unique name for the prompt")
763 # Authorizations
764 auth_type: Optional[str] = Field(None, description="auth_type: basic, bearer, headers or None")
765 auth_username: Optional[str] = Field(None, description="username for basic authentication")
766 auth_password: Optional[str] = Field(None, description="password for basic authentication")
767 auth_token: Optional[str] = Field(None, description="token for bearer authentication")
768 auth_header_key: Optional[str] = Field(None, description="key for custom headers authentication")
769 auth_header_value: Optional[str] = Field(None, description="vallue for custom headers authentication")
771 # Adding `auth_value` as an alias for better access post-validation
772 auth_value: Optional[str] = Field(None, validate_default=True)
774 @field_validator("url", mode="before")
775 def ensure_url_scheme(cls, v: Optional[str]) -> Optional[str]:
776 """
777 Ensure URL has an http/https scheme.
779 Args:
780 v: Input URL
782 Returns:
783 str: Validated URL
784 """
785 if isinstance(v, str) and not (v.startswith("http://") or v.startswith("https://")): 785 ↛ 786line 785 didn't jump to line 786 because the condition on line 785 was never true
786 return f"http://{v}"
787 return v
789 @field_validator("auth_value", mode="before")
790 def create_auth_value(cls, v, info):
791 """
792 This validator will run before the model is fully instantiated (mode="before")
793 It will process the auth fields based on auth_type and generate auth_value.
795 Args:
796 v: Input URL
797 info: ValidationInfo containing auth_type
799 Returns:
800 str: Auth value or URL
801 """
802 data = info.data
803 auth_type = data.get("auth_type")
805 if (auth_type is None) or (auth_type == ""): 805 ↛ 809line 805 didn't jump to line 809 because the condition on line 805 was always true
806 return v # If no auth_type is provided, no need to create auth_value
808 # Process the auth fields and generate auth_value based on auth_type
809 auth_value = cls._process_auth_fields(info)
811 return auth_value
813 @staticmethod
814 def _process_auth_fields(values: Dict[str, Any]) -> Optional[Dict[str, Any]]:
815 """
816 Processes the input authentication fields and returns the correct auth_value.
817 This method is called based on the selected auth_type.
819 Args:
820 values: Dict container auth information auth_type, auth_username, auth_password, auth_token, auth_header_key and auth_header_value
822 Returns:
823 dict: Encoded auth information
825 Raises:
826 ValueError: If auth type is invalid
827 """
828 auth_type = values.get("auth_type")
830 if auth_type == "basic":
831 # For basic authentication, both username and password must be present
832 username = values.get("auth_username")
833 password = values.get("auth_password")
835 if not username or not password:
836 raise ValueError("For 'basic' auth, both 'auth_username' and 'auth_password' must be provided.")
838 creds = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode()
839 return encode_auth({"Authorization": f"Basic {creds}"})
841 if auth_type == "bearer":
842 # For bearer authentication, only token is required
843 token = values.get("auth_token")
845 if not token:
846 raise ValueError("For 'bearer' auth, 'auth_token' must be provided.")
848 return encode_auth({"Authorization": f"Bearer {token}"})
850 if auth_type == "authheaders":
851 # For headers authentication, both key and value must be present
852 header_key = values.get("auth_header_key")
853 header_value = values.get("auth_header_value")
855 if not header_key or not header_value:
856 raise ValueError("For 'headers' auth, both 'auth_header_key' and 'auth_header_value' must be provided.")
858 return encode_auth({header_key: header_value})
860 raise ValueError("Invalid 'auth_type'. Must be one of: basic, bearer, or headers.")
863class GatewayRead(BaseModelWithConfigDict):
864 """Schema for reading gateway information.
866 Includes all gateway fields plus:
867 - Database ID
868 - Capabilities dictionary
869 - Creation/update timestamps
870 - enabled status
871 - reachable status
872 - Last seen timestamp
873 - Authentication type: basic, bearer, headers
874 - Authentication value: username/password or token or custom headers
876 Auto Populated fields:
877 - Authentication username: for basic auth
878 - Authentication password: for basic auth
879 - Authentication token: for bearer auth
880 - Authentication header key: for headers auth
881 - Authentication header value: for headers auth
882 """
884 id: str = Field(None, description="Unique ID of the gateway")
885 name: str = Field(..., description="Unique name for the gateway")
886 url: str = Field(..., description="Gateway endpoint URL")
887 description: Optional[str] = Field(None, description="Gateway description")
888 transport: str = Field(default="SSE", description="Transport used by MCP server: SSE or STREAMABLEHTTP")
889 capabilities: Dict[str, Any] = Field(default_factory=dict, description="Gateway capabilities")
890 created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), description="Creation timestamp")
891 updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), description="Last update timestamp")
892 enabled: bool = Field(default=True, description="Is the gateway enabled?")
893 reachable: bool = Field(default=True, description="Is the gateway reachable/online?")
895 last_seen: Optional[datetime] = Field(default_factory=lambda: datetime.now(timezone.utc), description="Last seen timestamp")
897 # Authorizations
898 auth_type: Optional[str] = Field(None, description="auth_type: basic, bearer, headers or None")
899 auth_value: Optional[str] = Field(None, description="auth value: username/password or token or custom headers")
901 # auth_value will populate the following fields
902 auth_username: Optional[str] = Field(None, description="username for basic authentication")
903 auth_password: Optional[str] = Field(None, description="password for basic authentication")
904 auth_token: Optional[str] = Field(None, description="token for bearer authentication")
905 auth_header_key: Optional[str] = Field(None, description="key for custom headers authentication")
906 auth_header_value: Optional[str] = Field(None, description="vallue for custom headers authentication")
908 slug: str = Field(None, description="Slug for gateway endpoint URL")
910 # This will be the main method to automatically populate fields
911 @model_validator(mode="after")
912 def _populate_auth(cls, values: Dict[str, Any]) -> Dict[str, Any]:
913 auth_type = values.auth_type
914 auth_value_encoded = values.auth_value
915 auth_value = decode_auth(auth_value_encoded)
916 if auth_type == "basic": 916 ↛ 917line 916 didn't jump to line 917 because the condition on line 916 was never true
917 u = auth_value.get("username")
918 p = auth_value.get("password")
919 if not u or not p:
920 raise ValueError("basic auth requires both username and password")
921 values.auth_username, values.auth_password = u, p
923 elif auth_type == "bearer":
924 auth = auth_value.get("Authorization")
925 if not (isinstance(auth, str) and auth.startswith("Bearer ")): 925 ↛ 926line 925 didn't jump to line 926 because the condition on line 925 was never true
926 raise ValueError("bearer auth requires an Authorization header of the form 'Bearer <token>'")
927 values.auth_token = auth.removeprefix("Bearer ")
929 elif auth_type == "authheaders": 929 ↛ 931line 929 didn't jump to line 931 because the condition on line 929 was never true
930 # must be exactly one header
931 if len(auth_value) != 1:
932 raise ValueError("authheaders requires exactly one key/value pair")
933 k, v = next(iter(auth_value.items()))
934 values.auth_header_key, values.auth_header_value = k, v
936 return values
939class FederatedTool(BaseModelWithConfigDict):
940 """Schema for tools provided by federated gateways.
942 Contains:
943 - Tool definition
944 - Source gateway information
945 """
947 tool: MCPTool
948 gateway_id: str
949 gateway_name: str
950 gateway_url: str
953class FederatedResource(BaseModelWithConfigDict):
954 """Schema for resources from federated gateways.
956 Contains:
957 - Resource definition
958 - Source gateway information
959 """
961 resource: MCPResource
962 gateway_id: str
963 gateway_name: str
964 gateway_url: str
967class FederatedPrompt(BaseModelWithConfigDict):
968 """Schema for prompts from federated gateways.
970 Contains:
971 - Prompt definition
972 - Source gateway information
973 """
975 prompt: MCPPrompt
976 gateway_id: str
977 gateway_name: str
978 gateway_url: str
981# --- RPC Schemas ---
984class RPCRequest(BaseModelWithConfigDict):
985 """Schema for JSON-RPC 2.0 requests.
987 Validates:
988 - Protocol version
989 - Method name
990 - Optional parameters
991 - Optional request ID
992 """
994 jsonrpc: Literal["2.0"]
995 method: str
996 params: Optional[Dict[str, Any]] = None
997 id: Optional[Union[int, str]] = None
1000class RPCResponse(BaseModelWithConfigDict):
1001 """Schema for JSON-RPC 2.0 responses.
1003 Contains:
1004 - Protocol version
1005 - Result or error
1006 - Request ID
1007 """
1009 jsonrpc: Literal["2.0"]
1010 result: Optional[Any] = None
1011 error: Optional[Dict[str, Any]] = None
1012 id: Optional[Union[int, str]] = None
1015# --- Event and Admin Schemas ---
1018class EventMessage(BaseModelWithConfigDict):
1019 """Schema for SSE event messages.
1021 Includes:
1022 - Event type
1023 - Event data payload
1024 - Event timestamp
1025 """
1027 type: str = Field(..., description="Event type (tool_added, resource_updated, etc)")
1028 data: Dict[str, Any] = Field(..., description="Event payload")
1029 timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
1031 @field_serializer("timestamp")
1032 def serialize_timestamp(self, dt: datetime) -> str:
1033 """
1034 Serialize the `timestamp` field as an ISO 8601 string with UTC timezone.
1036 Converts the given datetime to UTC and returns it in ISO 8601 format,
1037 replacing the "+00:00" suffix with "Z" to indicate UTC explicitly.
1039 Args:
1040 dt (datetime): The datetime object to serialize.
1042 Returns:
1043 str: ISO 8601 formatted string in UTC, ending with 'Z'.
1044 """
1045 return dt.astimezone(timezone.utc).isoformat().replace("+00:00", "Z")
1048class AdminToolCreate(BaseModelWithConfigDict):
1049 """Schema for creating tools via admin UI.
1051 Handles:
1052 - Basic tool information
1053 - JSON string inputs for headers/schema
1054 """
1056 name: str
1057 url: str
1058 description: Optional[str] = None
1059 integration_type: str = "MCP"
1060 headers: Optional[str] = None # JSON string
1061 input_schema: Optional[str] = None # JSON string
1063 @field_validator("headers", "input_schema")
1064 def validate_json(cls, v: Optional[str]) -> Optional[Dict[str, Any]]:
1065 """
1066 Validate and parse JSON string inputs.
1068 Args:
1069 v: Input string
1071 Returns:
1072 dict: Output JSON version of v
1074 Raises:
1075 ValueError: When unable to convert to JSON
1076 """
1077 if not v: 1077 ↛ 1078line 1077 didn't jump to line 1078 because the condition on line 1077 was never true
1078 return None
1079 try:
1080 return json.loads(v)
1081 except json.JSONDecodeError:
1082 raise ValueError("Invalid JSON")
1085class AdminGatewayCreate(BaseModelWithConfigDict):
1086 """Schema for creating gateways via admin UI.
1088 Captures:
1089 - Gateway name
1090 - Endpoint URL
1091 - Optional description
1092 """
1094 name: str
1095 url: str
1096 description: Optional[str] = None
1099# --- New Schemas for Status Toggle Operations ---
1102class StatusToggleRequest(BaseModelWithConfigDict):
1103 """Request schema for toggling active status."""
1105 activate: bool = Field(..., description="Whether to activate (true) or deactivate (false) the item")
1108class StatusToggleResponse(BaseModelWithConfigDict):
1109 """Response schema for status toggle operations."""
1111 id: int
1112 name: str
1113 is_active: bool
1114 message: str = Field(..., description="Success message")
1117# --- Optional Filter Parameters for Listing Operations ---
1120class ListFilters(BaseModelWithConfigDict):
1121 """Filtering options for list operations."""
1123 include_inactive: bool = Field(False, description="Whether to include inactive items in the results")
1126# --- Server Schemas ---
1129class ServerCreate(BaseModelWithConfigDict):
1130 """Schema for creating a new server.
1132 Attributes:
1133 name: The server's name (required).
1134 description: Optional text description.
1135 icon: Optional URL for the server's icon.
1136 associated_tools: Optional list of tool IDs (as strings).
1137 associated_resources: Optional list of resource IDs (as strings).
1138 associated_prompts: Optional list of prompt IDs (as strings).
1139 """
1141 name: str = Field(..., description="The server's name")
1142 description: Optional[str] = Field(None, description="Server description")
1143 icon: Optional[str] = Field(None, description="URL for the server's icon")
1144 associated_tools: Optional[List[str]] = Field(None, description="Comma-separated tool IDs")
1145 associated_resources: Optional[List[str]] = Field(None, description="Comma-separated resource IDs")
1146 associated_prompts: Optional[List[str]] = Field(None, description="Comma-separated prompt IDs")
1148 @field_validator("associated_tools", "associated_resources", "associated_prompts", mode="before")
1149 def split_comma_separated(cls, v):
1150 """
1151 Splits a comma-separated string into a list of strings if needed.
1153 Args:
1154 v: Input string
1156 Returns:
1157 list: Comma separated array of input string
1158 """
1159 if isinstance(v, str):
1160 return [item.strip() for item in v.split(",") if item.strip()]
1161 return v
1164class ServerUpdate(BaseModelWithConfigDict):
1165 """Schema for updating an existing server.
1167 All fields are optional to allow partial updates.
1168 """
1170 name: Optional[str] = Field(None, description="The server's name")
1171 description: Optional[str] = Field(None, description="Server description")
1172 icon: Optional[str] = Field(None, description="URL for the server's icon")
1173 associated_tools: Optional[List[str]] = Field(None, description="Comma-separated tool IDs")
1174 associated_resources: Optional[List[str]] = Field(None, description="Comma-separated resource IDs")
1175 associated_prompts: Optional[List[str]] = Field(None, description="Comma-separated prompt IDs")
1177 @field_validator("associated_tools", "associated_resources", "associated_prompts", mode="before")
1178 def split_comma_separated(cls, v):
1179 """
1180 Splits a comma-separated string into a list of strings if needed.
1182 Args:
1183 v: Input string
1185 Returns:
1186 list: Comma separated array of input string
1187 """
1188 if isinstance(v, str):
1189 return [item.strip() for item in v.split(",") if item.strip()]
1190 return v
1193class ServerRead(BaseModelWithConfigDict):
1194 """Schema for reading server information.
1196 Includes all server fields plus:
1197 - Database ID
1198 - Associated tool, resource, and prompt IDs
1199 - Creation/update timestamps
1200 - Active status
1201 - Metrics: Aggregated metrics for the server invocations.
1202 """
1204 id: str
1205 name: str
1206 description: Optional[str]
1207 icon: Optional[str]
1208 created_at: datetime
1209 updated_at: datetime
1210 is_active: bool
1211 associated_tools: List[str] = []
1212 associated_resources: List[int] = []
1213 associated_prompts: List[int] = []
1214 metrics: ServerMetrics
1216 @model_validator(mode="before")
1217 def populate_associated_ids(cls, values):
1218 """
1219 Pre-validation method that converts associated objects to their 'id'.
1221 This method checks 'associated_tools', 'associated_resources', and
1222 'associated_prompts' in the input and replaces each object with its `id`
1223 if present.
1225 Args:
1226 values (dict): The input values.
1228 Returns:
1229 dict: Updated values with object ids, or the original values if no
1230 changes are made.
1231 """
1232 # If values is not a dict (e.g. it's a Server instance), convert it
1233 if not isinstance(values, dict): 1233 ↛ 1234line 1233 didn't jump to line 1234 because the condition on line 1233 was never true
1234 try:
1235 values = vars(values)
1236 except Exception:
1237 return values
1238 if "associated_tools" in values and values["associated_tools"]: 1238 ↛ 1240line 1238 didn't jump to line 1240 because the condition on line 1238 was always true
1239 values["associated_tools"] = [tool.id if hasattr(tool, "id") else tool for tool in values["associated_tools"]]
1240 if "associated_resources" in values and values["associated_resources"]:
1241 values["associated_resources"] = [res.id if hasattr(res, "id") else res for res in values["associated_resources"]]
1242 if "associated_prompts" in values and values["associated_prompts"]:
1243 values["associated_prompts"] = [prompt.id if hasattr(prompt, "id") else prompt for prompt in values["associated_prompts"]]
1244 return values
1247class GatewayTestRequest(BaseModelWithConfigDict):
1248 """Schema for testing gateway connectivity.
1250 Includes the HTTP method, base URL, path, optional headers, and body.
1251 """
1253 method: str = Field(..., description="HTTP method to test (GET, POST, etc.)")
1254 base_url: AnyHttpUrl = Field(..., description="Base URL of the gateway to test")
1255 path: str = Field(..., description="Path to append to the base URL")
1256 headers: Optional[Dict[str, str]] = Field(None, description="Optional headers for the request")
1257 body: Optional[Union[str, Dict[str, Any]]] = Field(None, description="Optional body for the request, can be a string or JSON object")
1260class GatewayTestResponse(BaseModelWithConfigDict):
1261 """Schema for the response from a gateway test request.
1263 Contains:
1264 - HTTP status code
1265 - Latency in milliseconds
1266 - Optional response body, which can be a string or JSON object
1267 """
1269 status_code: int = Field(..., description="HTTP status code returned by the gateway")
1270 latency_ms: int = Field(..., description="Latency of the request in milliseconds")
1271 body: Optional[Union[str, Dict[str, Any]]] = Field(None, description="Response body, can be a string or JSON object")