Coverage for mcpgateway/schemas.py: 74%

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

2"""MCP Gateway Schema Definitions.

3

4 Copyright 2025

5 SPDX-License-Identifier: Apache-2.0

6 Authors: Mihai Criveti

7

8 This module provides Pydantic models for request/response validation in the MCP Gateway.

9 It 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

17

18 The schemas ensure proper validation according to the MCP specification while adding

19 gateway-specific extensions for federation support.

20"""

21

22# Standard

23 import base64

24 from datetime import datetime, timezone

25 import json

26 import logging

27 from typing import Any, Dict, List, Literal, Optional, Union

28

29# Third-Party

30 from pydantic import AnyHttpUrl, BaseModel, ConfigDict, Field, field_serializer, field_validator, model_validator, ValidationInfo

31

32# First-Party

33 from mcpgateway.models import ImageContent

34 from mcpgateway.models import Prompt as MCPPrompt

35 from mcpgateway.models import Resource as MCPResource

36 from mcpgateway.models import ResourceContent, TextContent

37 from mcpgateway.models import Tool as MCPTool

38 from mcpgateway.utils.services_auth import decode_auth, encode_auth

39

40 logger = logging.getLogger(__name__)

41

42

43 def to_camel_case(s: str) -> str:

44 """

45 Convert a string from snake_case to camelCase.

46

47 Args:

48 s (str): The string to be converted, which is assumed to be in snake_case.

49

50 Returns:

51 str: The string converted to camelCase.

52

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("_")))

58

59

60 def encode_datetime(v: datetime) -> str:

61 """

62 Convert a datetime object to an ISO 8601 formatted string.

63

64 Args:

65 v (datetime): The datetime object to be encoded.

66

67 Returns:

68 str: The ISO 8601 formatted string representation of the datetime object.

69

70 Example:

71 >>> encode_datetime(datetime(2023, 5, 22, 14, 30, 0))

72 '2023-05-22T14:30:00'

73 """

74 return v.isoformat()

75

76

77# --- Base Model ---

78 class BaseModelWithConfigDict(BaseModel):

79 """Base model with common configuration.

80

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 """

86

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 )

95

96 def to_dict(self, use_alias: bool = False) -> Dict[str, Any]:

97 """

98 Converts the model instance into a dictionary representation.

99

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.

103

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

112

113

114# --- Metrics Schemas ---

115

116

117 class ToolMetrics(BaseModelWithConfigDict):

118 """

119 Represents the performance and execution statistics for a tool.

120

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 """

131

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")

140

141

142 class ResourceMetrics(BaseModelWithConfigDict):

143 """

144 Represents the performance and execution statistics for a resource.

145

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 """

156

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")

165

166

167 class ServerMetrics(BaseModelWithConfigDict):

168 """

169 Represents the performance and execution statistics for a server.

170

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 """

181

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")

190

191

192 class PromptMetrics(BaseModelWithConfigDict):

193 """

194 Represents the performance and execution statistics for a prompt.

195

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 """

206

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")

215

216

217# --- JSON Path API modifier Schema

218

219

220 class JsonPathModifier(BaseModelWithConfigDict):

221 """Schema for JSONPath queries.

222

223 Provides the structure for parsing JSONPath queries and optional mapping.

224 """

225

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.")

228

229

230# --- Tool Schemas ---

231# Authentication model

232 class AuthenticationValues(BaseModelWithConfigDict):

233 """Schema for all Authentications.

234 Provides the authentication values for different types of authentication.

235 """

236

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")

239

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")

246

247

248 class ToolCreate(BaseModelWithConfigDict):

249 """Schema for creating a new tool.

250

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 """

259

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")

277

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.

282

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.

285

286 Args:

287 values: Dict with authentication information

288

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 )

303

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

317

318

319 class ToolUpdate(BaseModelWithConfigDict):

320 """Schema for updating an existing tool.

321

322 Similar to ToolCreate but all fields are optional to allow partial updates.

323 """

324

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")

336

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.

341

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.

344

345 Args:

346 values: Dict with authentication information

347

348 Returns:

349 Dict: Reformatedd values dict

350 """

351

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 )

363

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

377

378

379 class ToolRead(BaseModelWithConfigDict):

380 """Schema for reading tool information.

381

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 """

392

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

414

415

416 class ToolInvocation(BaseModelWithConfigDict):

417 """Schema for tool invocation requests.

418

419 Captures:

420 - Tool name to invoke

421 - Arguments matching tool's input schema

422 """

423

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")

426

427

428 class ToolResult(BaseModelWithConfigDict):

429 """Schema for tool invocation results.

430

431 Supports:

432 - Multiple content types (text/image)

433 - Error reporting

434 - Optional error messages

435 """

436

437 content: List[Union[TextContent, ImageContent]]

438 is_error: bool = False

439 error_message: Optional[str] = None

440

441

442 class ResourceCreate(BaseModelWithConfigDict):

443 """Schema for creating a new resource.

444

445 Supports:

446 - Static resources with URI

447 - Template resources with parameters

448 - Both text and binary content

449 """

450

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)")

457

458

459 class ResourceUpdate(BaseModelWithConfigDict):

460 """Schema for updating an existing resource.

461

462 Similar to ResourceCreate but URI is not required and all fields are optional.

463 """

464

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)")

470

471

472 class ResourceRead(BaseModelWithConfigDict):

473 """Schema for reading resource information.

474

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 """

482

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

493

494

495 class ResourceSubscription(BaseModelWithConfigDict):

496 """Schema for resource subscriptions.

497

498 Tracks:

499 - Resource URI being subscribed to

500 - Unique subscriber identifier

501 """

502

503 uri: str = Field(..., description="URI of resource to subscribe to")

504 subscriber_id: str = Field(..., description="Unique subscriber identifier")

505

506

507 class ResourceNotification(BaseModelWithConfigDict):

508 """Schema for resource update notifications.

509

510 Contains:

511 - Resource URI

512 - Updated content

513 - Update timestamp

514 """

515

516 uri: str

517 content: ResourceContent

518 timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))

519

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.

523

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.

526

527 Args:

528 dt (datetime): The datetime object to serialize.

529

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")

534

535

536# --- Prompt Schemas ---

537

538

539 class PromptArgument(BaseModelWithConfigDict):

540 """Schema for prompt template arguments.

541

542 Defines:

543 - Argument name

544 - Optional description

545 - Required flag

546 """

547

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")

551

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 )

567

568

569 class PromptCreate(BaseModelWithConfigDict):

570 """Schema for creating a new prompt template.

571

572 Includes:

573 - Template name and description

574 - Template text

575 - Expected arguments

576 """

577

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")

582

583

584 class PromptUpdate(BaseModelWithConfigDict):

585 """Schema for updating an existing prompt.

586

587 Similar to PromptCreate but all fields are optional to allow partial updates.

588 """

589

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")

594

595

596 class PromptRead(BaseModelWithConfigDict):

597 """Schema for reading prompt information.

598

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 """

605

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

615

616

617 class PromptInvocation(BaseModelWithConfigDict):

618 """Schema for prompt invocation requests.

619

620 Contains:

621 - Prompt name to use

622 - Arguments for template rendering

623 """

624

625 name: str = Field(..., description="Name of prompt to use")

626 arguments: Dict[str, str] = Field(default_factory=dict, description="Arguments for template rendering")

627

628

629# --- Gateway Schemas ---

630

631

632 class GatewayCreate(BaseModelWithConfigDict):

633 """Schema for registering a new federation gateway.

634

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 """

642

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")

647

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")

656

657 # Adding `auth_value` as an alias for better access post-validation

658 auth_value: Optional[str] = Field(None, validate_default=True)

659

660 @field_validator("url", mode="before")

661 def ensure_url_scheme(cls, v: str) -> str:

662 """

663 Ensure URL has an http/https scheme.

664

665 Args:

666 v: Input url

667

668 Returns:

669 str: URL with correct schema

670

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

675

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.

681

682 Args:

683 v: Input url

684 info: ValidationInfo containing auth_type

685

686 Returns:

687 str: Auth value

688 """

689 data = info.data

690 auth_type = data.get("auth_type")

691

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

694

695 # Process the auth fields and generate auth_value based on auth_type

696 auth_value = cls._process_auth_fields(info)

697

698 return auth_value

699

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.

705

706 Args:

707 info: ValidationInfo containing auth fields

708

709 Returns:

710 Dict with encoded auth

711

712 Raises:

713 ValueError: If auth_type is invalid

714 """

715 data = info.data

716 auth_type = data.get("auth_type")

717

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")

722

723 if not username or not password:

724 raise ValueError("For 'basic' auth, both 'auth_username' and 'auth_password' must be provided.")

725

726 creds = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode()

727 return encode_auth({"Authorization": f"Basic {creds}"})

728

729 if auth_type == "bearer":

730 # For bearer authentication, only token is required

731 token = data.get("auth_token")

732

733 if not token:

734 raise ValueError("For 'bearer' auth, 'auth_token' must be provided.")

735

736 return encode_auth({"Authorization": f"Bearer {token}"})

737

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")

742

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.")

745

746 return encode_auth({header_key: header_value})

747

748 raise ValueError("Invalid 'auth_type'. Must be one of: basic, bearer, or headers.")

749

750

751 class GatewayUpdate(BaseModelWithConfigDict):

752 """Schema for updating an existing federation gateway.

753

754 Similar to GatewayCreate but all fields are optional to allow partial updates.

755 """

756

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")

761

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")

770

771 # Adding `auth_value` as an alias for better access post-validation

772 auth_value: Optional[str] = Field(None, validate_default=True)

773

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.

778

779 Args:

780 v: Input URL

781

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

788

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.

794

795 Args:

796 v: Input URL

797 info: ValidationInfo containing auth_type

798

799 Returns:

800 str: Auth value or URL

801 """

802 data = info.data

803 auth_type = data.get("auth_type")

804

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

807

808 # Process the auth fields and generate auth_value based on auth_type

809 auth_value = cls._process_auth_fields(info)

810

811 return auth_value

812

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.

818

819 Args:

820 values: Dict container auth information auth_type, auth_username, auth_password, auth_token, auth_header_key and auth_header_value

821

822 Returns:

823 dict: Encoded auth information

824

825 Raises:

826 ValueError: If auth type is invalid

827 """

828 auth_type = values.get("auth_type")

829

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")

834

835 if not username or not password:

836 raise ValueError("For 'basic' auth, both 'auth_username' and 'auth_password' must be provided.")

837

838 creds = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode()

839 return encode_auth({"Authorization": f"Basic {creds}"})

840

841 if auth_type == "bearer":

842 # For bearer authentication, only token is required

843 token = values.get("auth_token")

844

845 if not token:

846 raise ValueError("For 'bearer' auth, 'auth_token' must be provided.")

847

848 return encode_auth({"Authorization": f"Bearer {token}"})

849

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")

854

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.")

857

858 return encode_auth({header_key: header_value})

859

860 raise ValueError("Invalid 'auth_type'. Must be one of: basic, bearer, or headers.")

861

862

863 class GatewayRead(BaseModelWithConfigDict):

864 """Schema for reading gateway information.

865

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

875

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 """

883

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?")

894

895 last_seen: Optional[datetime] = Field(default_factory=lambda: datetime.now(timezone.utc), description="Last seen timestamp")

896

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")

900

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")

907

908 slug: str = Field(None, description="Slug for gateway endpoint URL")

909

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

922

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 ")

928

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

935

936 return values

937

938

939 class FederatedTool(BaseModelWithConfigDict):

940 """Schema for tools provided by federated gateways.

941

942 Contains:

943 - Tool definition

944 - Source gateway information

945 """

946

947 tool: MCPTool

948 gateway_id: str

949 gateway_name: str

950 gateway_url: str

951

952

953 class FederatedResource(BaseModelWithConfigDict):

954 """Schema for resources from federated gateways.

955

956 Contains:

957 - Resource definition

958 - Source gateway information

959 """

960

961 resource: MCPResource

962 gateway_id: str

963 gateway_name: str

964 gateway_url: str

965

966

967 class FederatedPrompt(BaseModelWithConfigDict):

968 """Schema for prompts from federated gateways.

969

970 Contains:

971 - Prompt definition

972 - Source gateway information

973 """

974

975 prompt: MCPPrompt

976 gateway_id: str

977 gateway_name: str

978 gateway_url: str

979

980

981# --- RPC Schemas ---

982

983

984 class RPCRequest(BaseModelWithConfigDict):

985 """Schema for JSON-RPC 2.0 requests.

986

987 Validates:

988 - Protocol version

989 - Method name

990 - Optional parameters

991 - Optional request ID

992 """

993

994 jsonrpc: Literal["2.0"]

995 method: str

996 params: Optional[Dict[str, Any]] = None

997 id: Optional[Union[int, str]] = None

998

999

1000 class RPCResponse(BaseModelWithConfigDict):

1001 """Schema for JSON-RPC 2.0 responses.

1002

1003 Contains:

1004 - Protocol version

1005 - Result or error

1006 - Request ID

1007 """

1008

1009 jsonrpc: Literal["2.0"]

1010 result: Optional[Any] = None

1011 error: Optional[Dict[str, Any]] = None

1012 id: Optional[Union[int, str]] = None

1013

1014

1015# --- Event and Admin Schemas ---

1016

1017

1018 class EventMessage(BaseModelWithConfigDict):

1019 """Schema for SSE event messages.

1020

1021 Includes:

1022 - Event type

1023 - Event data payload

1024 - Event timestamp

1025 """

1026

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))

1030

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.

1035

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.

1038

1039 Args:

1040 dt (datetime): The datetime object to serialize.

1041

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")

1046

1047

1048 class AdminToolCreate(BaseModelWithConfigDict):

1049 """Schema for creating tools via admin UI.

1050

1051 Handles:

1052 - Basic tool information

1053 - JSON string inputs for headers/schema

1054 """

1055

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

1062

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.

1067

1068 Args:

1069 v: Input string

1070

1071 Returns:

1072 dict: Output JSON version of v

1073

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")

1083

1084

1085 class AdminGatewayCreate(BaseModelWithConfigDict):

1086 """Schema for creating gateways via admin UI.

1087

1088 Captures:

1089 - Gateway name

1090 - Endpoint URL

1091 - Optional description

1092 """

1093

1094 name: str

1095 url: str

1096 description: Optional[str] = None

1097

1098

1099# --- New Schemas for Status Toggle Operations ---

1100

1101

1102 class StatusToggleRequest(BaseModelWithConfigDict):

1103 """Request schema for toggling active status."""

1104

1105 activate: bool = Field(..., description="Whether to activate (true) or deactivate (false) the item")

1106

1107

1108 class StatusToggleResponse(BaseModelWithConfigDict):

1109 """Response schema for status toggle operations."""

1110

1111 id: int

1112 name: str

1113 is_active: bool

1114 message: str = Field(..., description="Success message")

1115

1116

1117# --- Optional Filter Parameters for Listing Operations ---

1118

1119

1120 class ListFilters(BaseModelWithConfigDict):

1121 """Filtering options for list operations."""

1122

1123 include_inactive: bool = Field(False, description="Whether to include inactive items in the results")

1124

1125

1126# --- Server Schemas ---

1127

1128

1129 class ServerCreate(BaseModelWithConfigDict):

1130 """Schema for creating a new server.

1131

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 """

1140

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")

1147

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.

1152

1153 Args:

1154 v: Input string

1155

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

1162

1163

1164 class ServerUpdate(BaseModelWithConfigDict):

1165 """Schema for updating an existing server.

1166

1167 All fields are optional to allow partial updates.

1168 """

1169

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")

1176

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.

1181

1182 Args:

1183 v: Input string

1184

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

1191

1192

1193 class ServerRead(BaseModelWithConfigDict):

1194 """Schema for reading server information.

1195

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 """

1203

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

1215

1216 @model_validator(mode="before")

1217 def populate_associated_ids(cls, values):

1218 """

1219 Pre-validation method that converts associated objects to their 'id'.

1220

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.

1224

1225 Args:

1226 values (dict): The input values.

1227

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

1245

1246

1247 class GatewayTestRequest(BaseModelWithConfigDict):

1248 """Schema for testing gateway connectivity.

1249

1250 Includes the HTTP method, base URL, path, optional headers, and body.

1251 """

1252

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")

1258

1259

1260 class GatewayTestResponse(BaseModelWithConfigDict):

1261 """Schema for the response from a gateway test request.

1262

1263 Contains:

1264 - HTTP status code

1265 - Latency in milliseconds

1266 - Optional response body, which can be a string or JSON object

1267 """

1268

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")