ctxprotocol 0.5.6__tar.gz → 0.6.0__tar.gz

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.
Files changed (24) hide show
  1. {ctxprotocol-0.5.6 → ctxprotocol-0.6.0}/.gitignore +1 -0
  2. {ctxprotocol-0.5.6 → ctxprotocol-0.6.0}/PKG-INFO +32 -2
  3. {ctxprotocol-0.5.6 → ctxprotocol-0.6.0}/README.md +31 -1
  4. {ctxprotocol-0.5.6 → ctxprotocol-0.6.0}/ctxprotocol/__init__.py +45 -0
  5. {ctxprotocol-0.5.6 → ctxprotocol-0.6.0}/ctxprotocol/auth/__init__.py +111 -12
  6. {ctxprotocol-0.5.6 → ctxprotocol-0.6.0}/ctxprotocol/client/resources/discovery.py +2 -1
  7. {ctxprotocol-0.5.6 → ctxprotocol-0.6.0}/ctxprotocol/client/resources/tools.py +1 -1
  8. {ctxprotocol-0.5.6 → ctxprotocol-0.6.0}/ctxprotocol/client/types.py +31 -8
  9. {ctxprotocol-0.5.6 → ctxprotocol-0.6.0}/ctxprotocol/context/__init__.py +22 -8
  10. ctxprotocol-0.6.0/ctxprotocol/handshake/__init__.py +427 -0
  11. {ctxprotocol-0.5.6 → ctxprotocol-0.6.0}/examples/server/hummingbot-contributor/README.md +65 -23
  12. {ctxprotocol-0.5.6 → ctxprotocol-0.6.0}/examples/server/hummingbot-contributor/env.example +1 -0
  13. ctxprotocol-0.6.0/examples/server/hummingbot-contributor/requirements.txt +23 -0
  14. ctxprotocol-0.6.0/examples/server/hummingbot-contributor/server.py +769 -0
  15. {ctxprotocol-0.5.6 → ctxprotocol-0.6.0}/pyproject.toml +1 -1
  16. ctxprotocol-0.5.6/examples/server/hummingbot-contributor/requirements.txt +0 -20
  17. ctxprotocol-0.5.6/examples/server/hummingbot-contributor/server.py +0 -784
  18. {ctxprotocol-0.5.6 → ctxprotocol-0.6.0}/ctxprotocol/client/__init__.py +0 -0
  19. {ctxprotocol-0.5.6 → ctxprotocol-0.6.0}/ctxprotocol/client/client.py +0 -0
  20. {ctxprotocol-0.5.6 → ctxprotocol-0.6.0}/ctxprotocol/client/resources/__init__.py +0 -0
  21. {ctxprotocol-0.5.6 → ctxprotocol-0.6.0}/ctxprotocol/context/hyperliquid.py +0 -0
  22. {ctxprotocol-0.5.6 → ctxprotocol-0.6.0}/ctxprotocol/context/polymarket.py +0 -0
  23. {ctxprotocol-0.5.6 → ctxprotocol-0.6.0}/ctxprotocol/context/wallet.py +0 -0
  24. {ctxprotocol-0.5.6 → ctxprotocol-0.6.0}/ctxprotocol/py.typed +0 -0
@@ -48,3 +48,4 @@ htmlcov/
48
48
  # Shell scripts (deployment)
49
49
  *.sh
50
50
 
51
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ctxprotocol
3
- Version: 0.5.6
3
+ Version: 0.6.0
4
4
  Summary: Official Python SDK for the Context Protocol - Discover and execute AI tools programmatically
5
5
  Project-URL: Homepage, https://ctxprotocol.com
6
6
  Project-URL: Documentation, https://docs.ctxprotocol.com
@@ -48,12 +48,26 @@ Context Protocol is **pip for AI capabilities**. Just as you install packages to
48
48
  [![Python versions](https://img.shields.io/pypi/pyversions/ctxprotocol.svg)](https://pypi.org/project/ctxprotocol/)
49
49
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
50
50
 
51
+ ---
52
+
53
+ ### 💰 $10,000 Developer Grant Program
54
+
55
+ We're funding the initial supply of MCP Tools for the Context Marketplace. **Become a Data Broker.**
56
+
57
+ - **🛠️ Build:** Create an MCP Server using this SDK (Solana data, Trading tools, Scrapers, etc.)
58
+ - **📦 List:** Publish it to the Context Registry
59
+ - **💵 Earn:** Get a **$250–$1,000 Grant** + earn USDC every time an agent queries your tool
60
+
61
+ 👉 [**View Open Bounties & Apply Here**](https://docs.ctxprotocol.com/grants)
62
+
63
+ ---
64
+
51
65
  ## Why use Context?
52
66
 
53
67
  - **🔌 One Interface, Everything:** Stop integrating APIs one by one. Use a single SDK to access any tool in the marketplace.
54
68
  - **🧠 Zero-Ops:** We're a gateway to the best MCP tools. Just send the JSON and get the result.
55
69
  - **⚡️ Agentic Discovery:** Your Agent can search the marketplace at runtime to find tools it didn't know it needed.
56
- - **💸 Micro-Billing:** Pay only for what you use (e.g., $0.001/query). No monthly subscriptions for tools you rarely use.
70
+ - **💸 Pay-Per-Response:** The $500/year subscription? Now $0.01/response. No monthly fees, just results.
57
71
 
58
72
  ## Who Is This SDK For?
59
73
 
@@ -282,6 +296,22 @@ The SDK implements a **selective authentication** model — discovery is open, e
282
296
 
283
297
  This matches standard API patterns (OpenAPI schemas are public, GraphQL introspection is open).
284
298
 
299
+ ## Execution Timeout & Product Design
300
+
301
+ ⚠️ **Important**: MCP tool execution has a **~60 second timeout** (enforced at the platform/client level, not by MCP itself). This is intentional—it encourages building pre-computed insight products rather than raw data access.
302
+
303
+ **Best practice**: Run heavy queries offline (via cron jobs), store results in your database, and serve instant results via MCP. This is how Bloomberg, Nansen, and Arkham work.
304
+
305
+ ```python
306
+ # ❌ BAD: Raw access (timeout-prone, no moat)
307
+ {"name": "run_sql", "description": "Run any SQL against blockchain data"}
308
+
309
+ # ✅ GOOD: Pre-computed product (instant, defensible)
310
+ {"name": "get_smart_money_wallets", "description": "Top 100 wallets that timed market tops"}
311
+ ```
312
+
313
+ See the [full documentation](https://docs.ctxprotocol.com/guides/build-tools#execution-limits--product-design) for detailed guidance.
314
+
285
315
  ## Context Injection (Personalized Tools)
286
316
 
287
317
  For tools that analyze user data, Context automatically injects user context:
@@ -10,12 +10,26 @@ Context Protocol is **pip for AI capabilities**. Just as you install packages to
10
10
  [![Python versions](https://img.shields.io/pypi/pyversions/ctxprotocol.svg)](https://pypi.org/project/ctxprotocol/)
11
11
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
12
12
 
13
+ ---
14
+
15
+ ### 💰 $10,000 Developer Grant Program
16
+
17
+ We're funding the initial supply of MCP Tools for the Context Marketplace. **Become a Data Broker.**
18
+
19
+ - **🛠️ Build:** Create an MCP Server using this SDK (Solana data, Trading tools, Scrapers, etc.)
20
+ - **📦 List:** Publish it to the Context Registry
21
+ - **💵 Earn:** Get a **$250–$1,000 Grant** + earn USDC every time an agent queries your tool
22
+
23
+ 👉 [**View Open Bounties & Apply Here**](https://docs.ctxprotocol.com/grants)
24
+
25
+ ---
26
+
13
27
  ## Why use Context?
14
28
 
15
29
  - **🔌 One Interface, Everything:** Stop integrating APIs one by one. Use a single SDK to access any tool in the marketplace.
16
30
  - **🧠 Zero-Ops:** We're a gateway to the best MCP tools. Just send the JSON and get the result.
17
31
  - **⚡️ Agentic Discovery:** Your Agent can search the marketplace at runtime to find tools it didn't know it needed.
18
- - **💸 Micro-Billing:** Pay only for what you use (e.g., $0.001/query). No monthly subscriptions for tools you rarely use.
32
+ - **💸 Pay-Per-Response:** The $500/year subscription? Now $0.01/response. No monthly fees, just results.
19
33
 
20
34
  ## Who Is This SDK For?
21
35
 
@@ -244,6 +258,22 @@ The SDK implements a **selective authentication** model — discovery is open, e
244
258
 
245
259
  This matches standard API patterns (OpenAPI schemas are public, GraphQL introspection is open).
246
260
 
261
+ ## Execution Timeout & Product Design
262
+
263
+ ⚠️ **Important**: MCP tool execution has a **~60 second timeout** (enforced at the platform/client level, not by MCP itself). This is intentional—it encourages building pre-computed insight products rather than raw data access.
264
+
265
+ **Best practice**: Run heavy queries offline (via cron jobs), store results in your database, and serve instant results via MCP. This is how Bloomberg, Nansen, and Arkham work.
266
+
267
+ ```python
268
+ # ❌ BAD: Raw access (timeout-prone, no moat)
269
+ {"name": "run_sql", "description": "Run any SQL against blockchain data"}
270
+
271
+ # ✅ GOOD: Pre-computed product (instant, defensible)
272
+ {"name": "get_smart_money_wallets", "description": "Top 100 wallets that timed market tops"}
273
+ ```
274
+
275
+ See the [full documentation](https://docs.ctxprotocol.com/guides/build-tools#execution-limits--product-design) for detailed guidance.
276
+
247
277
  ## Context Injection (Personalized Tools)
248
278
 
249
279
  For tools that analyze user data, Context automatically injects user context:
@@ -91,6 +91,31 @@ from ctxprotocol.auth import (
91
91
  CreateContextMiddlewareOptions,
92
92
  )
93
93
 
94
+ # Handshake types and helpers for tools that need user interaction
95
+ # (signatures, transactions, OAuth)
96
+ from ctxprotocol.handshake import (
97
+ # Types
98
+ HandshakeMeta,
99
+ EIP712Domain,
100
+ EIP712TypeField,
101
+ SignatureRequest,
102
+ TransactionProposalMeta,
103
+ TransactionProposal,
104
+ AuthRequiredMeta,
105
+ AuthRequired,
106
+ HandshakeAction,
107
+ # Type guards
108
+ is_handshake_action,
109
+ is_signature_request,
110
+ is_transaction_proposal,
111
+ is_auth_required,
112
+ # Helper functions
113
+ create_signature_request,
114
+ create_transaction_proposal,
115
+ create_auth_required,
116
+ wrap_handshake_response,
117
+ )
118
+
94
119
  __all__ = [
95
120
  # Version
96
121
  "__version__",
@@ -144,5 +169,25 @@ __all__ = [
144
169
  "ContextMiddleware",
145
170
  "VerifyRequestOptions",
146
171
  "CreateContextMiddlewareOptions",
172
+ # Handshake types
173
+ "HandshakeMeta",
174
+ "EIP712Domain",
175
+ "EIP712TypeField",
176
+ "SignatureRequest",
177
+ "TransactionProposalMeta",
178
+ "TransactionProposal",
179
+ "AuthRequiredMeta",
180
+ "AuthRequired",
181
+ "HandshakeAction",
182
+ # Handshake type guards
183
+ "is_handshake_action",
184
+ "is_signature_request",
185
+ "is_transaction_proposal",
186
+ "is_auth_required",
187
+ # Handshake helper functions
188
+ "create_signature_request",
189
+ "create_transaction_proposal",
190
+ "create_auth_required",
191
+ "wrap_handshake_response",
147
192
  ]
148
193
 
@@ -38,6 +38,54 @@ weipe6amt9lzQzi8WXaFKpOXHQs//WDlUytz/Hl8pvd5craZKzo6Kyrg1Vfan7H3
38
38
  TQIDAQAB
39
39
  -----END PUBLIC KEY-----"""
40
40
 
41
+ # ============================================================================
42
+ # JWKS Key Fetching (with hardcoded fallback)
43
+ # ============================================================================
44
+
45
+ JWKS_URL = "https://ctxprotocol.com/.well-known/jwks.json"
46
+ _KEY_CACHE_TTL_SECONDS = 3600 # 1 hour
47
+
48
+ _cached_public_key: Any = None
49
+ _cache_timestamp: float = 0
50
+
51
+
52
+ async def _get_platform_public_key() -> Any:
53
+ """Get the platform public key, trying JWKS endpoint first with hardcoded fallback.
54
+
55
+ Caches the result for 1 hour.
56
+ """
57
+ import time
58
+ global _cached_public_key, _cache_timestamp
59
+
60
+ now = time.time()
61
+
62
+ # Return cached key if still valid
63
+ if _cached_public_key is not None and now - _cache_timestamp < _KEY_CACHE_TTL_SECONDS:
64
+ return _cached_public_key
65
+
66
+ # Try JWKS endpoint first
67
+ try:
68
+ import httpx
69
+ async with httpx.AsyncClient(timeout=5.0) as client:
70
+ response = await client.get(JWKS_URL)
71
+ if response.is_success:
72
+ jwks = response.json()
73
+ if jwks.get("keys") and len(jwks["keys"]) > 0:
74
+ # For now, use hardcoded key (JWKS parsing requires additional logic)
75
+ # This establishes the pattern for when the endpoint is deployed
76
+ pass
77
+ except Exception:
78
+ # JWKS fetch failed - fall back to hardcoded key
79
+ pass
80
+
81
+ # Fallback: use hardcoded key
82
+ _cached_public_key = serialization.load_pem_public_key(
83
+ CONTEXT_PLATFORM_PUBLIC_KEY_PEM.encode()
84
+ )
85
+ _cache_timestamp = now
86
+ return _cached_public_key
87
+
88
+
41
89
  # MCP methods that require authentication
42
90
  # - tools/call: Executes tool logic, may cost money
43
91
  # - resources/read: Reads potentially sensitive data
@@ -155,10 +203,8 @@ async def verify_context_request(
155
203
  token = authorization_header.split(" ", 1)[1]
156
204
 
157
205
  try:
158
- # Load the public key
159
- public_key = serialization.load_pem_public_key(
160
- CONTEXT_PLATFORM_PUBLIC_KEY_PEM.encode()
161
- )
206
+ # Load the public key (tries JWKS endpoint first, falls back to hardcoded)
207
+ public_key = await _get_platform_public_key()
162
208
 
163
209
  # Build decode options - match TypeScript SDK behavior
164
210
  decode_options: dict[str, Any] = {
@@ -300,22 +346,74 @@ class ContextMiddleware:
300
346
  await self.app(scope, receive, send)
301
347
  return
302
348
 
303
- # We need to read the body to check the method
304
- # This is a simplified version - in production you might want to
305
- # use a more sophisticated approach to avoid reading the body twice
349
+ # Buffer the request body to inspect the MCP method
306
350
  body_parts: list[bytes] = []
351
+ body_complete = False
307
352
 
308
353
  async def receive_wrapper() -> dict[str, Any]:
354
+ nonlocal body_complete
355
+ if body_parts and body_complete:
356
+ # Replay the buffered body
357
+ body = b"".join(body_parts)
358
+ return {"type": "http.request", "body": body, "more_body": False}
359
+
309
360
  message = await receive()
310
361
  if message["type"] == "http.request":
311
362
  body_parts.append(message.get("body", b""))
363
+ if not message.get("more_body", False):
364
+ body_complete = True
312
365
  return message
313
366
 
314
- # For this middleware to work properly with body reading,
315
- # we need a stateful request object. In FastAPI/Starlette,
316
- # this is typically handled at the Request level.
317
- # Here we just pass through and let the endpoint handle auth.
318
- await self.app(scope, receive, send)
367
+ # Read the body to check the method
368
+ while not body_complete:
369
+ await receive_wrapper()
370
+
371
+ # Parse the body to get the MCP method
372
+ try:
373
+ import json
374
+ body_bytes = b"".join(body_parts)
375
+ body_json = json.loads(body_bytes)
376
+ method = body_json.get("method", "")
377
+ except Exception:
378
+ # Can't parse body - let the endpoint handle it
379
+ await self.app(scope, receive_wrapper, send)
380
+ return
381
+
382
+ # Allow discovery methods without authentication
383
+ if not method or not is_protected_mcp_method(method):
384
+ await self.app(scope, receive_wrapper, send)
385
+ return
386
+
387
+ # Protected method - require authentication
388
+ # Extract Authorization header from ASGI scope
389
+ headers = dict(scope.get("headers", []))
390
+ auth_header = headers.get(b"authorization", b"").decode("utf-8")
391
+
392
+ try:
393
+ payload = await verify_context_request(
394
+ authorization_header=auth_header if auth_header else None,
395
+ audience=self.audience,
396
+ )
397
+ # Attach payload to scope state for downstream use
398
+ if "state" not in scope:
399
+ scope["state"] = {}
400
+ scope["state"]["context"] = payload
401
+ await self.app(scope, receive_wrapper, send)
402
+ except ContextError as e:
403
+ # Return 401 JSON response
404
+ import json
405
+ response_body = json.dumps({"error": str(e)}).encode("utf-8")
406
+ await send({
407
+ "type": "http.response.start",
408
+ "status": e.status_code or 401,
409
+ "headers": [
410
+ [b"content-type", b"application/json"],
411
+ ],
412
+ })
413
+ await send({
414
+ "type": "http.response.body",
415
+ "body": response_body,
416
+ })
319
417
 
320
418
 
321
419
  def create_context_middleware(
@@ -380,6 +478,7 @@ def create_context_middleware(
380
478
  __all__ = [
381
479
  # Constants
382
480
  "CONTEXT_PLATFORM_PUBLIC_KEY_PEM",
481
+ "JWKS_URL",
383
482
  "PROTECTED_MCP_METHODS",
384
483
  "OPEN_MCP_METHODS",
385
484
  # Method classification
@@ -46,7 +46,8 @@ class Discovery:
46
46
  if limit is not None:
47
47
  params["limit"] = str(limit)
48
48
 
49
- query_string = "&".join(f"{k}={v}" for k, v in params.items())
49
+ from urllib.parse import urlencode
50
+ query_string = urlencode(params) if params else ""
50
51
  endpoint = f"/api/v1/tools/search{'?' + query_string if query_string else ''}"
51
52
 
52
53
  response = await self._client.fetch(endpoint)
@@ -82,7 +82,7 @@ class Tools:
82
82
  raise ContextError(
83
83
  message=error_response.error,
84
84
  code=error_response.code,
85
- status_code=400,
85
+ status_code=None, # Don't hardcode - this was a 200 OK with error body
86
86
  help_url=error_response.help_url,
87
87
  )
88
88
 
@@ -60,9 +60,13 @@ class Tool(BaseModel):
60
60
  price: Price per execution in USDC
61
61
  category: Tool category (e.g., "defi", "nft")
62
62
  is_verified: Whether the tool is verified by Context Protocol
63
+ kind: Tool type - currently always "mcp"
63
64
  mcp_tools: Available MCP tool methods
64
- created_at: Creation timestamp
65
- updated_at: Last update timestamp
65
+ total_queries: Total number of queries processed
66
+ success_rate: Success rate percentage (0-100)
67
+ uptime_percent: Uptime percentage (0-100)
68
+ total_staked: Total USDC staked by the developer
69
+ is_proven: Whether the tool has "Proven" status
66
70
  """
67
71
 
68
72
  id: str = Field(..., description="Unique identifier for the tool (UUID)")
@@ -75,20 +79,39 @@ class Tool(BaseModel):
75
79
  alias="isVerified",
76
80
  description="Whether the tool is verified by Context Protocol",
77
81
  )
82
+ kind: str | None = Field(
83
+ default=None,
84
+ description="Tool type - currently always 'mcp'",
85
+ )
78
86
  mcp_tools: list[McpTool] | None = Field(
79
87
  default=None,
80
88
  alias="mcpTools",
81
89
  description="Available MCP tool methods",
82
90
  )
83
- created_at: str | None = Field(
91
+ total_queries: int | None = Field(
92
+ default=None,
93
+ alias="totalQueries",
94
+ description="Total number of queries processed",
95
+ )
96
+ success_rate: str | None = Field(
97
+ default=None,
98
+ alias="successRate",
99
+ description="Success rate percentage",
100
+ )
101
+ uptime_percent: str | None = Field(
102
+ default=None,
103
+ alias="uptimePercent",
104
+ description="Uptime percentage",
105
+ )
106
+ total_staked: str | None = Field(
84
107
  default=None,
85
- alias="createdAt",
86
- description="Creation timestamp",
108
+ alias="totalStaked",
109
+ description="Total USDC staked by developer",
87
110
  )
88
- updated_at: str | None = Field(
111
+ is_proven: bool | None = Field(
89
112
  default=None,
90
- alias="updatedAt",
91
- description="Last update timestamp",
113
+ alias="isProven",
114
+ description="Whether tool has Proven status",
92
115
  )
93
116
 
94
117
  model_config = {"populate_by_name": True}
@@ -69,24 +69,37 @@ from ctxprotocol.context.hyperliquid import (
69
69
 
70
70
  CONTEXT_REQUIREMENTS_KEY = "x-context-requirements"
71
71
  """
72
- JSON Schema extension key for declaring context requirements.
72
+ DEPRECATED: Use _meta.contextRequirements instead.
73
73
 
74
- WHY THIS APPROACH?
75
- - MCP protocol only transmits: name, description, inputSchema, outputSchema
76
- - Custom fields like `requirements` get stripped by MCP SDK during transport
77
- - JSON Schema allows custom "x-" prefixed extension properties
78
- - inputSchema is preserved end-to-end through MCP transport
74
+ The primary mechanism for declaring context requirements is via the MCP _meta field
75
+ at the tool level, which is preserved by the MCP SDK through transport:
79
76
 
80
- Example:
81
77
  >>> tool = {
82
78
  ... "name": "analyze_my_positions",
79
+ ... "_meta": {"contextRequirements": ["hyperliquid"]},
83
80
  ... "inputSchema": {
84
81
  ... "type": "object",
85
- ... CONTEXT_REQUIREMENTS_KEY: ["hyperliquid"],
86
82
  ... "properties": {"portfolio": {"type": "object"}},
87
83
  ... "required": ["portfolio"]
88
84
  ... }
89
85
  ... }
86
+
87
+ This constant is kept for backwards compatibility but _meta.contextRequirements
88
+ is what the Context Platform reads. The x-context-requirements inputSchema extension
89
+ may be stripped by some MCP transports.
90
+ """
91
+
92
+ META_CONTEXT_REQUIREMENTS_KEY = "contextRequirements"
93
+ """
94
+ The key used inside _meta to declare context requirements.
95
+ This is the primary mechanism - the Context Platform reads _meta.contextRequirements.
96
+
97
+ Example:
98
+ >>> tool = {
99
+ ... "name": "analyze_positions",
100
+ ... "_meta": {META_CONTEXT_REQUIREMENTS_KEY: ["hyperliquid"]},
101
+ ... "inputSchema": {...}
102
+ ... }
90
103
  """
91
104
 
92
105
  # Context requirement types supported by the Context marketplace
@@ -158,6 +171,7 @@ class UserContext(BaseModel):
158
171
  __all__ = [
159
172
  # Constants
160
173
  "CONTEXT_REQUIREMENTS_KEY",
174
+ "META_CONTEXT_REQUIREMENTS_KEY",
161
175
  # Type aliases
162
176
  "ContextRequirementType",
163
177
  # Wallet types