Skip to content

Commit 2912750

Browse files
mattsanchezMatt Sanchezcrivetimihai
authored
feat: Tool output schema support in db and service layer (IBM#1263)
* feat: Add outputSchema field support to tools - Add output_schema column to Tool database model (db.py) - Add output_schema field to Tool Pydantic model (models.py) - Add output_schema field to ToolCreate, ToolUpdate, and ToolRead schemas - Support both 'output_schema' and 'outputSchema' (camelCase) via alias - Create database migration to add output_schema column to tools table - Field is optional (nullable) to maintain backward compatibility This resolves the issue where outputSchema was documented but not actually stored or returned when listing/describing tools to clients. Signed-off-by: Matt Sanchez <mattsanchez@ibm.com> * feat(output-schema): add default output_schema and improve field handling - Add default empty object schema for output_schema in ToolCreate (previously None, now {"type": "object", "properties": {}}) - Pass output_schema through gateway_service and tool_service - Add output_schema update support in tool update operations - Refactor alembic migration: organize imports and use double quotes for consistency This ensures output_schema is properly initialized and can be updated across all tool operations, improving schema validation capabilities. * Code Review and test fix Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> --------- Signed-off-by: Matt Sanchez <mattsanchez@ibm.com> Signed-off-by: Mihai Criveti <crivetimihai@gmail.com> Co-authored-by: Matt Sanchez <mattsanchez@ibm.com> Co-authored-by: Mihai Criveti <crivetimihai@gmail.com>
1 parent 5e4b5c7 commit 2912750

13 files changed

Lines changed: 55 additions & 5 deletions

enable_payload_logging.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,4 +141,4 @@ The logging middleware automatically masks sensitive information:
141141
tail -f logs/mcpgateway.log
142142
```
143143

144-
Now you can see the full request payloads (with sensitive data masked) to debug tool registration and other API issues.
144+
Now you can see the full request payloads (with sensitive data masked) to debug tool registration and other API issues.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# -*- coding: utf-8 -*-
2+
"""add_output_schema_to_tools
3+
4+
Revision ID: 9aaa90ad26d9
5+
Revises: 9c99ec6872ed
6+
Create Date: 2025-10-15 17:29:38.801771
7+
8+
"""
9+
10+
# Standard
11+
from typing import Sequence, Union
12+
13+
# Third-Party
14+
from alembic import op
15+
import sqlalchemy as sa
16+
17+
# revision identifiers, used by Alembic.
18+
revision: str = "9aaa90ad26d9"
19+
down_revision: Union[str, Sequence[str], None] = "9c99ec6872ed"
20+
branch_labels: Union[str, Sequence[str], None] = None
21+
depends_on: Union[str, Sequence[str], None] = None
22+
23+
24+
def upgrade() -> None:
25+
"""Upgrade schema."""
26+
# Add output_schema column to tools table
27+
op.add_column("tools", sa.Column("output_schema", sa.JSON(), nullable=True))
28+
29+
30+
def downgrade() -> None:
31+
"""Downgrade schema."""
32+
# Remove output_schema column from tools table
33+
op.drop_column("tools", "output_schema")

mcpgateway/db.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1570,6 +1570,7 @@ class Tool(Base):
15701570
request_type: Mapped[str] = mapped_column(String(20), default="SSE")
15711571
headers: Mapped[Optional[Dict[str, str]]] = mapped_column(JSON)
15721572
input_schema: Mapped[Dict[str, Any]] = mapped_column(JSON)
1573+
output_schema: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, nullable=True)
15731574
annotations: Mapped[Optional[Dict[str, Any]]] = mapped_column(JSON, default=lambda: {})
15741575
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now)
15751576
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=utc_now, onupdate=utc_now)

mcpgateway/middleware/request_logging_middleware.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ def mask_sensitive_data(data):
4242
"""
4343
if isinstance(data, dict):
4444
return {k: ("******" if k.lower() in SENSITIVE_KEYS else mask_sensitive_data(v)) for k, v in data.items()}
45-
elif isinstance(data, list):
45+
if isinstance(data, list):
4646
return [mask_sensitive_data(i) for i in data]
4747
return data
4848

@@ -64,7 +64,7 @@ def mask_jwt_in_cookies(cookie_header):
6464
for cookie in cookie_header.split(";"):
6565
cookie = cookie.strip()
6666
if "=" in cookie:
67-
name, value = cookie.split("=", 1)
67+
name, _ = cookie.split("=", 1)
6868
name = name.strip()
6969
# Mask JWT tokens and other sensitive cookies
7070
if any(sensitive in name.lower() for sensitive in ["jwt", "token", "auth", "session"]):

mcpgateway/models.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,7 @@ class Tool(CommonAttributes):
468468
requestType (str): The HTTP method used to invoke the tool (GET, POST, PUT, DELETE, SSE, STDIO).
469469
headers (Dict[str, Any]): A JSON object representing HTTP headers.
470470
input_schema (Dict[str, Any]): A JSON Schema for validating the tool's input.
471+
output_schema (Optional[Dict[str, Any]]): A JSON Schema for validating the tool's output.
471472
annotations (Optional[Dict[str, Any]]): Tool annotations for behavior hints.
472473
auth_username (Optional[str]): The username for basic authentication.
473474
auth_password (Optional[str]): The password for basic authentication.
@@ -485,6 +486,7 @@ class Tool(CommonAttributes):
485486
request_type: str = "SSE"
486487
headers: Optional[Dict[str, Any]] = Field(default_factory=dict)
487488
input_schema: Dict[str, Any] = Field(default_factory=lambda: {"type": "object", "properties": {}})
489+
output_schema: Optional[Dict[str, Any]] = Field(default=None, description="JSON Schema for validating the tool's output")
488490
annotations: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Tool annotations for behavior hints")
489491
auth_username: Optional[str] = None
490492
auth_password: Optional[str] = None

mcpgateway/schemas.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,7 @@ class ToolCreate(BaseModel):
389389
request_type (Literal["GET", "POST", "PUT", "DELETE", "PATCH"]): HTTP method to be used for invoking the tool.
390390
headers (Optional[Dict[str, str]]): Additional headers to send when invoking the tool.
391391
input_schema (Optional[Dict[str, Any]]): JSON Schema for validating tool parameters. Alias 'inputSchema'.
392+
output_schema (Optional[Dict[str, Any]]): JSON Schema for validating tool output. Alias 'outputSchema'.
392393
annotations (Optional[Dict[str, Any]]): Tool annotations for behavior hints such as title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint.
393394
jsonpath_filter (Optional[str]): JSON modification filter.
394395
auth (Optional[AuthenticationValues]): Authentication credentials (Basic or Bearer Token or custom headers) if required.
@@ -406,6 +407,7 @@ class ToolCreate(BaseModel):
406407
request_type: Literal["GET", "POST", "PUT", "DELETE", "PATCH", "SSE", "STDIO", "STREAMABLEHTTP"] = Field("SSE", description="HTTP method to be used for invoking the tool")
407408
headers: Optional[Dict[str, str]] = Field(None, description="Additional headers to send when invoking the tool")
408409
input_schema: Optional[Dict[str, Any]] = Field(default_factory=lambda: {"type": "object", "properties": {}}, description="JSON Schema for validating tool parameters", alias="inputSchema")
410+
output_schema: Optional[Dict[str, Any]] = Field(default=None, description="JSON Schema for validating tool output", alias="outputSchema")
409411
annotations: Optional[Dict[str, Any]] = Field(
410412
default_factory=dict,
411413
description="Tool annotations for behavior hints (title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint)",
@@ -767,6 +769,7 @@ class ToolUpdate(BaseModelWithConfigDict):
767769
request_type: Optional[Literal["GET", "POST", "PUT", "DELETE", "PATCH"]] = Field(None, description="HTTP method to be used for invoking the tool")
768770
headers: Optional[Dict[str, str]] = Field(None, description="Additional headers to send when invoking the tool")
769771
input_schema: Optional[Dict[str, Any]] = Field(None, description="JSON Schema for validating tool parameters")
772+
output_schema: Optional[Dict[str, Any]] = Field(None, description="JSON Schema for validating tool output")
770773
annotations: Optional[Dict[str, Any]] = Field(None, description="Tool annotations for behavior hints")
771774
jsonpath_filter: Optional[str] = Field(None, description="JSON path filter for rpc tool calls")
772775
auth: Optional[AuthenticationValues] = Field(None, description="Authentication credentials (Basic or Bearer Token or custom headers) if required")
@@ -1032,6 +1035,7 @@ class ToolRead(BaseModelWithConfigDict):
10321035
integration_type: str
10331036
headers: Optional[Dict[str, str]]
10341037
input_schema: Dict[str, Any]
1038+
output_schema: Optional[Dict[str, Any]] = Field(None)
10351039
annotations: Optional[Dict[str, Any]]
10361040
jsonpath_filter: Optional[str]
10371041
auth: Optional[AuthenticationValues]

mcpgateway/services/export_service.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ async def _export_tools(self, db: Session, tags: Optional[List[str]], include_in
294294
"description": tool.description,
295295
"headers": tool.headers or {},
296296
"input_schema": tool.input_schema or {"type": "object", "properties": {}},
297+
"output_schema": tool.output_schema,
297298
"annotations": tool.annotations or {},
298299
"jsonpath_filter": tool.jsonpath_filter,
299300
"tags": tool.tags or [],

mcpgateway/services/gateway_service.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,7 @@ async def register_gateway(
596596
request_type=tool.request_type,
597597
headers=tool.headers,
598598
input_schema=tool.input_schema,
599+
output_schema=tool.output_schema,
599600
annotations=tool.annotations,
600601
jsonpath_filter=tool.jsonpath_filter,
601602
auth_type=auth_type,

mcpgateway/services/import_service.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,7 @@ def _convert_to_tool_create(self, tool_data: Dict[str, Any]) -> ToolCreate:
954954
request_type=tool_data.get("request_type", "GET"),
955955
headers=tool_data.get("headers"),
956956
input_schema=tool_data.get("input_schema"),
957+
output_schema=tool_data.get("output_schema"),
957958
annotations=tool_data.get("annotations"),
958959
jsonpath_filter=tool_data.get("jsonpath_filter"),
959960
auth=auth_info,
@@ -982,6 +983,7 @@ def _convert_to_tool_update(self, tool_data: Dict[str, Any]) -> ToolUpdate:
982983
request_type=tool_data.get("request_type"),
983984
headers=tool_data.get("headers"),
984985
input_schema=tool_data.get("input_schema"),
986+
output_schema=tool_data.get("output_schema"),
985987
annotations=tool_data.get("annotations"),
986988
jsonpath_filter=tool_data.get("jsonpath_filter"),
987989
auth=auth_info,

mcpgateway/services/tool_service.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,6 +467,7 @@ async def register_tool(
467467
request_type=tool.request_type,
468468
headers=tool.headers,
469469
input_schema=tool.input_schema,
470+
output_schema=tool.output_schema,
470471
annotations=tool.annotations,
471472
jsonpath_filter=tool.jsonpath_filter,
472473
auth_type=auth_type,
@@ -1247,6 +1248,8 @@ async def update_tool(
12471248
tool.headers = tool_update.headers
12481249
if tool_update.input_schema is not None:
12491250
tool.input_schema = tool_update.input_schema
1251+
if tool_update.output_schema is not None:
1252+
tool.output_schema = tool_update.output_schema
12501253
if tool_update.annotations is not None:
12511254
tool.annotations = tool_update.annotations
12521255
if tool_update.jsonpath_filter is not None:

0 commit comments

Comments
 (0)