This commit is contained in:
Suhas Aitham 2026-11-06 05:26:06 +00:00 • committed by GitHub
commit f9f04489d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 292 additions and 2 deletions

View file

@ -37597,6 +37597,57 @@
"title": "MCPEnvVarScope",
"type": "string"
},
"MCPToolCallRequest": {
"additionalProperties": true,
"description": "Request body for `POST /mcp-rest/tools/call`.\n\nFields stay optional here on purpose: the endpoint answers a missing\n`server_id` or `name` with its 400 `missing_parameter` response rather\nthan a framework 422, and unknown fields pass through to the pre-call\npipeline, matching the previous untyped `request.json()` handling.",
"properties": {
"arguments": {
"additionalProperties": true,
"description": "Arguments to pass to the tool.",
"title": "Arguments",
"type": "object"
},
"metadata": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"description": "Optional request metadata, forwarded to logging and guardrails.",
"title": "Metadata"
},
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required. The name of the tool to call.",
"title": "Name"
},
"server_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required. The id of the MCP server that owns the tool to call.",
"title": "Server Id"
}
},
"title": "MCPToolCallRequest",
"type": "object"
},
"NewMCPServerRequest": {
"properties": {
"alias": {
@ -38362,6 +38413,23 @@
"post": {
"description": "REST API to call a specific MCP tool with the provided arguments",
"operationId": "call_tool_rest_api_mcp_rest_tools_call_post_2",
"requestBody": {
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/MCPToolCallRequest"
},
{
"type": "null"
}
],
"title": "Body"
}
}
}
},
"responses": {
"200": {
"content": {
@ -38370,6 +38438,16 @@
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"security": [
@ -47932,6 +48010,57 @@
"title": "MCPEnvVarScope",
"type": "string"
},
"MCPToolCallRequest": {
"additionalProperties": true,
"description": "Request body for `POST /mcp-rest/tools/call`.\n\nFields stay optional here on purpose: the endpoint answers a missing\n`server_id` or `name` with its 400 `missing_parameter` response rather\nthan a framework 422, and unknown fields pass through to the pre-call\npipeline, matching the previous untyped `request.json()` handling.",
"properties": {
"arguments": {
"additionalProperties": true,
"description": "Arguments to pass to the tool.",
"title": "Arguments",
"type": "object"
},
"metadata": {
"anyOf": [
{
"additionalProperties": true,
"type": "object"
},
{
"type": "null"
}
],
"description": "Optional request metadata, forwarded to logging and guardrails.",
"title": "Metadata"
},
"name": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required. The name of the tool to call.",
"title": "Name"
},
"server_id": {
"anyOf": [
{
"type": "string"
},
{
"type": "null"
}
],
"description": "Required. The id of the MCP server that owns the tool to call.",
"title": "Server Id"
}
},
"title": "MCPToolCallRequest",
"type": "object"
},
"NewMCPServerRequest": {
"properties": {
"alias": {
@ -48569,6 +48698,23 @@
"post": {
"description": "REST API to call a specific MCP tool with the provided arguments",
"operationId": "call_tool_rest_api_mcp_rest_tools_call_post",
"requestBody": {
"content": {
"application/json": {
"schema": {
"anyOf": [
{
"$ref": "#/components/schemas/MCPToolCallRequest"
},
{
"type": "null"
}
],
"title": "Body"
}
}
}
},
"responses": {
"200": {
"content": {
@ -48577,6 +48723,16 @@
}
},
"description": "Successful Response"
},
"422": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HTTPValidationError"
}
}
},
"description": "Validation Error"
}
},
"security": [

View file

@ -1519,6 +1519,40 @@ class NewMCPServerRequest(LiteLLMPydanticObjectBase):
return values
class MCPToolCallRequest(LiteLLMPydanticObjectBase):
"""Request body for `POST /mcp-rest/tools/call`.
Fields stay optional here on purpose: the endpoint answers a missing
`server_id` or `name` with its 400 `missing_parameter` response rather
than a framework 422, and unknown fields pass through to the pre-call
pipeline, matching the previous untyped `request.json()` handling.
"""
model_config = ConfigDict(extra="allow")
server_id: str | None = Field(
default=None,
description="Required. The id of the MCP server that owns the tool to call.",
)
name: str | None = Field(
default=None,
description="Required. The name of the tool to call.",
)
arguments: dict[str, object] = Field(
default_factory=dict,
description="Arguments to pass to the tool.",
)
metadata: dict[str, object] | None = Field(
default=None,
description="Optional request metadata, forwarded to logging and guardrails.",
)
@field_validator("arguments", mode="before")
@classmethod
def _coerce_null_arguments(cls, value: object) -> object:
return {} if value is None else value
class UpdateMCPServerRequest(LiteLLMPydanticObjectBase):
server_id: str
server_name: str | None = None

View file

@ -26,6 +26,7 @@ from litellm.exceptions import (
)
from litellm.proxy._types import (
LitellmUserRoles,
MCPToolCallRequest,
UserAPIKeyAuth,
user_api_key_has_admin_view,
)
@ -1319,6 +1320,7 @@ if MCP_AVAILABLE:
@catalog_operation(global_manager)
async def call_tool_rest_api(
request: Request,
body: MCPToolCallRequest | None = None,
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
):
"""
@ -1339,7 +1341,7 @@ if MCP_AVAILABLE:
reject_disallowed_mcp_client(request.headers, user_api_key_dict)
try:
user_api_key_dict = await acting_user_auth(user_api_key_dict)
data = await request.json()
data = body.model_dump() if body is not None else await request.json()
tool_name: Final[str | None] = data.get("name")
tool_arguments: Final[dict[str, object]] = data.get("arguments") or {}

View file

@ -19,6 +19,7 @@ from starlette.requests import Request
from litellm.constants import MCP_TOOL_LISTING_TIMEOUT
from litellm.proxy._types import (
MCPToolCallRequest,
NewMCPServerRequest,
UpdateMCPServerRequest,
UserAPIKeyAuth,
@ -5106,3 +5107,50 @@ async def test_rest_guardrail_bearer_excludes_admission_key(
assert (authorization.removeprefix("Bearer ") or None) == expected
finally:
global_mcp_tool_registry.unregister_tools_with_prefix("guarded-")
class TestCallToolRestApiRequestBody:
"""POST /mcp-rest/tools/call documents its body in the generated OpenAPI
spec, so a client built from openapi.json passes server_id, name and
arguments instead of calling the tool blind (issue #32121)."""
def test_openapi_documents_the_tools_call_body(self) -> None:
from fastapi import FastAPI
app = FastAPI()
app.include_router(rest_endpoints.router)
paths: Final = app.openapi()["paths"]
operation: Final = paths["/mcp-rest/tools/call"]["post"]
body_schema: Final = operation["requestBody"]["content"]["application/json"]["schema"]
refs: Final = body_schema.get("anyOf", [body_schema])
model_ref: Final = next(ref["$ref"] for ref in refs if "$ref" in ref)
components: Final = app.openapi()["components"]["schemas"]
properties: Final = components[model_ref.rsplit("/", 1)[-1]]["properties"]
assert {"server_id", "name", "arguments", "metadata"} <= set(properties)
assert all(field.get("description") for field in properties.values())
@pytest.mark.parametrize(
("body", "message"),
(
(MCPToolCallRequest(name="demo-tool"), "server_id is required in request body"),
(MCPToolCallRequest(server_id="server-1"), "name is required in request body"),
),
)
async def test_missing_required_fields_keep_the_400_response(self, body: MCPToolCallRequest, message: str) -> None:
request: Final = _build_request(path="/mcp-rest/tools/call", method="POST", json_body={})
with pytest.raises(HTTPException) as exc_info:
await rest_endpoints.call_tool_rest_api(request, body=body, user_api_key_dict=UserAPIKeyAuth())
assert exc_info.value.status_code == 400
assert exc_info.value.detail == {"error": "missing_parameter", "message": message}
def test_null_arguments_are_treated_as_no_arguments(self) -> None:
body: Final = MCPToolCallRequest.model_validate(
{"server_id": "server-1", "name": "demo-tool", "arguments": None}
)
assert body.arguments == {}
assert body.model_dump()["arguments"] == {}

View file

@ -39087,6 +39087,43 @@ export interface components {
/** Total */
total: number;
};
/**
* MCPToolCallRequest
* @description Request body for `POST /mcp-rest/tools/call`.
*
* Fields stay optional here on purpose: the endpoint answers a missing
* `server_id` or `name` with its 400 `missing_parameter` response rather
* than a framework 422, and unknown fields pass through to the pre-call
* pipeline, matching the previous untyped `request.json()` handling.
*/
MCPToolCallRequest: {
/**
* Arguments
* @description Arguments to pass to the tool.
*/
arguments?: {
[key: string]: unknown;
};
/**
* Metadata
* @description Optional request metadata, forwarded to logging and guardrails.
*/
metadata?: {
[key: string]: unknown;
} | null;
/**
* Name
* @description Required. The name of the tool to call.
*/
name?: string | null;
/**
* Server Id
* @description Required. The id of the MCP server that owns the tool to call.
*/
server_id?: string | null;
} & {
[key: string]: unknown;
};
/**
* MCPToolSearchSettings
* @description `litellm_settings.mcp_tool_search`: how the native `mcp_tool_search` virtual tool ranks the caller's tools.
@ -67029,7 +67066,11 @@ export interface operations {
path?: never;
cookie?: never;
};
requestBody?: never;
requestBody?: {
content: {
"application/json": components["schemas"]["MCPToolCallRequest"] | null;
};
};
responses: {
/** @description Successful Response */
200: {
@ -67040,6 +67081,15 @@ export interface operations {
"application/json": unknown;
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
list_tool_rest_api_mcp_rest_tools_list_get: {