Skip to content

Commit 119ca37

Browse files
cpsievertclaude
andcommitted
Improve MCP tool compatibility and documentation
- Add `strict` parameter to Tool class to support OpenAI's strict mode - Set strict=False for MCP tools to preserve optional params (MCP uses standard JSON Schema conventions where optional params aren't in required) - Add `sanitize_schema()` function that strips `format` field (e.g., "uri") which OpenAI rejects, in addition to existing `title` removal - Restructure MCP tools guide with practical Quick Start example using MCP Fetch server - Update MCP server link to glama.ai/mcp/servers - Add section headers for Registering tools vs Authoring tools - Rename "Motivating example" to "Advanced example: Code execution" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a7d47e4 commit 119ca37

File tree

2 files changed

+109
-61
lines changed

2 files changed

+109
-61
lines changed

chatlas/_tools.py

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -71,19 +71,26 @@ def __init__(
7171
description: str,
7272
parameters: dict[str, Any],
7373
annotations: "Optional[ToolAnnotations]" = None,
74+
strict: Optional[bool] = None,
7475
):
7576
self.name = name
7677
self.func = func
7778
self.annotations = annotations
7879
self._is_async = _utils.is_async_callable(func)
79-
self.schema: "ChatCompletionToolParam" = {
80-
"type": "function",
81-
"function": {
82-
"name": name,
83-
"description": description,
84-
"parameters": parameters,
85-
},
80+
func_schema: dict[str, Any] = {
81+
"name": name,
82+
"description": description,
83+
"parameters": parameters,
8684
}
85+
if strict is not None:
86+
func_schema["strict"] = strict
87+
self.schema: "ChatCompletionToolParam" = cast(
88+
"ChatCompletionToolParam",
89+
{
90+
"type": "function",
91+
"function": func_schema,
92+
},
93+
)
8794

8895
@classmethod
8996
def from_func(
@@ -226,6 +233,9 @@ async def _call(**args: Any) -> AsyncGenerator[ContentToolResult, None]:
226233
description=mcp_tool.description or "",
227234
parameters=params,
228235
annotations=annotations,
236+
# MCP tools use standard JSON Schema conventions for optional params
237+
# (not in required array), which requires strict=False for OpenAI
238+
strict=False,
229239
)
230240

231241

@@ -441,25 +451,37 @@ def _validate_model_vs_function(model: type[BaseModel], func: Callable) -> None:
441451
def mcp_tool_input_schema_to_param_schema(
442452
input_schema: dict[str, Any],
443453
) -> dict[str, object]:
444-
params = rm_param_titles(input_schema)
454+
params = sanitize_schema(input_schema)
445455

446456
if "additionalProperties" not in params:
447457
params["additionalProperties"] = False
448458

449459
return params
450460

451461

452-
def rm_param_titles(
462+
def sanitize_schema(
453463
params: dict[str, object],
454464
) -> dict[str, object]:
455-
# For some reason, pydantic wants to include a title at the model and field
456-
# level. I don't think we actually need or want this.
465+
"""
466+
Sanitize JSON Schema for provider compatibility.
467+
468+
- `title`: Pydantic includes titles at model/field level, but they're not needed
469+
- `format`: JSON Schema format hints (e.g., "uri", "date-time") that some
470+
providers like OpenAI reject
471+
"""
457472
if "title" in params:
458473
del params["title"]
459474

475+
if "format" in params:
476+
del params["format"]
477+
460478
if "properties" in params and isinstance(params["properties"], dict):
461479
for prop in params["properties"].values():
462-
if "title" in prop:
463-
del prop["title"]
480+
if isinstance(prop, dict):
481+
sanitize_schema(prop)
464482

465483
return params
484+
485+
486+
# Keep for backwards compatibility
487+
rm_param_titles = sanitize_schema

docs/misc/mcp-tools.qmd

Lines changed: 74 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -3,67 +3,101 @@ title: MCP tools
33
callout-appearance: simple
44
---
55

6-
[Model Context Protocol (MCP)](https://modelcontextprotocol.io) provides a standard
6+
[Model Context Protocol (MCP)](https://modelcontextprotocol.io) provides a standard
77
way to build services that LLMs can use to gain context.
8-
Most significantly, MCP provides a standard way to serve [tools](../get-started/tools.qmd) (i.e., functions) for an LLM to call from another program or machine.
9-
As a result, there are now [many useful MCP server implementations available](https://github.com/punkpeye/awesome-mcp-servers?tab=readme-ov-file#server-implementations) to help extend the capabilities of your chat application.
10-
In this article, you'll learn the basics of implementing and using MCP tools in chatlas.
8+
This includes a standard way to provide [tools](../get-started/tools.qmd) (i.e., functions) for an LLM to call from another program or machine.
9+
There are now [many useful MCP server implementations available](https://glama.ai/mcp/servers) to help extend the capabilities of your chat application with minimal effort.
1110

11+
In this article, you'll learn how to both register existing MCP tools with chatlas as well as author your own custom MCP tools.
1212

1313
::: callout-note
1414
## Prerequisites
1515

16-
To leverage MCP tools from chatlas, you'll need to install the `mcp` library.
16+
To leverage MCP tools from chatlas, you'll want to install the `mcp` extra.
1717

1818
```bash
1919
pip install 'chatlas[mcp]'
2020
```
2121
:::
2222

2323

24-
## Basic usage
24+
## Registering tools
2525

26-
Chatlas provides two ways to register MCP tools: [`.register_mcp_tools_http_stream_async()`](../reference/Chat.qmd#register_mcp_tools_http_stream_async) and [`.register_mcp_tools_stdio_async()`](../reference/Chat.qmd#register_mcp_tools_stdio_async).
26+
### Quick start {#quick-start}
2727

28+
Let's start with a practical example: using the [MCP Fetch server](https://github.com/modelcontextprotocol/servers/tree/main/src/fetch) to give an LLM the ability to fetch and read web pages.
29+
This server is maintained by Anthropic and can be run via `uvx` (which comes with [uv](https://docs.astral.sh/uv/)).
2830

29-
The main difference is how they interact with the MCP server: the former connects to an already running HTTP server, while the latter executes a system command to run the server locally.
30-
Roughly speaking, usage looks something like this:
31-
32-
::: panel-tabset
33-
34-
### Streaming HTTP
31+
For simplicity and convenience, we'll use the [`.register_mcp_tools_stdio_async()`](../reference/Chat.qmd#register_mcp_tools_stdio_async) method to both run the MCP Fetch server locally and register its tools with our `ChatOpenAI` instance:
3532

3633
```python
34+
import asyncio
3735
from chatlas import ChatOpenAI
3836

39-
chat = ChatOpenAI()
37+
async def main():
38+
chat = ChatOpenAI()
39+
await chat.register_mcp_tools_stdio_async(
40+
command="uvx",
41+
args=["mcp-server-fetch"],
42+
)
43+
await chat.chat_async(
44+
"Summarize the first paragraph of https://en.wikipedia.org/wiki/Python_(programming_language)"
45+
)
46+
await chat.cleanup_mcp_tools()
4047

41-
# Assuming you have an MCP server running at the specified URL
42-
await chat.register_mcp_tools_http_stream_async(
43-
url="http://localhost:8000/mcp",
44-
)
48+
asyncio.run(main())
4549
```
4650

47-
### Stdio (Standard Input/Output)
48-
51+
::: chatlas-response-container
4952
```python
50-
from chatlas import ChatOpenAI
53+
# 🔧 tool request
54+
fetch(url="https://en.wikipedia.org/wiki/Python_(programming_language)")
55+
```
56+
57+
Python is a high-level, general-purpose programming language known for its emphasis on code readability through significant indentation. It supports multiple programming paradigms including structured, object-oriented, and functional programming, and is dynamically typed with garbage collection.
58+
:::
59+
60+
::: callout-tip
61+
### Built-in fetch/search tools
62+
63+
For providers with native web fetch support (Claude, Google), consider using [`tool_web_fetch()`](../reference/tool_web_fetch.qmd) instead -- it's simpler and doesn't require MCP setup.
64+
Similarly, [`tool_web_search()`](../reference/tool_web_search.qmd) provides native web search for OpenAI, Claude, and Google.
65+
:::
5166

52-
chat = ChatOpenAI()
5367

54-
# Assuming my_mcp_server.py is a valid MCP server script
68+
### Basic usage {#basic-usage}
69+
70+
Chatlas provides two ways to register MCP tools:
71+
72+
1. Stdio ([`.register_mcp_tools_stdio_async()`](../reference/Chat.qmd#register_mcp_tools_stdio_async))
73+
2. Streamble HTTP [`.register_mcp_tools_http_stream_async()`](../reference/Chat.qmd#register_mcp_tools_http_stream_async).
74+
75+
The main difference is how they communicate with the MCP server: the former (Stdio) executes a system command to run the server locally, while the latter (HTTP) connects to an already running HTTP server.
76+
77+
This makes the Stdio method more ergonomic for local development and testing. For instance, recall the example above, which runs `uvx mcp-server-fetch` locally to provide web fetching capabilities to the chat instance:
78+
79+
```python
80+
# Run a server via uvx, npx, or any other command
5581
await chat.register_mcp_tools_stdio_async(
56-
command="mcp",
57-
args=["run", "my_mcp_server.py"],
82+
command="uvx",
83+
args=["mcp-server-fetch"],
5884
)
5985
```
6086

61-
:::
87+
On the other hand, the HTTP method is better for production environments where the server is hosted remotely or in a longer-running process.
88+
For example, if you have an MCP server already running at `http://localhost:8000/mcp`, you can connect to it as follows:
89+
90+
```python
91+
# Connect to a server already running at the specified URL
92+
await chat.register_mcp_tools_http_stream_async(
93+
url="http://localhost:8000/mcp",
94+
)
95+
```
6296

6397
::: callout-warning
6498
### Async methods
6599

66-
For performance reasons, the methods for registering MCP tools are asynchronous, so you'll need to use `await` when calling them.
100+
For performance, the methods for registering MCP tools are asynchronous, so you'll need to use `await` when calling them.
67101
In some environments, such as Jupyter notebooks and the [Positron IDE](https://positron.posit.co/) console, you can simply use `await` directly (as is done above).
68102
However, in other environments, you may need to wrap your code in an `async` function and use `asyncio.run()` to execute it.
69103
The examples below use `asyncio.run()` to run the asynchronous code, but you can adapt them to your environment as needed.
@@ -75,23 +109,14 @@ Note that these methods work by:
75109
2. Requesting the available tools and making them available to the chat instance
76110
3. Keeping the connection open for tool calls during the chat session
77111

112+
This means, when you no longer need the MCP tools, it's good practice to clean up the connection to the MCP server, as well `Chat`'s tool state.
113+
This is done by calling [`.cleanup_mcp_tools()`](../reference/Chat.qmd#cleanup_mcp_tools) at the end of your chat session (the examples demonstrate how to do this).
78114

79-
::: callout-warning
80-
### Cleanup
81-
82-
When you no longer need the MCP tools, it's important to clean up the connection to the MCP server, as well `Chat`'s tool state.
83-
This is done by calling [`.cleanup_mcp_tools()`](../reference/Chat.qmd#cleanup_mcp_tools) at the end of your chat session (the examples demonstrate how to do this).
84-
:::
85-
86-
87-
## Basic example
88-
89-
Let's walk through a full-fledged example of using MCP tools in chatlas, including implementing our own MCP server.
115+
## Authoring tools
90116

91-
### Basic server {#basic-server}
117+
If existing MCP servers don't meet your needs, you can implement your own without much effort thanks to the [mcp](https://pypi.org/project/mcp/) Python library (you can also [work in other languages](https://modelcontextprotocol.io/docs/sdk), if you like).
92118

93119
Below is a basic MCP server with a simple `add` tool to add two numbers together.
94-
This particular server is implemented in Python (via [mcp](https://pypi.org/project/mcp/)), but remember that MCP servers can be implemented in any programming language.
95120

96121
```python
97122
from mcp.server.fastmcp import FastMCP
@@ -103,14 +128,15 @@ def add(x: int, y: int) -> int:
103128
return x + y
104129
```
105130

131+
That's it! You can now run this server through the streaming HTTP or Stdio protocols and connect its tools to chatlas.
106132

107133
### HTTP Stream
108134

109135
The `mcp` library provides a CLI tool to run the MCP server over HTTP transport.
110136
As long as you have `mcp` installed, and the [server above](#basic-server) saved as `my_mcp_server.py`, this can be done as follows:
111137

112138
```bash
113-
$ mcp run -t sse my_mcp_server.py
139+
$ mcp run -t sse my_mcp_server.py
114140
INFO: Started server process [19144]
115141
INFO: Waiting for application startup.
116142
INFO: Application startup complete.
@@ -141,12 +167,12 @@ asyncio.run(do_chat("What is 5 - 3?"))
141167
::: chatlas-response-container
142168

143169
```python
144-
# 🔧 tool request
170+
# 🔧 tool request
145171
add(x=5, y=-3)
146172
```
147173

148174
```python
149-
# ✅ tool result
175+
# ✅ tool result
150176
2
151177
```
152178

@@ -186,27 +212,27 @@ asyncio.run(do_chat("What is 5 - 3?"))
186212
::: chatlas-response-container
187213

188214
```python
189-
# 🔧 tool request
215+
# 🔧 tool request
190216
add(x=5, y=-3)
191217
```
192218

193219
```python
194-
# ✅ tool result
220+
# ✅ tool result
195221
2
196222
```
197223

198224
5 - 3 equals 2.
199225
:::
200226

201227

202-
## Motivating example
228+
## Advanced example: Code execution
203229

204230
Let's look at a more compelling use case for MCP tools: code execution.
205231
A tool that can execute code and return the results is a powerful way to extend the capabilities of an LLM.
206232
This way, LLMs can generate code based on natural language prompts (which they are quite good at!) and then execute that code to get precise and reliable results from data (which LLMs are not so good at!).
207233
However, allowing an LLM to execute arbitrary code is risky, as the generated code could potentially be destructive, harmful, or even malicious.
208234

209-
To mitigate these risks, it's important to implement safeguards around code execution.
235+
To mitigate these risks, it's important to implement safeguards around code execution.
210236
This can include running code in isolated environments, restricting access to sensitive resources, and carefully validating and sanitizing inputs to the code execution tool.
211237
One such implementation is Pydantic's [Run Python MCP server](https://github.com/pydantic/pydantic-ai/tree/main/mcp-run-python), which provides a sandboxed environment for executing Python code safely via [Pyodide](https://pyodide.org/en/stable/) and [Deno](https://deno.com/).
212238

@@ -242,4 +268,4 @@ async def _(user_input: str):
242268
await chat.append_message_stream(stream)
243269
```
244270

245-
![Screenshot of a LLM executing Python code via a tool call in a Shiny chatbot](../images/shiny-mcp-run-python.png){class="shadow rounded"}
271+
![Screenshot of a LLM executing Python code via a tool call in a Shiny chatbot](../images/shiny-mcp-run-python.png){class="shadow rounded"}

0 commit comments

Comments
 (0)