Skip to content

Granular control over which endpoints are being exposed as tools #49

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

Merged
merged 5 commits into from
Apr 10, 2025
Merged
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
3 changes: 2 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
[run]
omit =
examples/*
examples/*
tests/*
27 changes: 26 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
72 changes: 72 additions & 0 deletions examples/filtered_tools_example.py
Original file line number Diff line number Diff line change
@@ -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)
87 changes: 86 additions & 1 deletion fastapi_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@
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.
Expand All @@ -42,7 +46,17 @@
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]
Expand All @@ -55,6 +69,10 @@
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()

Expand All @@ -71,12 +89,15 @@
)

# 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
Expand Down Expand Up @@ -266,3 +287,67 @@
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

Check warning on line 314 in fastapi_mcp/server.py

View check run for this annotation

Codecov / codecov/patch

fastapi_mcp/server.py#L314

Added line #L314 was not covered by tests

operation_id = operation.get("operationId")
if not operation_id:
continue

Check warning on line 318 in fastapi_mcp/server.py

View check run for this annotation

Codecov / codecov/patch

fastapi_mcp/server.py#L318

Added line #L318 was not covered by tests

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
2 changes: 1 addition & 1 deletion pytest.ini
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading