Skip to content

limit tool name length #69

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions docs/configurations/customization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,11 @@ You can control which FastAPI endpoints are exposed as MCP tools using Open API
- Only include operations with specific tags
- Exclude operations with specific tags
- Combine operation IDs and tags
- Filter operation IDs that exceed a maximum allowed length

### Code samples

The relevant arguments for these configurations are `include_operations`, `exclude_operations`, `include_tags`, `exclude_tags` and can be used as follows:
The relevant arguments for these configurations are `include_operations`, `exclude_operations`, `include_tags`, `exclude_tags`, `max_tool_name_length` and can be used as follows:

<CodeGroup>
```python Include Operations {8}
Expand Down Expand Up @@ -121,11 +122,25 @@ The relevant arguments for these configurations are `include_operations`, `exclu
)
mcp.mount()
```

```python Max Length {8}
from fastapi import FastAPI
from fastapi_mcp import FastApiMCP

app = FastAPI()

mcp = FastApiMCP(
app,
max_tool_name_length=25,
)
mcp.mount()
```
</CodeGroup>

### Notes on filtering

- You cannot use both `include_operations` and `exclude_operations` at the same time
- You cannot use both `include_tags` and `exclude_tags` at the same time
- You can combine operation filtering with tag filtering (e.g., use `include_operations` with `include_tags`)
- When combining filters, a greedy approach will be taken. Endpoints matching either criteria will be included
- When combining filters, a greedy approach will be taken. Endpoints matching either criteria will be included
- Max length filtering can combined with any other filter
4 changes: 2 additions & 2 deletions fastapi_mcp/openapi/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ def convert_openapi_to_mcp_tools(
for method, operation in path_item.items():
# Skip non-HTTP methods
if method not in ["get", "post", "put", "delete", "patch"]:
logger.warning(f"Skipping non-HTTP method: {method}")
logger.warning(f"Skipping non-HTTP method: {method.upper()} {path}")
continue

# Get operation metadata
operation_id = operation.get("operationId")
if not operation_id:
logger.warning(f"Skipping operation with no operationId: {operation}")
logger.warning(f"Skipping operation with no operationId: {method.upper()} {path}, details: {operation}")
continue

# Save operation details for later HTTP calls
Expand Down
60 changes: 51 additions & 9 deletions fastapi_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ def __init__(
Optional[List[str]],
Doc("List of tags to exclude from MCP tools. Cannot be used with include_tags."),
] = None,
max_tool_name_length: Annotated[
Optional[int], Doc("Maximum length allowed for tools (some vendors prohibit long names).")
] = None,
auth_config: Annotated[
Optional[AuthConfig],
Doc("Configuration for MCP authentication"),
Expand Down Expand Up @@ -136,6 +139,7 @@ def __init__(
self._exclude_operations = exclude_operations
self._include_tags = include_tags
self._exclude_tags = exclude_tags
self._max_tool_name_length = max_tool_name_length
self._auth_config = auth_config

if self._auth_config:
Expand Down Expand Up @@ -478,22 +482,28 @@ def _filter_tools(self, tools: List[types.Tool], openapi_schema: Dict[str, Any])
Returns:
Filtered list of tools
"""
if (
self._include_operations is None
and self._exclude_operations is None
and self._include_tags is None
and self._exclude_tags is None
):
include_exclude_conditions_exist = (
self._include_operations is not None
or self._exclude_operations is not None
or self._include_tags is not None
or self._exclude_tags is not None
)

if not include_exclude_conditions_exist and self._max_tool_name_length is None:
return tools

operations_by_tag: Dict[str, List[str]] = {}
for path, path_item in openapi_schema.get("paths", {}).items():
for method, operation in path_item.items():
operation_id = operation.get("operationId")
if method not in ["get", "post", "put", "delete", "patch"]:
logger.warning(f"Skipping non-HTTP method: {method.upper()} {path}, operation_id: {operation_id}")
continue

operation_id = operation.get("operationId")
if not operation_id:
logger.warning(
f"Skipping operation with no operationId: {method.upper()} {path}, details: {operation}"
)
continue

tags = operation.get("tags", [])
Expand All @@ -504,10 +514,11 @@ def _filter_tools(self, tools: List[types.Tool], openapi_schema: Dict[str, Any])

operations_to_include = set()

all_operations = {tool.name for tool in tools}

if self._include_operations is not None:
operations_to_include.update(self._include_operations)
elif self._exclude_operations is not None:
all_operations = {tool.name for tool in tools}
operations_to_include.update(all_operations - set(self._exclude_operations))

if self._include_tags is not None:
Expand All @@ -518,9 +529,25 @@ def _filter_tools(self, tools: List[types.Tool], openapi_schema: Dict[str, Any])
for tag in self._exclude_tags:
excluded_operations.update(operations_by_tag.get(tag, []))

all_operations = {tool.name for tool in tools}
operations_to_include.update(all_operations - excluded_operations)

# This condition means that no include/exclude conditions exist,
# and we've reached this point because we have a max-length limit
if not include_exclude_conditions_exist:
operations_to_include = all_operations

if self._max_tool_name_length is not None:
long_operations = {
tool.name for tool in tools if len(self.get_combined_full_name(tool.name)) > self._max_tool_name_length
}

if long_operations:
logger.warning(
f"Some operations exceed allowed max tool name length of {str(self._max_tool_name_length)} characters: {long_operations}"
)

operations_to_include = operations_to_include - long_operations

filtered_tools = [tool for tool in tools if tool.name in operations_to_include]

if filtered_tools:
Expand All @@ -530,3 +557,18 @@ def _filter_tools(self, tools: List[types.Tool], openapi_schema: Dict[str, Any])
}

return filtered_tools

def get_combined_full_name(self, operation_id: str) -> str:
"""
Combined name consists of server name + operation_id

Args:
operation_id: As defined during creation

Returns:
concatenated string of server name + operation_id
"""
if not self.name:
return operation_id

return f"{self.name}\\{operation_id}"
44 changes: 39 additions & 5 deletions tests/test_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ def test_default_configuration(simple_fastapi_app: FastAPI):
assert mcp_server._describe_all_responses is False
assert mcp_server._describe_full_response_schema is False

assert mcp_server._max_tool_name_length is None


def test_custom_configuration(simple_fastapi_app: FastAPI):
"""Test a custom configuration of FastApiMCP."""
Expand Down Expand Up @@ -372,13 +374,21 @@ async def delete_item(item_id: int):
async def search_items():
return [{"id": 1}]

@app.get("/long_search/", operation_id="search_items_long_id", tags=["search"])
async def search_items_long():
return [{"id": 1}]

# Benchmark - no filter
no_filter = FastApiMCP(app)
assert len(no_filter.tools) == 7

# Test include_operations
include_ops_mcp = FastApiMCP(app, include_operations=["get_item", "list_items"])
assert len(include_ops_mcp.tools) == 2
assert {tool.name for tool in include_ops_mcp.tools} == {"get_item", "list_items"}

# Test exclude_operations
exclude_ops_mcp = FastApiMCP(app, exclude_operations=["delete_item", "search_items"])
exclude_ops_mcp = FastApiMCP(app, exclude_operations=["delete_item", "search_items", "search_items_long_id"])
assert len(exclude_ops_mcp.tools) == 4
assert {tool.name for tool in exclude_ops_mcp.tools} == {"get_item", "list_items", "create_item", "update_item"}

Expand All @@ -389,13 +399,37 @@ async def search_items():

# Test exclude_tags
exclude_tags_mcp = FastApiMCP(app, exclude_tags=["write", "delete"])
assert len(exclude_tags_mcp.tools) == 3
assert {tool.name for tool in exclude_tags_mcp.tools} == {"get_item", "list_items", "search_items"}
assert len(exclude_tags_mcp.tools) == 4
assert {tool.name for tool in exclude_tags_mcp.tools} == {
"get_item",
"list_items",
"search_items",
"search_items_long_id",
}

# Test combining include_operations and include_tags
combined_include_mcp = FastApiMCP(app, include_operations=["delete_item"], include_tags=["search"])
assert len(combined_include_mcp.tools) == 2
assert {tool.name for tool in combined_include_mcp.tools} == {"delete_item", "search_items"}
assert len(combined_include_mcp.tools) == 3
assert {tool.name for tool in combined_include_mcp.tools} == {"delete_item", "search_items", "search_items_long_id"}

max_long_name_mcp = FastApiMCP(app, name="mcp_server", max_tool_name_length=25)
assert len(max_long_name_mcp.tools) == 6
assert {tool.name for tool in max_long_name_mcp.tools} == {
"list_items",
"get_item",
"create_item",
"update_item",
"delete_item",
"search_items",
}

combined_exclude_and_max_tags_mcp = FastApiMCP(app, exclude_tags=["write", "delete"], max_tool_name_length=25)
assert len(combined_exclude_and_max_tags_mcp.tools) == 3
assert {tool.name for tool in combined_exclude_and_max_tags_mcp.tools} == {
"get_item",
"list_items",
"search_items",
}

# Test invalid combinations
with pytest.raises(ValueError):
Expand Down