mirror of
https://github.com/BerriAI/litellm.git
synced 2026-11-14 09:03:16 +00:00
Merge c380d78cca into f82e4c80ac
This commit is contained in:
commit
f9f04489d2
5 changed files with 292 additions and 2 deletions
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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"] == {}
|
||||
|
|
|
|||
52
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
52
ui/litellm-dashboard/src/lib/http/schema.d.ts
generated
vendored
|
|
@ -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: {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue