diff --git a/README.md b/README.md index 8efdc36..b215fbd 100644 --- a/README.md +++ b/README.md @@ -14,19 +14,55 @@ Two main flows: ## Installation -OpenAI (default) support: +Base (no LLM SDK dependency): ```bash pip install mcphero ``` -For Google Gemini support: +For OpenAI support: +```bash +pip install "mcphero[openai]" +``` +For Google Gemini support: ```bash pip install "mcphero[google-genai]" ``` ## Quick Start +### Generic (provider-agnostic) + +Use `MCPToolAdapter` when your framework has its own tool-call loop, or when you just need raw MCP tool execution without any LLM SDK dependency. + +```python +import asyncio +from mcphero import MCPToolAdapter, GenericToolCall + +async def main(): + adapter = MCPToolAdapter("https://api.mcphero.app/mcp/your-server-id") + + # Discover available tools + tools = await adapter.discover_tools() + for tool in tools: + print(tool.name, tool.description) + + # Execute tool calls directly + results = await adapter.process_tool_calls([ + GenericToolCall(name="get_weather", arguments={"city": "London"}, id="1"), + ]) + for result in results: + print(result.content) + +asyncio.run(main()) +``` + +Or call a single tool directly: + +```python +result = await adapter.call_tool("get_weather", {"city": "London"}) +``` + ### OpenAI ```python @@ -235,6 +271,23 @@ MCPServerConfig(url="...", tool_prefix="wx") ## API Reference +### MCPToolAdapter + +```python +from mcphero import MCPToolAdapter, GenericToolCall + +adapter = MCPToolAdapter("https://api.mcphero.app/mcp/your-server-id") +``` + +#### Methods + +| Method | Returns | Description | +|--------|---------|-------------| +| `discover_tools()` | `list[MCPToolDefinition]` | Discover tools with routing metadata | +| `process_tool_calls(tool_calls, return_errors=True)` | `list[GenericToolResult]` | Execute tool calls and return generic results | +| `call_tool(name, arguments)` | `JsonRpcResponse` | Call a single tool by name | +| `initialize_all()` | `dict[str, JsonRpcResponse \| Exception]` | Pre-initialize all server connections | + ### MCPToolAdapterOpenAI ```python @@ -290,7 +343,7 @@ adapter = MCPToolAdapterGemini("https://api.mcphero.app/mcp/your-server-id") ## Error Handling -Both adapters handle errors gracefully. When `return_errors=True` (default), failed tool calls return error messages that can be sent back to the model: +All adapters handle errors gracefully. When `return_errors=True` (default), failed tool calls return error messages that can be sent back to the model: ```python # Tool call fails -> returns error in result diff --git a/mcphero/__init__.py b/mcphero/__init__.py index 732f8fb..af316d6 100644 --- a/mcphero/__init__.py +++ b/mcphero/__init__.py @@ -1,14 +1,21 @@ -from mcphero.adapters.openai import MCPToolAdapterOpenAI from mcphero.adapters.base_adapter import MCPServerConfig +from mcphero.adapters.generic import GenericToolCall, GenericToolResult, MCPToolAdapter __all__ = [ - "MCPToolAdapterOpenAI", + "GenericToolCall", + "GenericToolResult", + "MCPToolAdapter", + "MCPToolAdapterOpenAI", # pyright: ignore[reportUnsupportedDunderAll] "MCPServerConfig", "MCPToolAdapterGemini", # pyright: ignore[reportUnsupportedDunderAll] ] def __getattr__(name: str): + if name == "MCPToolAdapterOpenAI": + from mcphero.adapters.openai import MCPToolAdapterOpenAI + + return MCPToolAdapterOpenAI if name == "MCPToolAdapterGemini": from mcphero.adapters.gemini import MCPToolAdapterGemini diff --git a/mcphero/adapters/__init__.py b/mcphero/adapters/__init__.py index 60b8aee..e92f202 100644 --- a/mcphero/adapters/__init__.py +++ b/mcphero/adapters/__init__.py @@ -1,14 +1,21 @@ from mcphero.adapters.base_adapter import BaseAdapter -from mcphero.adapters.openai import MCPToolAdapterOpenAI +from mcphero.adapters.generic import GenericToolCall, GenericToolResult, MCPToolAdapter __all__ = [ "BaseAdapter", - "MCPToolAdapterOpenAI", + "GenericToolCall", + "GenericToolResult", + "MCPToolAdapter", + "MCPToolAdapterOpenAI", # pyright: ignore[reportUnsupportedDunderAll] "MCPToolAdapterGemini", # pyright: ignore[reportUnsupportedDunderAll] ] def __getattr__(name: str): + if name == "MCPToolAdapterOpenAI": + from mcphero.adapters.openai import MCPToolAdapterOpenAI + + return MCPToolAdapterOpenAI if name == "MCPToolAdapterGemini": from mcphero.adapters.gemini import MCPToolAdapterGemini diff --git a/mcphero/adapters/generic.py b/mcphero/adapters/generic.py new file mode 100644 index 0000000..bf5fa74 --- /dev/null +++ b/mcphero/adapters/generic.py @@ -0,0 +1,66 @@ +# mcphero/adapters/generic.py +"""Provider-agnostic MCP adapter.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Any + +import httpx + +from mcphero.adapters.base_adapter import BaseAdapter + + +@dataclass +class GenericToolCall: + """A provider-agnostic tool call.""" + + name: str + arguments: dict[str, Any] = field(default_factory=dict) + id: str = "" + + +@dataclass +class GenericToolResult: + """A provider-agnostic tool result.""" + + id: str + content: Any # Raw result (dict/list/str) from the MCP server + is_error: bool = False + + +class MCPToolAdapter(BaseAdapter[GenericToolCall, GenericToolResult]): + """ + Provider-agnostic MCP adapter. Works without any LLM SDK dependency. + + Use this when your framework has its own tool-call loop and you just need + to execute MCP tools and get results back. + + Usage: + adapter = MCPToolAdapter("https://mcp.example.com/server") + + tools = await adapter.discover_tools() # list[MCPToolDefinition] + + results = await adapter.process_tool_calls([ + GenericToolCall(name="tool_name", arguments={"key": "value"}, id="1"), + ]) + # results: list[GenericToolResult] + + Or call a single tool directly (no wrapping needed): + result = await adapter.call_tool("tool_name", {"key": "value"}) + """ + + async def _execute_single_tool_call( + self, tool_call: GenericToolCall, *, return_errors: bool + ) -> GenericToolResult | None: + try: + response = await self.call_tool(tool_call.name, tool_call.arguments) + return GenericToolResult(id=tool_call.id, content=response) + except (KeyError, httpx.HTTPError, Exception) as e: + if return_errors: + return GenericToolResult( + id=tool_call.id, + content={"error": str(e)}, + is_error=True, + ) + return None diff --git a/mcphero/adapters/openai.py b/mcphero/adapters/openai.py index 7406f90..ec8b237 100644 --- a/mcphero/adapters/openai.py +++ b/mcphero/adapters/openai.py @@ -6,11 +6,17 @@ import json import httpx -from openai.types.chat import ( - ChatCompletionMessageToolCall, - ChatCompletionToolMessageParam, - ChatCompletionToolParam, -) + +try: + from openai.types.chat import ( # pyright: ignore[reportMissingImports] + ChatCompletionMessageToolCall, + ChatCompletionToolMessageParam, + ChatCompletionToolParam, + ) +except ImportError as e: + from mcphero.exceptions import INSTALL_OPENAI + + raise ImportError(INSTALL_OPENAI) from e from mcphero.adapters.base_adapter import BaseAdapter, MCPToolDefinition diff --git a/mcphero/exceptions.py b/mcphero/exceptions.py index 20c3816..065bbe9 100644 --- a/mcphero/exceptions.py +++ b/mcphero/exceptions.py @@ -2,6 +2,12 @@ class MCPHeroException(Exception): """Base MCPHero Exception class""" +INSTALL_OPENAI = """ +To use OpenAI with mcphero, please install dependencies: + +pip install "mcphero[openai]" +""" + INSTALL_GOOGLE_GENAI = """ To use Google Gemini with mcphero, please install dependencies: diff --git a/pyproject.toml b/pyproject.toml index b4ae09e..a1cff73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mcphero" -version = "1.1.0" +version = "2.0.0" description = "Library to use MCP servers natively with AI clients as tools." readme = "README.md" requires-python = ">=3.11" @@ -32,10 +32,10 @@ authors = [ dependencies = [ "httpx", - "openai", ] [project.optional-dependencies] +openai = ["openai"] google-genai = ["google-genai"] [dependency-groups]