Skip to content

Commit 018a2bb

Browse files
authored
feat: add McpClient wrapper for MCP client transport parity (#96)
* feat: add McpClient wrapper for MCP client transport parity Add McpClient that wraps an MCP SDK ClientSession with automatic payment handling - matching the TypeScript McpClient.wrap API. When call_tool gets a -32042 error, McpClient: 1. Parses challenges from error.data 2. Matches to an installed payment method by name+intent 3. Creates a credential and retries with it in _meta 4. Extracts receipts from the result - New: src/mpp/extensions/mcp/client.py (McpClient, McpToolResult) - Export McpClient and McpToolResult from mpp.extensions.mcp - 22 new tests covering free/paid tools, method matching, receipt extraction, error propagation, meta forwarding - Updated example client to use McpClient instead of manual flow - Updated example README * fix: harden MCP client payment flow
1 parent e1a9d76 commit 018a2bb

5 files changed

Lines changed: 843 additions & 71 deletions

File tree

examples/mcp-server/README.md

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Payment-protected MCP tools using Server-Sent Events (SSE).
66

77
This example demonstrates:
88
- **Server**: SSE-based MCP server with free and paid tools
9-
- **Client**: Connects to server and handles the payment flow
9+
- **Client**: Connects to server and handles payment automatically via `McpClient`
1010

1111
The server and client run in separate terminals, communicating via SSE.
1212

@@ -73,15 +73,9 @@ Available tools:
7373
1. Calling free tool (echo)...
7474
Result: Echo: Hello, world!
7575
76-
2. Calling paid tool without credential (premium_echo)...
77-
Got error code: -32042
78-
Challenge ID: abc123...
79-
80-
3. Creating payment credential...
81-
Credential created for challenge: abc123...
82-
83-
4. Retrying with credential...
76+
2. Calling paid tool (premium_echo)...
8477
Result: ✨ Premium Echo ✨: Hello, premium! (paid by 0x..., tx: 0x...)
78+
Receipt: success, ref=0x...
8579
```
8680

8781
## Server Implementations

examples/mcp-server/client.py

Lines changed: 19 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
#!/usr/bin/env python3
2-
"""MCP client demonstrating the payment flow.
2+
"""MCP client demonstrating automatic payment handling.
33
44
Connects to an already-running MCP server via SSE and demonstrates:
55
1. Calling a free tool (echo)
6-
2. Calling a paid tool without credentials (gets -32042 error)
7-
3. Parsing the challenge, creating a credential, and retrying
6+
2. Calling a paid tool (premium_echo) with automatic payment
7+
8+
Uses McpClient to handle the payment flow automatically—no manual
9+
challenge parsing or credential creation needed.
810
911
Usage:
1012
# Terminal 1: Start the server
@@ -27,11 +29,7 @@
2729
from mcp import ClientSession
2830
from mcp.client.sse import sse_client
2931

30-
from mpp.extensions.mcp import (
31-
CODE_PAYMENT_REQUIRED,
32-
MCPChallenge,
33-
MCPCredential,
34-
)
32+
from mpp.extensions.mcp import McpClient
3533
from mpp.methods.tempo import ChargeIntent, TempoAccount, tempo
3634

3735
SERVER_URL = os.environ.get("MCP_SERVER_URL", "http://127.0.0.1:8000/sse")
@@ -57,69 +55,28 @@ async def run_client() -> None:
5755
async with ClientSession(streams[0], streams[1]) as session:
5856
await session.initialize()
5957

60-
tools = await session.list_tools()
58+
# Wrap the session with automatic payment handling
59+
client = McpClient(session, methods=[method])
60+
61+
tools = await client.list_tools()
6162
print("Available tools:")
6263
for tool in tools.tools:
6364
print(f" - {tool.name}: {tool.description}")
6465
print()
6566

67+
# 1. Free tool — works without payment
6668
print("1. Calling free tool (echo)...")
67-
result = await session.call_tool("echo", {"message": "Hello, world!"})
69+
result = await client.call_tool("echo", {"message": "Hello, world!"})
6870
print(f" Result: {result.content[0].text}")
6971
print()
7072

71-
print("2. Calling paid tool without credential (premium_echo)...")
72-
try:
73-
result = await session.call_tool(
74-
"premium_echo", {"message": "Hello, premium!"}
75-
)
76-
print(f" Result: {result.content[0].text}")
77-
except Exception as e:
78-
error_data = getattr(e, "error", None) or {}
79-
error_code = (
80-
error_data.get("code")
81-
if isinstance(error_data, dict)
82-
else getattr(error_data, "code", None)
83-
)
84-
85-
print(f" Got error code: {error_code}")
86-
87-
if error_code == CODE_PAYMENT_REQUIRED:
88-
data = (
89-
error_data.get("data", {})
90-
if isinstance(error_data, dict)
91-
else getattr(error_data, "data", {})
92-
)
93-
challenges = (
94-
data.get("challenges", []) if isinstance(data, dict) else []
95-
)
96-
97-
if challenges:
98-
challenge_data = challenges[0]
99-
print(f" Challenge ID: {challenge_data.get('id', 'unknown')}")
100-
print()
101-
102-
print("3. Creating payment credential...")
103-
challenge = MCPChallenge.from_dict(challenge_data)
104-
core_credential = await method.create_credential(
105-
challenge.to_core()
106-
)
107-
108-
mcp_credential = MCPCredential.from_core(
109-
core_credential, challenge
110-
)
111-
print(f" Credential created for challenge: {challenge.id}")
112-
print()
113-
114-
print("4. Retrying with credential...")
115-
result = await session.call_tool(
116-
"premium_echo",
117-
{"message": "Hello, premium!"},
118-
meta=mcp_credential.to_meta(),
119-
)
120-
print(f" Result: {result.content[0].text}")
121-
else:
122-
print(f" Unexpected error: {e}")
73+
# 2. Paid tool — McpClient handles payment automatically
74+
print("2. Calling paid tool (premium_echo)...")
75+
result = await client.call_tool("premium_echo", {"message": "Hello, premium!"})
76+
print(f" Result: {result.content[0].text}")
77+
if result.receipt:
78+
print(f" Receipt: {result.receipt.status}, ref={result.receipt.reference}")
79+
print()
12380

12481

12582
def main() -> None:

src/mpp/extensions/mcp/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,11 @@ async def expensive_tool(query: str, *, credential, receipt) -> str:
5959
"""
6060

6161
from mpp.extensions.mcp.capabilities import payment_capabilities
62+
from mpp.extensions.mcp.client import (
63+
McpClient,
64+
McpToolResult,
65+
PaymentOutcomeUnknownError,
66+
)
6267
from mpp.extensions.mcp.constants import (
6368
CODE_MALFORMED_CREDENTIAL,
6469
CODE_PAYMENT_REQUIRED,

0 commit comments

Comments
 (0)