Repository: jlowin/fastmcp (python)

Issue:
Default values in elicitation schemas error when attempting to Proxy
### Description

My setup is the folliowing: Client -> Proxy -> Server.  When the Server initiates elicitation, and the schema includes a default value, the proxy elicitation handler throws an error. The same schema works without using the proxy.

The issue arises because `json_schema_to_type` (incorrectly?) marks the field as nullable when the original type did not specify that, and the elicitation handler in the proxy tries to serialize the proxy type, it is invalid elicitation schema.

I have provided a minimal code which reproduces that while it's possible to get an elicitation schema for the initial type, the inverse type creates a different schema and throws an error

The relevant lines of code are here:
https://github.com/jlowin/fastmcp/blob/5fc7aa04aa82da6c06e757b4916df977a3f88f0f/src/fastmcp/utilities/json_schema_type.py#L551-L570

The other thing which I checked is that the ElicitRequestParams do have the json schema from the original server here:
https://github.com/jlowin/fastmcp/blob/5fc7aa04aa82da6c06e757b4916df977a3f88f0f/src/fastmcp/server/proxy.py#L545-L552



### Example Code

```Python
from pydantic import BaseModel, Field, TypeAdapter
from fastmcp.utilities.json_schema_type import json_schema_to_type
from fastmcp.server.elicitation import get_elicitation_schema
import json


def create_type() -> BaseModel:

    class TestModel(BaseModel):
        content: str = Field(description="Your reply content")
        acknowledge: bool = Field(
            default=False,
            description="Send immediately or save as draft",
        )
    return TestModel


initial = create_type()
initial_schema = json.loads(initial.schema_json())
# schema["type"] = "object"
inverse = json_schema_to_type(initial_schema)

print("Original:", get_elicitation_schema(initial))
print("Inverse Schema:", TypeAdapter(inverse).json_schema())
print("Inverse:", get_elicitation_schema(inverse)) # Errors out because acknowledge becomes nullable
```

### Version Information

```Text
FastMCP version:                                                                 2.10.5
MCP version:                                                                     1.10.1
Python version:                                                                  3.12.4
Platform:                                                    macOS-13.5-arm64-arm-64bit
```

### Additional Context

The output of the example code below is here:
```
Original: {'properties': {'content': {'description': 'Your reply content', 'title': 'Content', 'type': 'string'}, 'acknowledge': {'default': False, 'description': 'Send immediately or save as draft', 'title': 'Acknowledge', 'type': 'boolean'}}, 'required': ['content'], 'title': 'TestModel', 'type': 'object'}
Inverse Schema: {'properties': {'content': {'title': 'Content', 'type': 'string'}, 'acknowledge': {'anyOf': [{'type': 'boolean'}, {'type': 'null'}], 'default': False, 'title': 'Acknowledge'}}, 'required': ['content'], 'title': 'TestModel', 'type': 'object'}
```


Pull request description:
Skip repeated type conversion and validation in proxy client elicitation handler
fixes #1167

Instead of going through the context, the proxy elicitation handler here assumes that the original server did schema validation properly and bypasses it by going through the session directly.

Patch:
diff --git a/src/fastmcp/experimental/utilities/openapi/parser.py b/src/fastmcp/experimental/utilities/openapi/parser.py
index 98532c5..551555f 100644
--- a/src/fastmcp/experimental/utilities/openapi/parser.py
+++ b/src/fastmcp/experimental/utilities/openapi/parser.py
@@ -146,6 +146,9 @@ class OpenAPIParser(
         """Resolves a reference to its target definition."""
         if isinstance(item, self.reference_cls):
             ref_str = item.ref
+            # Ensure ref_str is a string before calling startswith()
+            if not isinstance(ref_str, str):
+                return item
             try:
                 if not ref_str.startswith("#/"):
                     raise ValueError(
diff --git a/src/fastmcp/experimental/utilities/openapi/schemas.py b/src/fastmcp/experimental/utilities/openapi/schemas.py
index 5c86378..7ccc182 100644
--- a/src/fastmcp/experimental/utilities/openapi/schemas.py
+++ b/src/fastmcp/experimental/utilities/openapi/schemas.py
@@ -93,15 +93,16 @@ def _replace_ref_with_defs(
     """
     schema = info.copy()
     if ref_path := schema.get("$ref"):
-        if ref_path.startswith("#/components/schemas/"):
-            schema_name = ref_path.split("/")[-1]
-            schema["$ref"] = f"#/$defs/{schema_name}"
-        elif not ref_path.startswith("#/"):
-            raise ValueError(
-                f"External or non-local reference not supported: {ref_path}. "
-                f"FastMCP only supports local schema references starting with '#/'. "
-                f"Please include all schema definitions within the OpenAPI document."
-            )
+        if isinstance(ref_path, str):
+            if ref_path.startswith("#/components/schemas/"):
+                schema_name = ref_path.split("/")[-1]
+                schema["$ref"] = f"#/$defs/{schema_name}"
+            elif not ref_path.startswith("#/"):
+                raise ValueError(
+                    f"External or non-local reference not supported: {ref_path}. "
+                    f"FastMCP only supports local schema references starting with '#/'. "
+                    f"Please include all schema definitions within the OpenAPI document."
+                )
     elif properties := schema.get("properties"):
         if "$ref" in properties:
             schema["properties"] = _replace_ref_with_defs(properties)
diff --git a/src/fastmcp/server/proxy.py b/src/fastmcp/server/proxy.py
index cfd8ba5..ebcb0f8 100644
--- a/src/fastmcp/server/proxy.py
+++ b/src/fastmcp/server/proxy.py
@@ -566,11 +566,12 @@ class ProxyClient(Client[ClientTransportT]):
         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.
         """
         ctx = get_context()
-        result = await ctx.elicit(message, response_type)
-        if result.action == "accept":
-            return result.data
-        else:
-            return ElicitResult(action=result.action)
+        result = await ctx.session.elicit(
+            message=message,
+            requestedSchema=params.requestedSchema,
+            related_request_id=ctx.request_id,
+        )
+        return ElicitResult(action=result.action, content=result.content)
 
     @classmethod
     async def default_log_handler(cls, message: LogMessage) -> None:


Test patch:
diff --git a/tests/server/proxy/test_proxy_client.py b/tests/server/proxy/test_proxy_client.py
index ffd1711..6239a9e 100644
--- a/tests/server/proxy/test_proxy_client.py
+++ b/tests/server/proxy/test_proxy_client.py
@@ -4,6 +4,7 @@ from typing import cast
 import pytest
 from anyio import create_task_group
 from mcp.types import LoggingLevel, ModelHint, ModelPreferences, TextContent
+from pydantic import BaseModel, Field
 
 from fastmcp import Client, Context, FastMCP
 from fastmcp.client.elicitation import ElicitRequestParams, ElicitResult
@@ -330,6 +331,50 @@ class TestProxyClient:
         assert results["elicitation_a"] == "Hello, Alice!"
         assert results["elicitation_b"] == "Hello, Bob!"
 
+    async def test_elicit_with_default_values(self, fastmcp_server: FastMCP):
+        """
+        Test that the proxy client correctly handles elicitation with default values (fixes #1167).
+        """
+
+        @fastmcp_server.tool
+        async def elicit_with_defaults(context: Context) -> str:
+            class TestModel(BaseModel):
+                content: str = Field(description="Your reply content")
+                acknowledge: bool = Field(
+                    default=False, description="Send immediately or save as draft"
+                )
+
+            result = await context.elicit(
+                "Please provide input:", response_type=TestModel
+            )
+
+            if result.action == "accept":
+                return f"Content: {result.data.content}, Acknowledge: {result.data.acknowledge}"
+            else:
+                return f"Elicitation {result.action}"
+
+        proxy_server = FastMCP.as_proxy(ProxyClient(fastmcp_server))
+
+        # Test that elicitation works correctly through the proxy
+        async def elicitation_handler(
+            message: str,
+            response_type: type,
+            params: ElicitRequestParams,
+            ctx: RequestContext,
+        ):
+            # Verify the schema is correct - acknowledge should have default=False, not be nullable
+            schema = params.requestedSchema
+            assert schema["properties"]["acknowledge"]["type"] == "boolean"
+            assert schema["properties"]["acknowledge"]["default"] is False
+
+            return {"content": "Test content", "acknowledge": True}
+
+        async with Client(
+            proxy_server, elicitation_handler=elicitation_handler
+        ) as client:
+            result = await client.call_tool("elicit_with_defaults", {})
+            assert result.data == "Content: Test content, Acknowledge: True"
+
     async def test_client_factory_creates_fresh_sessions(self, fastmcp_server: FastMCP):
         """Test that the client factory pattern creates fresh sessions for each request."""
         from fastmcp.server.proxy import FastMCPProxy