Skip to content

Commit 0822aa6

Browse files
Merge branch 'main' into feat/oidc-fallback
2 parents 3dab5c0 + 11162d7 commit 0822aa6

File tree

6 files changed

+244
-2
lines changed

6 files changed

+244
-2
lines changed

.github/ISSUE_TEMPLATE/config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
blank_issues_enabled: true
1+
blank_issues_enabled: false

CONTRIBUTING.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ Thank you for your interest in contributing to the MCP Python SDK! This document
1414
uv sync --frozen --all-extras --dev
1515
```
1616

17+
6. Set up pre-commit hooks:
18+
19+
```bash
20+
uv tool install pre-commit --with pre-commit-uv --force-reinstall
21+
```
22+
1723
## Development Workflow
1824

1925
1. Choose the correct branch for your changes:
@@ -50,7 +56,13 @@ uv run ruff format .
5056
uv run scripts/update_readme_snippets.py
5157
```
5258

53-
8. Submit a pull request to the same branch you branched from
59+
8. (Optional) Run pre-commit hooks on all files:
60+
61+
```bash
62+
pre-commit run --all-files
63+
```
64+
65+
9. Submit a pull request to the same branch you branched from
5466

5567
## Code Style
5668

README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
- [Advanced Usage](#advanced-usage)
4545
- [Low-Level Server](#low-level-server)
4646
- [Writing MCP Clients](#writing-mcp-clients)
47+
- [Parsing Tool Results](#parsing-tool-results)
4748
- [MCP Primitives](#mcp-primitives)
4849
- [Server Capabilities](#server-capabilities)
4950
- [Documentation](#documentation)
@@ -1605,6 +1606,75 @@ async def main():
16051606

16061607
For a complete working example, see [`examples/clients/simple-auth-client/`](examples/clients/simple-auth-client/).
16071608

1609+
### Parsing Tool Results
1610+
1611+
When calling tools through MCP, the `CallToolResult` object contains the tool's response in a structured format. Understanding how to parse this result is essential for properly handling tool outputs.
1612+
1613+
```python
1614+
"""examples/snippets/clients/parsing_tool_results.py"""
1615+
1616+
import asyncio
1617+
1618+
from mcp import ClientSession, StdioServerParameters, types
1619+
from mcp.client.stdio import stdio_client
1620+
1621+
1622+
async def parse_tool_results():
1623+
"""Demonstrates how to parse different types of content in CallToolResult."""
1624+
server_params = StdioServerParameters(
1625+
command="python", args=["path/to/mcp_server.py"]
1626+
)
1627+
1628+
async with stdio_client(server_params) as (read, write):
1629+
async with ClientSession(read, write) as session:
1630+
await session.initialize()
1631+
1632+
# Example 1: Parsing text content
1633+
result = await session.call_tool("get_data", {"format": "text"})
1634+
for content in result.content:
1635+
if isinstance(content, types.TextContent):
1636+
print(f"Text: {content.text}")
1637+
1638+
# Example 2: Parsing structured content from JSON tools
1639+
result = await session.call_tool("get_user", {"id": "123"})
1640+
if hasattr(result, "structuredContent") and result.structuredContent:
1641+
# Access structured data directly
1642+
user_data = result.structuredContent
1643+
print(f"User: {user_data.get('name')}, Age: {user_data.get('age')}")
1644+
1645+
# Example 3: Parsing embedded resources
1646+
result = await session.call_tool("read_config", {})
1647+
for content in result.content:
1648+
if isinstance(content, types.EmbeddedResource):
1649+
resource = content.resource
1650+
if isinstance(resource, types.TextResourceContents):
1651+
print(f"Config from {resource.uri}: {resource.text}")
1652+
elif isinstance(resource, types.BlobResourceContents):
1653+
print(f"Binary data from {resource.uri}")
1654+
1655+
# Example 4: Parsing image content
1656+
result = await session.call_tool("generate_chart", {"data": [1, 2, 3]})
1657+
for content in result.content:
1658+
if isinstance(content, types.ImageContent):
1659+
print(f"Image ({content.mimeType}): {len(content.data)} bytes")
1660+
1661+
# Example 5: Handling errors
1662+
result = await session.call_tool("failing_tool", {})
1663+
if result.isError:
1664+
print("Tool execution failed!")
1665+
for content in result.content:
1666+
if isinstance(content, types.TextContent):
1667+
print(f"Error: {content.text}")
1668+
1669+
1670+
async def main():
1671+
await parse_tool_results()
1672+
1673+
1674+
if __name__ == "__main__":
1675+
asyncio.run(main())
1676+
```
1677+
16081678
### MCP Primitives
16091679

16101680
The MCP protocol defines three core primitives that servers can implement:
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
"""examples/snippets/clients/parsing_tool_results.py"""
2+
3+
import asyncio
4+
5+
from mcp import ClientSession, StdioServerParameters, types
6+
from mcp.client.stdio import stdio_client
7+
8+
9+
async def parse_tool_results():
10+
"""Demonstrates how to parse different types of content in CallToolResult."""
11+
server_params = StdioServerParameters(command="python", args=["path/to/mcp_server.py"])
12+
13+
async with stdio_client(server_params) as (read, write):
14+
async with ClientSession(read, write) as session:
15+
await session.initialize()
16+
17+
# Example 1: Parsing text content
18+
result = await session.call_tool("get_data", {"format": "text"})
19+
for content in result.content:
20+
if isinstance(content, types.TextContent):
21+
print(f"Text: {content.text}")
22+
23+
# Example 2: Parsing structured content from JSON tools
24+
result = await session.call_tool("get_user", {"id": "123"})
25+
if hasattr(result, "structuredContent") and result.structuredContent:
26+
# Access structured data directly
27+
user_data = result.structuredContent
28+
print(f"User: {user_data.get('name')}, Age: {user_data.get('age')}")
29+
30+
# Example 3: Parsing embedded resources
31+
result = await session.call_tool("read_config", {})
32+
for content in result.content:
33+
if isinstance(content, types.EmbeddedResource):
34+
resource = content.resource
35+
if isinstance(resource, types.TextResourceContents):
36+
print(f"Config from {resource.uri}: {resource.text}")
37+
elif isinstance(resource, types.BlobResourceContents):
38+
print(f"Binary data from {resource.uri}")
39+
40+
# Example 4: Parsing image content
41+
result = await session.call_tool("generate_chart", {"data": [1, 2, 3]})
42+
for content in result.content:
43+
if isinstance(content, types.ImageContent):
44+
print(f"Image ({content.mimeType}): {len(content.data)} bytes")
45+
46+
# Example 5: Handling errors
47+
result = await session.call_tool("failing_tool", {})
48+
if result.isError:
49+
print("Tool execution failed!")
50+
for content in result.content:
51+
if isinstance(content, types.TextContent):
52+
print(f"Error: {content.text}")
53+
54+
55+
async def main():
56+
await parse_tool_results()
57+
58+
59+
if __name__ == "__main__":
60+
asyncio.run(main())

tests/cli/__init__.py

Whitespace-only changes.

tests/cli/test_utils.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import subprocess
2+
import sys
3+
from pathlib import Path
4+
5+
import pytest
6+
7+
from mcp.cli.cli import _build_uv_command, _get_npx_command, _parse_file_path
8+
9+
10+
@pytest.mark.parametrize(
11+
"spec, expected_obj",
12+
[
13+
("server.py", None),
14+
("foo.py:srv_obj", "srv_obj"),
15+
],
16+
)
17+
def test_parse_file_path_accepts_valid_specs(tmp_path, spec, expected_obj):
18+
"""Should accept valid file specs."""
19+
file = tmp_path / spec.split(":")[0]
20+
file.write_text("x = 1")
21+
path, obj = _parse_file_path(f"{file}:{expected_obj}" if ":" in spec else str(file))
22+
assert path == file.resolve()
23+
assert obj == expected_obj
24+
25+
26+
def test_parse_file_path_missing(tmp_path):
27+
"""Should system exit if a file is missing."""
28+
with pytest.raises(SystemExit):
29+
_parse_file_path(str(tmp_path / "missing.py"))
30+
31+
32+
def test_parse_file_exit_on_dir(tmp_path):
33+
"""Should system exit if a directory is passed"""
34+
dir_path = tmp_path / "dir"
35+
dir_path.mkdir()
36+
with pytest.raises(SystemExit):
37+
_parse_file_path(str(dir_path))
38+
39+
40+
def test_build_uv_command_minimal():
41+
"""Should emit core command when no extras specified."""
42+
cmd = _build_uv_command("foo.py")
43+
assert cmd == ["uv", "run", "--with", "mcp", "mcp", "run", "foo.py"]
44+
45+
46+
def test_build_uv_command_adds_editable_and_packages():
47+
"""Should include --with-editable and every --with pkg in correct order."""
48+
test_path = Path("/pkg")
49+
cmd = _build_uv_command(
50+
"foo.py",
51+
with_editable=test_path,
52+
with_packages=["package1", "package2"],
53+
)
54+
assert cmd == [
55+
"uv",
56+
"run",
57+
"--with",
58+
"mcp",
59+
"--with-editable",
60+
str(test_path), # Use str() to match what the function does
61+
"--with",
62+
"package1",
63+
"--with",
64+
"package2",
65+
"mcp",
66+
"run",
67+
"foo.py",
68+
]
69+
70+
71+
def test_get_npx_unix_like(monkeypatch):
72+
"""Should return "npx" on unix-like systems."""
73+
monkeypatch.setattr(sys, "platform", "linux")
74+
assert _get_npx_command() == "npx"
75+
76+
77+
def test_get_npx_windows(monkeypatch):
78+
"""Should return one of the npx candidates on Windows."""
79+
candidates = ["npx.cmd", "npx.exe", "npx"]
80+
81+
def fake_run(cmd, **kw):
82+
if cmd[0] in candidates:
83+
return subprocess.CompletedProcess(cmd, 0)
84+
else:
85+
raise subprocess.CalledProcessError(1, cmd[0])
86+
87+
monkeypatch.setattr(sys, "platform", "win32")
88+
monkeypatch.setattr(subprocess, "run", fake_run)
89+
assert _get_npx_command() in candidates
90+
91+
92+
def test_get_npx_returns_none_when_npx_missing(monkeypatch):
93+
"""Should give None if every candidate fails."""
94+
monkeypatch.setattr(sys, "platform", "win32", raising=False)
95+
96+
def always_fail(*args, **kwargs):
97+
raise subprocess.CalledProcessError(1, args[0])
98+
99+
monkeypatch.setattr(subprocess, "run", always_fail)
100+
assert _get_npx_command() is None

0 commit comments

Comments
 (0)