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
|
@@ -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
|
+
|