mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-12 23:01:41 +00:00
fix(proxy): document /mcp-rest/tools/call request body in OpenAPI
Co-Authored-By: bot_apk <apk@cognition.ai>
This commit is contained in:
parent
0932dde167
commit
723126b457
4 changed files with 256 additions and 2 deletions
|
|
@ -15,7 +15,8 @@ from typing import (
|
|||
)
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request, status
|
||||
from fastapi import APIRouter, Body, Depends, HTTPException, Query, Request, status
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from litellm._logging import verbose_logger
|
||||
from litellm.proxy._experimental.mcp_server.exceptions import MCPUpstreamAuthError
|
||||
|
|
@ -64,6 +65,42 @@ def _connection_error_message(exc: BaseException) -> str:
|
|||
return "Failed to connect to MCP server. Check proxy logs for details."
|
||||
|
||||
|
||||
class MCPRestToolCallRequest(BaseModel):
|
||||
"""Request body for ``POST /mcp-rest/tools/call``.
|
||||
|
||||
Declared so the generated OpenAPI spec documents the fields a client must
|
||||
send; the handler still reads the raw JSON body. Extra keys are allowed
|
||||
because the same route also serves the tool-search / tool-call virtual-tool
|
||||
flows, which carry a different payload shape.
|
||||
"""
|
||||
|
||||
model_config = ConfigDict(
|
||||
extra="allow",
|
||||
json_schema_extra={
|
||||
"example": {
|
||||
"server_id": "17a4490465f74d3696caf12b30220166",
|
||||
"name": "google_maps-getPlaces",
|
||||
"arguments": {"query": "coffee near me"},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
name: str = Field(
|
||||
description="Name of the MCP tool to invoke, as returned by GET /mcp-rest/tools/list.",
|
||||
)
|
||||
arguments: dict[str, object] = Field(
|
||||
default_factory=dict,
|
||||
description="Tool arguments matching the tool's inputSchema. Pass {} when the tool takes no arguments.",
|
||||
)
|
||||
server_id: Optional[str] = Field(
|
||||
default=None,
|
||||
description=(
|
||||
"MCP server the tool belongs to (UUID, server_name, or alias). "
|
||||
"Required when calling a tool on a specific server."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
if MCP_AVAILABLE:
|
||||
from mcp.types import Tool as MCPTool
|
||||
|
||||
|
|
@ -738,6 +775,7 @@ if MCP_AVAILABLE:
|
|||
@router.post("/tools/call", dependencies=[Depends(user_api_key_auth)])
|
||||
async def call_tool_rest_api(
|
||||
request: Request,
|
||||
body: Optional[MCPRestToolCallRequest] = Body(default=None),
|
||||
user_api_key_dict: UserAPIKeyAuth = Depends(user_api_key_auth),
|
||||
):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -13602,6 +13602,47 @@
|
|||
"title": "MCPCredentials",
|
||||
"type": "object"
|
||||
},
|
||||
"MCPRestToolCallRequest": {
|
||||
"additionalProperties": true,
|
||||
"description": "Request body for ``POST /mcp-rest/tools/call``.\n\nDeclared so the generated OpenAPI spec documents the fields a client must\nsend; the handler still reads the raw JSON body. Extra keys are allowed\nbecause the same route also serves the tool-search / tool-call virtual-tool\nflows, which carry a different payload shape.",
|
||||
"example": {
|
||||
"arguments": {
|
||||
"query": "coffee near me"
|
||||
},
|
||||
"name": "google_maps-getPlaces",
|
||||
"server_id": "17a4490465f74d3696caf12b30220166"
|
||||
},
|
||||
"properties": {
|
||||
"arguments": {
|
||||
"additionalProperties": true,
|
||||
"description": "Tool arguments matching the tool's inputSchema. Pass {} when the tool takes no arguments.",
|
||||
"title": "Arguments",
|
||||
"type": "object"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the MCP tool to invoke, as returned by GET /mcp-rest/tools/list.",
|
||||
"title": "Name",
|
||||
"type": "string"
|
||||
},
|
||||
"server_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "MCP server the tool belongs to (UUID, server_name, or alias). Required when calling a tool on a specific server.",
|
||||
"title": "Server Id"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"title": "MCPRestToolCallRequest",
|
||||
"type": "object"
|
||||
},
|
||||
"NewMCPServerRequest": {
|
||||
"properties": {
|
||||
"alias": {
|
||||
|
|
@ -14099,6 +14140,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/MCPRestToolCallRequest"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Body"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
|
|
@ -17509,6 +17567,47 @@
|
|||
"title": "MCPCredentials",
|
||||
"type": "object"
|
||||
},
|
||||
"MCPRestToolCallRequest": {
|
||||
"additionalProperties": true,
|
||||
"description": "Request body for ``POST /mcp-rest/tools/call``.\n\nDeclared so the generated OpenAPI spec documents the fields a client must\nsend; the handler still reads the raw JSON body. Extra keys are allowed\nbecause the same route also serves the tool-search / tool-call virtual-tool\nflows, which carry a different payload shape.",
|
||||
"example": {
|
||||
"arguments": {
|
||||
"query": "coffee near me"
|
||||
},
|
||||
"name": "google_maps-getPlaces",
|
||||
"server_id": "17a4490465f74d3696caf12b30220166"
|
||||
},
|
||||
"properties": {
|
||||
"arguments": {
|
||||
"additionalProperties": true,
|
||||
"description": "Tool arguments matching the tool's inputSchema. Pass {} when the tool takes no arguments.",
|
||||
"title": "Arguments",
|
||||
"type": "object"
|
||||
},
|
||||
"name": {
|
||||
"description": "Name of the MCP tool to invoke, as returned by GET /mcp-rest/tools/list.",
|
||||
"title": "Name",
|
||||
"type": "string"
|
||||
},
|
||||
"server_id": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"description": "MCP server the tool belongs to (UUID, server_name, or alias). Required when calling a tool on a specific server.",
|
||||
"title": "Server Id"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
],
|
||||
"title": "MCPRestToolCallRequest",
|
||||
"type": "object"
|
||||
},
|
||||
"NewMCPServerRequest": {
|
||||
"properties": {
|
||||
"alias": {
|
||||
|
|
@ -18006,6 +18105,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/MCPRestToolCallRequest"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"title": "Body"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"content": {
|
||||
|
|
|
|||
|
|
@ -2115,3 +2115,62 @@ class TestToolResponseMcpInfoEnrichment:
|
|||
"server_id": "server-uuid",
|
||||
"alias": None,
|
||||
}
|
||||
|
||||
|
||||
class TestToolsCallOpenAPIDocumentation:
|
||||
"""Regression coverage for issue #32121: POST /mcp-rest/tools/call must
|
||||
document its request body (server_id / name / arguments) in the OpenAPI
|
||||
spec, both on the live route and in the served lazy-feature snapshot."""
|
||||
|
||||
def test_route_documents_request_body(self):
|
||||
from fastapi.openapi.utils import get_openapi
|
||||
|
||||
routes = [
|
||||
r
|
||||
for r in rest_endpoints.router.routes
|
||||
if getattr(r, "path", "") == "/mcp-rest/tools/call"
|
||||
and "POST" in getattr(r, "methods", set())
|
||||
]
|
||||
assert routes, "POST /mcp-rest/tools/call route not registered"
|
||||
|
||||
spec = get_openapi(title="t", version="1", routes=routes)
|
||||
operation = spec["paths"]["/mcp-rest/tools/call"]["post"]
|
||||
assert "requestBody" in operation
|
||||
|
||||
schema = spec["components"]["schemas"]["MCPRestToolCallRequest"]
|
||||
assert {"server_id", "name", "arguments"} <= set(schema["properties"])
|
||||
assert schema["required"] == ["name"]
|
||||
assert schema["additionalProperties"] is True
|
||||
|
||||
def test_request_model_requires_name_and_allows_extra_keys(self):
|
||||
from pydantic import ValidationError
|
||||
|
||||
from litellm.proxy._experimental.mcp_server.rest_endpoints import (
|
||||
MCPRestToolCallRequest,
|
||||
)
|
||||
|
||||
parsed = MCPRestToolCallRequest.model_validate(
|
||||
{"name": "mcp_tool_search", "arguments": {"query": "x"}, "extra": "kept"}
|
||||
)
|
||||
assert parsed.name == "mcp_tool_search"
|
||||
assert parsed.arguments == {"query": "x"}
|
||||
assert parsed.model_dump()["extra"] == "kept"
|
||||
|
||||
assert MCPRestToolCallRequest(name="demo").arguments == {}
|
||||
|
||||
with pytest.raises(ValidationError):
|
||||
MCPRestToolCallRequest.model_validate({"arguments": {}})
|
||||
|
||||
def test_served_snapshot_documents_request_body(self):
|
||||
from litellm.proxy._lazy_openapi_snapshot import load_snapshot
|
||||
|
||||
snapshot = load_snapshot()
|
||||
assert snapshot is not None
|
||||
|
||||
for fragment_name in ("mcp_rest", "mcp_app"):
|
||||
fragment = snapshot[fragment_name]
|
||||
post = fragment["paths"]["/mcp-rest/tools/call"]["post"]
|
||||
schema = post["requestBody"]["content"]["application/json"]["schema"]
|
||||
refs = [entry.get("$ref") for entry in schema.get("anyOf", [])] or [schema.get("$ref")]
|
||||
assert "#/components/schemas/MCPRestToolCallRequest" in refs
|
||||
assert "MCPRestToolCallRequest" in fragment["components"]["schemas"]
|
||||
|
|
|
|||
43
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
43
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -27002,6 +27002,43 @@ export interface components {
|
|||
*/
|
||||
transport: "sse" | "http" | "stdio";
|
||||
};
|
||||
/**
|
||||
* MCPRestToolCallRequest
|
||||
* @description Request body for ``POST /mcp-rest/tools/call``.
|
||||
*
|
||||
* Declared so the generated OpenAPI spec documents the fields a client must
|
||||
* send; the handler still reads the raw JSON body. Extra keys are allowed
|
||||
* because the same route also serves the tool-search / tool-call virtual-tool
|
||||
* flows, which carry a different payload shape.
|
||||
* @example {
|
||||
* "arguments": {
|
||||
* "query": "coffee near me"
|
||||
* },
|
||||
* "name": "google_maps-getPlaces",
|
||||
* "server_id": "17a4490465f74d3696caf12b30220166"
|
||||
* }
|
||||
*/
|
||||
MCPRestToolCallRequest: {
|
||||
/**
|
||||
* Arguments
|
||||
* @description Tool arguments matching the tool's inputSchema. Pass {} when the tool takes no arguments.
|
||||
*/
|
||||
arguments?: {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
/**
|
||||
* Name
|
||||
* @description Name of the MCP tool to invoke, as returned by GET /mcp-rest/tools/list.
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* Server Id
|
||||
* @description MCP server the tool belongs to (UUID, server_name, or alias). Required when calling a tool on a specific server.
|
||||
*/
|
||||
server_id?: string | null;
|
||||
} & {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
/**
|
||||
* MCPSemanticFilterSettings
|
||||
* @description Configuration for MCP Semantic Tool Filter
|
||||
|
|
@ -42578,7 +42615,11 @@ export interface operations {
|
|||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
requestBody?: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["MCPRestToolCallRequest"] | null;
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue