mcp_client.py 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. from __future__ import annotations
  2. import json
  3. import urllib.error
  4. import urllib.request
  5. from typing import Any
  6. from .models import McpToolDescriptor, McpToolResult
  7. class McpClient:
  8. def __init__(self, endpoint: str, token: str = "") -> None:
  9. self.endpoint = endpoint
  10. self.token = token
  11. self._request_id = 1
  12. def status(self) -> Any:
  13. request = urllib.request.Request(self.endpoint, headers=self._headers(), method="GET")
  14. return self._send(request)
  15. def list_tools(self) -> list[McpToolDescriptor]:
  16. result = self._rpc("tools/list", {})
  17. tools = result.get("tools") if isinstance(result, dict) else None
  18. return tools if isinstance(tools, list) else []
  19. def call_tool(self, name: str, arguments: dict[str, Any]) -> McpToolResult:
  20. return self._rpc("tools/call", {"name": name, "arguments": arguments})
  21. def _rpc(self, method: str, params: dict[str, Any]) -> Any:
  22. payload = {
  23. "jsonrpc": "2.0",
  24. "id": self._request_id,
  25. "method": method,
  26. "params": params,
  27. }
  28. self._request_id += 1
  29. data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
  30. request = urllib.request.Request(self.endpoint, data=data, headers=self._headers(), method="POST")
  31. response = self._send(request)
  32. if not isinstance(response, dict):
  33. raise RuntimeError("MCP returned invalid JSON-RPC payload")
  34. if response.get("error"):
  35. error = response["error"]
  36. if isinstance(error, dict):
  37. raise RuntimeError(str(error.get("message") or "MCP call failed"))
  38. raise RuntimeError(str(error))
  39. if "result" not in response:
  40. raise RuntimeError("MCP returned no result")
  41. return response["result"]
  42. def _send(self, request: urllib.request.Request) -> Any:
  43. try:
  44. with urllib.request.urlopen(request, timeout=45) as response:
  45. raw = response.read().decode("utf-8")
  46. return json.loads(raw) if raw else {}
  47. except urllib.error.HTTPError as error:
  48. raw = error.read().decode("utf-8", errors="replace")
  49. try:
  50. payload = json.loads(raw)
  51. except json.JSONDecodeError:
  52. payload = {}
  53. message = ""
  54. if isinstance(payload, dict):
  55. rpc_error = payload.get("error")
  56. if isinstance(rpc_error, dict):
  57. message = str(rpc_error.get("message") or "")
  58. message = message or str(payload.get("message") or "")
  59. raise RuntimeError(message or f"MCP HTTP {error.code}") from error
  60. except urllib.error.URLError as error:
  61. raise RuntimeError(f"MCP connection failed: {error.reason}") from error
  62. def _headers(self) -> dict[str, str]:
  63. headers = {"Content-Type": "application/json"}
  64. token = self.token.strip()
  65. if token:
  66. headers["Authorization"] = f"Bearer {token}"
  67. headers["X-MCP-Token"] = token
  68. return headers