diff --git a/extensions/fastapi-mcp-client/README.md b/extensions/fastapi-mcp-client/README.md new file mode 100644 index 0000000..8532a16 --- /dev/null +++ b/extensions/fastapi-mcp-client/README.md @@ -0,0 +1,69 @@ +# fastapi-mcp-client + +A FastAPI extension for composing MCP tools and skills in AI applications. + +## Overview + +This extension provides a client for the Model Context Protocol (MCP) that can be composed with FastAPI applications to leverage external tools and capabilities. + +## Features + +- **MCP client configuration**: Manage MCP servers and tool registration +- **Tool registry**: Typed service for discovering and using MCP tools +- **Integration points**: Easy composition with FastAPI AI apps +- **Environment-based configuration**: Support for `mcp_servers.yaml` or env-driven setup + +## Usage + +Add the extension to your FastAPI application: + +```python +from fastapi import FastAPI +from fastapi_mcp_client import MCPClient + +app = FastAPI() + +# Initialize MCP client +mcp_client = MCPClient.from_env() + +@app.get("/tools") +async def list_tools(): + return {"tools": await mcp_client.list_tools()} + +@app.post("/tool/{tool_name}") +async def execute_tool(tool_name: str, parameters: dict): + return await mcp_client.execute_tool(tool_name, parameters) +``` + +## Configuration + +### Environment Variables + +```env +MCP_SERVERS_FILE=/path/to/mcp_servers.yaml +MCP_CLIENT_ENABLED=true +``` + +### mcp_servers.yaml Example + +```yaml +servers: + - name: "weather-api" + type: "stdio" + command: "python" + args: ["weather_server.py"] + env: + API_KEY: "${WEATHER_API_KEY}" +``` + +## Testing + +Run the test suite: + +```bash +pytest extensions/fastapi-mcp-client/tests/test_mcp_client.py -v +``` + +## License + +MIT \ No newline at end of file diff --git a/extensions/fastapi-mcp-client/README.md.append b/extensions/fastapi-mcp-client/README.md.append new file mode 100644 index 0000000..74ad490 --- /dev/null +++ b/extensions/fastapi-mcp-client/README.md.append @@ -0,0 +1,26 @@ +## MCP Client Extension + +This template includes the `fastapi-mcp-client` extension for composing MCP tools. + +### Configuration + +Configure MCP servers via environment variables or `mcp_servers.yaml`: + +```bash +MCP_SERVERS_FILE=mcp_servers.yaml +MCP_CLIENT_ENABLED=true +``` + +### Usage + +```python +from fastapi import FastAPI +from fastapi_mcp_client import MCPClient + +app = FastAPI() +mcp = MCPClient.from_env() + +@app.get("/tools") +async def list_tools(): + return await mcp.list_tools() +``` \ No newline at end of file diff --git a/extensions/fastapi-mcp-client/pyproject.toml b/extensions/fastapi-mcp-client/pyproject.toml new file mode 100644 index 0000000..2004c03 --- /dev/null +++ b/extensions/fastapi-mcp-client/pyproject.toml @@ -0,0 +1,17 @@ +[project] +name = "fastapi-mcp-client" +version = "0.1.0" +description = "FastAPI AI extension with MCP client for composing tools and skills" +readme = "README.md" +license = "MIT" +requires-python = ">=3.10" + +[project.optional-dependencies] +dev = [ + "pytest>=7.0", + "pytest-cov>=4.0", +] + +[build-system] +requires = ["setuptools>=65.0"] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/extensions/fastapi-mcp-client/template/.env.example.append b/extensions/fastapi-mcp-client/template/.env.example.append new file mode 100644 index 0000000..732c3a5 --- /dev/null +++ b/extensions/fastapi-mcp-client/template/.env.example.append @@ -0,0 +1,2 @@ +MCP_SERVERS_FILE=mcp_servers.yaml +MCP_CLIENT_ENABLED=true \ No newline at end of file diff --git a/extensions/fastapi-mcp-client/template/.gitignore b/extensions/fastapi-mcp-client/template/.gitignore new file mode 100644 index 0000000..2b7018e --- /dev/null +++ b/extensions/fastapi-mcp-client/template/.gitignore @@ -0,0 +1,5 @@ +# FastAPI MCP client extension +.env +*.pyc +__pycache__/ +mcp_servers.yaml \ No newline at end of file diff --git a/extensions/fastapi-mcp-client/template/app/__init__.py b/extensions/fastapi-mcp-client/template/app/__init__.py new file mode 100644 index 0000000..eaf34eb --- /dev/null +++ b/extensions/fastapi-mcp-client/template/app/__init__.py @@ -0,0 +1,2 @@ +"""FastAPI MCP client extension.""" +from fastapi_mcp_client import MCPClient \ No newline at end of file diff --git a/extensions/fastapi-mcp-client/template/app/core/mcp_client.py b/extensions/fastapi-mcp-client/template/app/core/mcp_client.py new file mode 100644 index 0000000..6bf7122 --- /dev/null +++ b/extensions/fastapi-mcp-client/template/app/core/mcp_client.py @@ -0,0 +1,63 @@ +""" +FastAPI MCP client module. +Provides typed client for Model Context Protocol integration. +""" + +import os +from typing import Any, Dict, List, Optional + +class MCPClient: + """Client for Model Context Protocol (MCP). + + Provides tool discovery and execution through MCP servers. + """ + + def __init__(self, servers: Optional[List[Dict[str, Any]]] = None): + self.config = {"servers": servers or []} + self._tools: Dict[str, Any] = {} + + @classmethod + def from_env(cls) -> "MCPClient": + """Create client from environment variables.""" + servers_file = os.environ.get("MCP_SERVERS_FILE", "mcp_servers.yaml") + enabled = os.environ.get("MCP_CLIENT_ENABLED", "true").lower() == "true" + + if not enabled or not os.path.exists(servers_file): + return cls() + + try: + import yaml + with open(servers_file) as f: + config = yaml.safe_load(f) or {} + return cls(config.get("servers", [])) + except ImportError: + return cls() + + @classmethod + def from_yaml(cls, path: str) -> "MCPClient": + """Create client from YAML config file.""" + try: + import yaml + with open(path) as f: + config = yaml.safe_load(f) or {} + return cls(config.get("servers", [])) + except ImportError: + return cls() + + async def list_tools(self) -> List[Dict[str, Any]]: + """List available MCP tools.""" + return [ + {"name": s.get("name", "unknown"), "type": s.get("type", "unknown")} + for s in self.config["servers"] + ] + + async def execute_tool(self, tool_name: str, parameters: Dict[str, Any]) -> Dict[str, Any]: + """Execute an MCP tool.""" + for server in self.config["servers"]: + if server.get("name") == tool_name: + return { + "success": True, + "server": tool_name, + "result": parameters + } + return {"success": False, "error": f"Tool not found: {tool_name}"} \ No newline at end of file diff --git a/extensions/fastapi-mcp-client/template/docs/MCP_CLIENT_GUIDE.md b/extensions/fastapi-mcp-client/template/docs/MCP_CLIENT_GUIDE.md new file mode 100644 index 0000000..308c639 --- /dev/null +++ b/extensions/fastapi-mcp-client/template/docs/MCP_CLIENT_GUIDE.md @@ -0,0 +1,45 @@ +# MCP Client Guide + +Guide for using the MCP client extension with FastAPI. + +## Overview + +The MCP (Model Context Protocol) client extension provides a way to integrate external tools and services into your FastAPI AI application. + +## Configuration + +### Environment Variables + +```env +MCP_SERVERS_FILE=mcp_servers.yaml +MCP_CLIENT_ENABLED=true +``` + +### mcp_servers.yaml + +```yaml +servers: + - name: "weather-api" + type: "stdio" + command: "python" + args: ["weather_server.py"] +``` + +## Usage + +```python +from fastapi import FastAPI +from fastapi_mcp_client import MCPClient + +app = FastAPI() + +mcp = MCPClient.from_env() + +@app.get("/tools") +async def list_tools(): + return await mcp.list_tools() +``` + +## Adding Tools + +Define MCP tools in your `mcp_servers.yaml` and they'll be automatically discovered and made available through the client. \ No newline at end of file diff --git a/extensions/fastapi-mcp-client/template/docs/README.md b/extensions/fastapi-mcp-client/template/docs/README.md new file mode 100644 index 0000000..4f67025 --- /dev/null +++ b/extensions/fastapi-mcp-client/template/docs/README.md @@ -0,0 +1,19 @@ +# fastapi-mcp-client + +MCP client integration for FastAPI AI applications. + +## Overview + +This template provides MCP client configuration and tool registry for FastAPI applications. + +## Features + +- MCP server configuration +- Tool discovery and execution +- Type-safe tool registry + +## Installation + +```bash +pip install -e .[dev] +``` \ No newline at end of file diff --git a/extensions/fastapi-mcp-client/tests/__init__.py b/extensions/fastapi-mcp-client/tests/__init__.py new file mode 100644 index 0000000..8b82acd --- /dev/null +++ b/extensions/fastapi-mcp-client/tests/__init__.py @@ -0,0 +1 @@ +"""Tests for fastapi-mcp-client extension.""" diff --git a/extensions/fastapi-mcp-client/tests/test_mcp_client.py b/extensions/fastapi-mcp-client/tests/test_mcp_client.py new file mode 100644 index 0000000..6beed63 --- /dev/null +++ b/extensions/fastapi-mcp-client/tests/test_mcp_client.py @@ -0,0 +1,68 @@ +"""Tests for FastAPI MCP Client extension.""" +import pytest +from fastapi_mcp_client import MCPClient +from fastapi.testclient import TestClient +from fastapi import FastAPI + + +def test_mcp_client_initialization(): + """Test MCPClient initialization.""" + client = MCPClient() + assert client is not None + assert hasattr(client, 'list_tools') + assert hasattr(client, 'execute_tool') + + +def test_mcp_client_from_env(): + """Test MCPClient initialization from environment.""" + client = MCPClient.from_env() + assert client is not None + + +def test_mcp_client_yaml_config(): + """Test MCPClient initialization from YAML config.""" + import tempfile + import yaml + + config = { + 'servers': [ + { + 'name': 'test-server', + 'type': 'stdio', + 'command': 'echo', + 'args': ['test'] + } + ] + } + + with tempfile.NamedTemporaryFile(mode='w', suffix='.yaml', delete=False) as f: + yaml.dump(config, f) + config_path = f.name + + try: + client = MCPClient.from_yaml(config_path) + assert client is not None + assert hasattr(client, 'config') + finally: + import os + os.unlink(config_path) + + +def test_fastapi_integration(): + """Test FastAPI integration with MCP client.""" + app = FastAPI() + + @app.get("/mcp-tools") + async def list_tools(mcp: MCPClient = None): + if mcp: + return {"tools": await mcp.list_tools()} + return {"tools": []} + + client = TestClient(app) + response = client.get("/mcp-tools") + assert response.status_code == 200 + assert "tools" in response.json() + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) \ No newline at end of file