Skip to content
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
59 changes: 56 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 9 additions & 2 deletions mcphero/__init__.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
11 changes: 9 additions & 2 deletions mcphero/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down
66 changes: 66 additions & 0 deletions mcphero/adapters/generic.py
Original file line number Diff line number Diff line change
@@ -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
16 changes: 11 additions & 5 deletions mcphero/adapters/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions mcphero/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -32,10 +32,10 @@ authors = [

dependencies = [
"httpx",
"openai",
]

[project.optional-dependencies]
openai = ["openai"]
google-genai = ["google-genai"]

[dependency-groups]
Expand Down
Loading