asap-protocol 0.5.0__py3-none-any.whl → 1.0.0__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.
Files changed (59) hide show
  1. asap/__init__.py +1 -1
  2. asap/cli.py +137 -2
  3. asap/examples/README.md +81 -13
  4. asap/examples/auth_patterns.py +212 -0
  5. asap/examples/error_recovery.py +248 -0
  6. asap/examples/long_running.py +287 -0
  7. asap/examples/mcp_integration.py +240 -0
  8. asap/examples/multi_step_workflow.py +134 -0
  9. asap/examples/orchestration.py +293 -0
  10. asap/examples/rate_limiting.py +137 -0
  11. asap/examples/run_demo.py +0 -2
  12. asap/examples/secure_handler.py +84 -0
  13. asap/examples/state_migration.py +240 -0
  14. asap/examples/streaming_response.py +108 -0
  15. asap/examples/websocket_concept.py +129 -0
  16. asap/mcp/__init__.py +43 -0
  17. asap/mcp/client.py +224 -0
  18. asap/mcp/protocol.py +179 -0
  19. asap/mcp/server.py +333 -0
  20. asap/mcp/server_runner.py +40 -0
  21. asap/models/base.py +0 -3
  22. asap/models/constants.py +3 -1
  23. asap/models/entities.py +21 -6
  24. asap/models/envelope.py +7 -0
  25. asap/models/ids.py +8 -4
  26. asap/models/parts.py +33 -3
  27. asap/models/validators.py +16 -0
  28. asap/observability/__init__.py +6 -0
  29. asap/observability/dashboards/README.md +24 -0
  30. asap/observability/dashboards/asap-detailed.json +131 -0
  31. asap/observability/dashboards/asap-red.json +129 -0
  32. asap/observability/logging.py +81 -1
  33. asap/observability/metrics.py +15 -1
  34. asap/observability/trace_parser.py +238 -0
  35. asap/observability/trace_ui.py +218 -0
  36. asap/observability/tracing.py +293 -0
  37. asap/state/machine.py +15 -2
  38. asap/state/snapshot.py +0 -9
  39. asap/testing/__init__.py +31 -0
  40. asap/testing/assertions.py +108 -0
  41. asap/testing/fixtures.py +113 -0
  42. asap/testing/mocks.py +152 -0
  43. asap/transport/__init__.py +28 -0
  44. asap/transport/cache.py +180 -0
  45. asap/transport/circuit_breaker.py +9 -8
  46. asap/transport/client.py +418 -36
  47. asap/transport/compression.py +389 -0
  48. asap/transport/handlers.py +106 -53
  49. asap/transport/middleware.py +58 -34
  50. asap/transport/server.py +429 -139
  51. asap/transport/validators.py +0 -4
  52. asap/utils/sanitization.py +0 -5
  53. asap_protocol-1.0.0.dist-info/METADATA +264 -0
  54. asap_protocol-1.0.0.dist-info/RECORD +70 -0
  55. asap_protocol-0.5.0.dist-info/METADATA +0 -244
  56. asap_protocol-0.5.0.dist-info/RECORD +0 -41
  57. {asap_protocol-0.5.0.dist-info → asap_protocol-1.0.0.dist-info}/WHEEL +0 -0
  58. {asap_protocol-0.5.0.dist-info → asap_protocol-1.0.0.dist-info}/entry_points.txt +0 -0
  59. {asap_protocol-0.5.0.dist-info → asap_protocol-1.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,129 @@
1
+ """WebSocket concept for ASAP protocol (not implemented).
2
+
3
+ This module describes how WebSocket support would work with ASAP.
4
+ No WebSocket server or client is implemented here; the protocol currently
5
+ uses HTTP + JSON-RPC. This file is for documentation and design reference.
6
+
7
+ Concept:
8
+ - Manifest Endpoint can expose an optional "events" URL (wss://...).
9
+ - Same Envelope format: clients send and receive Envelopes as JSON over the wire.
10
+ - Use cases: streaming task updates, real-time notifications, low-latency duplex.
11
+
12
+ Pseudocode (server side):
13
+ 1. Accept WebSocket connection at e.g. /asap/events.
14
+ 2. Optional: validate Bearer token from query or first message.
15
+ 3. Loop: receive JSON message -> parse as Envelope -> dispatch to handler
16
+ -> optional: send response Envelope(s) back (e.g. task.update, task.response).
17
+ 4. On disconnect or error, close connection.
18
+
19
+ Pseudocode (client side):
20
+ 1. Connect to wss://agent.example.com/asap/events (from manifest.endpoints.events).
21
+ 2. Send Envelope as JSON: json.dumps(envelope.model_dump()).
22
+ 3. Loop: receive JSON -> parse Envelope -> handle (e.g. task.update, task.response).
23
+ 4. Optional: ping/pong for keepalive; reconnect with backoff on disconnect.
24
+
25
+ Message framing:
26
+ - One Envelope per WebSocket text message (JSON).
27
+ - Binary frames could carry compressed or binary-serialized Envelopes (future).
28
+
29
+ Run:
30
+ uv run python -m asap.examples.websocket_concept
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import argparse
36
+ from typing import Sequence
37
+
38
+ from asap.models.entities import Endpoint
39
+ from asap.observability import get_logger
40
+
41
+ logger = get_logger(__name__)
42
+
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # Pseudocode: how a WebSocket server would integrate (NOT IMPLEMENTED)
46
+ # ---------------------------------------------------------------------------
47
+ #
48
+ # async def websocket_asap_endpoint(websocket: WebSocket) -> None:
49
+ # await websocket.accept()
50
+ # # Optional: require auth via query param or first message
51
+ # # token = websocket.query_params.get("token") or await read_first_message()
52
+ # # agent_id = token_validator(token); if not agent_id: await websocket.close(4001)
53
+ # try:
54
+ # while True:
55
+ # data = await websocket.receive_text()
56
+ # envelope_dict = json.loads(data)
57
+ # envelope = Envelope.model_validate(envelope_dict)
58
+ # # Dispatch same as HTTP: handler = registry.get(envelope.payload_type)
59
+ # # response_envelopes = await handler(envelope)
60
+ # # for resp in response_envelopes:
61
+ # # await websocket.send_text(json.dumps(resp.model_dump()))
62
+ # except WebSocketDisconnect:
63
+ # pass
64
+ # finally:
65
+ # await websocket.close()
66
+ #
67
+ # # In FastAPI: @app.websocket("/asap/events"); def events(ws: WebSocket): asyncio.run(websocket_asap_endpoint(ws))
68
+ # ---------------------------------------------------------------------------
69
+
70
+ # ---------------------------------------------------------------------------
71
+ # Pseudocode: how a WebSocket client would integrate (NOT IMPLEMENTED)
72
+ # ---------------------------------------------------------------------------
73
+ #
74
+ # async def send_envelope_over_websocket(ws: ClientSession, envelope: Envelope) -> None:
75
+ # await ws.send_str(json.dumps(envelope.model_dump()))
76
+ #
77
+ # async def receive_envelope(ws: ClientSession) -> Envelope | None:
78
+ # msg = await ws.receive()
79
+ # if msg.type == aiohttp.WSMsgType.TEXT:
80
+ # return Envelope.model_validate(json.loads(msg.data))
81
+ # return None
82
+ #
83
+ # # Client gets events URL from manifest: manifest.endpoints.events (wss://...)
84
+ # ---------------------------------------------------------------------------
85
+
86
+
87
+ def get_events_endpoint_concept() -> str:
88
+ """Return the optional WebSocket events URL from an Endpoint (concept).
89
+
90
+ In a real implementation, the agent manifest would expose
91
+ endpoints.events (e.g. wss://agent.example.com/asap/events) for
92
+ WebSocket connections. HTTP remains the primary transport.
93
+
94
+ Returns:
95
+ Example events URL string for documentation.
96
+ """
97
+ endpoint = Endpoint(
98
+ asap="https://api.example.com/asap",
99
+ events="wss://api.example.com/asap/events",
100
+ )
101
+ return endpoint.events or ""
102
+
103
+
104
+ def run_demo() -> None:
105
+ """Print/log the WebSocket concept summary (no implementation)."""
106
+ events_url = get_events_endpoint_concept()
107
+ logger.info(
108
+ "asap.websocket_concept.summary",
109
+ message="WebSocket support is not implemented; same Envelope format would be used over WS",
110
+ example_events_url=events_url,
111
+ )
112
+
113
+
114
+ def parse_args(argv: Sequence[str] | None = None) -> argparse.Namespace:
115
+ """Parse command-line arguments for the WebSocket concept demo."""
116
+ parser = argparse.ArgumentParser(
117
+ description="WebSocket concept for ASAP (comments/pseudocode only, not implemented)."
118
+ )
119
+ return parser.parse_args(argv)
120
+
121
+
122
+ def main(argv: Sequence[str] | None = None) -> None:
123
+ """Run the WebSocket concept demo (documentation only)."""
124
+ parse_args(argv)
125
+ run_demo()
126
+
127
+
128
+ if __name__ == "__main__":
129
+ main()
asap/mcp/__init__.py ADDED
@@ -0,0 +1,43 @@
1
+ """Model Context Protocol (MCP) integration for ASAP.
2
+
3
+ Implements MCP spec 2025-11-25: JSON-RPC 2.0 over stdio (or other transports),
4
+ with support for initialize, tools/list, and tools/call.
5
+
6
+ Example:
7
+ >>> from asap.mcp import MCPServer
8
+ >>> server = MCPServer(name="my-server", version="1.0.0")
9
+ >>> server.register_tool("echo", lambda message: message, {"type": "object", "properties": {"message": {"type": "string"}}})
10
+ >>> # asyncio.run(server.run_stdio())
11
+ """
12
+
13
+ from asap.mcp.client import MCPClient
14
+ from asap.mcp.protocol import (
15
+ MCP_PROTOCOL_VERSION,
16
+ CallToolRequestParams,
17
+ CallToolResult,
18
+ InitializeRequestParams,
19
+ InitializeResult,
20
+ JSONRPCError,
21
+ JSONRPCRequest,
22
+ JSONRPCResponse,
23
+ ListToolsResult,
24
+ TextContent,
25
+ Tool,
26
+ )
27
+ from asap.mcp.server import MCPServer
28
+
29
+ __all__ = [
30
+ "MCPClient",
31
+ "MCPServer",
32
+ "MCP_PROTOCOL_VERSION",
33
+ "CallToolRequestParams",
34
+ "CallToolResult",
35
+ "InitializeRequestParams",
36
+ "InitializeResult",
37
+ "JSONRPCError",
38
+ "JSONRPCRequest",
39
+ "JSONRPCResponse",
40
+ "ListToolsResult",
41
+ "TextContent",
42
+ "Tool",
43
+ ]
asap/mcp/client.py ADDED
@@ -0,0 +1,224 @@
1
+ """MCP client implementation (spec 2025-11-25).
2
+
3
+ Connects to an MCP server over stdio (subprocess) or provides a transport
4
+ abstraction for sending requests and receiving responses.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import json
11
+ from typing import Any, Literal, cast
12
+
13
+ from asap.mcp.protocol import (
14
+ MCP_PROTOCOL_VERSION,
15
+ CallToolRequestParams,
16
+ CallToolResult,
17
+ Implementation,
18
+ InitializeRequestParams,
19
+ InitializeResult,
20
+ ListToolsResult,
21
+ Tool,
22
+ )
23
+ from asap.observability import get_logger
24
+
25
+ logger = get_logger(__name__)
26
+
27
+
28
+ class MCPClient:
29
+ """MCP client with stdio transport.
30
+
31
+ Connects to an MCP server process via stdin/stdout, performs
32
+ initialize handshake, then supports tools/list and tools/call.
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ server_command: list[str],
38
+ *,
39
+ name: str = "asap-mcp-client",
40
+ version: str = "1.0.0",
41
+ receive_timeout: float | None = 60.0,
42
+ request_id_type: Literal["int", "str"] = "str",
43
+ ) -> None:
44
+ """Initialize the client.
45
+
46
+ Args:
47
+ server_command: Command and args to start the server (e.g. ["python", "-m", "asap.mcp.server_runner"]).
48
+ name: Client name for initialize.
49
+ version: Client version for initialize.
50
+ receive_timeout: Seconds to wait for a response (None = no timeout).
51
+ request_id_type: Request id type for JSON-RPC id field ("int" or "str").
52
+ """
53
+ self._server_command = server_command
54
+ self._client_info = Implementation(name=name, version=version)
55
+ self._receive_timeout = receive_timeout
56
+ self._process: asyncio.subprocess.Process | None = None
57
+ self._initialized = False
58
+ self._request_id = 0
59
+ self._use_str_ids = request_id_type == "str"
60
+ self._init_result: InitializeResult | None = None
61
+
62
+ def _next_id(self) -> str | int:
63
+ self._request_id += 1
64
+ return str(self._request_id) if self._use_str_ids else self._request_id
65
+
66
+ async def _send(self, payload: dict[str, Any]) -> None:
67
+ """Send one JSON-RPC message (request or notification) to server stdin."""
68
+ if self._process is None or self._process.stdin is None:
69
+ raise RuntimeError("Not connected; call connect() first")
70
+ line = json.dumps(payload) + "\n"
71
+ self._process.stdin.write(line.encode("utf-8"))
72
+ await self._process.stdin.drain()
73
+
74
+ async def _receive(self) -> dict[str, Any] | None:
75
+ """Read one JSON-RPC response from server stdout."""
76
+ if self._process is None or self._process.stdout is None:
77
+ raise RuntimeError("Not connected; call connect() first")
78
+ read_coro = self._process.stdout.readline()
79
+ if self._receive_timeout is not None:
80
+ try:
81
+ raw_line = await asyncio.wait_for(read_coro, timeout=self._receive_timeout)
82
+ except asyncio.TimeoutError:
83
+ raise RuntimeError(f"No response within {self._receive_timeout}s") from None
84
+ else:
85
+ raw_line = await read_coro
86
+ if not raw_line:
87
+ return None
88
+ line = raw_line.decode("utf-8").rstrip("\n\r")
89
+ if not line:
90
+ return None
91
+ try:
92
+ data = json.loads(line)
93
+ if not isinstance(data, dict):
94
+ return None
95
+ return cast("dict[str, Any]", data)
96
+ except json.JSONDecodeError as e:
97
+ logger.warning("mcp.client.parse_error", error=str(e))
98
+ return None
99
+
100
+ async def connect(self) -> InitializeResult:
101
+ """Start the server process and perform initialize handshake.
102
+
103
+ Returns:
104
+ InitializeResult from the server.
105
+
106
+ Raises:
107
+ RuntimeError: If already connected or server fails to respond.
108
+ """
109
+ if self._process is not None:
110
+ raise RuntimeError("Already connected")
111
+ self._process = await asyncio.create_subprocess_exec(
112
+ *self._server_command,
113
+ stdin=asyncio.subprocess.PIPE,
114
+ stdout=asyncio.subprocess.PIPE,
115
+ stderr=asyncio.subprocess.PIPE,
116
+ )
117
+ if self._process.stdin is None or self._process.stdout is None:
118
+ raise RuntimeError("Failed to get server stdin/stdout")
119
+
120
+ req_id = self._next_id()
121
+ init_req = {
122
+ "jsonrpc": "2.0",
123
+ "id": req_id,
124
+ "method": "initialize",
125
+ "params": InitializeRequestParams(
126
+ protocolVersion=MCP_PROTOCOL_VERSION,
127
+ capabilities={},
128
+ clientInfo=self._client_info,
129
+ ).model_dump(by_alias=True, exclude_none=True),
130
+ }
131
+ await self._send(init_req)
132
+ raw = await self._receive()
133
+ if raw is None:
134
+ raise RuntimeError("No response to initialize")
135
+ if "error" in raw:
136
+ err = raw["error"]
137
+ raise RuntimeError(f"Initialize failed: {err.get('message', err)}")
138
+ self._init_result = InitializeResult.model_validate(raw["result"])
139
+ self._initialized = True
140
+
141
+ await self._send(
142
+ {
143
+ "jsonrpc": "2.0",
144
+ "method": "notifications/initialized",
145
+ }
146
+ )
147
+ return self._init_result
148
+
149
+ async def list_tools(self) -> list[Tool]:
150
+ """Request the list of tools from the server.
151
+
152
+ Returns:
153
+ List of Tool definitions.
154
+ """
155
+ if not self._initialized:
156
+ raise RuntimeError("Not initialized; call connect() first")
157
+ req_id = self._next_id()
158
+ await self._send(
159
+ {
160
+ "jsonrpc": "2.0",
161
+ "id": req_id,
162
+ "method": "tools/list",
163
+ "params": {},
164
+ }
165
+ )
166
+ raw = await self._receive()
167
+ if raw is None:
168
+ raise RuntimeError("No response to tools/list")
169
+ if "error" in raw:
170
+ err = raw["error"]
171
+ raise RuntimeError(f"tools/list failed: {err.get('message', err)}")
172
+ result = ListToolsResult(**raw["result"])
173
+ return result.tools
174
+
175
+ async def call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> CallToolResult:
176
+ """Invoke a tool by name with the given arguments.
177
+
178
+ Args:
179
+ name: Tool name (as returned by list_tools).
180
+ arguments: Tool arguments (keyword dict).
181
+
182
+ Returns:
183
+ CallToolResult with content and is_error.
184
+ """
185
+ if not self._initialized:
186
+ raise RuntimeError("Not initialized; call connect() first")
187
+ req_id = self._next_id()
188
+ params = CallToolRequestParams(name=name, arguments=arguments or {})
189
+ await self._send(
190
+ {
191
+ "jsonrpc": "2.0",
192
+ "id": req_id,
193
+ "method": "tools/call",
194
+ "params": params.model_dump(by_alias=True),
195
+ }
196
+ )
197
+ raw = await self._receive()
198
+ if raw is None:
199
+ raise RuntimeError("No response to tools/call")
200
+ if "error" in raw:
201
+ err = raw["error"]
202
+ return CallToolResult(
203
+ content=[{"type": "text", "text": err.get("message", str(err))}],
204
+ isError=True,
205
+ )
206
+ return CallToolResult.model_validate(raw["result"])
207
+
208
+ async def disconnect(self) -> None:
209
+ """Close the connection to the server (and terminate the process)."""
210
+ if self._process is not None:
211
+ if self._process.stdin:
212
+ self._process.stdin.close()
213
+ await self._process.stdin.wait_closed()
214
+ self._process.terminate()
215
+ await self._process.wait()
216
+ self._process = None
217
+ self._initialized = False
218
+
219
+ async def __aenter__(self) -> MCPClient:
220
+ await self.connect()
221
+ return self
222
+
223
+ async def __aexit__(self, *args: Any) -> None:
224
+ await self.disconnect()
asap/mcp/protocol.py ADDED
@@ -0,0 +1,179 @@
1
+ """MCP protocol types (spec 2025-11-25).
2
+
3
+ JSON-RPC 2.0 and MCP message types for initialize, tools/list, and tools/call.
4
+ Models use extra="ignore" for forward compatibility with future spec fields.
5
+ """
6
+
7
+ from typing import Any, Literal
8
+
9
+ from pydantic import BaseModel, ConfigDict, Field
10
+
11
+ # Protocol version supported by this implementation
12
+ MCP_PROTOCOL_VERSION = "2025-11-25"
13
+
14
+ # JSON-RPC 2.0 standard error codes
15
+ PARSE_ERROR = -32700
16
+ INVALID_REQUEST = -32600
17
+ METHOD_NOT_FOUND = -32601
18
+ INVALID_PARAMS = -32602
19
+ INTERNAL_ERROR = -32603
20
+
21
+
22
+ def _mcp_model_config() -> ConfigDict:
23
+ """Pydantic config for MCP models: allow extra fields for forward compatibility."""
24
+ return ConfigDict(extra="ignore", populate_by_name=True)
25
+
26
+
27
+ # --- JSON-RPC 2.0 ---
28
+
29
+
30
+ class JSONRPCError(BaseModel):
31
+ """JSON-RPC 2.0 error object."""
32
+
33
+ model_config = _mcp_model_config()
34
+
35
+ code: int = Field(description="Error code (integer)")
36
+ message: str = Field(description="Short error description")
37
+ data: Any = Field(default=None, description="Optional additional data")
38
+
39
+
40
+ class JSONRPCRequest(BaseModel):
41
+ """JSON-RPC 2.0 request (has id, expects response)."""
42
+
43
+ model_config = _mcp_model_config()
44
+
45
+ jsonrpc: Literal["2.0"] = Field(default="2.0")
46
+ id: str | int = Field(description="Request id (must not be null)")
47
+ method: str = Field(description="Method name")
48
+ params: dict[str, Any] | None = Field(default=None)
49
+
50
+
51
+ class JSONRPCResponse(BaseModel):
52
+ """JSON-RPC 2.0 success response."""
53
+
54
+ model_config = _mcp_model_config()
55
+
56
+ jsonrpc: Literal["2.0"] = Field(default="2.0")
57
+ id: str | int = Field(description="Same id as request")
58
+ result: dict[str, Any] = Field(description="Result payload")
59
+
60
+
61
+ class JSONRPCErrorResponse(BaseModel):
62
+ """JSON-RPC 2.0 error response."""
63
+
64
+ model_config = _mcp_model_config()
65
+
66
+ jsonrpc: Literal["2.0"] = Field(default="2.0")
67
+ id: str | int | None = Field(description="Same id as request, or null")
68
+ error: JSONRPCError = Field(description="Error object")
69
+
70
+
71
+ class JSONRPCNotification(BaseModel):
72
+ """JSON-RPC 2.0 notification (no id, no response)."""
73
+
74
+ model_config = _mcp_model_config()
75
+
76
+ jsonrpc: Literal["2.0"] = Field(default="2.0")
77
+ method: str = Field(description="Method name")
78
+ params: dict[str, Any] | None = Field(default=None)
79
+
80
+
81
+ # --- Implementation (clientInfo / serverInfo) ---
82
+
83
+
84
+ class Implementation(BaseModel):
85
+ """MCP implementation info (client or server)."""
86
+
87
+ model_config = _mcp_model_config()
88
+
89
+ name: str = Field(description="Programmatic name")
90
+ version: str = Field(description="Version string")
91
+ title: str | None = Field(default=None, description="Human-readable title")
92
+ description: str | None = Field(default=None)
93
+ icons: list[dict[str, Any]] | None = Field(default=None)
94
+ website_url: str | None = Field(default=None, alias="websiteUrl")
95
+
96
+
97
+ # --- Initialize ---
98
+
99
+
100
+ class InitializeRequestParams(BaseModel):
101
+ """Params for initialize request."""
102
+
103
+ model_config = _mcp_model_config()
104
+
105
+ protocol_version: str = Field(alias="protocolVersion")
106
+ capabilities: dict[str, Any] = Field(default_factory=dict)
107
+ client_info: Implementation = Field(alias="clientInfo")
108
+
109
+
110
+ class InitializeResult(BaseModel):
111
+ """Result of initialize (server response)."""
112
+
113
+ model_config = _mcp_model_config()
114
+
115
+ protocol_version: str = Field(alias="protocolVersion", default=MCP_PROTOCOL_VERSION)
116
+ capabilities: dict[str, Any] = Field(default_factory=dict)
117
+ server_info: Implementation = Field(alias="serverInfo")
118
+ instructions: str | None = Field(default=None)
119
+
120
+
121
+ # --- Tools ---
122
+
123
+
124
+ class Tool(BaseModel):
125
+ """MCP tool definition (tools/list item)."""
126
+
127
+ model_config = _mcp_model_config()
128
+
129
+ name: str = Field(description="Unique tool name")
130
+ description: str = Field(description="Human-readable description")
131
+ input_schema: dict[str, Any] = Field(
132
+ alias="inputSchema",
133
+ description="JSON Schema for parameters (object, not null)",
134
+ )
135
+ title: str | None = Field(default=None)
136
+ icons: list[dict[str, Any]] | None = Field(default=None)
137
+ output_schema: dict[str, Any] | None = Field(default=None, alias="outputSchema")
138
+ annotations: dict[str, Any] | None = Field(default=None)
139
+
140
+
141
+ class TextContent(BaseModel):
142
+ """Text content item in tool result."""
143
+
144
+ model_config = _mcp_model_config()
145
+
146
+ type: Literal["text"] = Field(default="text")
147
+ text: str = Field(description="Text content")
148
+ annotations: dict[str, Any] | None = Field(default=None)
149
+
150
+
151
+ class CallToolRequestParams(BaseModel):
152
+ """Params for tools/call request."""
153
+
154
+ model_config = _mcp_model_config()
155
+
156
+ name: str = Field(description="Tool name")
157
+ arguments: dict[str, Any] = Field(default_factory=dict, description="Tool arguments")
158
+
159
+
160
+ class CallToolResult(BaseModel):
161
+ """Result of tools/call (content + isError)."""
162
+
163
+ model_config = _mcp_model_config()
164
+
165
+ content: list[dict[str, Any]] = Field(
166
+ default_factory=list,
167
+ description="Content blocks (e.g. TextContent)",
168
+ )
169
+ is_error: bool = Field(default=False, alias="isError")
170
+ structured_content: dict[str, Any] | None = Field(default=None, alias="structuredContent")
171
+
172
+
173
+ class ListToolsResult(BaseModel):
174
+ """Result of tools/list."""
175
+
176
+ model_config = _mcp_model_config()
177
+
178
+ tools: list[Tool] = Field(default_factory=list)
179
+ next_cursor: str | None = Field(default=None, alias="nextCursor")