From d4cd0b03dd635eafa2350d07f5868cd533c15cc4 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Tue, 10 Feb 2026 16:11:38 +0000 Subject: [PATCH] Fix hidden path parameters appearing in tool descriptions Remove path parameter descriptions from format_simple_description since parameters are already visible in the tool's inputSchema with their descriptions. This prevents hidden parameters (via transforms) from still appearing in descriptions and avoids redundant information. Fixes #3130 Co-authored-by: Jeremiah Lowin --- src/fastmcp/utilities/openapi/formatters.py | 18 +- .../openapi/test_hidden_path_params.py | 249 ++++++++++++++++++ 2 files changed, 253 insertions(+), 14 deletions(-) create mode 100644 tests/server/providers/openapi/test_hidden_path_params.py diff --git a/src/fastmcp/utilities/openapi/formatters.py b/src/fastmcp/utilities/openapi/formatters.py index 27580fcdd..cdd0b4513 100644 --- a/src/fastmcp/utilities/openapi/formatters.py +++ b/src/fastmcp/utilities/openapi/formatters.py @@ -206,20 +206,10 @@ def format_simple_description( Returns: str: The formatted description string with minimal details. """ - desc_parts = [base_description] - - # Only add critical parameter information if they have descriptions - if parameters: - path_params = [p for p in parameters if p.location == "path" and p.description] - if path_params: - desc_parts.append("\n\n**Path Parameters:**") - for param in path_params: - desc_parts.append(f"\n- **{param.name}**: {param.description}") - - # Skip query parameters, request body details, and all response information - # These are already captured in the inputSchema - - return "\n".join(desc_parts) + # All parameters (path, query, etc.) are already visible in the tool's inputSchema, + # so we don't need to duplicate them in the description. This also prevents issues + # where hidden parameters (via transforms) would still appear in the description. + return base_description def format_description_with_responses( diff --git a/tests/server/providers/openapi/test_hidden_path_params.py b/tests/server/providers/openapi/test_hidden_path_params.py new file mode 100644 index 000000000..0508571fb --- /dev/null +++ b/tests/server/providers/openapi/test_hidden_path_params.py @@ -0,0 +1,249 @@ +"""Tests for hidden path parameters in OpenAPI tool descriptions.""" + +import httpx +import pytest + +from fastmcp import FastMCP +from fastmcp.client import Client +from fastmcp.server.transforms import ToolTransform +from fastmcp.tools.tool_transform import ArgTransformConfig, ToolTransformConfig + + +@pytest.mark.asyncio +async def test_hidden_path_parameter_not_in_description(): + """Test that hidden path parameters are not included in tool descriptions. + + This test replicates the bug described in issue #3130 where path parameters + that are hidden via ToolTransform still appear in the tool description. + """ + openapi_spec = { + "openapi": "3.1.0", + "info": {"title": "Test API", "version": "0.1.0"}, + "paths": { + "/hello/{not_for_agent}": { + "get": { + "operationId": "hello", + "summary": "Say hello", + "parameters": [ + { + "name": "not_for_agent", + "in": "path", + "required": True, + "description": "Secret setting", + "schema": { + "type": "string", + "title": "Not For Agent" + } + } + ], + "responses": { + "200": { + "description": "Success response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "message": {"type": "string"} + } + } + } + } + } + } + } + } + } + } + + async with httpx.AsyncClient(base_url="http://localhost:8000") as http_client: + mcp = FastMCP.from_openapi( + openapi_spec=openapi_spec, + client=http_client, + ) + + # Hide not_for_agent parameter and set default + mcp.add_transform( + ToolTransform({ + "hello": ToolTransformConfig( + arguments={ + "not_for_agent": ArgTransformConfig( + hide=True, + default="secret", + ) + } + ) + }) + ) + + async with Client(mcp) as mcp_client: + tools = await mcp_client.list_tools() + assert len(tools) == 1 + + hello_tool = tools[0] + assert hello_tool.name == "hello" + + # The parameter should not be in the schema + assert "not_for_agent" not in hello_tool.inputSchema.get("properties", {}) + + # The BUG: parameter description should not appear in the tool description + assert "Secret setting" not in hello_tool.description + assert "not_for_agent" not in hello_tool.description + + # The description should only contain the summary + assert "Say hello" in hello_tool.description + + +@pytest.mark.asyncio +async def test_visible_path_parameter_in_schema(): + """Test that visible path parameters are included in the inputSchema. + + Path parameters are not included in the description since they're already + visible in the inputSchema with their descriptions. + """ + openapi_spec = { + "openapi": "3.1.0", + "info": {"title": "Test API", "version": "0.1.0"}, + "paths": { + "/users/{user_id}": { + "get": { + "operationId": "get_user", + "summary": "Get user by ID", + "parameters": [ + { + "name": "user_id", + "in": "path", + "required": True, + "description": "The user's unique identifier", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "name": {"type": "string"} + } + } + } + } + } + } + } + } + } + } + + async with httpx.AsyncClient(base_url="http://localhost:8000") as http_client: + mcp = FastMCP.from_openapi( + openapi_spec=openapi_spec, + client=http_client, + ) + + async with Client(mcp) as mcp_client: + tools = await mcp_client.list_tools() + assert len(tools) == 1 + + user_tool = tools[0] + assert user_tool.name == "get_user" + + # The parameter should be in the schema with its description + assert "user_id" in user_tool.inputSchema.get("properties", {}) + assert user_tool.inputSchema["properties"]["user_id"]["description"] == "The user's unique identifier" + + # The description should only contain the summary, not parameter details + assert user_tool.description == "Get user by ID" + + +@pytest.mark.asyncio +async def test_mixed_path_parameters_in_schema(): + """Test that when some path params are hidden, only visible ones appear in schema.""" + openapi_spec = { + "openapi": "3.1.0", + "info": {"title": "Test API", "version": "0.1.0"}, + "paths": { + "/api/{api_version}/users/{user_id}": { + "get": { + "operationId": "get_versioned_user", + "summary": "Get user from specific API version", + "parameters": [ + { + "name": "api_version", + "in": "path", + "required": True, + "description": "API version to use", + "schema": { + "type": "string" + } + }, + { + "name": "user_id", + "in": "path", + "required": True, + "description": "The user's unique identifier", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success response", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "name": {"type": "string"} + } + } + } + } + } + } + } + } + } + } + + async with httpx.AsyncClient(base_url="http://localhost:8000") as http_client: + mcp = FastMCP.from_openapi( + openapi_spec=openapi_spec, + client=http_client, + ) + + # Hide api_version but not user_id + mcp.add_transform( + ToolTransform({ + "get_versioned_user": ToolTransformConfig( + arguments={ + "api_version": ArgTransformConfig( + hide=True, + default="v1", + ) + } + ) + }) + ) + + async with Client(mcp) as mcp_client: + tools = await mcp_client.list_tools() + assert len(tools) == 1 + + tool = tools[0] + assert tool.name == "get_versioned_user" + + # Only user_id should be in the schema + assert "user_id" in tool.inputSchema.get("properties", {}) + assert "api_version" not in tool.inputSchema.get("properties", {}) + + # The description should only contain the summary, not parameter details + assert tool.description == "Get user from specific API version"