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.
- asap/__init__.py +1 -1
- asap/cli.py +137 -2
- asap/examples/README.md +81 -13
- asap/examples/auth_patterns.py +212 -0
- asap/examples/error_recovery.py +248 -0
- asap/examples/long_running.py +287 -0
- asap/examples/mcp_integration.py +240 -0
- asap/examples/multi_step_workflow.py +134 -0
- asap/examples/orchestration.py +293 -0
- asap/examples/rate_limiting.py +137 -0
- asap/examples/run_demo.py +0 -2
- asap/examples/secure_handler.py +84 -0
- asap/examples/state_migration.py +240 -0
- asap/examples/streaming_response.py +108 -0
- asap/examples/websocket_concept.py +129 -0
- asap/mcp/__init__.py +43 -0
- asap/mcp/client.py +224 -0
- asap/mcp/protocol.py +179 -0
- asap/mcp/server.py +333 -0
- asap/mcp/server_runner.py +40 -0
- asap/models/base.py +0 -3
- asap/models/constants.py +3 -1
- asap/models/entities.py +21 -6
- asap/models/envelope.py +7 -0
- asap/models/ids.py +8 -4
- asap/models/parts.py +33 -3
- asap/models/validators.py +16 -0
- asap/observability/__init__.py +6 -0
- asap/observability/dashboards/README.md +24 -0
- asap/observability/dashboards/asap-detailed.json +131 -0
- asap/observability/dashboards/asap-red.json +129 -0
- asap/observability/logging.py +81 -1
- asap/observability/metrics.py +15 -1
- asap/observability/trace_parser.py +238 -0
- asap/observability/trace_ui.py +218 -0
- asap/observability/tracing.py +293 -0
- asap/state/machine.py +15 -2
- asap/state/snapshot.py +0 -9
- asap/testing/__init__.py +31 -0
- asap/testing/assertions.py +108 -0
- asap/testing/fixtures.py +113 -0
- asap/testing/mocks.py +152 -0
- asap/transport/__init__.py +28 -0
- asap/transport/cache.py +180 -0
- asap/transport/circuit_breaker.py +9 -8
- asap/transport/client.py +418 -36
- asap/transport/compression.py +389 -0
- asap/transport/handlers.py +106 -53
- asap/transport/middleware.py +58 -34
- asap/transport/server.py +429 -139
- asap/transport/validators.py +0 -4
- asap/utils/sanitization.py +0 -5
- asap_protocol-1.0.0.dist-info/METADATA +264 -0
- asap_protocol-1.0.0.dist-info/RECORD +70 -0
- asap_protocol-0.5.0.dist-info/METADATA +0 -244
- asap_protocol-0.5.0.dist-info/RECORD +0 -41
- {asap_protocol-0.5.0.dist-info → asap_protocol-1.0.0.dist-info}/WHEEL +0 -0
- {asap_protocol-0.5.0.dist-info → asap_protocol-1.0.0.dist-info}/entry_points.txt +0 -0
- {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")
|