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.
- ctxprotocol/__init__.py +148 -0
- ctxprotocol/auth/__init__.py +369 -0
- ctxprotocol/client/__init__.py +46 -0
- ctxprotocol/client/client.py +148 -0
- ctxprotocol/client/resources/__init__.py +11 -0
- ctxprotocol/client/resources/discovery.py +70 -0
- ctxprotocol/client/resources/tools.py +103 -0
- ctxprotocol/client/types.py +265 -0
- ctxprotocol/context/__init__.py +184 -0
- ctxprotocol/context/hyperliquid.py +252 -0
- ctxprotocol/context/polymarket.py +103 -0
- ctxprotocol/context/wallet.py +59 -0
- ctxprotocol/py.typed +0 -0
- ctxprotocol-0.5.5.dist-info/METADATA +326 -0
- ctxprotocol-0.5.5.dist-info/RECORD +16 -0
- ctxprotocol-0.5.5.dist-info/WHEEL +4 -0
ctxprotocol/__init__.py
ADDED
|
@@ -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
|
+
|