Skip to content
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

Utilize tag on fastAPI paths to only create tools when opted-in #5

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: 18 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,62 @@ 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).

## [0.1.5]

### Changed

- Introduced `include_in_mcp` tag- unless this is included in tags for FastAPI paths,
we will exclude the paths from being converted into MCP tools. This is to introduce
opt-in conversion of paths-> tools.
- This feature is only enabled when the user sets the 'create_tools_by_default'
variable to false when calling `add_mcp_server`

## [0.1.4]

### Fixed
- [Issue #8](https://github.com/tadata-org/fastapi_mcp/issues/8): Converted tools unuseable due to wrong passing of arguments.


## [0.1.3]

### Fixed

- Dependency resolution issue with `mcp` package and `pydantic-settings`

## [0.1.2]

### Changed

- Complete refactor: transformed from a code generator to a direct integration library
- Replaced the CLI-based approach with a direct API for adding MCP servers to FastAPI applications
- Integrated MCP servers now mount directly to FastAPI apps at runtime instead of generating separate code
- Simplified the API with a single `add_mcp_server` function for quick integration
- Removed code generation entirely in favor of runtime integration

### Added

- Main `add_mcp_server` function for simple MCP server integration
- Support for adding custom MCP tools alongside API-derived tools
- Improved test suite
- Manage with uv

### Removed

- CLI interface and all associated commands (generate, run, install, etc.)
- Code generation functionality

## [0.1.1] - 2024-07-03

### Fixed

- Added support for PEP 604 union type syntax (e.g., `str | None`) in FastAPI endpoints
- Improved type handling in model field generation for newer Python versions (3.10+)
- Fixed compatibility issues with modern type annotations in path parameters, query parameters, and Pydantic models

## [0.1.0] - 2024-03-08

### Added

- Initial release of FastAPI-MCP
- Core functionality for converting FastAPI applications to MCP servers
- CLI tool for generating, running, and installing MCP servers
Expand All @@ -53,4 +70,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
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,39 @@ add_mcp_server(

That's it! Your auto-generated MCP server is now available at `https://app.base.url/mcp`.

By default, all registered FastAPI paths will have MCP tools created for them

If you wish to turn on the ability to not have tools created by default, pass
the `create_tools_by_default = false` flag into your `add_mcp_server` call.

Ex )

```
add_mcp_server(
app,
mount_path="/mcp",
name="My API MCP",
create_tools_by_default = false
Comment on lines +55 to +64

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

False should be capitalized.

)
```

Then, for each path you wish to create a tool for, include the following in the path's tags: `include_in_mcp`

Ex)

```
@app.post("/items/", response_model=Item, tags=["items", "include_in_mcp"])

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am interested in using tags to classify resource types for mcp.

I was thinking about using something like mcp_{resource_type}, where resource type is resource, prompt, etc.

How would you feel about changing the keyword tag to mcp_include rather than include_in_mcp? That way we can start to develop a tag structure for future functionality.

I'm not an owner/official maintainer of this repository, just voicing my thoughts.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's actually funny you suggest- I'm not an owner or maintainer either, but felt like this was needed so we could use the library.

Someone at my company actually suggested we have a list of tags that we utilize instead for defining mcp tools. the idea is that folks could reuse the tags they already have, or add new ones, to indicate what should have tools built upon them.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, 100% agreed.

Also, I only included half a thought in my review. What I meant to say is that I just started working on a piece of code to expose endpoints as specific MCP types. I was thinking about using tags to indicate how each endpoint should be classified.

I'm wondering if maybe that might be the solution. In the code I'm working on, maybe you add tags to endpoints if you want to classify the endpoint as a different MCP type (defaults to tool as normal).

Then you could have an option to exclude_untagged or something like that to only include endpoints that have an MCP type tag.

async def create_item(item: Item):
"""
Create a new item.

Returns the created item.
"""
return item
```

If `create_tools_by_default` is set to 'false', any paths _without_ the `include_in_mcp` tag will not have a corresponding MCP tool created.

## Advanced Usage

FastAPI-MCP provides several ways to customize and control how your MCP server is created and configured. Here are some advanced usage patterns:
Expand Down
12 changes: 6 additions & 6 deletions examples/simple_integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class Item(BaseModel):


# Define some endpoints
@app.get("/items/", response_model=List[Item], tags=["items"])
@app.get("/items/", response_model=List[Item], tags=["items", "include_in_mcp"])
async def list_items(skip: int = 0, limit: int = 10):
"""
List all items in the database.
Expand All @@ -41,7 +41,7 @@ async def list_items(skip: int = 0, limit: int = 10):
return list(items_db.values())[skip : skip + limit]


@app.get("/items/{item_id}", response_model=Item, tags=["items"])
@app.get("/items/{item_id}", response_model=Item, tags=["items", "include_in_mcp"])
async def read_item(item_id: int):
"""
Get a specific item by its ID.
Expand All @@ -53,7 +53,7 @@ async def read_item(item_id: int):
return items_db[item_id]


@app.post("/items/", response_model=Item, tags=["items"])
@app.post("/items/", response_model=Item, tags=["items", "include_in_mcp"])
async def create_item(item: Item):
"""
Create a new item in the database.
Expand All @@ -64,7 +64,7 @@ async def create_item(item: Item):
return item


@app.put("/items/{item_id}", response_model=Item, tags=["items"])
@app.put("/items/{item_id}", response_model=Item, tags=["items", "include_in_mcp"])
async def update_item(item_id: int, item: Item):
"""
Update an existing item.
Expand All @@ -79,7 +79,7 @@ async def update_item(item_id: int, item: Item):
return item


@app.delete("/items/{item_id}", tags=["items"])
@app.delete("/items/{item_id}", tags=["items", "include_in_mcp"])
async def delete_item(item_id: int):
"""
Delete an item from the database.
Expand All @@ -93,7 +93,7 @@ async def delete_item(item_id: int):
return {"message": "Item deleted successfully"}


@app.get("/items/search/", response_model=List[Item], tags=["search"])
@app.get("/items/search/", response_model=List[Item], tags=["search", "include_in_mcp"])
async def search_items(
q: Optional[str] = Query(None, description="Search query string"),
min_price: Optional[float] = Query(None, description="Minimum price"),
Expand Down
8 changes: 7 additions & 1 deletion fastapi_mcp/http_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ def create_mcp_tools_from_openapi(
base_url: Optional[str] = None,
describe_all_responses: bool = False,
describe_full_response_schema: bool = False,
create_tools_by_default: bool = True,
) -> None:
"""
Create MCP tools from a FastAPI app's OpenAPI schema.
Expand Down Expand Up @@ -150,11 +151,16 @@ def create_mcp_tools_from_openapi(
if method not in ["get", "post", "put", "delete", "patch"]:
continue

# If we do not create tools by default,
# Skip registering tools unless they are explicitly allowed
if not create_tools_by_default:
if 'include_in_mcp' not in operation.get("tags", []) and "handle_mcp_connection_mcp_get" not in operation.get("operationId", ""):
continue
# Get operation metadata
operation_id = operation.get("operationId")
if not operation_id:
continue

# Create MCP tool for this operation
create_http_tool(
mcp_server=mcp_server,
Expand Down
10 changes: 10 additions & 0 deletions fastapi_mcp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@
from .http_tools import create_mcp_tools_from_openapi


create_tools_by_default = True

def create_mcp_server(
app: FastAPI,
name: Optional[str] = None,
description: Optional[str] = None,
capabilities: Optional[Dict[str, Any]] = None,
create_tools_by_default: Optional[bool] = True
) -> FastMCP:
"""
Create an MCP server from a FastAPI app.
Expand All @@ -28,6 +31,9 @@ def create_mcp_server(
name: Name for the MCP server (defaults to app.title)
description: Description for the MCP server (defaults to app.description)
capabilities: Optional capabilities for the MCP server
create_tools_by_default: Optional bool value use to indicate if we should
consider 'include_in_mcp' when creating tools from FastAPI paths


Returns:
The created FastMCP instance
Expand Down Expand Up @@ -56,6 +62,7 @@ def mount_mcp_server(
base_url: Optional[str] = None,
describe_all_responses: bool = False,
describe_full_response_schema: bool = False,
create_tools_by_default: bool = True
) -> None:
"""
Mount an MCP server to a FastAPI app.
Expand Down Expand Up @@ -99,6 +106,7 @@ async def handle_mcp_connection(request: Request):
base_url,
describe_all_responses=describe_all_responses,
describe_full_response_schema=describe_full_response_schema,
create_tools_by_default=create_tools_by_default
)


Expand All @@ -112,6 +120,7 @@ def add_mcp_server(
base_url: Optional[str] = None,
describe_all_responses: bool = False,
describe_full_response_schema: bool = False,
create_tools_by_default: bool = True
) -> FastMCP:
"""
Add an MCP server to a FastAPI app.
Expand Down Expand Up @@ -142,6 +151,7 @@ def add_mcp_server(
base_url,
describe_all_responses=describe_all_responses,
describe_full_response_schema=describe_full_response_schema,
create_tools_by_default=create_tools_by_default
)

return mcp_server
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "fastapi-mcp"
version = "0.1.4"
version = "0.1.5"
description = "Automatic MCP server generator for FastAPI applications - converts FastAPI endpoints to MCP tools for LLM integration"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@

setup(
name="fastapi-mcp",
version="0.1.4",
version="0.1.5",
description="Automatic MCP server generator for FastAPI applications - converts FastAPI endpoints to MCP tools for LLM integration",
author="Tadata Inc.",
author_email="[email protected]",
Expand Down
55 changes: 52 additions & 3 deletions tests/test_tool_generation.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def sample_app():
version="0.1.0",
)

@app.get("/items/", response_model=List[Item], tags=["items"])
@app.get("/items/", response_model=List[Item], tags=["items","include_in_mcp"])
async def list_items(skip: int = 0, limit: int = 10):
"""
List all items.
Expand All @@ -32,7 +32,7 @@ async def list_items(skip: int = 0, limit: int = 10):
"""
return []

@app.get("/items/{item_id}", response_model=Item, tags=["items"])
@app.get("/items/{item_id}", response_model=Item, tags=["items","include_in_mcp"])
async def read_item(item_id: int):
"""
Get a specific item by ID.
Expand All @@ -41,7 +41,7 @@ async def read_item(item_id: int):
"""
return {"id": item_id, "name": "Test Item", "price": 0.0}

@app.post("/items/", response_model=Item, tags=["items"])
@app.post("/items/", response_model=Item, tags=["items", "include_in_mcp"])
async def create_item(item: Item):
"""
Create a new item.
Expand All @@ -53,6 +53,35 @@ async def create_item(item: Item):
return app


@pytest.fixture
def sample_app_with_tool_exclusions():
"""Create a sample FastAPI app with some tools excluded from MCP."""
app = FastAPI(
title="Test API",
description="A test API for unit testing",
version="0.1.0",
)

@app.get("/items/", response_model=List[Item], tags=["items", "include_in_mcp"])
async def list_items(skip: int = 0, limit: int = 10):
"""
List all items.

Returns a list of items, with pagination support.
"""
return []

@app.post("/items/", response_model=Item, tags=["items"])
async def create_item(item: Item):
"""
Create a new item.

Returns the created item.
"""
return item

return app

def test_tool_generation_basic(sample_app):
"""Test that MCP tools are properly generated with default settings."""
# Create MCP server and register tools
Expand Down Expand Up @@ -92,6 +121,26 @@ def test_tool_generation_basic(sample_app):
assert "limit" in list_items_tool.parameters["properties"], "Expected 'limit' parameter"


def test_tool_generation_with_exclusions(sample_app_with_tool_exclusions):
"""Test that only tools tagged with "include_in_mcp"' are registered."""
# Create MCP server and register tools
mcp_server = add_mcp_server(sample_app_with_tool_exclusions, serve_tools=True, base_url="http://localhost:8000", create_tools_by_default = False)

# Extract tools for inspection
tools = mcp_server._tool_manager.list_tools()

# Should only have list_items endpoint and MCP endpoint (create_item excluded)
assert len(tools) ==2, f"Expected 2 tools (list_items + MCP endpoint), got {len(tools)}"

# Verify list_items endpoint is present
list_items_tool = next((t for t in tools if t.name == "list_items_items__get"), None)
assert list_items_tool is not None, "list_items tool not found"

# Verify create_item endpoint is not present (should be excluded)
create_item_tool = next((t for t in tools if t.name == "create_item_items__post"), None)
assert create_item_tool is None, "create_item tool should be excluded but was found"


def test_tool_generation_with_full_schema(sample_app):
"""Test that MCP tools include full response schema when requested."""
# Create MCP server with full schema for all operations
Expand Down