Skip to content

Commit 62131d9

Browse files
authored
Merge pull request #50 from Kode-Rex/test/serper-scrape-integration
test: Add MCP search integration test (requires MCP client)
2 parents abfcf9b + 6fb4b9d commit 62131d9

File tree

1 file changed

+296
-0
lines changed

1 file changed

+296
-0
lines changed
Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
#!/usr/bin/env python3
2+
# Copyright (c) 2024 Travis Frisinger
3+
#
4+
# This source code is licensed under the MIT license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""Integration test for MCP search tool with Serper scraping."""
8+
9+
import json
10+
import os
11+
import sys
12+
import time
13+
14+
import requests
15+
16+
# Test configuration
17+
MCP_SERVER_URL = os.environ.get("MCP_SERVER_URL", "http://localhost:8000/mcp")
18+
SERPER_API_KEY = os.environ.get("SERPER_API_KEY", "")
19+
20+
21+
def wait_for_server(url: str, timeout: int = 30) -> bool:
22+
"""Wait for MCP server to be ready."""
23+
print(f"⏳ Waiting for MCP server at {url}...")
24+
start = time.time()
25+
26+
while time.time() - start < timeout:
27+
try:
28+
# Try MCP endpoint directly with proper headers
29+
headers = {
30+
"Content-Type": "application/json",
31+
"Accept": "application/json, text/event-stream",
32+
}
33+
response = requests.post(
34+
url,
35+
json={"jsonrpc": "2.0", "id": 0, "method": "tools/list"},
36+
headers=headers,
37+
timeout=2,
38+
)
39+
# Any 200 response means server is up
40+
if response.status_code == 200:
41+
print("✅ Server is ready!")
42+
return True
43+
except requests.RequestException:
44+
time.sleep(1)
45+
46+
print(f"❌ Server not ready after {timeout}s")
47+
return False
48+
49+
50+
def test_mcp_search_with_serper():
51+
"""Test MCP search tool with Serper API (includes scraping)."""
52+
if not SERPER_API_KEY:
53+
print("⚠️ SERPER_API_KEY not set - skipping Serper test")
54+
return True
55+
56+
print("\n🧪 Testing MCP search tool with Serper...")
57+
58+
# MCP JSON-RPC 2.0 request for search tool
59+
payload = {
60+
"jsonrpc": "2.0",
61+
"id": 1,
62+
"method": "tools/call",
63+
"params": {
64+
"name": "search",
65+
"arguments": {"query": "anthropic claude", "max_results": 2},
66+
},
67+
}
68+
69+
try:
70+
headers = {
71+
"Content-Type": "application/json",
72+
"Accept": "application/json, text/event-stream",
73+
}
74+
response = requests.post(
75+
MCP_SERVER_URL,
76+
json=payload,
77+
headers=headers,
78+
timeout=30,
79+
)
80+
81+
if response.status_code != 200:
82+
print(f"❌ MCP server returned status {response.status_code}")
83+
print(f"Response: {response.text}")
84+
return False
85+
86+
data = response.json()
87+
88+
# Check for JSON-RPC error
89+
if "error" in data:
90+
print(f"❌ MCP returned error: {data['error']}")
91+
return False
92+
93+
# Check result structure
94+
if "result" not in data:
95+
print(f"❌ Missing result in response: {data}")
96+
return False
97+
98+
result = data["result"]
99+
100+
# Parse the content (should be JSON string)
101+
if "content" not in result or not result["content"]:
102+
print(f"❌ Missing content in result: {result}")
103+
return False
104+
105+
content_items = result["content"]
106+
if not isinstance(content_items, list) or len(content_items) == 0:
107+
print(f"❌ Content is not a list or is empty: {content_items}")
108+
return False
109+
110+
# Get the text content
111+
text_content = content_items[0].get("text", "")
112+
if not text_content:
113+
print(f"❌ No text content found: {content_items}")
114+
return False
115+
116+
# Parse the JSON response from search tool
117+
try:
118+
search_results = json.loads(text_content)
119+
except json.JSONDecodeError as e:
120+
print(f"❌ Failed to parse search results JSON: {e}")
121+
print(f"Content: {text_content[:500]}")
122+
return False
123+
124+
# Validate search results structure
125+
if "results" not in search_results:
126+
print(f"❌ No 'results' key in search results: {search_results.keys()}")
127+
return False
128+
129+
results = search_results["results"]
130+
if not isinstance(results, list):
131+
print(f"❌ Results is not a list: {type(results)}")
132+
return False
133+
134+
if len(results) == 0:
135+
print("❌ No search results returned")
136+
return False
137+
138+
print(f"✅ Got {len(results)} search results")
139+
140+
# Verify each result has scraped content
141+
for i, result in enumerate(results):
142+
if "title" not in result:
143+
print(f"❌ Result {i} missing title")
144+
return False
145+
146+
if "url" not in result:
147+
print(f"❌ Result {i} missing url")
148+
return False
149+
150+
if "content" not in result:
151+
print(f"❌ Result {i} missing content (scraping failed?)")
152+
return False
153+
154+
content = result["content"]
155+
156+
# Check for Serper scraping indicators
157+
if "# " in content and "*Source:" in content:
158+
print(
159+
f"✅ Result {i}: '{result['title']}' - {len(content)} chars (Serper scraping worked!)"
160+
)
161+
else:
162+
print(
163+
f"⚠️ Result {i}: '{result['title']}' - {len(content)} chars (may have used fallback)"
164+
)
165+
166+
# Show preview
167+
if i == 0:
168+
preview = content[:300]
169+
print(f"\n📄 Content preview:\n{preview}...\n")
170+
171+
return True
172+
173+
except requests.exceptions.Timeout:
174+
print("❌ Request timeout - server may be slow or unresponsive")
175+
return False
176+
except requests.exceptions.RequestException as e:
177+
print(f"❌ Request failed: {str(e)}")
178+
return False
179+
except Exception as e:
180+
print(f"❌ Unexpected error: {str(e)}")
181+
import traceback
182+
183+
traceback.print_exc()
184+
return False
185+
186+
187+
def test_mcp_search_without_serper():
188+
"""Test MCP search tool without Serper (DuckDuckGo + Trafilatura)."""
189+
print("\n🧪 Testing MCP search tool without Serper (fallback mode)...")
190+
191+
# Temporarily remove API key
192+
original_key = os.environ.get("SERPER_API_KEY")
193+
if original_key:
194+
os.environ["SERPER_API_KEY"] = ""
195+
196+
try:
197+
payload = {
198+
"jsonrpc": "2.0",
199+
"id": 2,
200+
"method": "tools/call",
201+
"params": {
202+
"name": "search",
203+
"arguments": {"query": "python programming", "max_results": 2},
204+
},
205+
}
206+
207+
headers = {
208+
"Content-Type": "application/json",
209+
"Accept": "application/json, text/event-stream",
210+
}
211+
response = requests.post(
212+
MCP_SERVER_URL,
213+
json=payload,
214+
headers=headers,
215+
timeout=30,
216+
)
217+
218+
if response.status_code != 200:
219+
print(
220+
f"⚠️ Fallback test skipped - server may require Serper key (status {response.status_code})"
221+
)
222+
return True
223+
224+
data = response.json()
225+
226+
if "error" in data:
227+
print(f"⚠️ Fallback test got error (may be expected): {data['error']}")
228+
return True
229+
230+
if "result" in data and "content" in data["result"]:
231+
content_items = data["result"]["content"]
232+
if content_items and len(content_items) > 0:
233+
text_content = content_items[0].get("text", "")
234+
if text_content:
235+
search_results = json.loads(text_content)
236+
if (
237+
"results" in search_results
238+
and len(search_results["results"]) > 0
239+
):
240+
print(
241+
f"✅ Fallback mode working! Got {len(search_results['results'])} results"
242+
)
243+
return True
244+
245+
print("⚠️ Fallback test inconclusive")
246+
return True
247+
248+
except Exception as e:
249+
print(f"⚠️ Fallback test failed: {str(e)}")
250+
return True # Don't fail on fallback test
251+
finally:
252+
# Restore API key
253+
if original_key:
254+
os.environ["SERPER_API_KEY"] = original_key
255+
256+
257+
def main():
258+
"""Run all integration tests."""
259+
print("=" * 70)
260+
print("🚀 MCP Search Integration Tests")
261+
print("=" * 70)
262+
263+
# Wait for server
264+
if not wait_for_server(MCP_SERVER_URL):
265+
print("\n❌ Server not available - cannot run tests")
266+
return 1
267+
268+
results = []
269+
270+
# Test 1: Search with Serper
271+
results.append(("MCP search with Serper", test_mcp_search_with_serper()))
272+
273+
# Test 2: Search without Serper (fallback)
274+
results.append(("MCP search fallback", test_mcp_search_without_serper()))
275+
276+
# Summary
277+
print("\n" + "=" * 70)
278+
print("📊 Test Summary")
279+
print("=" * 70)
280+
281+
passed = sum(1 for _, result in results if result)
282+
total = len(results)
283+
284+
for test_name, result in results:
285+
status = "✅ PASS" if result else "❌ FAIL"
286+
print(f"{status}: {test_name}")
287+
288+
print("=" * 70)
289+
print(f"Results: {passed}/{total} tests passed")
290+
print("=" * 70)
291+
292+
return 0 if passed == total else 1
293+
294+
295+
if __name__ == "__main__":
296+
sys.exit(main())

0 commit comments

Comments
 (0)