Skip to content

Commit 6c8f728

Browse files
committed
Fix: validate Xquik MCP sample
1 parent 92865cb commit 6c8f728

4 files changed

Lines changed: 97 additions & 11 deletions

File tree

functionality/xquik_mcp_discovery/README.md

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ This sample shows how to connect AgentScope's MCP client to Xquik's remote MCP s
66

77
```
88
.
9+
├── __init__.py # Enables package-based test discovery
910
├── README.md # Documentation
1011
├── main.py # Entry point
12+
├── test_main.py # Offline configuration and query tests
1113
└── requirements.txt # Dependencies
1214
```
1315

@@ -17,7 +19,7 @@ The sample demonstrates a minimal AgentScope MCP integration for a hosted, authe
1719

1820
- Connects to `https://xquik.com/mcp` with an `Authorization: Bearer <token>` header.
1921
- Enables only the read-only `explore` MCP tool.
20-
- Searches the Xquik API catalog for endpoint categories or summaries that match a query.
22+
- Searches the Xquik API catalogue for methods, paths, categories, or summaries that match a query.
2123
- Prints the matching endpoint method, path, category, summary, and free/paid status.
2224

2325
No write or publish operations are executed. The sample does not call Xquik's live `xquik` execution tool.
@@ -41,11 +43,7 @@ pip install -r requirements.txt
4143
export XQUIK_API_KEY="your-api-key"
4244
```
4345

44-
Optional: override the MCP URL when testing a compatible endpoint.
45-
46-
```bash
47-
export XQUIK_MCP_URL="https://xquik.com/mcp"
48-
```
46+
The sample fixes the remote URL to `https://xquik.com/mcp` so the API key is never sent to a configurable origin.
4947

5048
### Usage
5149

@@ -63,9 +61,19 @@ python main.py monitors
6361

6462
If no query is provided, the sample searches for `radar`.
6563

64+
### Tests
65+
66+
Run the offline tests from the repository root:
67+
68+
```bash
69+
python -m unittest -v functionality.xquik_mcp_discovery.test_main
70+
```
71+
6672
## Features
6773

6874
- Uses AgentScope's `MCPClient` and `HttpMCPConfig`.
6975
- Keeps the MCP connection stateless.
7076
- Restricts the remote server to the read-only `explore` tool.
7177
- Reads configuration only from environment variables.
78+
79+
Xquik is an independent third-party service. It is not affiliated with X Corp. "Twitter" and "X" are trademarks of X Corp.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# -*- coding: utf-8 -*-
2+
"""Xquik MCP discovery sample."""

functionality/xquik_mcp_discovery/main.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,12 @@ def _explore_code(query: str) -> str:
4343
return f"""
4444
async () => spec.endpoints
4545
.filter((endpoint) => {{
46-
const haystack = `${{endpoint.method}} ${{endpoint.path}} ${{endpoint.category}} ${{endpoint.summary}}`.toLowerCase();
46+
const haystack = [
47+
endpoint.method,
48+
endpoint.path,
49+
endpoint.category,
50+
endpoint.summary
51+
].join(" ").toLowerCase();
4752
return haystack.includes({query_literal});
4853
}})
4954
.slice(0, 10)
@@ -57,18 +62,21 @@ def _explore_code(query: str) -> str:
5762
"""
5863

5964

60-
async def main() -> None:
61-
client = MCPClient(
65+
def _mcp_client(api_key: str) -> MCPClient:
66+
return MCPClient(
6267
name="xquik",
6368
is_stateful=False,
6469
enable_tools=["explore"],
6570
mcp_config=HttpMCPConfig(
6671
type="http_mcp",
67-
url=os.environ.get("XQUIK_MCP_URL", DEFAULT_MCP_URL),
68-
headers={"Authorization": f"Bearer {_required_api_key()}"},
72+
url=DEFAULT_MCP_URL,
73+
headers={"Authorization": f"Bearer {api_key}"},
6974
),
7075
)
7176

77+
78+
async def main() -> None:
79+
client = _mcp_client(_required_api_key())
7280
explore = await client.get_tool("explore")
7381
result = await explore(code=_explore_code(_query_from_args()))
7482
print(_extract_text(result))
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# -*- coding: utf-8 -*-
2+
"""Offline tests for the Xquik MCP discovery sample."""
3+
4+
import json
5+
import os
6+
import sys
7+
import unittest
8+
from unittest.mock import patch
9+
10+
from .main import (
11+
DEFAULT_MCP_URL,
12+
DEFAULT_QUERY,
13+
_explore_code,
14+
_extract_text,
15+
_mcp_client,
16+
_query_from_args,
17+
_required_api_key,
18+
)
19+
20+
21+
class XquikMCPDiscoveryTest(unittest.TestCase):
22+
def test_required_api_key_is_trimmed(self) -> None:
23+
with patch.dict(
24+
os.environ,
25+
{"XQUIK_API_KEY": " test-key "},
26+
clear=False,
27+
):
28+
self.assertEqual(_required_api_key(), "test-key")
29+
30+
def test_required_api_key_rejects_empty_value(self) -> None:
31+
with patch.dict(os.environ, {"XQUIK_API_KEY": ""}, clear=False):
32+
with self.assertRaisesRegex(SystemExit, "Set XQUIK_API_KEY"):
33+
_required_api_key()
34+
35+
def test_query_uses_arguments_or_default(self) -> None:
36+
with patch.object(sys, "argv", ["main.py", "Twitter", "Trends"]):
37+
self.assertEqual(_query_from_args(), "twitter trends")
38+
with patch.object(sys, "argv", ["main.py"]):
39+
self.assertEqual(_query_from_args(), DEFAULT_QUERY)
40+
41+
def test_explore_code_escapes_query_and_bounds_results(self) -> None:
42+
query = 'trends"); throw new Error("unexpected")'
43+
code = _explore_code(query)
44+
self.assertIn(json.dumps(query), code)
45+
self.assertIn(".slice(0, 10)", code)
46+
47+
def test_client_uses_fixed_origin_and_read_only_tool(self) -> None:
48+
client = _mcp_client("test-key")
49+
self.assertEqual(client.mcp_config.url, DEFAULT_MCP_URL)
50+
self.assertEqual(
51+
client.mcp_config.headers,
52+
{"Authorization": "Bearer test-key"},
53+
)
54+
self.assertEqual(client.enable_tools, ["explore"])
55+
self.assertFalse(client.is_stateful)
56+
57+
def test_extract_text_joins_text_and_fallback_content(self) -> None:
58+
text_item = type("TextItem", (), {"text": "first"})()
59+
fallback = type("Fallback", (), {"text": None})()
60+
result = type("Result", (), {"content": [text_item, fallback]})()
61+
self.assertEqual(
62+
_extract_text(result),
63+
f"first\n{fallback}",
64+
)
65+
66+
67+
if __name__ == "__main__":
68+
unittest.main()

0 commit comments

Comments
 (0)