Spaces:
Running
Running
Skip repeated type conversion and validation in proxy client elicitation handler (#1222)
Browse files
src/fastmcp/server/proxy.py
CHANGED
|
@@ -566,11 +566,12 @@ class ProxyClient(Client[ClientTransportT]):
|
|
| 566 |
A handler that forwards the elicitation request from the remote server to the proxy's connected clients and relays the response back to the remote server.
|
| 567 |
"""
|
| 568 |
ctx = get_context()
|
| 569 |
-
result = await ctx.elicit(
|
| 570 |
-
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
|
|
|
|
| 574 |
|
| 575 |
@classmethod
|
| 576 |
async def default_log_handler(cls, message: LogMessage) -> None:
|
|
|
|
| 566 |
A handler that forwards the elicitation request from the remote server to the proxy's connected clients and relays the response back to the remote server.
|
| 567 |
"""
|
| 568 |
ctx = get_context()
|
| 569 |
+
result = await ctx.session.elicit(
|
| 570 |
+
message=message,
|
| 571 |
+
requestedSchema=params.requestedSchema,
|
| 572 |
+
related_request_id=ctx.request_id,
|
| 573 |
+
)
|
| 574 |
+
return ElicitResult(action=result.action, content=result.content)
|
| 575 |
|
| 576 |
@classmethod
|
| 577 |
async def default_log_handler(cls, message: LogMessage) -> None:
|
tests/server/proxy/test_proxy_client.py
CHANGED
|
@@ -4,6 +4,7 @@ from typing import cast
|
|
| 4 |
import pytest
|
| 5 |
from anyio import create_task_group
|
| 6 |
from mcp.types import LoggingLevel, ModelHint, ModelPreferences, TextContent
|
|
|
|
| 7 |
|
| 8 |
from fastmcp import Client, Context, FastMCP
|
| 9 |
from fastmcp.client.elicitation import ElicitRequestParams, ElicitResult
|
|
@@ -330,6 +331,50 @@ class TestProxyClient:
|
|
| 330 |
assert results["elicitation_a"] == "Hello, Alice!"
|
| 331 |
assert results["elicitation_b"] == "Hello, Bob!"
|
| 332 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
async def test_client_factory_creates_fresh_sessions(self, fastmcp_server: FastMCP):
|
| 334 |
"""Test that the client factory pattern creates fresh sessions for each request."""
|
| 335 |
from fastmcp.server.proxy import FastMCPProxy
|
|
|
|
| 4 |
import pytest
|
| 5 |
from anyio import create_task_group
|
| 6 |
from mcp.types import LoggingLevel, ModelHint, ModelPreferences, TextContent
|
| 7 |
+
from pydantic import BaseModel, Field
|
| 8 |
|
| 9 |
from fastmcp import Client, Context, FastMCP
|
| 10 |
from fastmcp.client.elicitation import ElicitRequestParams, ElicitResult
|
|
|
|
| 331 |
assert results["elicitation_a"] == "Hello, Alice!"
|
| 332 |
assert results["elicitation_b"] == "Hello, Bob!"
|
| 333 |
|
| 334 |
+
async def test_elicit_with_default_values(self, fastmcp_server: FastMCP):
|
| 335 |
+
"""
|
| 336 |
+
Test that the proxy client correctly handles elicitation with default values (fixes #1167).
|
| 337 |
+
"""
|
| 338 |
+
|
| 339 |
+
@fastmcp_server.tool
|
| 340 |
+
async def elicit_with_defaults(context: Context) -> str:
|
| 341 |
+
class TestModel(BaseModel):
|
| 342 |
+
content: str = Field(description="Your reply content")
|
| 343 |
+
acknowledge: bool = Field(
|
| 344 |
+
default=False, description="Send immediately or save as draft"
|
| 345 |
+
)
|
| 346 |
+
|
| 347 |
+
result = await context.elicit(
|
| 348 |
+
"Please provide input:", response_type=TestModel
|
| 349 |
+
)
|
| 350 |
+
|
| 351 |
+
if result.action == "accept":
|
| 352 |
+
return f"Content: {result.data.content}, Acknowledge: {result.data.acknowledge}"
|
| 353 |
+
else:
|
| 354 |
+
return f"Elicitation {result.action}"
|
| 355 |
+
|
| 356 |
+
proxy_server = FastMCP.as_proxy(ProxyClient(fastmcp_server))
|
| 357 |
+
|
| 358 |
+
# Test that elicitation works correctly through the proxy
|
| 359 |
+
async def elicitation_handler(
|
| 360 |
+
message: str,
|
| 361 |
+
response_type: type,
|
| 362 |
+
params: ElicitRequestParams,
|
| 363 |
+
ctx: RequestContext,
|
| 364 |
+
):
|
| 365 |
+
# Verify the schema is correct - acknowledge should have default=False, not be nullable
|
| 366 |
+
schema = params.requestedSchema
|
| 367 |
+
assert schema["properties"]["acknowledge"]["type"] == "boolean"
|
| 368 |
+
assert schema["properties"]["acknowledge"]["default"] is False
|
| 369 |
+
|
| 370 |
+
return {"content": "Test content", "acknowledge": True}
|
| 371 |
+
|
| 372 |
+
async with Client(
|
| 373 |
+
proxy_server, elicitation_handler=elicitation_handler
|
| 374 |
+
) as client:
|
| 375 |
+
result = await client.call_tool("elicit_with_defaults", {})
|
| 376 |
+
assert result.data == "Content: Test content, Acknowledge: True"
|
| 377 |
+
|
| 378 |
async def test_client_factory_creates_fresh_sessions(self, fastmcp_server: FastMCP):
|
| 379 |
"""Test that the client factory pattern creates fresh sessions for each request."""
|
| 380 |
from fastmcp.server.proxy import FastMCPProxy
|