mseep-rmcp 0.3.3__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.
- mseep_rmcp-0.3.3.dist-info/METADATA +50 -0
- mseep_rmcp-0.3.3.dist-info/RECORD +34 -0
- mseep_rmcp-0.3.3.dist-info/WHEEL +5 -0
- mseep_rmcp-0.3.3.dist-info/entry_points.txt +2 -0
- mseep_rmcp-0.3.3.dist-info/licenses/LICENSE +21 -0
- mseep_rmcp-0.3.3.dist-info/top_level.txt +1 -0
- rmcp/__init__.py +31 -0
- rmcp/cli.py +317 -0
- rmcp/core/__init__.py +14 -0
- rmcp/core/context.py +150 -0
- rmcp/core/schemas.py +156 -0
- rmcp/core/server.py +261 -0
- rmcp/r_assets/__init__.py +8 -0
- rmcp/r_integration.py +112 -0
- rmcp/registries/__init__.py +26 -0
- rmcp/registries/prompts.py +316 -0
- rmcp/registries/resources.py +266 -0
- rmcp/registries/tools.py +223 -0
- rmcp/scripts/__init__.py +9 -0
- rmcp/security/__init__.py +15 -0
- rmcp/security/vfs.py +233 -0
- rmcp/tools/descriptive.py +279 -0
- rmcp/tools/econometrics.py +250 -0
- rmcp/tools/fileops.py +315 -0
- rmcp/tools/machine_learning.py +299 -0
- rmcp/tools/regression.py +287 -0
- rmcp/tools/statistical_tests.py +332 -0
- rmcp/tools/timeseries.py +239 -0
- rmcp/tools/transforms.py +293 -0
- rmcp/tools/visualization.py +590 -0
- rmcp/transport/__init__.py +16 -0
- rmcp/transport/base.py +130 -0
- rmcp/transport/jsonrpc.py +243 -0
- rmcp/transport/stdio.py +201 -0
rmcp/transport/base.py
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
"""
|
2
|
+
Base transport interface for MCP server.
|
3
|
+
|
4
|
+
Defines the contract that all transports must implement,
|
5
|
+
enabling clean composition at the server edge.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from abc import ABC, abstractmethod
|
9
|
+
from typing import Any, Dict, Optional, AsyncIterator, Callable, Awaitable
|
10
|
+
import logging
|
11
|
+
|
12
|
+
logger = logging.getLogger(__name__)
|
13
|
+
|
14
|
+
|
15
|
+
class Transport(ABC):
|
16
|
+
"""
|
17
|
+
Abstract base class for MCP transports.
|
18
|
+
|
19
|
+
All transports must implement:
|
20
|
+
- Message receiving (async iterator)
|
21
|
+
- Message sending
|
22
|
+
- Lifecycle management (startup/shutdown)
|
23
|
+
- Error handling
|
24
|
+
"""
|
25
|
+
|
26
|
+
def __init__(self, name: str):
|
27
|
+
self.name = name
|
28
|
+
self._running = False
|
29
|
+
self._message_handler: Optional[Callable[[Dict[str, Any]], Awaitable[Dict[str, Any]]]] = None
|
30
|
+
|
31
|
+
def set_message_handler(self, handler: Callable[[Dict[str, Any]], Awaitable[Dict[str, Any]]]) -> None:
|
32
|
+
"""Set the message handler that will process incoming messages."""
|
33
|
+
self._message_handler = handler
|
34
|
+
|
35
|
+
@abstractmethod
|
36
|
+
async def startup(self) -> None:
|
37
|
+
"""Initialize the transport."""
|
38
|
+
logger.info(f"Starting {self.name} transport")
|
39
|
+
self._running = True
|
40
|
+
|
41
|
+
@abstractmethod
|
42
|
+
async def shutdown(self) -> None:
|
43
|
+
"""Clean up the transport."""
|
44
|
+
logger.info(f"Shutting down {self.name} transport")
|
45
|
+
self._running = False
|
46
|
+
|
47
|
+
@abstractmethod
|
48
|
+
async def receive_messages(self) -> AsyncIterator[Dict[str, Any]]:
|
49
|
+
"""
|
50
|
+
Async iterator that yields incoming messages.
|
51
|
+
|
52
|
+
Messages are already parsed from transport format (JSON-RPC).
|
53
|
+
"""
|
54
|
+
pass
|
55
|
+
|
56
|
+
@abstractmethod
|
57
|
+
async def send_message(self, message: Dict[str, Any]) -> None:
|
58
|
+
"""
|
59
|
+
Send a message via the transport.
|
60
|
+
|
61
|
+
Message will be encoded to transport format (JSON-RPC).
|
62
|
+
"""
|
63
|
+
pass
|
64
|
+
|
65
|
+
async def run(self) -> None:
|
66
|
+
"""
|
67
|
+
Run the transport event loop.
|
68
|
+
|
69
|
+
This is the main entry point that:
|
70
|
+
1. Starts the transport
|
71
|
+
2. Processes incoming messages
|
72
|
+
3. Handles errors gracefully
|
73
|
+
4. Ensures clean shutdown
|
74
|
+
"""
|
75
|
+
if not self._message_handler:
|
76
|
+
raise RuntimeError("Message handler not set")
|
77
|
+
|
78
|
+
try:
|
79
|
+
await self.startup()
|
80
|
+
|
81
|
+
async for message in self.receive_messages():
|
82
|
+
try:
|
83
|
+
# Process message through handler
|
84
|
+
response = await self._message_handler(message)
|
85
|
+
|
86
|
+
# Send response if there is one
|
87
|
+
if response:
|
88
|
+
await self.send_message(response)
|
89
|
+
|
90
|
+
except Exception as e:
|
91
|
+
logger.error(f"Error processing message: {e}")
|
92
|
+
|
93
|
+
# Send error response if possible
|
94
|
+
error_response = self._create_error_response(message, e)
|
95
|
+
if error_response:
|
96
|
+
try:
|
97
|
+
await self.send_message(error_response)
|
98
|
+
except Exception as send_error:
|
99
|
+
logger.error(f"Failed to send error response: {send_error}")
|
100
|
+
|
101
|
+
except Exception as e:
|
102
|
+
logger.error(f"Transport error: {e}")
|
103
|
+
|
104
|
+
finally:
|
105
|
+
await self.shutdown()
|
106
|
+
|
107
|
+
def _create_error_response(self, request: Dict[str, Any], error: Exception) -> Optional[Dict[str, Any]]:
|
108
|
+
"""Create an error response for a failed request."""
|
109
|
+
|
110
|
+
request_id = request.get("id")
|
111
|
+
if request_id is None:
|
112
|
+
# No ID means no response expected (notification)
|
113
|
+
return None
|
114
|
+
|
115
|
+
return {
|
116
|
+
"jsonrpc": "2.0",
|
117
|
+
"id": request_id,
|
118
|
+
"error": {
|
119
|
+
"code": -32603, # Internal error
|
120
|
+
"message": str(error),
|
121
|
+
"data": {
|
122
|
+
"type": type(error).__name__
|
123
|
+
}
|
124
|
+
}
|
125
|
+
}
|
126
|
+
|
127
|
+
@property
|
128
|
+
def is_running(self) -> bool:
|
129
|
+
"""Check if transport is running."""
|
130
|
+
return self._running
|
@@ -0,0 +1,243 @@
|
|
1
|
+
"""
|
2
|
+
JSON-RPC 2.0 envelope handling.
|
3
|
+
|
4
|
+
Implements proper JSON-RPC 2.0 specification:
|
5
|
+
- Request/response/notification parsing
|
6
|
+
- Error code handling per spec
|
7
|
+
- Message validation
|
8
|
+
- Single-line encoding for stdio transport
|
9
|
+
|
10
|
+
Following the principle: "No hand-rolled JSON-RPC, no 'close enough' message shapes."
|
11
|
+
"""
|
12
|
+
|
13
|
+
import json
|
14
|
+
import logging
|
15
|
+
from typing import Any, Dict, Optional, Union
|
16
|
+
from dataclasses import dataclass
|
17
|
+
|
18
|
+
logger = logging.getLogger(__name__)
|
19
|
+
|
20
|
+
|
21
|
+
class JSONRPCError(Exception):
|
22
|
+
"""JSON-RPC error with proper error code."""
|
23
|
+
|
24
|
+
# Standard JSON-RPC 2.0 error codes
|
25
|
+
PARSE_ERROR = -32700
|
26
|
+
INVALID_REQUEST = -32600
|
27
|
+
METHOD_NOT_FOUND = -32601
|
28
|
+
INVALID_PARAMS = -32602
|
29
|
+
INTERNAL_ERROR = -32603
|
30
|
+
|
31
|
+
def __init__(self, code: int, message: str, data: Any = None, request_id: Any = None):
|
32
|
+
super().__init__(message)
|
33
|
+
self.code = code
|
34
|
+
self.message = message
|
35
|
+
self.data = data
|
36
|
+
self.request_id = request_id
|
37
|
+
|
38
|
+
def to_dict(self) -> Dict[str, Any]:
|
39
|
+
"""Convert to JSON-RPC error response format."""
|
40
|
+
error_obj = {
|
41
|
+
"code": self.code,
|
42
|
+
"message": self.message
|
43
|
+
}
|
44
|
+
|
45
|
+
if self.data is not None:
|
46
|
+
error_obj["data"] = self.data
|
47
|
+
|
48
|
+
response = {
|
49
|
+
"jsonrpc": "2.0",
|
50
|
+
"id": self.request_id,
|
51
|
+
"error": error_obj
|
52
|
+
}
|
53
|
+
|
54
|
+
return response
|
55
|
+
|
56
|
+
|
57
|
+
@dataclass
|
58
|
+
class JSONRPCMessage:
|
59
|
+
"""Parsed JSON-RPC message."""
|
60
|
+
|
61
|
+
jsonrpc: str
|
62
|
+
id: Optional[Union[str, int, None]] = None
|
63
|
+
method: Optional[str] = None
|
64
|
+
params: Optional[Union[Dict[str, Any], list]] = None
|
65
|
+
result: Optional[Any] = None
|
66
|
+
error: Optional[Dict[str, Any]] = None
|
67
|
+
|
68
|
+
@property
|
69
|
+
def is_request(self) -> bool:
|
70
|
+
"""Check if this is a request message."""
|
71
|
+
return self.method is not None and self.id is not None
|
72
|
+
|
73
|
+
@property
|
74
|
+
def is_notification(self) -> bool:
|
75
|
+
"""Check if this is a notification message."""
|
76
|
+
return self.method is not None and self.id is None
|
77
|
+
|
78
|
+
@property
|
79
|
+
def is_response(self) -> bool:
|
80
|
+
"""Check if this is a response message."""
|
81
|
+
return self.method is None and (self.result is not None or self.error is not None)
|
82
|
+
|
83
|
+
|
84
|
+
class JSONRPCEnvelope:
|
85
|
+
"""JSON-RPC 2.0 message envelope handler."""
|
86
|
+
|
87
|
+
@staticmethod
|
88
|
+
def encode(message: Dict[str, Any]) -> str:
|
89
|
+
"""
|
90
|
+
Encode message to single-line JSON for stdio transport.
|
91
|
+
|
92
|
+
Following the principle: "One JSON-RPC message per line."
|
93
|
+
"""
|
94
|
+
try:
|
95
|
+
# Ensure no embedded newlines in the JSON
|
96
|
+
json_str = json.dumps(message, separators=(',', ':'), ensure_ascii=False)
|
97
|
+
|
98
|
+
# Validate it's actually single line
|
99
|
+
if '\n' in json_str or '\r' in json_str:
|
100
|
+
# This should not happen with separators, but be safe
|
101
|
+
json_str = json_str.replace('\n', '\\n').replace('\r', '\\r')
|
102
|
+
|
103
|
+
return json_str
|
104
|
+
|
105
|
+
except (TypeError, ValueError) as e:
|
106
|
+
raise JSONRPCError(
|
107
|
+
JSONRPCError.INTERNAL_ERROR,
|
108
|
+
f"Failed to encode JSON-RPC message: {e}",
|
109
|
+
data={"original_message": str(message)[:200]} # Truncate for safety
|
110
|
+
)
|
111
|
+
|
112
|
+
@staticmethod
|
113
|
+
def decode(line: str) -> JSONRPCMessage:
|
114
|
+
"""
|
115
|
+
Decode single line of JSON to JSON-RPC message.
|
116
|
+
|
117
|
+
Validates JSON-RPC 2.0 specification compliance.
|
118
|
+
"""
|
119
|
+
if not line.strip():
|
120
|
+
raise JSONRPCError(
|
121
|
+
JSONRPCError.INVALID_REQUEST,
|
122
|
+
"Empty message"
|
123
|
+
)
|
124
|
+
|
125
|
+
try:
|
126
|
+
data = json.loads(line.strip())
|
127
|
+
except json.JSONDecodeError as e:
|
128
|
+
raise JSONRPCError(
|
129
|
+
JSONRPCError.PARSE_ERROR,
|
130
|
+
f"Invalid JSON: {e}"
|
131
|
+
) from e
|
132
|
+
|
133
|
+
# Validate JSON-RPC 2.0 structure
|
134
|
+
if not isinstance(data, dict):
|
135
|
+
raise JSONRPCError(
|
136
|
+
JSONRPCError.INVALID_REQUEST,
|
137
|
+
"JSON-RPC message must be an object"
|
138
|
+
)
|
139
|
+
|
140
|
+
jsonrpc_version = data.get("jsonrpc")
|
141
|
+
if jsonrpc_version != "2.0":
|
142
|
+
raise JSONRPCError(
|
143
|
+
JSONRPCError.INVALID_REQUEST,
|
144
|
+
f"Invalid JSON-RPC version: {jsonrpc_version}. Must be '2.0'"
|
145
|
+
)
|
146
|
+
|
147
|
+
# Extract message components
|
148
|
+
message = JSONRPCMessage(
|
149
|
+
jsonrpc=jsonrpc_version,
|
150
|
+
id=data.get("id"),
|
151
|
+
method=data.get("method"),
|
152
|
+
params=data.get("params"),
|
153
|
+
result=data.get("result"),
|
154
|
+
error=data.get("error")
|
155
|
+
)
|
156
|
+
|
157
|
+
# Validate message type
|
158
|
+
if message.is_request or message.is_notification:
|
159
|
+
if not isinstance(message.method, str):
|
160
|
+
raise JSONRPCError(
|
161
|
+
JSONRPCError.INVALID_REQUEST,
|
162
|
+
"Method must be a string",
|
163
|
+
request_id=message.id
|
164
|
+
)
|
165
|
+
|
166
|
+
# Params are optional, but if present must be object or array
|
167
|
+
if message.params is not None:
|
168
|
+
if not isinstance(message.params, (dict, list)):
|
169
|
+
raise JSONRPCError(
|
170
|
+
JSONRPCError.INVALID_REQUEST,
|
171
|
+
"Params must be object or array",
|
172
|
+
request_id=message.id
|
173
|
+
)
|
174
|
+
|
175
|
+
elif message.is_response:
|
176
|
+
# Response must have either result or error, not both
|
177
|
+
has_result = message.result is not None
|
178
|
+
has_error = message.error is not None
|
179
|
+
|
180
|
+
if has_result and has_error:
|
181
|
+
raise JSONRPCError(
|
182
|
+
JSONRPCError.INVALID_REQUEST,
|
183
|
+
"Response cannot have both result and error",
|
184
|
+
request_id=message.id
|
185
|
+
)
|
186
|
+
|
187
|
+
if not has_result and not has_error:
|
188
|
+
raise JSONRPCError(
|
189
|
+
JSONRPCError.INVALID_REQUEST,
|
190
|
+
"Response must have either result or error",
|
191
|
+
request_id=message.id
|
192
|
+
)
|
193
|
+
|
194
|
+
else:
|
195
|
+
raise JSONRPCError(
|
196
|
+
JSONRPCError.INVALID_REQUEST,
|
197
|
+
"Invalid message type",
|
198
|
+
request_id=data.get("id")
|
199
|
+
)
|
200
|
+
|
201
|
+
return message
|
202
|
+
|
203
|
+
@staticmethod
|
204
|
+
def create_request(method: str, params: Optional[Union[Dict[str, Any], list]] = None,
|
205
|
+
request_id: Union[str, int] = "1") -> Dict[str, Any]:
|
206
|
+
"""Create a JSON-RPC 2.0 request."""
|
207
|
+
request = {
|
208
|
+
"jsonrpc": "2.0",
|
209
|
+
"method": method,
|
210
|
+
"id": request_id
|
211
|
+
}
|
212
|
+
|
213
|
+
if params is not None:
|
214
|
+
request["params"] = params
|
215
|
+
|
216
|
+
return request
|
217
|
+
|
218
|
+
@staticmethod
|
219
|
+
def create_notification(method: str, params: Optional[Union[Dict[str, Any], list]] = None) -> Dict[str, Any]:
|
220
|
+
"""Create a JSON-RPC 2.0 notification."""
|
221
|
+
notification = {
|
222
|
+
"jsonrpc": "2.0",
|
223
|
+
"method": method
|
224
|
+
}
|
225
|
+
|
226
|
+
if params is not None:
|
227
|
+
notification["params"] = params
|
228
|
+
|
229
|
+
return notification
|
230
|
+
|
231
|
+
@staticmethod
|
232
|
+
def create_response(request_id: Union[str, int], result: Any) -> Dict[str, Any]:
|
233
|
+
"""Create a JSON-RPC 2.0 success response."""
|
234
|
+
return {
|
235
|
+
"jsonrpc": "2.0",
|
236
|
+
"id": request_id,
|
237
|
+
"result": result
|
238
|
+
}
|
239
|
+
|
240
|
+
@staticmethod
|
241
|
+
def create_error_response(request_id: Union[str, int, None], error: JSONRPCError) -> Dict[str, Any]:
|
242
|
+
"""Create a JSON-RPC 2.0 error response."""
|
243
|
+
return error.to_dict()
|
rmcp/transport/stdio.py
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
"""
|
2
|
+
Stdio transport for MCP server.
|
3
|
+
|
4
|
+
Implements JSON-RPC 2.0 over stdin/stdout with proper hygiene:
|
5
|
+
- Never prints to stdout (only stderr for logging)
|
6
|
+
- One JSON message per line
|
7
|
+
- Proper error handling and cleanup
|
8
|
+
- Async I/O for non-blocking operation
|
9
|
+
|
10
|
+
Following mature MCP patterns: "stdio servers never print to stdout."
|
11
|
+
"""
|
12
|
+
|
13
|
+
import asyncio
|
14
|
+
import sys
|
15
|
+
import logging
|
16
|
+
from typing import Any, Dict, AsyncIterator
|
17
|
+
from ..transport.base import Transport
|
18
|
+
from ..transport.jsonrpc import JSONRPCEnvelope, JSONRPCError, JSONRPCMessage
|
19
|
+
|
20
|
+
# Configure logging to stderr only (never stdout)
|
21
|
+
logger = logging.getLogger(__name__)
|
22
|
+
|
23
|
+
|
24
|
+
class StdioTransport(Transport):
|
25
|
+
"""
|
26
|
+
Stdio transport using JSON-RPC 2.0 over stdin/stdout.
|
27
|
+
|
28
|
+
Key characteristics:
|
29
|
+
- Reads JSON-RPC messages from stdin (one per line)
|
30
|
+
- Writes JSON-RPC responses to stdout (one per line)
|
31
|
+
- Logs only to stderr (never stdout)
|
32
|
+
- Non-blocking async I/O
|
33
|
+
- Graceful error handling
|
34
|
+
"""
|
35
|
+
|
36
|
+
def __init__(self):
|
37
|
+
super().__init__("stdio")
|
38
|
+
self._stdin_reader: asyncio.StreamReader = None
|
39
|
+
self._stdout_writer: asyncio.StreamWriter = None
|
40
|
+
self._shutdown_event = asyncio.Event()
|
41
|
+
|
42
|
+
async def startup(self) -> None:
|
43
|
+
"""Initialize stdin/stdout streams."""
|
44
|
+
await super().startup()
|
45
|
+
|
46
|
+
# Set up async stdin/stdout
|
47
|
+
loop = asyncio.get_event_loop()
|
48
|
+
|
49
|
+
# Create stdin reader
|
50
|
+
self._stdin_reader = asyncio.StreamReader()
|
51
|
+
stdin_protocol = asyncio.StreamReaderProtocol(self._stdin_reader)
|
52
|
+
await loop.connect_read_pipe(lambda: stdin_protocol, sys.stdin)
|
53
|
+
|
54
|
+
# Create stdout writer
|
55
|
+
stdout_transport, stdout_protocol = await loop.connect_write_pipe(
|
56
|
+
asyncio.streams.FlowControlMixin, sys.stdout
|
57
|
+
)
|
58
|
+
self._stdout_writer = asyncio.StreamWriter(stdout_transport, stdout_protocol, None, loop)
|
59
|
+
|
60
|
+
logger.info("Stdio transport initialized", file=sys.stderr)
|
61
|
+
|
62
|
+
async def shutdown(self) -> None:
|
63
|
+
"""Clean up streams."""
|
64
|
+
await super().shutdown()
|
65
|
+
|
66
|
+
if self._stdout_writer:
|
67
|
+
self._stdout_writer.close()
|
68
|
+
await self._stdout_writer.wait_closed()
|
69
|
+
|
70
|
+
self._shutdown_event.set()
|
71
|
+
logger.info("Stdio transport shutdown complete", file=sys.stderr)
|
72
|
+
|
73
|
+
async def receive_messages(self) -> AsyncIterator[Dict[str, Any]]:
|
74
|
+
"""
|
75
|
+
Read JSON-RPC messages from stdin.
|
76
|
+
|
77
|
+
Yields parsed and validated JSON-RPC messages.
|
78
|
+
"""
|
79
|
+
if not self._stdin_reader:
|
80
|
+
raise RuntimeError("Transport not started")
|
81
|
+
|
82
|
+
logger.info("Starting to read messages from stdin", file=sys.stderr)
|
83
|
+
|
84
|
+
try:
|
85
|
+
while self._running and not self._shutdown_event.is_set():
|
86
|
+
try:
|
87
|
+
# Read one line from stdin
|
88
|
+
line = await asyncio.wait_for(
|
89
|
+
self._stdin_reader.readline(),
|
90
|
+
timeout=0.1 # Short timeout to check shutdown
|
91
|
+
)
|
92
|
+
|
93
|
+
if not line:
|
94
|
+
# EOF reached
|
95
|
+
logger.info("EOF reached on stdin", file=sys.stderr)
|
96
|
+
break
|
97
|
+
|
98
|
+
line_str = line.decode('utf-8').rstrip('\n\r')
|
99
|
+
if not line_str:
|
100
|
+
continue # Skip empty lines
|
101
|
+
|
102
|
+
logger.debug(f"Received message: {line_str[:100]}...", file=sys.stderr)
|
103
|
+
|
104
|
+
# Parse JSON-RPC message
|
105
|
+
try:
|
106
|
+
message = JSONRPCEnvelope.decode(line_str)
|
107
|
+
|
108
|
+
# Convert to dict for handler
|
109
|
+
message_dict = {
|
110
|
+
"jsonrpc": message.jsonrpc,
|
111
|
+
"id": message.id,
|
112
|
+
}
|
113
|
+
|
114
|
+
if message.method:
|
115
|
+
message_dict["method"] = message.method
|
116
|
+
|
117
|
+
if message.params is not None:
|
118
|
+
message_dict["params"] = message.params
|
119
|
+
|
120
|
+
if message.result is not None:
|
121
|
+
message_dict["result"] = message.result
|
122
|
+
|
123
|
+
if message.error is not None:
|
124
|
+
message_dict["error"] = message.error
|
125
|
+
|
126
|
+
yield message_dict
|
127
|
+
|
128
|
+
except JSONRPCError as e:
|
129
|
+
logger.error(f"JSON-RPC parse error: {e}", file=sys.stderr)
|
130
|
+
|
131
|
+
# Send error response if we can determine request ID
|
132
|
+
error_response = e.to_dict()
|
133
|
+
await self.send_message(error_response)
|
134
|
+
|
135
|
+
except asyncio.TimeoutError:
|
136
|
+
# Timeout is expected, just check if we should continue
|
137
|
+
continue
|
138
|
+
|
139
|
+
except Exception as e:
|
140
|
+
logger.error(f"Error reading from stdin: {e}", file=sys.stderr)
|
141
|
+
break
|
142
|
+
|
143
|
+
except Exception as e:
|
144
|
+
logger.error(f"Fatal error in message reception: {e}", file=sys.stderr)
|
145
|
+
|
146
|
+
finally:
|
147
|
+
logger.info("Stopped reading messages", file=sys.stderr)
|
148
|
+
|
149
|
+
async def send_message(self, message: Dict[str, Any]) -> None:
|
150
|
+
"""
|
151
|
+
Send JSON-RPC message to stdout.
|
152
|
+
|
153
|
+
Encodes message as single line JSON and writes to stdout.
|
154
|
+
"""
|
155
|
+
if not self._stdout_writer:
|
156
|
+
raise RuntimeError("Transport not started")
|
157
|
+
|
158
|
+
try:
|
159
|
+
# Encode to single-line JSON
|
160
|
+
json_line = JSONRPCEnvelope.encode(message)
|
161
|
+
|
162
|
+
logger.debug(f"Sending message: {json_line[:100]}...", file=sys.stderr)
|
163
|
+
|
164
|
+
# Write to stdout with newline
|
165
|
+
self._stdout_writer.write((json_line + '\n').encode('utf-8'))
|
166
|
+
await self._stdout_writer.drain()
|
167
|
+
|
168
|
+
except Exception as e:
|
169
|
+
logger.error(f"Error sending message: {e}", file=sys.stderr)
|
170
|
+
raise
|
171
|
+
|
172
|
+
async def send_notification(self, method: str, params: Any = None) -> None:
|
173
|
+
"""Send a JSON-RPC notification."""
|
174
|
+
notification = JSONRPCEnvelope.create_notification(method, params)
|
175
|
+
await self.send_message(notification)
|
176
|
+
|
177
|
+
async def send_progress_notification(self, token: str, value: int, total: int, message: str = "") -> None:
|
178
|
+
"""Send MCP progress notification."""
|
179
|
+
await self.send_notification("notifications/progress", {
|
180
|
+
"progressToken": token,
|
181
|
+
"progress": value,
|
182
|
+
"total": total,
|
183
|
+
"message": message
|
184
|
+
})
|
185
|
+
|
186
|
+
async def send_log_notification(self, level: str, message: str, data: Any = None) -> None:
|
187
|
+
"""Send MCP log notification."""
|
188
|
+
log_params = {
|
189
|
+
"level": level,
|
190
|
+
"message": message
|
191
|
+
}
|
192
|
+
|
193
|
+
if data:
|
194
|
+
log_params["data"] = data
|
195
|
+
|
196
|
+
await self.send_notification("notifications/message", log_params)
|
197
|
+
|
198
|
+
def request_shutdown(self) -> None:
|
199
|
+
"""Request graceful shutdown."""
|
200
|
+
logger.info("Shutdown requested", file=sys.stderr)
|
201
|
+
self._shutdown_event.set()
|