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,11 @@
1
+ """
2
+ ctxprotocol.client.resources
3
+
4
+ Resource modules for the Context Protocol client.
5
+ """
6
+
7
+ from ctxprotocol.client.resources.discovery import Discovery
8
+ from ctxprotocol.client.resources.tools import Tools
9
+
10
+ __all__ = ["Discovery", "Tools"]
11
+
@@ -0,0 +1,70 @@
1
+ """
2
+ Discovery resource for searching and finding tools on the Context Protocol marketplace.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import TYPE_CHECKING
8
+
9
+ from ctxprotocol.client.types import SearchResponse, Tool
10
+
11
+ if TYPE_CHECKING:
12
+ from ctxprotocol.client.client import ContextClient
13
+
14
+
15
+ class Discovery:
16
+ """Discovery resource for searching and finding tools on the Context Protocol marketplace."""
17
+
18
+ def __init__(self, client: ContextClient) -> None:
19
+ """Initialize the Discovery resource.
20
+
21
+ Args:
22
+ client: The parent ContextClient instance
23
+ """
24
+ self._client = client
25
+
26
+ async def search(self, query: str, limit: int | None = None) -> list[Tool]:
27
+ """Search for tools matching a query string.
28
+
29
+ Args:
30
+ query: The search query (e.g., "gas prices", "nft metadata")
31
+ limit: Maximum number of results (1-50, default 10)
32
+
33
+ Returns:
34
+ Array of matching tools
35
+
36
+ Example:
37
+ >>> tools = await client.discovery.search("gas prices")
38
+ >>> print(tools[0].name) # "Gas Price Oracle"
39
+ >>> print(tools[0].mcp_tools) # Available methods
40
+ """
41
+ params: dict[str, str] = {}
42
+
43
+ if query:
44
+ params["q"] = query
45
+
46
+ if limit is not None:
47
+ params["limit"] = str(limit)
48
+
49
+ query_string = "&".join(f"{k}={v}" for k, v in params.items())
50
+ endpoint = f"/api/v1/tools/search{'?' + query_string if query_string else ''}"
51
+
52
+ response = await self._client.fetch(endpoint)
53
+ search_response = SearchResponse.model_validate(response)
54
+
55
+ return search_response.tools
56
+
57
+ async def get_featured(self, limit: int | None = None) -> list[Tool]:
58
+ """Get featured/popular tools (empty query search).
59
+
60
+ Args:
61
+ limit: Maximum number of results (1-50, default 10)
62
+
63
+ Returns:
64
+ Array of featured tools
65
+
66
+ Example:
67
+ >>> featured = await client.discovery.get_featured(5)
68
+ """
69
+ return await self.search("", limit)
70
+
@@ -0,0 +1,103 @@
1
+ """
2
+ Tools resource for executing tools on the Context Protocol marketplace.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ from ctxprotocol.client.types import (
10
+ ContextError,
11
+ ExecuteApiErrorResponse,
12
+ ExecuteApiSuccessResponse,
13
+ ExecutionResult,
14
+ ToolInfo,
15
+ )
16
+
17
+ if TYPE_CHECKING:
18
+ from ctxprotocol.client.client import ContextClient
19
+
20
+
21
+ class Tools:
22
+ """Tools resource for executing tools on the Context Protocol marketplace."""
23
+
24
+ def __init__(self, client: ContextClient) -> None:
25
+ """Initialize the Tools resource.
26
+
27
+ Args:
28
+ client: The parent ContextClient instance
29
+ """
30
+ self._client = client
31
+
32
+ async def execute(
33
+ self,
34
+ tool_id: str,
35
+ tool_name: str,
36
+ args: dict[str, Any] | None = None,
37
+ ) -> ExecutionResult:
38
+ """Execute a tool with the provided arguments.
39
+
40
+ Args:
41
+ tool_id: The UUID of the tool (from search results)
42
+ tool_name: The specific MCP tool method to call (from tool's mcp_tools array)
43
+ args: Arguments to pass to the tool
44
+
45
+ Returns:
46
+ The execution result with the tool's output data
47
+
48
+ Raises:
49
+ ContextError: With code `no_wallet` if wallet not set up
50
+ ContextError: With code `insufficient_allowance` if Auto Pay not enabled
51
+ ContextError: With code `payment_failed` if on-chain payment fails
52
+ ContextError: With code `execution_failed` if tool execution fails
53
+
54
+ Example:
55
+ >>> # First, search for a tool
56
+ >>> tools = await client.discovery.search("gas prices")
57
+ >>> tool = tools[0]
58
+ >>>
59
+ >>> # Execute a specific method from the tool's mcp_tools
60
+ >>> result = await client.tools.execute(
61
+ ... tool_id=tool.id,
62
+ ... tool_name=tool.mcp_tools[0].name, # e.g., "get_gas_prices"
63
+ ... args={"chainId": 1}
64
+ ... )
65
+ >>>
66
+ >>> print(result.result) # The tool's output
67
+ >>> print(result.duration_ms) # Execution time
68
+ """
69
+ response = await self._client.fetch(
70
+ "/api/v1/tools/execute",
71
+ method="POST",
72
+ json_body={
73
+ "toolId": tool_id,
74
+ "toolName": tool_name,
75
+ "args": args or {},
76
+ },
77
+ )
78
+
79
+ # Handle error response
80
+ if "error" in response:
81
+ error_response = ExecuteApiErrorResponse.model_validate(response)
82
+ raise ContextError(
83
+ message=error_response.error,
84
+ code=error_response.code,
85
+ status_code=400,
86
+ help_url=error_response.help_url,
87
+ )
88
+
89
+ # Handle success response
90
+ if response.get("success"):
91
+ success_response = ExecuteApiSuccessResponse.model_validate(response)
92
+ return ExecutionResult(
93
+ result=success_response.result,
94
+ tool=ToolInfo(
95
+ id=success_response.tool.id,
96
+ name=success_response.tool.name,
97
+ ),
98
+ duration_ms=success_response.duration_ms,
99
+ )
100
+
101
+ # Fallback - shouldn't reach here with valid API responses
102
+ raise ContextError("Unexpected response format from API")
103
+
@@ -0,0 +1,265 @@
1
+ """
2
+ Type definitions for the Context Protocol SDK.
3
+
4
+ This module contains all Pydantic models and type definitions used by the client.
5
+ """
6
+
7
+ from typing import Any, Literal
8
+
9
+ from pydantic import BaseModel, Field
10
+
11
+
12
+ class ContextClientOptions(BaseModel):
13
+ """Configuration options for initializing the ContextClient.
14
+
15
+ Attributes:
16
+ api_key: Your Context Protocol API key (e.g., "sk_live_abc123...")
17
+ base_url: Base URL for the Context Protocol API. Defaults to "https://ctxprotocol.com"
18
+ """
19
+
20
+ api_key: str = Field(..., description="Your Context Protocol API key")
21
+ base_url: str = Field(
22
+ default="https://ctxprotocol.com",
23
+ description="Base URL for the Context Protocol API",
24
+ )
25
+
26
+
27
+ class McpTool(BaseModel):
28
+ """An individual MCP tool exposed by a tool listing.
29
+
30
+ Attributes:
31
+ name: Name of the MCP tool method
32
+ description: Description of what this method does
33
+ input_schema: JSON Schema for the input arguments this tool accepts
34
+ output_schema: JSON Schema for the output this tool returns
35
+ """
36
+
37
+ name: str = Field(..., description="Name of the MCP tool method")
38
+ description: str = Field(..., description="Description of what this method does")
39
+ input_schema: dict[str, Any] | None = Field(
40
+ default=None,
41
+ alias="inputSchema",
42
+ description="JSON Schema for the input arguments this tool accepts",
43
+ )
44
+ output_schema: dict[str, Any] | None = Field(
45
+ default=None,
46
+ alias="outputSchema",
47
+ description="JSON Schema for the output this tool returns",
48
+ )
49
+
50
+ model_config = {"populate_by_name": True}
51
+
52
+
53
+ class Tool(BaseModel):
54
+ """Represents a tool available on the Context Protocol marketplace.
55
+
56
+ Attributes:
57
+ id: Unique identifier for the tool (UUID)
58
+ name: Human-readable name of the tool
59
+ description: Description of what the tool does
60
+ price: Price per execution in USDC
61
+ category: Tool category (e.g., "defi", "nft")
62
+ is_verified: Whether the tool is verified by Context Protocol
63
+ mcp_tools: Available MCP tool methods
64
+ created_at: Creation timestamp
65
+ updated_at: Last update timestamp
66
+ """
67
+
68
+ id: str = Field(..., description="Unique identifier for the tool (UUID)")
69
+ name: str = Field(..., description="Human-readable name of the tool")
70
+ description: str = Field(..., description="Description of what the tool does")
71
+ price: str = Field(..., description="Price per execution in USDC")
72
+ category: str | None = Field(default=None, description="Tool category")
73
+ is_verified: bool | None = Field(
74
+ default=None,
75
+ alias="isVerified",
76
+ description="Whether the tool is verified by Context Protocol",
77
+ )
78
+ mcp_tools: list[McpTool] | None = Field(
79
+ default=None,
80
+ alias="mcpTools",
81
+ description="Available MCP tool methods",
82
+ )
83
+ created_at: str | None = Field(
84
+ default=None,
85
+ alias="createdAt",
86
+ description="Creation timestamp",
87
+ )
88
+ updated_at: str | None = Field(
89
+ default=None,
90
+ alias="updatedAt",
91
+ description="Last update timestamp",
92
+ )
93
+
94
+ model_config = {"populate_by_name": True}
95
+
96
+
97
+ class SearchResponse(BaseModel):
98
+ """Response from the tools search endpoint.
99
+
100
+ Attributes:
101
+ tools: Array of matching tools
102
+ query: The search query that was used
103
+ count: Total number of results
104
+ """
105
+
106
+ tools: list[Tool] = Field(..., description="Array of matching tools")
107
+ query: str = Field(..., description="The search query that was used")
108
+ count: int = Field(..., description="Total number of results")
109
+
110
+
111
+ class SearchOptions(BaseModel):
112
+ """Options for searching tools.
113
+
114
+ Attributes:
115
+ query: Search query (semantic search)
116
+ limit: Maximum number of results (1-50, default 10)
117
+ """
118
+
119
+ query: str | None = Field(default=None, description="Search query (semantic search)")
120
+ limit: int | None = Field(
121
+ default=None,
122
+ ge=1,
123
+ le=50,
124
+ description="Maximum number of results (1-50, default 10)",
125
+ )
126
+
127
+
128
+ class ExecuteOptions(BaseModel):
129
+ """Options for executing a tool.
130
+
131
+ Attributes:
132
+ tool_id: The UUID of the tool to execute (from search results)
133
+ tool_name: The specific MCP tool name to call (from tool's mcp_tools array)
134
+ args: Arguments to pass to the tool
135
+ """
136
+
137
+ tool_id: str = Field(
138
+ ...,
139
+ alias="toolId",
140
+ description="The UUID of the tool to execute (from search results)",
141
+ )
142
+ tool_name: str = Field(
143
+ ...,
144
+ alias="toolName",
145
+ description="The specific MCP tool name to call (from tool's mcp_tools array)",
146
+ )
147
+ args: dict[str, Any] | None = Field(
148
+ default=None,
149
+ description="Arguments to pass to the tool",
150
+ )
151
+
152
+ model_config = {"populate_by_name": True}
153
+
154
+
155
+ class ToolInfo(BaseModel):
156
+ """Information about an executed tool."""
157
+
158
+ id: str
159
+ name: str
160
+
161
+
162
+ class ExecuteApiSuccessResponse(BaseModel):
163
+ """Successful execution response from the API.
164
+
165
+ Attributes:
166
+ success: Always True for success responses
167
+ result: The result data from the tool execution
168
+ tool: Information about the executed tool
169
+ duration_ms: Execution duration in milliseconds
170
+ """
171
+
172
+ success: Literal[True] = Field(..., description="Always True for success responses")
173
+ result: Any = Field(..., description="The result data from the tool execution")
174
+ tool: ToolInfo = Field(..., description="Information about the executed tool")
175
+ duration_ms: int = Field(
176
+ ...,
177
+ alias="durationMs",
178
+ description="Execution duration in milliseconds",
179
+ )
180
+
181
+ model_config = {"populate_by_name": True}
182
+
183
+
184
+ class ExecuteApiErrorResponse(BaseModel):
185
+ """Error response from the API.
186
+
187
+ Attributes:
188
+ error: Human-readable error message
189
+ code: Error code for programmatic handling
190
+ help_url: URL to help resolve the issue
191
+ """
192
+
193
+ error: str = Field(..., description="Human-readable error message")
194
+ code: str | None = Field(
195
+ default=None,
196
+ description="Error code for programmatic handling",
197
+ )
198
+ help_url: str | None = Field(
199
+ default=None,
200
+ alias="helpUrl",
201
+ description="URL to help resolve the issue",
202
+ )
203
+
204
+ model_config = {"populate_by_name": True}
205
+
206
+
207
+ class ExecutionResult(BaseModel):
208
+ """The resolved result returned to the user after SDK processing.
209
+
210
+ Attributes:
211
+ result: The data returned by the tool
212
+ tool: Information about the executed tool
213
+ duration_ms: Execution duration in milliseconds
214
+ """
215
+
216
+ result: Any = Field(..., description="The data returned by the tool")
217
+ tool: ToolInfo = Field(..., description="Information about the executed tool")
218
+ duration_ms: int = Field(..., description="Execution duration in milliseconds")
219
+
220
+
221
+ # Type alias for specific error codes returned by the Context Protocol API
222
+ ContextErrorCode = Literal[
223
+ "unauthorized",
224
+ "no_wallet",
225
+ "insufficient_allowance",
226
+ "payment_failed",
227
+ "execution_failed",
228
+ ]
229
+
230
+
231
+ class ContextError(Exception):
232
+ """Error thrown by the Context Protocol client.
233
+
234
+ Attributes:
235
+ message: Human-readable error message
236
+ code: Error code for programmatic handling
237
+ status_code: HTTP status code
238
+ help_url: URL to help resolve the issue
239
+ """
240
+
241
+ def __init__(
242
+ self,
243
+ message: str,
244
+ code: str | None = None,
245
+ status_code: int | None = None,
246
+ help_url: str | None = None,
247
+ ) -> None:
248
+ super().__init__(message)
249
+ self.message = message
250
+ self.code = code
251
+ self.status_code = status_code
252
+ self.help_url = help_url
253
+
254
+ def __str__(self) -> str:
255
+ parts = [self.message]
256
+ if self.code:
257
+ parts.append(f"[{self.code}]")
258
+ return " ".join(parts)
259
+
260
+ def __repr__(self) -> str:
261
+ return (
262
+ f"ContextError(message={self.message!r}, code={self.code!r}, "
263
+ f"status_code={self.status_code!r}, help_url={self.help_url!r})"
264
+ )
265
+
@@ -0,0 +1,184 @@
1
+ """
2
+ Context types for portfolio and protocol data injection.
3
+
4
+ These types allow MCP tools to receive personalized user context
5
+ (wallet addresses, positions, balances) for analysis.
6
+
7
+ =============================================================================
8
+ DECLARING CONTEXT REQUIREMENTS
9
+ =============================================================================
10
+
11
+ Since the MCP protocol only transmits standard fields (name, description,
12
+ inputSchema, outputSchema), context requirements MUST be embedded in the
13
+ inputSchema using the "x-context-requirements" JSON Schema extension.
14
+
15
+ Example:
16
+ >>> from ctxprotocol import CONTEXT_REQUIREMENTS_KEY, ContextRequirementType
17
+ >>> from ctxprotocol.context import HyperliquidContext
18
+ >>>
19
+ >>> tool = {
20
+ ... "name": "analyze_my_positions",
21
+ ... "inputSchema": {
22
+ ... "type": "object",
23
+ ... CONTEXT_REQUIREMENTS_KEY: ["hyperliquid"],
24
+ ... "properties": {
25
+ ... "portfolio": {"type": "object"}
26
+ ... },
27
+ ... "required": ["portfolio"]
28
+ ... }
29
+ ... }
30
+ >>>
31
+ >>> # Your handler receives the injected context:
32
+ >>> def handle_analyze_my_positions(portfolio: HyperliquidContext):
33
+ ... positions = portfolio.perp_positions
34
+ ... account = portfolio.account_summary
35
+ ... # ... analyze and return insights
36
+ """
37
+
38
+ from typing import Literal
39
+
40
+ from pydantic import BaseModel, Field
41
+
42
+ # Wallet context types
43
+ from ctxprotocol.context.wallet import ERC20Context, ERC20TokenBalance, WalletContext
44
+
45
+ # Protocol-specific context types
46
+ from ctxprotocol.context.polymarket import (
47
+ PolymarketContext,
48
+ PolymarketOrder,
49
+ PolymarketPosition,
50
+ )
51
+ from ctxprotocol.context.hyperliquid import (
52
+ CrossMarginSummary,
53
+ CumFunding,
54
+ HyperliquidAccountSummary,
55
+ HyperliquidContext,
56
+ HyperliquidOrder,
57
+ HyperliquidPerpPosition,
58
+ HyperliquidSpotBalance,
59
+ LeverageInfo,
60
+ )
61
+
62
+ # ============================================================================
63
+ # CONTEXT REQUIREMENTS
64
+ #
65
+ # MCP tools that need user portfolio data MUST declare this in inputSchema.
66
+ # The MCP protocol only transmits standard fields (name, description,
67
+ # inputSchema, outputSchema). Custom fields get stripped by the MCP SDK.
68
+ # ============================================================================
69
+
70
+ CONTEXT_REQUIREMENTS_KEY = "x-context-requirements"
71
+ """
72
+ JSON Schema extension key for declaring context requirements.
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
79
+
80
+ Example:
81
+ >>> tool = {
82
+ ... "name": "analyze_my_positions",
83
+ ... "inputSchema": {
84
+ ... "type": "object",
85
+ ... CONTEXT_REQUIREMENTS_KEY: ["hyperliquid"],
86
+ ... "properties": {"portfolio": {"type": "object"}},
87
+ ... "required": ["portfolio"]
88
+ ... }
89
+ ... }
90
+ """
91
+
92
+ # Context requirement types supported by the Context marketplace
93
+ ContextRequirementType = Literal["polymarket", "hyperliquid", "wallet"]
94
+ """
95
+ Context requirement types supported by the Context marketplace.
96
+ Maps to protocol-specific context builders on the platform.
97
+
98
+ Example:
99
+ >>> input_schema = {
100
+ ... "type": "object",
101
+ ... "x-context-requirements": ["hyperliquid"], # type: ContextRequirementType
102
+ ... "properties": {"portfolio": {"type": "object"}},
103
+ ... "required": ["portfolio"]
104
+ ... }
105
+ """
106
+
107
+
108
+ class ToolRequirements(BaseModel):
109
+ """
110
+ DEPRECATED: The `requirements` field at tool level gets stripped by MCP SDK.
111
+ Use `x-context-requirements` inside `inputSchema` instead.
112
+
113
+ Example:
114
+ >>> # ❌ OLD (doesn't work - stripped by MCP SDK)
115
+ >>> {"requirements": {"context": ["hyperliquid"]}}
116
+ >>>
117
+ >>> # ✅ NEW (works - preserved through MCP transport)
118
+ >>> {"inputSchema": {"x-context-requirements": ["hyperliquid"], ...}}
119
+ """
120
+
121
+ context: list[ContextRequirementType] | None = Field(
122
+ default=None,
123
+ description="DEPRECATED: Use x-context-requirements in inputSchema instead.",
124
+ )
125
+
126
+
127
+ class UserContext(BaseModel):
128
+ """Composite context for tools that need multiple data sources.
129
+
130
+ This is the unified structure that can be passed to MCP tools
131
+ to provide comprehensive user context.
132
+
133
+ Attributes:
134
+ wallet: Base wallet information
135
+ erc20: ERC20 token holdings
136
+ polymarket: Polymarket positions and orders
137
+ hyperliquid: Hyperliquid perpetual positions and account data
138
+ """
139
+
140
+ wallet: WalletContext | None = Field(
141
+ default=None,
142
+ description="Base wallet information",
143
+ )
144
+ erc20: ERC20Context | None = Field(
145
+ default=None,
146
+ description="ERC20 token holdings",
147
+ )
148
+ polymarket: PolymarketContext | None = Field(
149
+ default=None,
150
+ description="Polymarket positions and orders",
151
+ )
152
+ hyperliquid: HyperliquidContext | None = Field(
153
+ default=None,
154
+ description="Hyperliquid perpetual positions and account data",
155
+ )
156
+
157
+
158
+ __all__ = [
159
+ # Constants
160
+ "CONTEXT_REQUIREMENTS_KEY",
161
+ # Type aliases
162
+ "ContextRequirementType",
163
+ # Wallet types
164
+ "WalletContext",
165
+ "ERC20Context",
166
+ "ERC20TokenBalance",
167
+ # Polymarket types
168
+ "PolymarketContext",
169
+ "PolymarketPosition",
170
+ "PolymarketOrder",
171
+ # Hyperliquid types
172
+ "HyperliquidContext",
173
+ "HyperliquidPerpPosition",
174
+ "HyperliquidOrder",
175
+ "HyperliquidSpotBalance",
176
+ "HyperliquidAccountSummary",
177
+ "CrossMarginSummary",
178
+ "LeverageInfo",
179
+ "CumFunding",
180
+ # Composite types
181
+ "UserContext",
182
+ "ToolRequirements",
183
+ ]
184
+