ctxprotocol 0.5.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,148 @@
1
+ """
2
+ ctxprotocol - Official Python SDK for the Context Protocol
3
+
4
+ The Universal Adapter for AI Agents.
5
+
6
+ Connect your AI to the real world without managing API keys, hosting servers,
7
+ or reading documentation.
8
+
9
+ Context Protocol is **pip for AI capabilities**. Just as you install packages
10
+ to add functionality to your code, use the Context SDK to give your Agent
11
+ instant access to thousands of live data sources and actions—from DeFi and
12
+ Gas Oracles to Weather and Search.
13
+
14
+ Example:
15
+ >>> from ctxprotocol import ContextClient
16
+ >>>
17
+ >>> async with ContextClient(api_key="sk_live_...") as client:
18
+ ... # Search for tools
19
+ ... tools = await client.discovery.search("gas price")
20
+ ...
21
+ ... # Execute a tool
22
+ ... result = await client.tools.execute(
23
+ ... tool_id=tools[0].id,
24
+ ... tool_name=tools[0].mcp_tools[0].name,
25
+ ... args={"chainId": 8453},
26
+ ... )
27
+ ... print(result.result)
28
+
29
+ For more information, visit: https://ctxprotocol.com
30
+ """
31
+
32
+ __version__ = "0.5.5"
33
+
34
+ # Re-export everything from client module
35
+ from ctxprotocol.client import (
36
+ ContextClient,
37
+ ContextError,
38
+ Discovery,
39
+ Tools,
40
+ )
41
+ from ctxprotocol.client.types import (
42
+ ContextClientOptions,
43
+ ContextErrorCode,
44
+ ExecuteApiErrorResponse,
45
+ ExecuteApiSuccessResponse,
46
+ ExecuteOptions,
47
+ ExecutionResult,
48
+ McpTool,
49
+ SearchOptions,
50
+ SearchResponse,
51
+ Tool,
52
+ ToolInfo,
53
+ )
54
+
55
+ # Context types for portfolio injection
56
+ from ctxprotocol.context import (
57
+ # Constants
58
+ CONTEXT_REQUIREMENTS_KEY,
59
+ # Type aliases
60
+ ContextRequirementType,
61
+ # Wallet types
62
+ WalletContext,
63
+ ERC20Context,
64
+ ERC20TokenBalance,
65
+ # Polymarket types
66
+ PolymarketContext,
67
+ PolymarketPosition,
68
+ PolymarketOrder,
69
+ # Hyperliquid types
70
+ HyperliquidContext,
71
+ HyperliquidPerpPosition,
72
+ HyperliquidOrder,
73
+ HyperliquidSpotBalance,
74
+ HyperliquidAccountSummary,
75
+ CrossMarginSummary,
76
+ LeverageInfo,
77
+ CumFunding,
78
+ # Composite types
79
+ UserContext,
80
+ ToolRequirements,
81
+ )
82
+
83
+ # Auth utilities for verifying platform requests
84
+ from ctxprotocol.auth import (
85
+ verify_context_request,
86
+ is_protected_mcp_method,
87
+ is_open_mcp_method,
88
+ create_context_middleware,
89
+ ContextMiddleware,
90
+ VerifyRequestOptions,
91
+ CreateContextMiddlewareOptions,
92
+ )
93
+
94
+ __all__ = [
95
+ # Version
96
+ "__version__",
97
+ # Client
98
+ "ContextClient",
99
+ "Discovery",
100
+ "Tools",
101
+ # Client types
102
+ "ContextClientOptions",
103
+ "Tool",
104
+ "McpTool",
105
+ "SearchResponse",
106
+ "SearchOptions",
107
+ "ExecuteOptions",
108
+ "ExecutionResult",
109
+ "ExecuteApiSuccessResponse",
110
+ "ExecuteApiErrorResponse",
111
+ "ToolInfo",
112
+ "ContextErrorCode",
113
+ # Errors
114
+ "ContextError",
115
+ # Context constants
116
+ "CONTEXT_REQUIREMENTS_KEY",
117
+ # Context type aliases
118
+ "ContextRequirementType",
119
+ # Wallet context types
120
+ "WalletContext",
121
+ "ERC20Context",
122
+ "ERC20TokenBalance",
123
+ # Polymarket context types
124
+ "PolymarketContext",
125
+ "PolymarketPosition",
126
+ "PolymarketOrder",
127
+ # Hyperliquid context types
128
+ "HyperliquidContext",
129
+ "HyperliquidPerpPosition",
130
+ "HyperliquidOrder",
131
+ "HyperliquidSpotBalance",
132
+ "HyperliquidAccountSummary",
133
+ "CrossMarginSummary",
134
+ "LeverageInfo",
135
+ "CumFunding",
136
+ # Composite context types
137
+ "UserContext",
138
+ "ToolRequirements",
139
+ # Auth utilities
140
+ "verify_context_request",
141
+ "is_protected_mcp_method",
142
+ "is_open_mcp_method",
143
+ "create_context_middleware",
144
+ "ContextMiddleware",
145
+ "VerifyRequestOptions",
146
+ "CreateContextMiddlewareOptions",
147
+ ]
148
+
@@ -0,0 +1,369 @@
1
+ """
2
+ Authentication utilities for verifying Context Protocol requests.
3
+
4
+ This module provides JWT verification and middleware for MCP server contributors
5
+ to secure their endpoints and verify that requests originate from the Context Protocol Platform.
6
+
7
+ Example:
8
+ >>> from ctxprotocol.auth import verify_context_request, is_protected_mcp_method
9
+ >>>
10
+ >>> # Check if a method requires auth
11
+ >>> if is_protected_mcp_method(body["method"]):
12
+ ... payload = await verify_context_request(
13
+ ... authorization_header=request.headers.get("authorization"),
14
+ ... audience="https://your-tool.com/mcp", # optional
15
+ ... )
16
+ ... # payload contains verified JWT claims
17
+ """
18
+
19
+ from typing import Any, Awaitable, Callable
20
+
21
+ import jwt
22
+ from cryptography.hazmat.primitives import serialization
23
+
24
+ from ctxprotocol.client.types import ContextError
25
+
26
+ # ============================================================================
27
+ # Configuration
28
+ # ============================================================================
29
+
30
+ # Official Context Protocol Platform Public Key (RS256)
31
+ CONTEXT_PLATFORM_PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
32
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAs9YOgdpkmVQ5aoNovjsu
33
+ chJdV54OT7dUdbVXz914a7Px8EwnpDqhsvG7WO8xL8sj2Rn6ueAJBk+04Hy/P/UN
34
+ RJyp23XL5TsGmb4rbfg0ii0MiL2nbVXuqvAe3JSM2BOFZR5bpwIVIaa8aonfamUy
35
+ VXGc7OosF90ThdKjm9cXlVM+kV6IgSWc1502X7M3abQqRcTU/rluVXnky0eiWDQa
36
+ lfOKbr7w0u72dZjiZPwnNDsX6PEEgvfmoautTFYTQgnZjDzq8UimTcv3KF+hJ5Ep
37
+ weipe6amt9lzQzi8WXaFKpOXHQs//WDlUytz/Hl8pvd5craZKzo6Kyrg1Vfan7H3
38
+ TQIDAQAB
39
+ -----END PUBLIC KEY-----"""
40
+
41
+ # MCP methods that require authentication
42
+ # - tools/call: Executes tool logic, may cost money
43
+ # - resources/read: Reads potentially sensitive data
44
+ # - prompts/get: Gets prompt content
45
+ PROTECTED_MCP_METHODS: frozenset[str] = frozenset([
46
+ "tools/call",
47
+ # Uncomment these if you want to protect resource/prompt access:
48
+ # "resources/read",
49
+ # "prompts/get",
50
+ ])
51
+
52
+ # MCP methods that are always open (no auth required)
53
+ # These are discovery/listing operations that return metadata only
54
+ OPEN_MCP_METHODS: frozenset[str] = frozenset([
55
+ "initialize",
56
+ "tools/list",
57
+ "resources/list",
58
+ "prompts/list",
59
+ "ping",
60
+ "notifications/initialized",
61
+ ])
62
+
63
+
64
+ # ============================================================================
65
+ # Method Classification
66
+ # ============================================================================
67
+
68
+
69
+ def is_protected_mcp_method(method: str) -> bool:
70
+ """Determines if a given MCP method requires authentication.
71
+
72
+ Discovery methods (tools/list, resources/list, etc.) are open.
73
+ Execution methods (tools/call) require authentication.
74
+
75
+ Args:
76
+ method: The MCP JSON-RPC method (e.g., "tools/list", "tools/call")
77
+
78
+ Returns:
79
+ True if the method requires authentication
80
+
81
+ Example:
82
+ >>> if is_protected_mcp_method(body["method"]):
83
+ ... await verify_context_request(
84
+ ... authorization_header=req.headers.get("authorization")
85
+ ... )
86
+ """
87
+ return method in PROTECTED_MCP_METHODS
88
+
89
+
90
+ def is_open_mcp_method(method: str) -> bool:
91
+ """Determines if a given MCP method is explicitly open (no auth).
92
+
93
+ Args:
94
+ method: The MCP JSON-RPC method
95
+
96
+ Returns:
97
+ True if the method is known to be open
98
+ """
99
+ return method in OPEN_MCP_METHODS
100
+
101
+
102
+ # ============================================================================
103
+ # Request Verification
104
+ # ============================================================================
105
+
106
+
107
+ class VerifyRequestOptions:
108
+ """Options for verifying a Context Protocol request.
109
+
110
+ Attributes:
111
+ authorization_header: The full Authorization header string (e.g., "Bearer eyJ...")
112
+ audience: Expected Audience (your tool URL) for stricter validation
113
+ """
114
+
115
+ def __init__(
116
+ self,
117
+ authorization_header: str | None = None,
118
+ audience: str | None = None,
119
+ ) -> None:
120
+ self.authorization_header = authorization_header
121
+ self.audience = audience
122
+
123
+
124
+ async def verify_context_request(
125
+ authorization_header: str | None = None,
126
+ audience: str | None = None,
127
+ ) -> dict[str, Any]:
128
+ """Verifies that an incoming request originated from the Context Protocol Platform.
129
+
130
+ Args:
131
+ authorization_header: The full Authorization header string (e.g., "Bearer eyJ...")
132
+ audience: Expected Audience (your tool URL) for stricter validation
133
+
134
+ Returns:
135
+ The decoded JWT payload if valid
136
+
137
+ Raises:
138
+ ContextError: If the authorization header is missing or invalid
139
+ ContextError: If the JWT signature verification fails
140
+
141
+ Example:
142
+ >>> payload = await verify_context_request(
143
+ ... authorization_header=request.headers.get("authorization"),
144
+ ... audience="https://your-tool.com/mcp",
145
+ ... )
146
+ >>> user_id = payload.get("sub")
147
+ """
148
+ if not authorization_header or not authorization_header.startswith("Bearer "):
149
+ raise ContextError(
150
+ message="Missing or invalid Authorization header",
151
+ code="unauthorized",
152
+ status_code=401,
153
+ )
154
+
155
+ token = authorization_header.split(" ", 1)[1]
156
+
157
+ try:
158
+ # Load the public key
159
+ public_key = serialization.load_pem_public_key(
160
+ CONTEXT_PLATFORM_PUBLIC_KEY_PEM.encode()
161
+ )
162
+
163
+ # Verify the JWT
164
+ payload = jwt.decode(
165
+ token,
166
+ public_key,
167
+ algorithms=["RS256"],
168
+ issuer="https://ctxprotocol.com",
169
+ audience=audience,
170
+ options={
171
+ "require": ["iss", "sub", "exp", "iat"],
172
+ "verify_aud": audience is not None,
173
+ },
174
+ )
175
+
176
+ return payload
177
+
178
+ except jwt.ExpiredSignatureError:
179
+ raise ContextError(
180
+ message="JWT has expired",
181
+ code="unauthorized",
182
+ status_code=401,
183
+ )
184
+ except jwt.InvalidAudienceError:
185
+ raise ContextError(
186
+ message="Invalid JWT audience",
187
+ code="unauthorized",
188
+ status_code=401,
189
+ )
190
+ except jwt.InvalidIssuerError:
191
+ raise ContextError(
192
+ message="Invalid JWT issuer",
193
+ code="unauthorized",
194
+ status_code=401,
195
+ )
196
+ except jwt.PyJWTError:
197
+ raise ContextError(
198
+ message="Invalid Context Protocol signature",
199
+ code="unauthorized",
200
+ status_code=401,
201
+ )
202
+
203
+
204
+ # ============================================================================
205
+ # FastAPI/Starlette Middleware
206
+ # ============================================================================
207
+
208
+
209
+ class CreateContextMiddlewareOptions:
210
+ """Options for creating Context middleware.
211
+
212
+ Attributes:
213
+ audience: Expected Audience (your tool URL) for stricter validation
214
+ """
215
+
216
+ def __init__(self, audience: str | None = None) -> None:
217
+ self.audience = audience
218
+
219
+
220
+ class ContextMiddleware:
221
+ """ASGI middleware that secures your MCP endpoint.
222
+
223
+ This middleware automatically:
224
+ - Allows discovery methods (tools/list, initialize) without authentication
225
+ - Requires and verifies JWT for execution methods (tools/call)
226
+ - Attaches the verified payload to request.state.context for downstream use
227
+
228
+ Example with FastAPI:
229
+ >>> from fastapi import FastAPI, Request
230
+ >>> from ctxprotocol.auth import ContextMiddleware
231
+ >>>
232
+ >>> app = FastAPI()
233
+ >>> app.add_middleware(ContextMiddleware)
234
+ >>>
235
+ >>> @app.post("/mcp")
236
+ >>> async def handle_mcp(request: Request):
237
+ ... # request.state.context contains verified JWT payload (on protected methods)
238
+ ... context = getattr(request.state, "context", None)
239
+ ... # Handle MCP request...
240
+
241
+ Example with Starlette:
242
+ >>> from starlette.applications import Starlette
243
+ >>> from starlette.middleware import Middleware
244
+ >>> from ctxprotocol.auth import ContextMiddleware
245
+ >>>
246
+ >>> app = Starlette(
247
+ ... middleware=[Middleware(ContextMiddleware, audience="https://your-tool.com/mcp")]
248
+ ... )
249
+ """
250
+
251
+ def __init__(
252
+ self,
253
+ app: Callable[[dict[str, Any], Callable[..., Awaitable[Any]], Callable[..., Awaitable[Any]]], Awaitable[Any]],
254
+ audience: str | None = None,
255
+ ) -> None:
256
+ """Initialize the middleware.
257
+
258
+ Args:
259
+ app: The ASGI application to wrap
260
+ audience: Expected Audience (your tool URL) for stricter validation
261
+ """
262
+ self.app = app
263
+ self.audience = audience
264
+
265
+ async def __call__(
266
+ self,
267
+ scope: dict[str, Any],
268
+ receive: Callable[..., Awaitable[Any]],
269
+ send: Callable[..., Awaitable[Any]],
270
+ ) -> None:
271
+ """Process the request."""
272
+ if scope["type"] != "http":
273
+ await self.app(scope, receive, send)
274
+ return
275
+
276
+ # We need to read the body to check the method
277
+ # This is a simplified version - in production you might want to
278
+ # use a more sophisticated approach to avoid reading the body twice
279
+ body_parts: list[bytes] = []
280
+
281
+ async def receive_wrapper() -> dict[str, Any]:
282
+ message = await receive()
283
+ if message["type"] == "http.request":
284
+ body_parts.append(message.get("body", b""))
285
+ return message
286
+
287
+ # For this middleware to work properly with body reading,
288
+ # we need a stateful request object. In FastAPI/Starlette,
289
+ # this is typically handled at the Request level.
290
+ # Here we just pass through and let the endpoint handle auth.
291
+ await self.app(scope, receive, send)
292
+
293
+
294
+ def create_context_middleware(
295
+ audience: str | None = None,
296
+ ) -> Callable[[Callable[..., Awaitable[Any]]], Callable[..., Awaitable[Any]]]:
297
+ """Creates a dependency function for FastAPI that verifies Context Protocol requests.
298
+
299
+ This is the recommended way to secure your FastAPI MCP endpoint.
300
+ It automatically:
301
+ - Allows discovery methods (tools/list, initialize) without authentication
302
+ - Requires and verifies JWT for execution methods (tools/call)
303
+ - Returns the verified payload for downstream use
304
+
305
+ Args:
306
+ audience: Expected Audience (your tool URL) for stricter validation
307
+
308
+ Returns:
309
+ A FastAPI dependency function
310
+
311
+ Example with FastAPI:
312
+ >>> from fastapi import FastAPI, Request, Depends
313
+ >>> from ctxprotocol.auth import create_context_middleware
314
+ >>>
315
+ >>> app = FastAPI()
316
+ >>> verify_context = create_context_middleware(audience="https://your-tool.com/mcp")
317
+ >>>
318
+ >>> @app.post("/mcp")
319
+ >>> async def handle_mcp(request: Request, context: dict = Depends(verify_context)):
320
+ ... # context contains verified JWT payload (on protected methods)
321
+ ... # None for open methods
322
+ ... ...
323
+ """
324
+
325
+ async def dependency(request: Any) -> dict[str, Any] | None:
326
+ """FastAPI dependency for Context Protocol authentication."""
327
+ # Try to get the body - this works with FastAPI's Request object
328
+ try:
329
+ body = await request.json()
330
+ except Exception:
331
+ # If we can't parse the body, let the endpoint handle it
332
+ return None
333
+
334
+ method = body.get("method", "")
335
+
336
+ # Allow discovery methods without authentication
337
+ if not method or not is_protected_mcp_method(method):
338
+ return None
339
+
340
+ # Protected method - require authentication
341
+ authorization = request.headers.get("authorization")
342
+
343
+ payload = await verify_context_request(
344
+ authorization_header=authorization,
345
+ audience=audience,
346
+ )
347
+
348
+ return payload
349
+
350
+ return dependency # type: ignore[return-value]
351
+
352
+
353
+ __all__ = [
354
+ # Constants
355
+ "CONTEXT_PLATFORM_PUBLIC_KEY_PEM",
356
+ "PROTECTED_MCP_METHODS",
357
+ "OPEN_MCP_METHODS",
358
+ # Method classification
359
+ "is_protected_mcp_method",
360
+ "is_open_mcp_method",
361
+ # Request verification
362
+ "VerifyRequestOptions",
363
+ "verify_context_request",
364
+ # Middleware
365
+ "CreateContextMiddlewareOptions",
366
+ "ContextMiddleware",
367
+ "create_context_middleware",
368
+ ]
369
+
@@ -0,0 +1,46 @@
1
+ """
2
+ ctxprotocol.client
3
+
4
+ Client module for AI Agents to query marketplace and execute tools.
5
+ """
6
+
7
+ from ctxprotocol.client.client import ContextClient
8
+ from ctxprotocol.client.resources.discovery import Discovery
9
+ from ctxprotocol.client.resources.tools import Tools
10
+ from ctxprotocol.client.types import (
11
+ ContextClientOptions,
12
+ ContextError,
13
+ ContextErrorCode,
14
+ ExecuteApiErrorResponse,
15
+ ExecuteApiSuccessResponse,
16
+ ExecuteOptions,
17
+ ExecutionResult,
18
+ McpTool,
19
+ SearchOptions,
20
+ SearchResponse,
21
+ Tool,
22
+ ToolInfo,
23
+ )
24
+
25
+ __all__ = [
26
+ # Main client
27
+ "ContextClient",
28
+ # Resources
29
+ "Discovery",
30
+ "Tools",
31
+ # Types
32
+ "ContextClientOptions",
33
+ "Tool",
34
+ "McpTool",
35
+ "SearchResponse",
36
+ "SearchOptions",
37
+ "ExecuteOptions",
38
+ "ExecutionResult",
39
+ "ExecuteApiSuccessResponse",
40
+ "ExecuteApiErrorResponse",
41
+ "ToolInfo",
42
+ "ContextErrorCode",
43
+ # Errors
44
+ "ContextError",
45
+ ]
46
+
@@ -0,0 +1,148 @@
1
+ """
2
+ The official Python client for the Context Protocol.
3
+
4
+ Use this client to discover and execute AI tools programmatically.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any
10
+
11
+ import httpx
12
+
13
+ from ctxprotocol.client.types import ContextError
14
+
15
+
16
+ class ContextClient:
17
+ """The official Python client for the Context Protocol.
18
+
19
+ Use this client to discover and execute AI tools programmatically.
20
+
21
+ Example:
22
+ >>> from ctxprotocol import ContextClient
23
+ >>>
24
+ >>> async with ContextClient(api_key="sk_live_...") as client:
25
+ ... # Discover tools
26
+ ... tools = await client.discovery.search("gas prices")
27
+ ...
28
+ ... # Execute a tool method
29
+ ... result = await client.tools.execute(
30
+ ... tool_id=tools[0].id,
31
+ ... tool_name=tools[0].mcp_tools[0].name,
32
+ ... args={"chainId": 1}
33
+ ... )
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ api_key: str,
39
+ base_url: str = "https://ctxprotocol.com",
40
+ ) -> None:
41
+ """Creates a new Context Protocol client.
42
+
43
+ Args:
44
+ api_key: Your Context Protocol API key (format: sk_live_...)
45
+ base_url: Optional base URL override (defaults to https://ctxprotocol.com)
46
+
47
+ Raises:
48
+ ContextError: If API key is not provided
49
+ """
50
+ if not api_key:
51
+ raise ContextError("API key is required")
52
+
53
+ self._api_key = api_key
54
+ self._base_url = base_url.rstrip("/")
55
+ self._http_client: httpx.AsyncClient | None = None
56
+
57
+ # Import here to avoid circular imports
58
+ from ctxprotocol.client.resources.discovery import Discovery
59
+ from ctxprotocol.client.resources.tools import Tools
60
+
61
+ # Initialize resources
62
+ self.discovery = Discovery(self)
63
+ self.tools = Tools(self)
64
+
65
+ @property
66
+ def _client(self) -> httpx.AsyncClient:
67
+ """Get or create the HTTP client."""
68
+ if self._http_client is None:
69
+ self._http_client = httpx.AsyncClient(
70
+ base_url=self._base_url,
71
+ headers={
72
+ "Content-Type": "application/json",
73
+ "Authorization": f"Bearer {self._api_key}",
74
+ },
75
+ timeout=httpx.Timeout(30.0),
76
+ )
77
+ return self._http_client
78
+
79
+ async def __aenter__(self) -> ContextClient:
80
+ """Enter async context manager."""
81
+ return self
82
+
83
+ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
84
+ """Exit async context manager and close HTTP client."""
85
+ await self.close()
86
+
87
+ async def close(self) -> None:
88
+ """Close the HTTP client and release resources."""
89
+ if self._http_client is not None:
90
+ await self._http_client.aclose()
91
+ self._http_client = None
92
+
93
+ async def fetch(
94
+ self,
95
+ endpoint: str,
96
+ method: str = "GET",
97
+ json_body: dict[str, Any] | None = None,
98
+ ) -> dict[str, Any]:
99
+ """Internal method for making authenticated HTTP requests.
100
+
101
+ All requests include the Authorization header with the API key.
102
+
103
+ Args:
104
+ endpoint: API endpoint path
105
+ method: HTTP method (GET, POST, etc.)
106
+ json_body: Optional JSON body for POST requests
107
+
108
+ Returns:
109
+ Parsed JSON response
110
+
111
+ Raises:
112
+ ContextError: If the request fails
113
+ """
114
+ try:
115
+ if method == "GET":
116
+ response = await self._client.get(endpoint)
117
+ elif method == "POST":
118
+ response = await self._client.post(endpoint, json=json_body)
119
+ else:
120
+ raise ContextError(f"Unsupported HTTP method: {method}")
121
+
122
+ if not response.is_success:
123
+ error_message = f"HTTP {response.status_code}: {response.reason_phrase}"
124
+ error_code: str | None = None
125
+ help_url: str | None = None
126
+
127
+ try:
128
+ error_body = response.json()
129
+ if "error" in error_body:
130
+ error_message = error_body["error"]
131
+ error_code = error_body.get("code")
132
+ help_url = error_body.get("helpUrl")
133
+ except Exception:
134
+ # Use default error message if JSON parsing fails
135
+ pass
136
+
137
+ raise ContextError(
138
+ message=error_message,
139
+ code=error_code,
140
+ status_code=response.status_code,
141
+ help_url=help_url,
142
+ )
143
+
144
+ return response.json()
145
+
146
+ except httpx.HTTPError as e:
147
+ raise ContextError(f"HTTP request failed: {e}") from e
148
+