Skip to content

Commit 0ace30e

Browse files
committed
feat: add MCP SDK integration test, fix YAML flow syntax for {id} paths
- test_mcp_sdk.py: Full MCP protocol test using official Python SDK (15/15 pass: connect, list tools, create resource, 5 CRUD mocks, HTTP verification, invocation check) - Fix {id} in YAML flow mappings — must be quoted to avoid anchor parse - Docker compose validated: pulls ghcr.io/getmockd/mockd:0.6, serves seed data, CRUD works through container
1 parent 81f680a commit 0ace30e

1 file changed

Lines changed: 219 additions & 0 deletions

File tree

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
#!/usr/bin/env python3
2+
"""Test the MCP workflow sample using the official MCP Python SDK.
3+
4+
Connects to mockd's MCP server over stdio, creates a Todo API
5+
entirely through MCP tool calls, then validates it works.
6+
7+
Usage: python3 test_mcp_sdk.py
8+
Requires: mcp Python package, mockd binary in PATH or at MOCKD_BIN
9+
"""
10+
11+
import asyncio
12+
import json
13+
import os
14+
import sys
15+
import httpx
16+
from mcp import ClientSession, StdioServerParameters
17+
from mcp.client.stdio import stdio_client
18+
19+
MOCKD_BIN = os.environ.get("MOCKD_BIN", "mockd")
20+
PASS = 0
21+
FAIL = 0
22+
23+
24+
def passed(msg):
25+
global PASS
26+
PASS += 1
27+
print(f" ✓ {msg}")
28+
29+
30+
def failed(msg):
31+
global FAIL
32+
FAIL += 1
33+
print(f" ✗ {msg}")
34+
35+
36+
async def call_tool(session: ClientSession, name: str, args: dict) -> dict:
37+
"""Call an MCP tool and return the parsed JSON result."""
38+
result = await session.call_tool(name, args)
39+
for content in result.content:
40+
if hasattr(content, "text"):
41+
try:
42+
return json.loads(content.text)
43+
except json.JSONDecodeError:
44+
return {"_text": content.text, "_is_error": result.isError}
45+
return {}
46+
47+
48+
async def main():
49+
global PASS, FAIL
50+
51+
server_params = StdioServerParameters(
52+
command=MOCKD_BIN,
53+
args=["mcp"],
54+
env={
55+
**os.environ,
56+
"MOCKD_ADMIN_URL": "http://localhost:5280",
57+
"MOCKD_PORT": "5280",
58+
"MOCKD_ADMIN_PORT": "5290",
59+
},
60+
)
61+
62+
print("=== MCP SDK Integration Test ===\n")
63+
64+
async with stdio_client(server_params) as (read, write):
65+
async with ClientSession(read, write) as session:
66+
await session.initialize()
67+
68+
# List available tools
69+
tools = await session.list_tools()
70+
tool_names = [t.name for t in tools.tools]
71+
print(f"Connected. {len(tool_names)} tools available.")
72+
if "manage_mock" in tool_names:
73+
passed("manage_mock tool available")
74+
else:
75+
failed("manage_mock tool not found")
76+
if "manage_state" in tool_names:
77+
passed("manage_state tool available")
78+
else:
79+
failed("manage_state tool not found")
80+
81+
# 1. Get server status
82+
print("\n--- Server Status ---")
83+
status = await call_tool(session, "get_server_status", {})
84+
if status.get("healthy") or status.get("status") == "ok":
85+
passed(
86+
f"Server healthy (uptime: {status.get('uptime', '?')}s, mocks: {status.get('mockCount', '?')})"
87+
)
88+
else:
89+
failed(f"Server not healthy: {status}")
90+
91+
# 2. Create stateful resource "todos"
92+
print("\n--- Create Stateful Resource ---")
93+
state_result = await call_tool(
94+
session,
95+
"manage_state",
96+
{
97+
"action": "add_resource",
98+
"resource": "todos",
99+
},
100+
)
101+
text = str(state_result.get("_text", state_result))
102+
if state_result.get("_is_error") and "already exists" not in text:
103+
failed(f"Failed to create resource: {text}")
104+
else:
105+
passed("Created 'todos' resource")
106+
107+
# 3. Create CRUD mocks via manage_mock with extend
108+
print("\n--- Create CRUD Mocks ---")
109+
crud_ops = [
110+
("GET", "/api/todos", "list"),
111+
("POST", "/api/todos", "create"),
112+
("GET", "/api/todos/{id}", "get"),
113+
("PUT", "/api/todos/{id}", "update"),
114+
("DELETE", "/api/todos/{id}", "delete"),
115+
]
116+
for method, path, action in crud_ops:
117+
result = await call_tool(
118+
session,
119+
"manage_mock",
120+
{
121+
"action": "create",
122+
"type": "http",
123+
"http": {
124+
"matcher": {"method": method, "path": path},
125+
},
126+
"extend": {"table": "todos", "action": action},
127+
},
128+
)
129+
mock_id = result.get("id", result.get("mock", {}).get("id", "?"))
130+
if mock_id and mock_id != "?":
131+
passed(f"{method} {path}{action} (id: {mock_id})")
132+
else:
133+
failed(f"{method} {path}{action}: {result}")
134+
135+
# 4. Verify with HTTP requests
136+
print("\n--- Verify CRUD via HTTP ---")
137+
# Use the port from status, not hardcoded
138+
mock_port = 7280 # matches our running server
139+
for p in status.get("ports", []):
140+
if p.get("component") == "Mock Engine":
141+
mock_port = p["port"]
142+
base = f"http://localhost:{mock_port}"
143+
print(f" Using {base}")
144+
async with httpx.AsyncClient() as http:
145+
# Create a todo
146+
resp = await http.post(
147+
f"{base}/api/todos",
148+
json={
149+
"title": "MCP SDK test",
150+
"completed": False,
151+
},
152+
)
153+
if resp.status_code == 201:
154+
todo = resp.json()
155+
todo_id = todo.get("id")
156+
passed(f"POST /api/todos → 201 (id: {todo_id})")
157+
else:
158+
failed(f"POST /api/todos → {resp.status_code}")
159+
todo_id = None
160+
161+
# List todos
162+
resp = await http.get(f"{base}/api/todos")
163+
if resp.status_code == 200:
164+
data = resp.json()
165+
count = len(data.get("data", []))
166+
passed(f"GET /api/todos → 200 ({count} items)")
167+
else:
168+
failed(f"GET /api/todos → {resp.status_code}")
169+
170+
if todo_id:
171+
# Get by ID
172+
resp = await http.get(f"{base}/api/todos/{todo_id}")
173+
if resp.status_code == 200:
174+
passed(f"GET /api/todos/{todo_id} → 200")
175+
else:
176+
failed(f"GET /api/todos/{todo_id}{resp.status_code}")
177+
178+
# Update
179+
resp = await http.put(
180+
f"{base}/api/todos/{todo_id}",
181+
json={
182+
"title": "Updated via MCP SDK",
183+
"completed": True,
184+
},
185+
)
186+
if resp.status_code == 200:
187+
updated = resp.json()
188+
if updated.get("completed") is True:
189+
passed("PUT update → completed=true")
190+
else:
191+
failed(f"PUT update → completed={updated.get('completed')}")
192+
else:
193+
failed(f"PUT → {resp.status_code}")
194+
195+
# Delete
196+
resp = await http.delete(f"{base}/api/todos/{todo_id}")
197+
if resp.status_code in (200, 204):
198+
passed(f"DELETE → {resp.status_code}")
199+
else:
200+
failed(f"DELETE → {resp.status_code}")
201+
202+
# 5. Verify invocations via MCP
203+
print("\n--- Verify Invocations ---")
204+
mocks = await call_tool(session, "manage_mock", {"action": "list"})
205+
mock_list = mocks.get("mocks", mocks) if isinstance(mocks, dict) else mocks
206+
if isinstance(mock_list, list) and len(mock_list) >= 5:
207+
passed(f"5+ mocks registered ({len(mock_list)} total)")
208+
else:
209+
failed(f"Expected 5+ mocks, got {len(mock_list)}")
210+
211+
print(f"\n{'=' * 40}")
212+
print(f"Results: {PASS} passed, {FAIL} failed")
213+
if FAIL > 0:
214+
sys.exit(1)
215+
print("=== ALL MCP SDK TESTS PASSED ===")
216+
217+
218+
if __name__ == "__main__":
219+
asyncio.run(main())

0 commit comments

Comments
 (0)