Skip to content

Commit ca7b1f4

Browse files
authored
Merge pull request #49 from tadata-org/feature/filter-endpoints
Granular control over which endpoints are being exposed as tools
2 parents ba9897e + 72fdee3 commit ca7b1f4

File tree

7 files changed

+478
-4
lines changed

7 files changed

+478
-4
lines changed

.coveragerc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
[run]
22
omit =
3-
examples/*
3+
examples/*
4+
tests/*

CHANGELOG.md

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [Unreleased]
9+
10+
### Changed
11+
- Complete refactor from function-based API to a new class-based API with `FastApiMCP`
12+
- Explicit separation between MCP instance creation and mounting with `mcp = FastApiMCP(app)` followed by `mcp.mount()`
13+
- FastAPI-native approach for transport providing more flexible routing options
14+
- Updated minimum MCP dependency to v1.6.0
15+
16+
### Added
17+
- Support for deploying MCP servers separately from API service
18+
- Support for "refreshing" with `setup_server()` when dynamically adding FastAPI routes. Fixes [Issue #19](https://github.com/tadata-org/fastapi_mcp/issues/19)
19+
- Endpoint filtering capabilities through new parameters:
20+
- `include_operations`: Expose only specific operations by their operation IDs
21+
- `exclude_operations`: Expose all operations except those with specified operation IDs
22+
- `include_tags`: Expose only operations with specific tags
23+
- `exclude_tags`: Expose all operations except those with specific tags
24+
25+
### Fixed
26+
- FastAPI-native approach for transport. Fixes [Issue #28](https://github.com/tadata-org/fastapi_mcp/issues/28)
27+
- 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)
28+
29+
### Removed
30+
- Function-based API (`add_mcp_server`, `create_mcp_server`, etc.)
31+
- Custom tool support via `@mcp.tool()` decorator
32+
833
## [0.1.8]
934

1035
### Fixed
@@ -73,4 +98,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7398
- Claude integration for easy installation and use
7499
- API integration that automatically makes HTTP requests to FastAPI endpoints
75100
- Examples directory with sample FastAPI application
76-
- Basic test suite
101+
- Basic test suite

README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,56 @@ mcp = FastApiMCP(
110110
mcp.mount()
111111
```
112112

113+
### Customizing Exposed Endpoints
114+
115+
You can control which FastAPI endpoints are exposed as MCP tools using Open API operation IDs or tags:
116+
117+
```python
118+
from fastapi import FastAPI
119+
from fastapi_mcp import FastApiMCP
120+
121+
app = FastAPI()
122+
123+
# Only include specific operations
124+
mcp = FastApiMCP(
125+
app,
126+
include_operations=["get_user", "create_user"]
127+
)
128+
129+
# Exclude specific operations
130+
mcp = FastApiMCP(
131+
app,
132+
exclude_operations=["delete_user"]
133+
)
134+
135+
# Only include operations with specific tags
136+
mcp = FastApiMCP(
137+
app,
138+
include_tags=["users", "public"]
139+
)
140+
141+
# Exclude operations with specific tags
142+
mcp = FastApiMCP(
143+
app,
144+
exclude_tags=["admin", "internal"]
145+
)
146+
147+
# Combine operation IDs and tags (include mode)
148+
mcp = FastApiMCP(
149+
app,
150+
include_operations=["user_login"],
151+
include_tags=["public"]
152+
)
153+
154+
mcp.mount()
155+
```
156+
157+
Notes on filtering:
158+
- You cannot use both `include_operations` and `exclude_operations` at the same time
159+
- You cannot use both `include_tags` and `exclude_tags` at the same time
160+
- You can combine operation filtering with tag filtering (e.g., use `include_operations` with `include_tags`)
161+
- When combining filters, a greedy approach will be taken. Endpoints matching either criteria will be included
162+
113163
### Deploying Separately from Original FastAPI App
114164

115165
You are not limited to serving the MCP on the same FastAPI app from which it was created.

examples/filtered_tools_example.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
from examples.shared.apps import items
2+
from examples.shared.setup import setup_logging
3+
4+
from fastapi_mcp import FastApiMCP
5+
6+
setup_logging()
7+
8+
# Example demonstrating how to filter MCP tools by operation IDs and tags
9+
10+
# Filter by including specific operation IDs
11+
include_operations_mcp = FastApiMCP(
12+
items.app,
13+
name="Item API MCP - Included Operations",
14+
description="MCP server showing only specific operations",
15+
base_url="http://localhost:8001",
16+
include_operations=["get_item", "list_items"],
17+
)
18+
19+
# Filter by excluding specific operation IDs
20+
exclude_operations_mcp = FastApiMCP(
21+
items.app,
22+
name="Item API MCP - Excluded Operations",
23+
description="MCP server showing all operations except the excluded ones",
24+
base_url="http://localhost:8002",
25+
exclude_operations=["create_item", "update_item", "delete_item"],
26+
)
27+
28+
# Filter by including specific tags
29+
include_tags_mcp = FastApiMCP(
30+
items.app,
31+
name="Item API MCP - Included Tags",
32+
description="MCP server showing operations with specific tags",
33+
base_url="http://localhost:8003",
34+
include_tags=["items"],
35+
)
36+
37+
# Filter by excluding specific tags
38+
exclude_tags_mcp = FastApiMCP(
39+
items.app,
40+
name="Item API MCP - Excluded Tags",
41+
description="MCP server showing operations except those with specific tags",
42+
base_url="http://localhost:8004",
43+
exclude_tags=["search"],
44+
)
45+
46+
# Combine operation IDs and tags (include mode)
47+
combined_include_mcp = FastApiMCP(
48+
items.app,
49+
name="Item API MCP - Combined Include",
50+
description="MCP server showing operations by combining include filters",
51+
base_url="http://localhost:8005",
52+
include_operations=["delete_item"],
53+
include_tags=["search"],
54+
)
55+
56+
# Mount all MCP servers with different paths
57+
include_operations_mcp.mount(mount_path="/include-operations-mcp")
58+
exclude_operations_mcp.mount(mount_path="/exclude-operations-mcp")
59+
include_tags_mcp.mount(mount_path="/include-tags-mcp")
60+
exclude_tags_mcp.mount(mount_path="/exclude-tags-mcp")
61+
combined_include_mcp.mount(mount_path="/combined-include-mcp")
62+
63+
if __name__ == "__main__":
64+
import uvicorn
65+
66+
print("Server is running with multiple MCP endpoints:")
67+
print(" - /include-operations-mcp: Only get_item and list_items operations")
68+
print(" - /exclude-operations-mcp: All operations except create_item, update_item, and delete_item")
69+
print(" - /include-tags-mcp: Only operations with the 'items' tag")
70+
print(" - /exclude-tags-mcp: All operations except those with the 'search' tag")
71+
print(" - /combined-include-mcp: Operations with 'search' tag or delete_item operation")
72+
uvicorn.run(items.app, host="0.0.0.0", port=8000)

fastapi_mcp/server.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ def __init__(
2727
describe_all_responses: bool = False,
2828
describe_full_response_schema: bool = False,
2929
http_client: Optional[AsyncClientProtocol] = None,
30+
include_operations: Optional[List[str]] = None,
31+
exclude_operations: Optional[List[str]] = None,
32+
include_tags: Optional[List[str]] = None,
33+
exclude_tags: Optional[List[str]] = None,
3034
):
3135
"""
3236
Create an MCP server from a FastAPI app.
@@ -42,7 +46,17 @@ def __init__(
4246
describe_full_response_schema: Whether to include full json schema for responses in tool descriptions
4347
http_client: Optional HTTP client to use for API calls. If not provided, a new httpx.AsyncClient will be created.
4448
This is primarily for testing purposes.
49+
include_operations: List of operation IDs to include as MCP tools. Cannot be used with exclude_operations.
50+
exclude_operations: List of operation IDs to exclude from MCP tools. Cannot be used with include_operations.
51+
include_tags: List of tags to include as MCP tools. Cannot be used with exclude_tags.
52+
exclude_tags: List of tags to exclude from MCP tools. Cannot be used with include_tags.
4553
"""
54+
# Validate operation and tag filtering options
55+
if include_operations is not None and exclude_operations is not None:
56+
raise ValueError("Cannot specify both include_operations and exclude_operations")
57+
58+
if include_tags is not None and exclude_tags is not None:
59+
raise ValueError("Cannot specify both include_tags and exclude_tags")
4660

4761
self.operation_map: Dict[str, Dict[str, Any]]
4862
self.tools: List[types.Tool]
@@ -55,6 +69,10 @@ def __init__(
5569
self._base_url = base_url
5670
self._describe_all_responses = describe_all_responses
5771
self._describe_full_response_schema = describe_full_response_schema
72+
self._include_operations = include_operations
73+
self._exclude_operations = exclude_operations
74+
self._include_tags = include_tags
75+
self._exclude_tags = exclude_tags
5876

5977
self._http_client = http_client or httpx.AsyncClient()
6078

@@ -71,12 +89,15 @@ def setup_server(self) -> None:
7189
)
7290

7391
# Convert OpenAPI schema to MCP tools
74-
self.tools, self.operation_map = convert_openapi_to_mcp_tools(
92+
all_tools, self.operation_map = convert_openapi_to_mcp_tools(
7593
openapi_schema,
7694
describe_all_responses=self._describe_all_responses,
7795
describe_full_response_schema=self._describe_full_response_schema,
7896
)
7997

98+
# Filter tools based on operation IDs and tags
99+
self.tools = self._filter_tools(all_tools, openapi_schema)
100+
80101
# Determine base URL if not provided
81102
if not self._base_url:
82103
# Try to determine the base URL from FastAPI config
@@ -266,3 +287,67 @@ async def _request(
266287
return await client.patch(url, params=query, headers=headers, json=body)
267288
else:
268289
raise ValueError(f"Unsupported HTTP method: {method}")
290+
291+
def _filter_tools(self, tools: List[types.Tool], openapi_schema: Dict[str, Any]) -> List[types.Tool]:
292+
"""
293+
Filter tools based on operation IDs and tags.
294+
295+
Args:
296+
tools: List of tools to filter
297+
openapi_schema: The OpenAPI schema
298+
299+
Returns:
300+
Filtered list of tools
301+
"""
302+
if (
303+
self._include_operations is None
304+
and self._exclude_operations is None
305+
and self._include_tags is None
306+
and self._exclude_tags is None
307+
):
308+
return tools
309+
310+
operations_by_tag: Dict[str, List[str]] = {}
311+
for path, path_item in openapi_schema.get("paths", {}).items():
312+
for method, operation in path_item.items():
313+
if method not in ["get", "post", "put", "delete", "patch"]:
314+
continue
315+
316+
operation_id = operation.get("operationId")
317+
if not operation_id:
318+
continue
319+
320+
tags = operation.get("tags", [])
321+
for tag in tags:
322+
if tag not in operations_by_tag:
323+
operations_by_tag[tag] = []
324+
operations_by_tag[tag].append(operation_id)
325+
326+
operations_to_include = set()
327+
328+
if self._include_operations is not None:
329+
operations_to_include.update(self._include_operations)
330+
elif self._exclude_operations is not None:
331+
all_operations = {tool.name for tool in tools}
332+
operations_to_include.update(all_operations - set(self._exclude_operations))
333+
334+
if self._include_tags is not None:
335+
for tag in self._include_tags:
336+
operations_to_include.update(operations_by_tag.get(tag, []))
337+
elif self._exclude_tags is not None:
338+
excluded_operations = set()
339+
for tag in self._exclude_tags:
340+
excluded_operations.update(operations_by_tag.get(tag, []))
341+
342+
all_operations = {tool.name for tool in tools}
343+
operations_to_include.update(all_operations - excluded_operations)
344+
345+
filtered_tools = [tool for tool in tools if tool.name in operations_to_include]
346+
347+
if filtered_tools:
348+
filtered_operation_ids = {tool.name for tool in filtered_tools}
349+
self.operation_map = {
350+
op_id: details for op_id, details in self.operation_map.items() if op_id in filtered_operation_ids
351+
}
352+
353+
return filtered_tools

pytest.ini

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[pytest]
2-
addopts = -vvv --cov=. --cov-report xml --cov-report term-missing --cov-fail-under=92
2+
addopts = -vvv --cov=. --cov-report xml --cov-report term-missing --cov-fail-under=80
33
asyncio_mode = auto
44
log_cli = true
55
log_cli_level = DEBUG

0 commit comments

Comments
 (0)