Skip to content

Commit 4132470

Browse files
authored
Merge branch 'main' into fix/issue-971
2 parents 134e7e4 + e2e1b5d commit 4132470

18 files changed

Lines changed: 1410 additions & 292 deletions

File tree

cli/serve/app.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
) from e
2424

2525
from mellea.backends.model_options import ModelOption
26+
from mellea.core import MelleaLogger
2627
from mellea.helpers.openai_compatible_helpers import (
2728
build_completion_usage,
2829
build_tool_calls,
@@ -42,6 +43,8 @@
4243
from .streaming import stream_chat_completion_chunks
4344
from .utils import extract_finish_reason
4445

46+
logger = MelleaLogger.get_logger()
47+
4548
app = FastAPI(
4649
title="M serve OpenAI API Compatible Server",
4750
description="M programs that run as a simple OpenAI API-compatible server",
@@ -265,11 +268,11 @@ async def endpoint(request: ChatCompletionRequest):
265268
message=f"Invalid request: {e!s}",
266269
error_type="invalid_request_error",
267270
)
268-
except Exception as e:
269-
# Catch-all for any unexpected errors (including AttributeError)
271+
except Exception:
272+
logger.exception("Unhandled error in chat-completion handler")
270273
return create_openai_error_response(
271274
status_code=500,
272-
message=f"Internal server error: {e!s}",
275+
message="Internal server error",
273276
error_type="server_error",
274277
)
275278

docs/docs/how-to/tools-and-agents.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,46 @@ gets generated (see examples above).
275275
> **Warning:** `local_code_interpreter` executes Python code in the current process.
276276
> Do not use it in production contexts without sandboxing.
277277
278+
## MCP tools
279+
280+
Mellea can consume tools from any [MCP](https://modelcontextprotocol.io/) server
281+
and drop them into an agent loop. Install with `pip install 'mellea[tools]'`.
282+
283+
The workflow is two steps: discover what the server offers, then instantiate the
284+
tools you want.
285+
286+
```python
287+
# Requires: mellea[tools]
288+
# Returns: list[MelleaTool]
289+
from mellea.stdlib.tools.mcp import discover_mcp_tools, http_connection
290+
291+
connection = http_connection("https://api.example.com/mcp/", api_key="...")
292+
293+
specs = await discover_mcp_tools(connection)
294+
tools = [s.as_mellea_tool() for s in specs if s.name in {"search", "fetch"}]
295+
```
296+
297+
`http_connection`, `sse_connection`, and `stdio_connection` build the transport
298+
config. Each tool invocation opens a short-lived session, so callers do not need
299+
to manage the connection lifetime.
300+
301+
Once built, MCP tools work like any other `MelleaTool`: pass them via
302+
`ModelOption.TOOLS` to `instruct()` or to `react()`:
303+
304+
```python
305+
# Requires: mellea[tools]
306+
# Returns: str
307+
result, _ = await react(
308+
goal="Find recent pull requests I authored.",
309+
context=ChatContext(),
310+
backend=m.backend,
311+
tools=tools,
312+
)
313+
```
314+
315+
See [`docs/examples/mcp/github_activity_summary.py`](https://github.com/generative-computing/mellea/blob/main/docs/examples/mcp/github_activity_summary.py)
316+
for a complete example against the hosted GitHub MCP server.
317+
278318
---
279319

280320
**See also:** [Tutorial 04: Making Agents Reliable](../tutorials/04-making-agents-reliable) | [Instruct, Validate, Repair](../concepts/instruct-validate-repair)
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# pytest: unit
2+
"""Convert a heterogeneous context to a generic chat history.
3+
4+
The as_generic_chat_history() function converts any Context into a list of
5+
Messages, gracefully handling unknown component types by converting them to
6+
strings. This is useful for working with mixed-type contexts or when you need
7+
a more flexible interface than as_chat_history().
8+
"""
9+
10+
from mellea.core import CBlock, ModelOutputThunk
11+
from mellea.stdlib.components import Message, as_generic_chat_history
12+
from mellea.stdlib.context import ChatContext
13+
14+
15+
def basic_example() -> list[Message]:
16+
"""Convert a standard Message-based context to chat history."""
17+
ctx = ChatContext()
18+
ctx = ctx.add(Message("user", "What is 2+2?"))
19+
ctx = ctx.add(Message("assistant", "2+2 equals 4."))
20+
21+
history = as_generic_chat_history(ctx)
22+
assert len(history) == 2
23+
assert history[0].content == "What is 2+2?"
24+
assert history[1].content == "2+2 equals 4."
25+
return history
26+
27+
28+
def with_heterogeneous_components() -> list[Message]:
29+
"""Handle mixed component types gracefully.
30+
31+
Unlike as_chat_history(), as_generic_chat_history() can handle any
32+
component type by converting unknown types to strings.
33+
"""
34+
ctx = ChatContext()
35+
ctx = ctx.add(Message("user", "Summarize this"))
36+
ctx = ctx.add(CBlock("Some inline content to process"))
37+
mot = ModelOutputThunk(value="The summary is...")
38+
ctx = ctx.add(mot)
39+
40+
history = as_generic_chat_history(ctx)
41+
assert len(history) == 3
42+
assert history[0].role == "user"
43+
assert history[1].role == "user" # CBlock defaults to 'user'
44+
assert history[2].role == "assistant" # MOT defaults to 'assistant'
45+
return history
46+
47+
48+
def with_custom_formatter() -> list[Message]:
49+
"""Use a custom formatter for ModelOutputThunk with unparsed content.
50+
51+
You can provide a formatter function to customize how unparsed outputs
52+
or other unknown types are converted to strings.
53+
"""
54+
55+
def my_formatter(obj: object) -> str:
56+
return f"[Formatted: {type(obj).__name__}]"
57+
58+
ctx = ChatContext()
59+
ctx = ctx.add(Message("user", "Process this"))
60+
# Add a ModelOutputThunk with a non-Message parsed_repr
61+
mot = ModelOutputThunk(value="raw data")
62+
mot.parsed_repr = {"type": "dict", "data": "structured"}
63+
ctx = ctx.add(mot)
64+
65+
history = as_generic_chat_history(ctx, formatter=my_formatter)
66+
assert len(history) == 2
67+
assert "[Formatted:" in history[1].content
68+
return history
69+
70+
71+
if __name__ == "__main__":
72+
basic = basic_example()
73+
print("Basic example:")
74+
for msg in basic:
75+
print(f" {msg.role}: {msg.content}")
76+
77+
heterogeneous = with_heterogeneous_components()
78+
print("\nHeterogeneous example:")
79+
for msg in heterogeneous:
80+
print(f" {msg.role}: {msg.content}")
81+
82+
custom = with_custom_formatter()
83+
print("\nCustom formatter example:")
84+
for msg in custom:
85+
print(f" {msg.role}: {msg.content}")

docs/examples/mcp/README.md

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
1-
# Write a poem MCP
2-
This is a simple example to show how to write a MCP tool
3-
with Mellea and instruct-validate-repair. Being able to
4-
speak the tool language allows you to integrate with
5-
Claude Desktop, Langflow, ...
1+
# MCP examples
62

7-
See code in [mcp_example.py](mcp_example.py)
3+
Two directions are covered here:
4+
5+
- **Expose Mellea as an MCP server**[`mcp_example.py`](mcp_example.py).
6+
Makes a Mellea instruct-validate-repair loop callable as an MCP tool from
7+
Claude Desktop, Langflow, or any MCP client.
8+
- **Consume MCP server tools from Mellea**
9+
[`github_activity_summary.py`](github_activity_summary.py). Discovers tools on
10+
a remote MCP server and drops them into a Mellea `react()` loop.
11+
12+
## Write a poem MCP (Mellea as server)
13+
14+
A simple example to show how to write a MCP tool with Mellea and
15+
instruct-validate-repair. Being able to speak the tool language lets you integrate
16+
with Claude Desktop, Langflow, and other MCP clients.
17+
18+
See code in [`mcp_example.py`](mcp_example.py).
19+
20+
### Running the poem server
21+
22+
Install the MCP SDK:
823

9-
## Run the example
10-
You need to install the mcp package:
1124
```bash
1225
uv pip install "mcp[cli]"
1326
```
1427

15-
and run the example in MCP debug UI:
28+
Run the example in the MCP debug UI:
29+
1630
```bash
1731
uv run mcp dev docs/examples/mcp/mcp_example.py
1832
```
1933

34+
### Use in Langflow
2035

21-
## Use in Langflow
22-
Follow this path (JSON) to use it in Langflow: [https://docs.langflow.org/mcp-client#mcp-stdio-mode](https://docs.langflow.org/mcp-client#mcp-stdio-mode)
23-
24-
The JSON to register your MCP tool is the following. Be sure to insert the absolute path to the directory containing the mcp_example.py file:
36+
Follow [this guide](https://docs.langflow.org/mcp-client#mcp-stdio-mode) to register
37+
the tool. Insert the absolute path to the directory containing `mcp_example.py`:
2538

2639
```json
2740
{
@@ -41,6 +54,36 @@ The JSON to register your MCP tool is the following. Be sure to insert the absol
4154
}
4255
```
4356

57+
## GitHub activity summary (Mellea as client)
58+
59+
Uses the hosted GitHub MCP server to summarize recent pull requests.
60+
Demonstrates the two-step workflow (discover tools, pick the ones you need,
61+
wrap as `MelleaTool`) and drives multi-turn tool use via `react()`.
62+
63+
See code in [`github_activity_summary.py`](github_activity_summary.py), and
64+
the [Tools and Agents how-to guide](../../docs/how-to/tools-and-agents) for
65+
the API overview.
66+
67+
### Running the activity summary
4468

69+
Install Mellea with tools support:
4570

71+
```bash
72+
pip install 'mellea[tools]'
73+
```
74+
75+
Set a GitHub token with `repo` and `read:user` scopes:
76+
77+
```bash
78+
export GITHUB_TOKEN=<your token>
79+
```
80+
81+
Run:
82+
83+
```bash
84+
uv run python docs/examples/mcp/github_activity_summary.py --days 14
85+
```
4686

87+
The script discovers every tool on the GitHub MCP server, filters down to
88+
`get_me` and `search_pull_requests`, then asks the model to summarize your
89+
pull-request activity over the specified window.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# pytest: skip_always
2+
"""Example: summarise recent GitHub activity using the GitHub MCP server.
3+
4+
Demonstrates the mellea MCP workflow:
5+
1. Discover all tools on the server with discover_mcp_tools()
6+
2. Pick only the ones needed by name
7+
3. Drive multi-turn tool use with mellea's react() loop
8+
9+
Prerequisites:
10+
pip install 'mellea[tools]'
11+
export GITHUB_TOKEN=<token with repo + read:user scopes>
12+
13+
Usage:
14+
uv run python docs/examples/mcp/github_activity_summary.py
15+
"""
16+
17+
import argparse
18+
import asyncio
19+
import os
20+
from datetime import UTC, datetime, timedelta
21+
22+
from mellea import start_session
23+
from mellea.backends import model_ids
24+
from mellea.core.base import AbstractMelleaTool
25+
from mellea.stdlib.context import ChatContext
26+
from mellea.stdlib.frameworks.react import react
27+
from mellea.stdlib.tools.mcp import discover_mcp_tools, http_connection
28+
29+
GITHUB_MCP_URL = "https://api.githubcopilot.com/mcp/"
30+
TOOLS_NEEDED = {"get_me", "search_pull_requests"}
31+
32+
33+
async def main(days: int) -> None:
34+
token = os.environ.get("GITHUB_TOKEN")
35+
if not token:
36+
raise SystemExit("GITHUB_TOKEN environment variable is required")
37+
38+
now = datetime.now(UTC)
39+
since = (now - timedelta(days=days)).strftime("%Y-%m-%d")
40+
today = now.strftime("%Y-%m-%d")
41+
42+
connection = http_connection(GITHUB_MCP_URL, api_key=token)
43+
m = start_session(model_id=model_ids.IBM_GRANITE_4_1_8B)
44+
45+
# --- Tool discovery ---
46+
specs = await discover_mcp_tools(connection)
47+
print(f"Discovered {len(specs)} tools on the GitHub MCP server")
48+
49+
# --- Tool selection ---
50+
relevant = [s for s in specs if s.name in TOOLS_NEEDED]
51+
print(f"Using {len(relevant)} tools: {[s.name for s in relevant]}")
52+
tools: list[AbstractMelleaTool] = [s.as_mellea_tool() for s in relevant]
53+
54+
# --- Agent loop ---
55+
result, _ = await react(
56+
goal=(
57+
f"Today is {today}. Find my GitHub username, then search for pull requests "
58+
f"I authored since {since} filtering by my username. "
59+
"List each pull request with its title, number, and repository."
60+
),
61+
context=ChatContext(),
62+
backend=m.backend,
63+
tools=tools,
64+
loop_budget=6,
65+
)
66+
67+
print("\n--- Activity Summary ---")
68+
print(result.value)
69+
70+
71+
if __name__ == "__main__":
72+
parser = argparse.ArgumentParser()
73+
parser.add_argument(
74+
"--days", type=int, default=14, help="How many days back to look"
75+
)
76+
args = parser.parse_args()
77+
asyncio.run(main(args.days))

0 commit comments

Comments
 (0)