Skip to content

Commit b2ebc5c

Browse files
committed
refactor and add more examples
1 parent 005d54e commit b2ebc5c

File tree

10 files changed

+156
-125
lines changed

10 files changed

+156
-125
lines changed

examples/full_schema_description_example.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22
Simple example of using FastAPI-MCP to add an MCP server to a FastAPI app.
33
"""
44

5-
from examples.apps import items
6-
from examples.setup import setup_logging
5+
from examples.shared.apps import items
6+
from examples.shared.setup import setup_logging
77

88
from fastapi_mcp import FastApiMCP
99

10-
1110
setup_logging()
1211

1312

@@ -17,8 +16,8 @@
1716
name="Item API MCP",
1817
description="MCP server for the Item API",
1918
base_url="http://localhost:8000",
20-
describe_full_response_schema=True,
21-
describe_all_responses=True,
19+
describe_full_response_schema=True, # Describe the full response JSON-schema instead of just a response example
20+
describe_all_responses=True, # Describe all the possible responses instead of just the success (2XX) response
2221
)
2322

2423
mcp.mount()

examples/separate_server_example.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""
2+
Simple example of using FastAPI-MCP to add an MCP server to a FastAPI app.
3+
"""
4+
5+
from fastapi import FastAPI
6+
import asyncio
7+
import uvicorn
8+
9+
from examples.shared.apps import items
10+
from examples.shared.setup import setup_logging
11+
12+
from fastapi_mcp import FastApiMCP
13+
14+
setup_logging()
15+
16+
17+
MCP_SERVER_HOST = "localhost"
18+
MCP_SERVER_PORT = 8000
19+
ITEMS_API_HOST = "localhost"
20+
ITEMS_API_PORT = 8001
21+
22+
23+
# Take the FastAPI app only as a source for MCP server generation
24+
mcp = FastApiMCP(
25+
items.app,
26+
base_url=f"http://{ITEMS_API_HOST}:{ITEMS_API_PORT}", # Note how the base URL is the **Items API** URL, not the MCP server URL
27+
)
28+
29+
# And then mount the MCP server to a separate FastAPI app
30+
mcp_app = FastAPI()
31+
mcp.mount(mcp_app)
32+
33+
34+
def run_items_app():
35+
uvicorn.run(items.app, port=ITEMS_API_PORT)
36+
37+
38+
def run_mcp_app():
39+
uvicorn.run(mcp_app, port=MCP_SERVER_PORT)
40+
41+
42+
# The MCP server depends on the Items API to be available, so we need to run both.
43+
async def main():
44+
await asyncio.gather(asyncio.to_thread(run_items_app), asyncio.to_thread(run_mcp_app))
45+
46+
47+
if __name__ == "__main__":
48+
asyncio.run(main())
File renamed without changes.

examples/shared/apps/__init__.py

Whitespace-only changes.
File renamed without changes.
File renamed without changes.

examples/simple_example.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@
22
Simple example of using FastAPI-MCP to add an MCP server to a FastAPI app.
33
"""
44

5-
from examples.apps import items
6-
from examples.setup import setup_logging
5+
from examples.shared.apps import items
6+
from examples.shared.setup import setup_logging
77

88
from fastapi_mcp import FastApiMCP
99

10-
1110
setup_logging()
1211

1312

fastapi_mcp/execute.py

Lines changed: 0 additions & 94 deletions
This file was deleted.

fastapi_mcp/server.py

Lines changed: 90 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import json
2+
import httpx
13
from contextlib import asynccontextmanager
24
from typing import Dict, Optional, Any, List, Union, AsyncIterator
35

@@ -7,7 +9,6 @@
79
import mcp.types as types
810

911
from fastapi_mcp.openapi.convert import convert_openapi_to_mcp_tools
10-
from fastapi_mcp.execute import execute_api_tool
1112
from fastapi_mcp.transport.sse import FastApiSseTransport
1213

1314
from logging import getLogger
@@ -37,7 +38,7 @@ def __init__(
3738
self._describe_all_responses = describe_all_responses
3839
self._describe_full_response_schema = describe_full_response_schema
3940

40-
self.mcp_server = self.create_server()
41+
self.server = self.create_server()
4142

4243
def create_server(self) -> Server:
4344
"""
@@ -127,13 +128,15 @@ async def handle_call_tool(
127128
operation_map = ctx.lifespan_context["operation_map"]
128129

129130
# Execute the tool
130-
return await execute_api_tool(base_url, name, arguments, operation_map)
131+
return await self.execute_api_tool(base_url, name, arguments, operation_map)
131132

132133
return mcp_server
133134

134135
def mount(self, router: Optional[FastAPI | APIRouter] = None, mount_path: str = "/mcp") -> None:
135136
"""
136-
Mount the MCP server to the FastAPI app.
137+
Mount the MCP server to **any** FastAPI app or APIRouter.
138+
There is no requirement that the FastAPI app or APIRouter is the same as the one that the MCP
139+
server was created from.
137140
138141
Args:
139142
router: The FastAPI app or APIRouter to mount the MCP server to. If not provided, the MCP
@@ -156,12 +159,10 @@ def mount(self, router: Optional[FastAPI | APIRouter] = None, mount_path: str =
156159
@router.get(mount_path)
157160
async def handle_mcp_connection(request: Request):
158161
async with sse_transport.connect_sse(request.scope, request.receive, request._send) as (reader, writer):
159-
await self.mcp_server.run(
162+
await self.server.run(
160163
reader,
161164
writer,
162-
self.mcp_server.create_initialization_options(
163-
notification_options=None, experimental_capabilities={}
164-
),
165+
self.server.create_initialization_options(notification_options=None, experimental_capabilities={}),
165166
)
166167

167168
# Route for MCP messages
@@ -170,3 +171,84 @@ async def handle_post_message(request: Request):
170171
return await sse_transport.handle_fastapi_post_message(request)
171172

172173
logger.info(f"MCP server listening at {mount_path}")
174+
175+
async def execute_api_tool(
176+
self, base_url: str, tool_name: str, arguments: Dict[str, Any], operation_map: Dict[str, Dict[str, Any]]
177+
) -> List[Union[types.TextContent, types.ImageContent, types.EmbeddedResource]]:
178+
"""
179+
Execute an MCP tool by making an HTTP request to the corresponding API endpoint.
180+
181+
Args:
182+
base_url: The base URL for the API
183+
tool_name: The name of the tool to execute
184+
arguments: The arguments for the tool
185+
operation_map: A mapping from tool names to operation details
186+
187+
Returns:
188+
The result as MCP content types
189+
"""
190+
if tool_name not in operation_map:
191+
return [types.TextContent(type="text", text=f"Unknown tool: {tool_name}")]
192+
193+
operation = operation_map[tool_name]
194+
path: str = operation["path"]
195+
method: str = operation["method"]
196+
parameters: List[Dict[str, Any]] = operation.get("parameters", [])
197+
arguments = arguments.copy() if arguments else {} # Deep copy arguments to avoid mutating the original
198+
199+
# Prepare URL with path parameters
200+
url = f"{base_url}{path}"
201+
for param in parameters:
202+
if param.get("in") == "path" and param.get("name") in arguments:
203+
param_name = param.get("name", None)
204+
if param_name is None:
205+
raise ValueError(f"Parameter name is None for parameter: {param}")
206+
url = url.replace(f"{{{param_name}}}", str(arguments.pop(param_name)))
207+
208+
# Prepare query parameters
209+
query = {}
210+
for param in parameters:
211+
if param.get("in") == "query" and param.get("name") in arguments:
212+
param_name = param.get("name", None)
213+
if param_name is None:
214+
raise ValueError(f"Parameter name is None for parameter: {param}")
215+
query[param_name] = arguments.pop(param_name)
216+
217+
# Prepare headers
218+
headers = {}
219+
for param in parameters:
220+
if param.get("in") == "header" and param.get("name") in arguments:
221+
param_name = param.get("name", None)
222+
if param_name is None:
223+
raise ValueError(f"Parameter name is None for parameter: {param}")
224+
headers[param_name] = arguments.pop(param_name)
225+
226+
# Prepare request body (remaining kwargs)
227+
body = arguments if arguments else None
228+
229+
try:
230+
# Make request
231+
logger.debug(f"Making {method.upper()} request to {url}")
232+
async with httpx.AsyncClient() as client:
233+
if method.lower() == "get":
234+
response = await client.get(url, params=query, headers=headers)
235+
elif method.lower() == "post":
236+
response = await client.post(url, params=query, headers=headers, json=body)
237+
elif method.lower() == "put":
238+
response = await client.put(url, params=query, headers=headers, json=body)
239+
elif method.lower() == "delete":
240+
response = await client.delete(url, params=query, headers=headers)
241+
elif method.lower() == "patch":
242+
response = await client.patch(url, params=query, headers=headers, json=body)
243+
else:
244+
return [types.TextContent(type="text", text=f"Unsupported HTTP method: {method}")]
245+
246+
# Process response
247+
try:
248+
result = response.json()
249+
return [types.TextContent(type="text", text=json.dumps(result, indent=2))]
250+
except ValueError:
251+
return [types.TextContent(type="text", text=response.text)]
252+
253+
except Exception as e:
254+
return [types.TextContent(type="text", text=f"Error calling {tool_name}: {str(e)}")]

pyproject.toml

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,17 @@ dependencies = [
3939
"tomli>=2.2.1",
4040
]
4141

42+
[dependency-groups]
43+
dev = [
44+
"mypy>=1.15.0",
45+
"ruff>=0.9.10",
46+
"types-setuptools>=75.8.2.20250305",
47+
"pytest>=7.4.0",
48+
"pytest-asyncio>=0.23.0",
49+
"pytest-cov>=4.1.0",
50+
"pre-commit>=4.2.0",
51+
]
52+
4253
[project.urls]
4354
Homepage = "https://github.com/tadata-org/fastapi_mcp"
4455
Documentation = "https://github.com/tadata-org/fastapi_mcp#readme"
@@ -47,28 +58,14 @@ Documentation = "https://github.com/tadata-org/fastapi_mcp#readme"
4758
"Source Code" = "https://github.com/tadata-org/fastapi_mcp"
4859
"Changelog" = "https://github.com/tadata-org/fastapi_mcp/blob/main/CHANGELOG.md"
4960

50-
[project.scripts]
51-
fastapi-mcp = "fastapi_mcp.cli:app"
52-
5361
[tool.hatch.build.targets.wheel]
5462
packages = ["fastapi_mcp"]
5563

5664
[tool.ruff]
5765
line-length = 120
58-
target-version = "py310"
66+
target-version = "py312"
5967

6068
[tool.pytest.ini_options]
6169
asyncio_mode = "auto"
6270
testpaths = ["tests"]
6371
python_files = "test_*.py"
64-
65-
[dependency-groups]
66-
dev = [
67-
"mypy>=1.15.0",
68-
"ruff>=0.9.10",
69-
"types-setuptools>=75.8.2.20250305",
70-
"pytest>=7.4.0",
71-
"pytest-asyncio>=0.23.0",
72-
"pytest-cov>=4.1.0",
73-
"pre-commit>=4.2.0",
74-
]

0 commit comments

Comments
 (0)