feat: scripted MCP client (auth_headers + connect/list/call); offline tests pass
This commit is contained in:
@@ -0,0 +1,39 @@
|
|||||||
|
"""Minimal scripted MCP client for the Arcade eval.
|
||||||
|
|
||||||
|
Headless auth (confirmed from Arcade docs, see ../LIVE-POC.md):
|
||||||
|
Authorization: Bearer <ARCADE_API_KEY>
|
||||||
|
Arcade-User-ID: <user_id> (any stable string; an email works)
|
||||||
|
|
||||||
|
`auth_headers` is pure (no deps) so it unit-tests offline. The `mcp` SDK is imported
|
||||||
|
lazily inside the async helpers so this module loads even before deps are installed.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|
||||||
|
def auth_headers(api_key: str, user_id: str) -> dict[str, str]:
|
||||||
|
"""Build the headless MCP auth headers for a given Arcade user_id."""
|
||||||
|
return {"Authorization": f"Bearer {api_key}", "Arcade-User-ID": user_id}
|
||||||
|
|
||||||
|
|
||||||
|
async def connect_and_list(mcp_url: str, headers: dict[str, str]) -> list[dict]:
|
||||||
|
"""Connect to an MCP gateway over streamable HTTP and return the tool list."""
|
||||||
|
from mcp import ClientSession
|
||||||
|
from mcp.client.streamable_http import streamablehttp_client
|
||||||
|
|
||||||
|
async with streamablehttp_client(mcp_url, headers=headers) as (read, write, _):
|
||||||
|
async with ClientSession(read, write) as session:
|
||||||
|
await session.initialize()
|
||||||
|
result = await session.list_tools()
|
||||||
|
return [t.model_dump() for t in result.tools]
|
||||||
|
|
||||||
|
|
||||||
|
async def call_tool(mcp_url: str, headers: dict[str, str], name: str, args: dict) -> dict:
|
||||||
|
"""Invoke a single tool through an MCP gateway and return the result payload."""
|
||||||
|
from mcp import ClientSession
|
||||||
|
from mcp.client.streamable_http import streamablehttp_client
|
||||||
|
|
||||||
|
async with streamablehttp_client(mcp_url, headers=headers) as (read, write, _):
|
||||||
|
async with ClientSession(read, write) as session:
|
||||||
|
await session.initialize()
|
||||||
|
result = await session.call_tool(name, args)
|
||||||
|
return result.model_dump()
|
||||||
@@ -10,3 +10,4 @@ dev = ["pytest>=8.0"]
|
|||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
|
pythonpath = ["."]
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
from lib.mcp_client import auth_headers
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_headers_sets_key_and_user():
|
||||||
|
h = auth_headers("k_test", "user-a@servicetitan.com")
|
||||||
|
assert h["Authorization"] == "Bearer k_test"
|
||||||
|
assert h["Arcade-User-ID"] == "user-a@servicetitan.com"
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_headers_distinct_users():
|
||||||
|
a = auth_headers("k", "alice")
|
||||||
|
b = auth_headers("k", "bob")
|
||||||
|
assert a["Arcade-User-ID"] != b["Arcade-User-ID"]
|
||||||
Reference in New Issue
Block a user