mirror of
https://github.com/tadata-org/fastapi_mcp.git
synced 2025-04-13 23:32:11 +03:00
Merge pull request #49 from tadata-org/feature/filter-endpoints
Granular control over which endpoints are being exposed as tools
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
[run]
|
||||
omit =
|
||||
examples/*
|
||||
examples/*
|
||||
tests/*
|
||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
- Complete refactor from function-based API to a new class-based API with `FastApiMCP`
|
||||
- Explicit separation between MCP instance creation and mounting with `mcp = FastApiMCP(app)` followed by `mcp.mount()`
|
||||
- FastAPI-native approach for transport providing more flexible routing options
|
||||
- Updated minimum MCP dependency to v1.6.0
|
||||
|
||||
### Added
|
||||
- Support for deploying MCP servers separately from API service
|
||||
- Support for "refreshing" with `setup_server()` when dynamically adding FastAPI routes. Fixes [Issue #19](https://github.com/tadata-org/fastapi_mcp/issues/19)
|
||||
- Endpoint filtering capabilities through new parameters:
|
||||
- `include_operations`: Expose only specific operations by their operation IDs
|
||||
- `exclude_operations`: Expose all operations except those with specified operation IDs
|
||||
- `include_tags`: Expose only operations with specific tags
|
||||
- `exclude_tags`: Expose all operations except those with specific tags
|
||||
|
||||
### Fixed
|
||||
- FastAPI-native approach for transport. Fixes [Issue #28](https://github.com/tadata-org/fastapi_mcp/issues/28)
|
||||
- Numerous bugs in OpenAPI schema to tool conversion, addressing [Issue #40](https://github.com/tadata-org/fastapi_mcp/issues/40) and [Issue #45](https://github.com/tadata-org/fastapi_mcp/issues/45)
|
||||
|
||||
### Removed
|
||||
- Function-based API (`add_mcp_server`, `create_mcp_server`, etc.)
|
||||
- Custom tool support via `@mcp.tool()` decorator
|
||||
|
||||
## [0.1.8]
|
||||
|
||||
### Fixed
|
||||
@@ -73,4 +98,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Claude integration for easy installation and use
|
||||
- API integration that automatically makes HTTP requests to FastAPI endpoints
|
||||
- Examples directory with sample FastAPI application
|
||||
- Basic test suite
|
||||
- Basic test suite
|
||||
50
README.md
50
README.md
@@ -110,6 +110,56 @@ mcp = FastApiMCP(
|
||||
mcp.mount()
|
||||
```
|
||||
|
||||
### Customizing Exposed Endpoints
|
||||
|
||||
You can control which FastAPI endpoints are exposed as MCP tools using Open API operation IDs or tags:
|
||||
|
||||
```python
|
||||
from fastapi import FastAPI
|
||||
from fastapi_mcp import FastApiMCP
|
||||
|
||||
app = FastAPI()
|
||||
|
||||
# Only include specific operations
|
||||
mcp = FastApiMCP(
|
||||
app,
|
||||
include_operations=["get_user", "create_user"]
|
||||
)
|
||||
|
||||
# Exclude specific operations
|
||||
mcp = FastApiMCP(
|
||||
app,
|
||||
exclude_operations=["delete_user"]
|
||||
)
|
||||
|
||||
# Only include operations with specific tags
|
||||
mcp = FastApiMCP(
|
||||
app,
|
||||
include_tags=["users", "public"]
|
||||
)
|
||||
|
||||
# Exclude operations with specific tags
|
||||
mcp = FastApiMCP(
|
||||
app,
|
||||
exclude_tags=["admin", "internal"]
|
||||
)
|
||||
|
||||
# Combine operation IDs and tags (include mode)
|
||||
mcp = FastApiMCP(
|
||||
app,
|
||||
include_operations=["user_login"],
|
||||
include_tags=["public"]
|
||||
)
|
||||
|
||||
mcp.mount()
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
### Deploying Separately from Original FastAPI App
|
||||
|
||||
You are not limited to serving the MCP on the same FastAPI app from which it was created.
|
||||
|
||||
72
examples/filtered_tools_example.py
Normal file
72
examples/filtered_tools_example.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from examples.shared.apps import items
|
||||
from examples.shared.setup import setup_logging
|
||||
|
||||
from fastapi_mcp import FastApiMCP
|
||||
|
||||
setup_logging()
|
||||
|
||||
# Example demonstrating how to filter MCP tools by operation IDs and tags
|
||||
|
||||
# Filter by including specific operation IDs
|
||||
include_operations_mcp = FastApiMCP(
|
||||
items.app,
|
||||
name="Item API MCP - Included Operations",
|
||||
description="MCP server showing only specific operations",
|
||||
base_url="http://localhost:8001",
|
||||
include_operations=["get_item", "list_items"],
|
||||
)
|
||||
|
||||
# Filter by excluding specific operation IDs
|
||||
exclude_operations_mcp = FastApiMCP(
|
||||
items.app,
|
||||
name="Item API MCP - Excluded Operations",
|
||||
description="MCP server showing all operations except the excluded ones",
|
||||
base_url="http://localhost:8002",
|
||||
exclude_operations=["create_item", "update_item", "delete_item"],
|
||||
)
|
||||
|
||||
# Filter by including specific tags
|
||||
include_tags_mcp = FastApiMCP(
|
||||
items.app,
|
||||
name="Item API MCP - Included Tags",
|
||||
description="MCP server showing operations with specific tags",
|
||||
base_url="http://localhost:8003",
|
||||
include_tags=["items"],
|
||||
)
|
||||
|
||||
# Filter by excluding specific tags
|
||||
exclude_tags_mcp = FastApiMCP(
|
||||
items.app,
|
||||
name="Item API MCP - Excluded Tags",
|
||||
description="MCP server showing operations except those with specific tags",
|
||||
base_url="http://localhost:8004",
|
||||
exclude_tags=["search"],
|
||||
)
|
||||
|
||||
# Combine operation IDs and tags (include mode)
|
||||
combined_include_mcp = FastApiMCP(
|
||||
items.app,
|
||||
name="Item API MCP - Combined Include",
|
||||
description="MCP server showing operations by combining include filters",
|
||||
base_url="http://localhost:8005",
|
||||
include_operations=["delete_item"],
|
||||
include_tags=["search"],
|
||||
)
|
||||
|
||||
# Mount all MCP servers with different paths
|
||||
include_operations_mcp.mount(mount_path="/include-operations-mcp")
|
||||
exclude_operations_mcp.mount(mount_path="/exclude-operations-mcp")
|
||||
include_tags_mcp.mount(mount_path="/include-tags-mcp")
|
||||
exclude_tags_mcp.mount(mount_path="/exclude-tags-mcp")
|
||||
combined_include_mcp.mount(mount_path="/combined-include-mcp")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
print("Server is running with multiple MCP endpoints:")
|
||||
print(" - /include-operations-mcp: Only get_item and list_items operations")
|
||||
print(" - /exclude-operations-mcp: All operations except create_item, update_item, and delete_item")
|
||||
print(" - /include-tags-mcp: Only operations with the 'items' tag")
|
||||
print(" - /exclude-tags-mcp: All operations except those with the 'search' tag")
|
||||
print(" - /combined-include-mcp: Operations with 'search' tag or delete_item operation")
|
||||
uvicorn.run(items.app, host="0.0.0.0", port=8000)
|
||||
@@ -27,6 +27,10 @@ class FastApiMCP:
|
||||
describe_all_responses: bool = False,
|
||||
describe_full_response_schema: bool = False,
|
||||
http_client: Optional[AsyncClientProtocol] = None,
|
||||
include_operations: Optional[List[str]] = None,
|
||||
exclude_operations: Optional[List[str]] = None,
|
||||
include_tags: Optional[List[str]] = None,
|
||||
exclude_tags: Optional[List[str]] = None,
|
||||
):
|
||||
"""
|
||||
Create an MCP server from a FastAPI app.
|
||||
@@ -42,7 +46,17 @@ class FastApiMCP:
|
||||
describe_full_response_schema: Whether to include full json schema for responses in tool descriptions
|
||||
http_client: Optional HTTP client to use for API calls. If not provided, a new httpx.AsyncClient will be created.
|
||||
This is primarily for testing purposes.
|
||||
include_operations: List of operation IDs to include as MCP tools. Cannot be used with exclude_operations.
|
||||
exclude_operations: List of operation IDs to exclude from MCP tools. Cannot be used with include_operations.
|
||||
include_tags: List of tags to include as MCP tools. Cannot be used with exclude_tags.
|
||||
exclude_tags: List of tags to exclude from MCP tools. Cannot be used with include_tags.
|
||||
"""
|
||||
# Validate operation and tag filtering options
|
||||
if include_operations is not None and exclude_operations is not None:
|
||||
raise ValueError("Cannot specify both include_operations and exclude_operations")
|
||||
|
||||
if include_tags is not None and exclude_tags is not None:
|
||||
raise ValueError("Cannot specify both include_tags and exclude_tags")
|
||||
|
||||
self.operation_map: Dict[str, Dict[str, Any]]
|
||||
self.tools: List[types.Tool]
|
||||
@@ -55,6 +69,10 @@ class FastApiMCP:
|
||||
self._base_url = base_url
|
||||
self._describe_all_responses = describe_all_responses
|
||||
self._describe_full_response_schema = describe_full_response_schema
|
||||
self._include_operations = include_operations
|
||||
self._exclude_operations = exclude_operations
|
||||
self._include_tags = include_tags
|
||||
self._exclude_tags = exclude_tags
|
||||
|
||||
self._http_client = http_client or httpx.AsyncClient()
|
||||
|
||||
@@ -71,12 +89,15 @@ class FastApiMCP:
|
||||
)
|
||||
|
||||
# Convert OpenAPI schema to MCP tools
|
||||
self.tools, self.operation_map = convert_openapi_to_mcp_tools(
|
||||
all_tools, self.operation_map = convert_openapi_to_mcp_tools(
|
||||
openapi_schema,
|
||||
describe_all_responses=self._describe_all_responses,
|
||||
describe_full_response_schema=self._describe_full_response_schema,
|
||||
)
|
||||
|
||||
# Filter tools based on operation IDs and tags
|
||||
self.tools = self._filter_tools(all_tools, openapi_schema)
|
||||
|
||||
# Determine base URL if not provided
|
||||
if not self._base_url:
|
||||
# Try to determine the base URL from FastAPI config
|
||||
@@ -266,3 +287,67 @@ class FastApiMCP:
|
||||
return await client.patch(url, params=query, headers=headers, json=body)
|
||||
else:
|
||||
raise ValueError(f"Unsupported HTTP method: {method}")
|
||||
|
||||
def _filter_tools(self, tools: List[types.Tool], openapi_schema: Dict[str, Any]) -> List[types.Tool]:
|
||||
"""
|
||||
Filter tools based on operation IDs and tags.
|
||||
|
||||
Args:
|
||||
tools: List of tools to filter
|
||||
openapi_schema: The OpenAPI schema
|
||||
|
||||
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
|
||||
):
|
||||
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():
|
||||
if method not in ["get", "post", "put", "delete", "patch"]:
|
||||
continue
|
||||
|
||||
operation_id = operation.get("operationId")
|
||||
if not operation_id:
|
||||
continue
|
||||
|
||||
tags = operation.get("tags", [])
|
||||
for tag in tags:
|
||||
if tag not in operations_by_tag:
|
||||
operations_by_tag[tag] = []
|
||||
operations_by_tag[tag].append(operation_id)
|
||||
|
||||
operations_to_include = set()
|
||||
|
||||
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:
|
||||
for tag in self._include_tags:
|
||||
operations_to_include.update(operations_by_tag.get(tag, []))
|
||||
elif self._exclude_tags is not None:
|
||||
excluded_operations = set()
|
||||
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)
|
||||
|
||||
filtered_tools = [tool for tool in tools if tool.name in operations_to_include]
|
||||
|
||||
if filtered_tools:
|
||||
filtered_operation_ids = {tool.name for tool in filtered_tools}
|
||||
self.operation_map = {
|
||||
op_id: details for op_id, details in self.operation_map.items() if op_id in filtered_operation_ids
|
||||
}
|
||||
|
||||
return filtered_tools
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[pytest]
|
||||
addopts = -vvv --cov=. --cov-report xml --cov-report term-missing --cov-fail-under=92
|
||||
addopts = -vvv --cov=. --cov-report xml --cov-report term-missing --cov-fail-under=80
|
||||
asyncio_mode = auto
|
||||
log_cli = true
|
||||
log_cli_level = DEBUG
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from fastapi import FastAPI
|
||||
import pytest
|
||||
|
||||
from fastapi_mcp import FastApiMCP
|
||||
|
||||
@@ -374,3 +375,243 @@ def test_describe_all_responses_and_full_response_schema_config_complex_app(comp
|
||||
assert tool.description.count("**Output Schema:**") > 0, (
|
||||
"The description should contain full output schemas"
|
||||
)
|
||||
|
||||
|
||||
def test_filtering_functionality():
|
||||
"""Test that FastApiMCP correctly filters endpoints based on operation IDs and tags."""
|
||||
app = FastAPI()
|
||||
|
||||
# Define endpoints with different operation IDs and tags
|
||||
@app.get("/items/", operation_id="list_items", tags=["items"])
|
||||
async def list_items():
|
||||
return [{"id": 1}]
|
||||
|
||||
@app.get("/items/{item_id}", operation_id="get_item", tags=["items", "read"])
|
||||
async def get_item(item_id: int):
|
||||
return {"id": item_id}
|
||||
|
||||
@app.post("/items/", operation_id="create_item", tags=["items", "write"])
|
||||
async def create_item():
|
||||
return {"id": 2}
|
||||
|
||||
@app.put("/items/{item_id}", operation_id="update_item", tags=["items", "write"])
|
||||
async def update_item(item_id: int):
|
||||
return {"id": item_id}
|
||||
|
||||
@app.delete("/items/{item_id}", operation_id="delete_item", tags=["items", "delete"])
|
||||
async def delete_item(item_id: int):
|
||||
return {"id": item_id}
|
||||
|
||||
@app.get("/search/", operation_id="search_items", tags=["search"])
|
||||
async def search_items():
|
||||
return [{"id": 1}]
|
||||
|
||||
# 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"])
|
||||
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"}
|
||||
|
||||
# Test include_tags
|
||||
include_tags_mcp = FastApiMCP(app, include_tags=["read"])
|
||||
assert len(include_tags_mcp.tools) == 1
|
||||
assert {tool.name for tool in include_tags_mcp.tools} == {"get_item"}
|
||||
|
||||
# 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"}
|
||||
|
||||
# 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"}
|
||||
|
||||
# Test invalid combinations
|
||||
with pytest.raises(ValueError):
|
||||
FastApiMCP(app, include_operations=["get_item"], exclude_operations=["delete_item"])
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
FastApiMCP(app, include_tags=["items"], exclude_tags=["write"])
|
||||
|
||||
|
||||
def test_filtering_edge_cases():
|
||||
"""Test edge cases for the filtering functionality."""
|
||||
app = FastAPI()
|
||||
|
||||
# Define endpoints with different operation IDs and tags
|
||||
@app.get("/items/", operation_id="list_items", tags=["items"])
|
||||
async def list_items():
|
||||
return [{"id": 1}]
|
||||
|
||||
@app.get("/items/{item_id}", operation_id="get_item", tags=["items", "read"])
|
||||
async def get_item(item_id: int):
|
||||
return {"id": item_id}
|
||||
|
||||
# Test with no filtering (default behavior)
|
||||
default_mcp = FastApiMCP(app)
|
||||
assert len(default_mcp.tools) == 2
|
||||
assert {tool.name for tool in default_mcp.tools} == {"get_item", "list_items"}
|
||||
|
||||
# Test with empty include_operations
|
||||
empty_include_ops_mcp = FastApiMCP(app, include_operations=[])
|
||||
assert len(empty_include_ops_mcp.tools) == 0
|
||||
assert empty_include_ops_mcp.tools == []
|
||||
|
||||
# Test with empty exclude_operations (should include all)
|
||||
empty_exclude_ops_mcp = FastApiMCP(app, exclude_operations=[])
|
||||
assert len(empty_exclude_ops_mcp.tools) == 2
|
||||
assert {tool.name for tool in empty_exclude_ops_mcp.tools} == {"get_item", "list_items"}
|
||||
|
||||
# Test with empty include_tags
|
||||
empty_include_tags_mcp = FastApiMCP(app, include_tags=[])
|
||||
assert len(empty_include_tags_mcp.tools) == 0
|
||||
assert empty_include_tags_mcp.tools == []
|
||||
|
||||
# Test with empty exclude_tags (should include all)
|
||||
empty_exclude_tags_mcp = FastApiMCP(app, exclude_tags=[])
|
||||
assert len(empty_exclude_tags_mcp.tools) == 2
|
||||
assert {tool.name for tool in empty_exclude_tags_mcp.tools} == {"get_item", "list_items"}
|
||||
|
||||
# Test with non-existent operation IDs
|
||||
nonexistent_ops_mcp = FastApiMCP(app, include_operations=["non_existent_op"])
|
||||
assert len(nonexistent_ops_mcp.tools) == 0
|
||||
assert nonexistent_ops_mcp.tools == []
|
||||
|
||||
# Test with non-existent tags
|
||||
nonexistent_tags_mcp = FastApiMCP(app, include_tags=["non_existent_tag"])
|
||||
assert len(nonexistent_tags_mcp.tools) == 0
|
||||
assert nonexistent_tags_mcp.tools == []
|
||||
|
||||
# Test excluding non-existent operation IDs
|
||||
exclude_nonexistent_ops_mcp = FastApiMCP(app, exclude_operations=["non_existent_op"])
|
||||
assert len(exclude_nonexistent_ops_mcp.tools) == 2
|
||||
assert {tool.name for tool in exclude_nonexistent_ops_mcp.tools} == {"get_item", "list_items"}
|
||||
|
||||
# Test excluding non-existent tags
|
||||
exclude_nonexistent_tags_mcp = FastApiMCP(app, exclude_tags=["non_existent_tag"])
|
||||
assert len(exclude_nonexistent_tags_mcp.tools) == 2
|
||||
assert {tool.name for tool in exclude_nonexistent_tags_mcp.tools} == {"get_item", "list_items"}
|
||||
|
||||
# Test with an endpoint that has no tags
|
||||
@app.get("/no-tags", operation_id="no_tags")
|
||||
async def no_tags():
|
||||
return {"result": "no tags"}
|
||||
|
||||
# Test include_tags with an endpoint that has no tags
|
||||
no_tags_app_mcp = FastApiMCP(app, include_tags=["items"])
|
||||
assert len(no_tags_app_mcp.tools) == 2
|
||||
assert "no_tags" not in {tool.name for tool in no_tags_app_mcp.tools}
|
||||
|
||||
# Test exclude_tags with an endpoint that has no tags
|
||||
no_tags_exclude_mcp = FastApiMCP(app, exclude_tags=["items"])
|
||||
assert len(no_tags_exclude_mcp.tools) == 1
|
||||
assert {tool.name for tool in no_tags_exclude_mcp.tools} == {"no_tags"}
|
||||
|
||||
|
||||
def test_filtering_with_missing_operation_ids():
|
||||
"""Test filtering behavior with endpoints that don't have operation IDs."""
|
||||
app = FastAPI()
|
||||
|
||||
# Define an endpoint with an operation ID
|
||||
@app.get("/items/", operation_id="list_items", tags=["items"])
|
||||
async def list_items():
|
||||
return [{"id": 1}]
|
||||
|
||||
# Define an endpoint without an operation ID
|
||||
@app.get("/no-op-id/")
|
||||
async def no_op_id():
|
||||
return {"result": "no operation ID"}
|
||||
|
||||
# Test that both endpoints are discovered
|
||||
default_mcp = FastApiMCP(app)
|
||||
|
||||
# FastAPI-MCP will generate an operation ID for endpoints without one
|
||||
# The auto-generated ID will typically be 'no_op_id_no_op_id__get'
|
||||
assert len(default_mcp.tools) == 2
|
||||
|
||||
# Get the auto-generated operation ID
|
||||
auto_generated_op_id = None
|
||||
for tool in default_mcp.tools:
|
||||
if tool.name != "list_items":
|
||||
auto_generated_op_id = tool.name
|
||||
break
|
||||
|
||||
assert auto_generated_op_id is not None
|
||||
assert "list_items" in {tool.name for tool in default_mcp.tools}
|
||||
|
||||
# Test include_operations with the known operation ID
|
||||
include_ops_mcp = FastApiMCP(app, include_operations=["list_items"])
|
||||
assert len(include_ops_mcp.tools) == 1
|
||||
assert {tool.name for tool in include_ops_mcp.tools} == {"list_items"}
|
||||
|
||||
# Test include_operations with the auto-generated operation ID
|
||||
include_auto_ops_mcp = FastApiMCP(app, include_operations=[auto_generated_op_id])
|
||||
assert len(include_auto_ops_mcp.tools) == 1
|
||||
assert {tool.name for tool in include_auto_ops_mcp.tools} == {auto_generated_op_id}
|
||||
|
||||
# Test include_tags with a tag that matches the endpoint
|
||||
include_tags_mcp = FastApiMCP(app, include_tags=["items"])
|
||||
assert len(include_tags_mcp.tools) == 1
|
||||
assert {tool.name for tool in include_tags_mcp.tools} == {"list_items"}
|
||||
|
||||
|
||||
def test_filter_with_empty_tools():
|
||||
"""Test filtering with an empty tools list to ensure it handles this edge case correctly."""
|
||||
# Create a FastAPI app without any routes
|
||||
app = FastAPI()
|
||||
|
||||
# Create MCP server (should have no tools)
|
||||
empty_mcp = FastApiMCP(app)
|
||||
assert len(empty_mcp.tools) == 0
|
||||
|
||||
# Test filtering with various options on an empty app
|
||||
include_ops_mcp = FastApiMCP(app, include_operations=["some_op"])
|
||||
assert len(include_ops_mcp.tools) == 0
|
||||
|
||||
exclude_ops_mcp = FastApiMCP(app, exclude_operations=["some_op"])
|
||||
assert len(exclude_ops_mcp.tools) == 0
|
||||
|
||||
include_tags_mcp = FastApiMCP(app, include_tags=["some_tag"])
|
||||
assert len(include_tags_mcp.tools) == 0
|
||||
|
||||
exclude_tags_mcp = FastApiMCP(app, exclude_tags=["some_tag"])
|
||||
assert len(exclude_tags_mcp.tools) == 0
|
||||
|
||||
# Test combined filtering
|
||||
combined_mcp = FastApiMCP(app, include_operations=["op"], include_tags=["tag"])
|
||||
assert len(combined_mcp.tools) == 0
|
||||
|
||||
|
||||
def test_filtering_with_empty_tags_array():
|
||||
"""Test filtering behavior with endpoints that have empty tags array."""
|
||||
app = FastAPI()
|
||||
|
||||
# Define an endpoint with tags
|
||||
@app.get("/items/", operation_id="list_items", tags=["items"])
|
||||
async def list_items():
|
||||
return [{"id": 1}]
|
||||
|
||||
# Define an endpoint with an empty tags array
|
||||
@app.get("/empty-tags/", operation_id="empty_tags", tags=[])
|
||||
async def empty_tags():
|
||||
return {"result": "empty tags"}
|
||||
|
||||
# Test default behavior
|
||||
default_mcp = FastApiMCP(app)
|
||||
assert len(default_mcp.tools) == 2
|
||||
assert {tool.name for tool in default_mcp.tools} == {"list_items", "empty_tags"}
|
||||
|
||||
# Test include_tags
|
||||
include_tags_mcp = FastApiMCP(app, include_tags=["items"])
|
||||
assert len(include_tags_mcp.tools) == 1
|
||||
assert {tool.name for tool in include_tags_mcp.tools} == {"list_items"}
|
||||
|
||||
# Test exclude_tags
|
||||
exclude_tags_mcp = FastApiMCP(app, exclude_tags=["items"])
|
||||
assert len(exclude_tags_mcp.tools) == 1
|
||||
assert {tool.name for tool in exclude_tags_mcp.tools} == {"empty_tags"}
|
||||
|
||||
Reference in New Issue
Block a user