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/core/schemas.py
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
"""
|
2
|
+
JSON Schema validation helpers.
|
3
|
+
|
4
|
+
Provides utilities for:
|
5
|
+
- Schema validation with proper MCP error codes (-32602)
|
6
|
+
- Common schema patterns for statistical tools
|
7
|
+
- Type conversion helpers
|
8
|
+
"""
|
9
|
+
|
10
|
+
from typing import Any, Dict, List, Optional, Union
|
11
|
+
import json
|
12
|
+
import jsonschema
|
13
|
+
from jsonschema import validate, ValidationError as JsonSchemaValidationError
|
14
|
+
|
15
|
+
|
16
|
+
class SchemaError(Exception):
|
17
|
+
"""Schema validation error with MCP error code."""
|
18
|
+
|
19
|
+
def __init__(self, message: str, field: Optional[str] = None):
|
20
|
+
super().__init__(message)
|
21
|
+
self.field = field
|
22
|
+
self.code = -32602 # JSON-RPC invalid params error
|
23
|
+
|
24
|
+
|
25
|
+
def validate_schema(data: Any, schema: Dict[str, Any], context: str = "") -> None:
|
26
|
+
"""
|
27
|
+
Validate data against JSON schema.
|
28
|
+
|
29
|
+
Raises SchemaError with MCP-compatible error code on failure.
|
30
|
+
"""
|
31
|
+
try:
|
32
|
+
validate(instance=data, schema=schema)
|
33
|
+
except JsonSchemaValidationError as e:
|
34
|
+
field_path = ".".join(str(p) for p in e.absolute_path)
|
35
|
+
error_context = f" in {context}" if context else ""
|
36
|
+
field_info = f" (field: {field_path})" if field_path else ""
|
37
|
+
|
38
|
+
raise SchemaError(
|
39
|
+
f"Schema validation failed{error_context}: {e.message}{field_info}",
|
40
|
+
field=field_path
|
41
|
+
) from e
|
42
|
+
except Exception as e:
|
43
|
+
raise SchemaError(f"Schema validation error: {str(e)}") from e
|
44
|
+
|
45
|
+
|
46
|
+
# Common schema patterns for statistical tools
|
47
|
+
|
48
|
+
def table_schema(required_columns: Optional[List[str]] = None) -> Dict[str, Any]:
|
49
|
+
"""Schema for tabular data (dict with column arrays)."""
|
50
|
+
schema = {
|
51
|
+
"type": "object",
|
52
|
+
"properties": {},
|
53
|
+
"additionalProperties": {
|
54
|
+
"type": "array",
|
55
|
+
"items": {"type": ["number", "string", "null"]}
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
if required_columns:
|
60
|
+
schema["required"] = required_columns
|
61
|
+
for col in required_columns:
|
62
|
+
schema["properties"][col] = {
|
63
|
+
"type": "array",
|
64
|
+
"items": {"type": ["number", "string", "null"]}
|
65
|
+
}
|
66
|
+
|
67
|
+
return schema
|
68
|
+
|
69
|
+
|
70
|
+
def formula_schema() -> Dict[str, Any]:
|
71
|
+
"""Schema for R formula strings."""
|
72
|
+
return {
|
73
|
+
"type": "string",
|
74
|
+
"pattern": r"^[^~]+~[^~]+$",
|
75
|
+
"description": "R formula (e.g., 'y ~ x1 + x2')"
|
76
|
+
}
|
77
|
+
|
78
|
+
|
79
|
+
def numeric_array_schema(min_length: int = 1) -> Dict[str, Any]:
|
80
|
+
"""Schema for numeric arrays."""
|
81
|
+
return {
|
82
|
+
"type": "array",
|
83
|
+
"items": {"type": "number"},
|
84
|
+
"minItems": min_length
|
85
|
+
}
|
86
|
+
|
87
|
+
|
88
|
+
def positive_number_schema() -> Dict[str, Any]:
|
89
|
+
"""Schema for positive numbers."""
|
90
|
+
return {
|
91
|
+
"type": "number",
|
92
|
+
"minimum": 0,
|
93
|
+
"exclusiveMinimum": True
|
94
|
+
}
|
95
|
+
|
96
|
+
|
97
|
+
def confidence_level_schema() -> Dict[str, Any]:
|
98
|
+
"""Schema for confidence levels (0-1)."""
|
99
|
+
return {
|
100
|
+
"type": "number",
|
101
|
+
"minimum": 0,
|
102
|
+
"maximum": 1,
|
103
|
+
"exclusiveMinimum": True,
|
104
|
+
"exclusiveMaximum": True
|
105
|
+
}
|
106
|
+
|
107
|
+
|
108
|
+
def choice_schema(choices: List[str]) -> Dict[str, Any]:
|
109
|
+
"""Schema for enumerated choices."""
|
110
|
+
return {
|
111
|
+
"type": "string",
|
112
|
+
"enum": choices
|
113
|
+
}
|
114
|
+
|
115
|
+
|
116
|
+
# Tool result schemas
|
117
|
+
|
118
|
+
def statistical_result_schema() -> Dict[str, Any]:
|
119
|
+
"""Base schema for statistical analysis results."""
|
120
|
+
return {
|
121
|
+
"type": "object",
|
122
|
+
"properties": {
|
123
|
+
"success": {"type": "boolean"},
|
124
|
+
"message": {"type": "string"},
|
125
|
+
"data": {"type": "object"},
|
126
|
+
"metadata": {
|
127
|
+
"type": "object",
|
128
|
+
"properties": {
|
129
|
+
"method": {"type": "string"},
|
130
|
+
"n_obs": {"type": "integer", "minimum": 0},
|
131
|
+
"timestamp": {"type": "string", "format": "date-time"}
|
132
|
+
}
|
133
|
+
}
|
134
|
+
},
|
135
|
+
"required": ["success"]
|
136
|
+
}
|
137
|
+
|
138
|
+
|
139
|
+
def error_result_schema() -> Dict[str, Any]:
|
140
|
+
"""Schema for error results."""
|
141
|
+
return {
|
142
|
+
"type": "object",
|
143
|
+
"properties": {
|
144
|
+
"success": {"const": False},
|
145
|
+
"error": {
|
146
|
+
"type": "object",
|
147
|
+
"properties": {
|
148
|
+
"type": {"type": "string"},
|
149
|
+
"message": {"type": "string"},
|
150
|
+
"details": {"type": "object"}
|
151
|
+
},
|
152
|
+
"required": ["type", "message"]
|
153
|
+
}
|
154
|
+
},
|
155
|
+
"required": ["success", "error"]
|
156
|
+
}
|
rmcp/core/server.py
ADDED
@@ -0,0 +1,261 @@
|
|
1
|
+
"""
|
2
|
+
MCP Server shell with lifecycle hooks.
|
3
|
+
|
4
|
+
This module provides the main server class that:
|
5
|
+
- Initializes the MCP app using official SDK
|
6
|
+
- Manages lifespan hooks (startup/shutdown)
|
7
|
+
- Composes transports at the edge
|
8
|
+
- Centralizes registry management
|
9
|
+
|
10
|
+
Following the principle: "A single shell centralizes initialization and teardown."
|
11
|
+
"""
|
12
|
+
|
13
|
+
import asyncio
|
14
|
+
import logging
|
15
|
+
from typing import Any, Dict, Optional, Callable, Awaitable, List
|
16
|
+
from pathlib import Path
|
17
|
+
import sys
|
18
|
+
|
19
|
+
# Official MCP SDK imports (to be added when SDK is available)
|
20
|
+
# from mcp import Server, initialize_server
|
21
|
+
# from mcp.types import Request, Response, Notification
|
22
|
+
|
23
|
+
from .context import Context, LifespanState, RequestState
|
24
|
+
from ..registries.tools import ToolsRegistry
|
25
|
+
from ..registries.resources import ResourcesRegistry
|
26
|
+
from ..registries.prompts import PromptsRegistry
|
27
|
+
from ..security.vfs import VFS
|
28
|
+
|
29
|
+
logger = logging.getLogger(__name__)
|
30
|
+
|
31
|
+
|
32
|
+
class MCPServer:
|
33
|
+
"""
|
34
|
+
Main MCP server shell that manages lifecycle and registries.
|
35
|
+
|
36
|
+
Centralizes:
|
37
|
+
- Lifespan management (startup/shutdown)
|
38
|
+
- Registry composition (tools/resources/prompts)
|
39
|
+
- Security policy enforcement
|
40
|
+
- Transport-agnostic request handling
|
41
|
+
"""
|
42
|
+
|
43
|
+
def __init__(
|
44
|
+
self,
|
45
|
+
name: str = "RMCP MCP Server",
|
46
|
+
version: str = "0.3.2",
|
47
|
+
description: str = "R-based statistical analysis MCP server",
|
48
|
+
):
|
49
|
+
self.name = name
|
50
|
+
self.version = version
|
51
|
+
self.description = description
|
52
|
+
|
53
|
+
# Lifespan state
|
54
|
+
self.lifespan_state = LifespanState()
|
55
|
+
|
56
|
+
# Registries
|
57
|
+
self.tools = ToolsRegistry()
|
58
|
+
self.resources = ResourcesRegistry()
|
59
|
+
self.prompts = PromptsRegistry()
|
60
|
+
|
61
|
+
# Security
|
62
|
+
self.vfs: Optional[VFS] = None
|
63
|
+
|
64
|
+
# Callbacks
|
65
|
+
self._startup_callbacks: List[Callable[[], Awaitable[None]]] = []
|
66
|
+
self._shutdown_callbacks: List[Callable[[], Awaitable[None]]] = []
|
67
|
+
|
68
|
+
# Request tracking for cancellation
|
69
|
+
self._active_requests: Dict[str, RequestState] = {}
|
70
|
+
|
71
|
+
def configure(
|
72
|
+
self,
|
73
|
+
allowed_paths: Optional[List[str]] = None,
|
74
|
+
cache_root: Optional[str] = None,
|
75
|
+
read_only: bool = True,
|
76
|
+
**settings: Any,
|
77
|
+
) -> "MCPServer":
|
78
|
+
"""Configure server settings."""
|
79
|
+
|
80
|
+
if allowed_paths:
|
81
|
+
self.lifespan_state.allowed_paths = [Path(p) for p in allowed_paths]
|
82
|
+
|
83
|
+
if cache_root:
|
84
|
+
cache_path = Path(cache_root)
|
85
|
+
cache_path.mkdir(parents=True, exist_ok=True)
|
86
|
+
self.lifespan_state.cache_root = cache_path
|
87
|
+
|
88
|
+
self.lifespan_state.read_only = read_only
|
89
|
+
self.lifespan_state.settings.update(settings)
|
90
|
+
|
91
|
+
# Initialize VFS
|
92
|
+
self.vfs = VFS(
|
93
|
+
allowed_roots=self.lifespan_state.allowed_paths,
|
94
|
+
read_only=read_only
|
95
|
+
)
|
96
|
+
|
97
|
+
return self
|
98
|
+
|
99
|
+
def on_startup(self, func: Callable[[], Awaitable[None]]) -> Callable[[], Awaitable[None]]:
|
100
|
+
"""Register startup callback."""
|
101
|
+
self._startup_callbacks.append(func)
|
102
|
+
return func
|
103
|
+
|
104
|
+
def on_shutdown(self, func: Callable[[], Awaitable[None]]) -> Callable[[], Awaitable[None]]:
|
105
|
+
"""Register shutdown callback."""
|
106
|
+
self._shutdown_callbacks.append(func)
|
107
|
+
return func
|
108
|
+
|
109
|
+
async def startup(self) -> None:
|
110
|
+
"""Run startup callbacks."""
|
111
|
+
logger.info(f"Starting {self.name} v{self.version}")
|
112
|
+
|
113
|
+
for callback in self._startup_callbacks:
|
114
|
+
await callback()
|
115
|
+
|
116
|
+
logger.info("Server startup complete")
|
117
|
+
|
118
|
+
async def shutdown(self) -> None:
|
119
|
+
"""Run shutdown callbacks."""
|
120
|
+
logger.info("Shutting down server")
|
121
|
+
|
122
|
+
# Cancel active requests
|
123
|
+
for request in self._active_requests.values():
|
124
|
+
request.cancel()
|
125
|
+
|
126
|
+
# Run shutdown callbacks
|
127
|
+
for callback in self._shutdown_callbacks:
|
128
|
+
try:
|
129
|
+
await callback()
|
130
|
+
except Exception as e:
|
131
|
+
logger.error(f"Error in shutdown callback: {e}")
|
132
|
+
|
133
|
+
logger.info("Server shutdown complete")
|
134
|
+
|
135
|
+
def create_context(
|
136
|
+
self,
|
137
|
+
request_id: str,
|
138
|
+
method: str,
|
139
|
+
progress_token: Optional[str] = None,
|
140
|
+
) -> Context:
|
141
|
+
"""Create request context."""
|
142
|
+
|
143
|
+
async def progress_callback(message: str, current: int, total: int) -> None:
|
144
|
+
# TODO: Send MCP progress notification
|
145
|
+
logger.info(f"Progress {request_id}: {message} ({current}/{total})")
|
146
|
+
|
147
|
+
async def log_callback(level: str, message: str, data: Dict[str, Any]) -> None:
|
148
|
+
# TODO: Send MCP log notification
|
149
|
+
log_level = getattr(logging, level.upper(), logging.INFO)
|
150
|
+
logger.log(log_level, f"{request_id}: {message} {data}")
|
151
|
+
|
152
|
+
context = Context.create(
|
153
|
+
request_id=request_id,
|
154
|
+
method=method,
|
155
|
+
lifespan_state=self.lifespan_state,
|
156
|
+
progress_token=progress_token,
|
157
|
+
progress_callback=progress_callback,
|
158
|
+
log_callback=log_callback,
|
159
|
+
)
|
160
|
+
|
161
|
+
# Track request for cancellation
|
162
|
+
self._active_requests[request_id] = context.request
|
163
|
+
|
164
|
+
return context
|
165
|
+
|
166
|
+
def finish_request(self, request_id: str) -> None:
|
167
|
+
"""Clean up request tracking."""
|
168
|
+
self._active_requests.pop(request_id, None)
|
169
|
+
|
170
|
+
async def cancel_request(self, request_id: str) -> None:
|
171
|
+
"""Cancel active request."""
|
172
|
+
if request_id in self._active_requests:
|
173
|
+
self._active_requests[request_id].cancel()
|
174
|
+
logger.info(f"Cancelled request {request_id}")
|
175
|
+
|
176
|
+
async def handle_request(self, request: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
177
|
+
"""
|
178
|
+
Handle incoming MCP request.
|
179
|
+
|
180
|
+
This is the main dispatch point that routes requests to appropriate handlers.
|
181
|
+
Returns None for notifications (no response expected).
|
182
|
+
"""
|
183
|
+
method = request.get("method")
|
184
|
+
request_id = request.get("id")
|
185
|
+
params = request.get("params", {})
|
186
|
+
|
187
|
+
# Handle notifications (no response expected)
|
188
|
+
if request_id is None:
|
189
|
+
await self._handle_notification(method, params)
|
190
|
+
return None
|
191
|
+
|
192
|
+
try:
|
193
|
+
context = self.create_context(request_id, method)
|
194
|
+
|
195
|
+
# Route to appropriate handler
|
196
|
+
if method == "tools/list":
|
197
|
+
result = await self.tools.list_tools(context)
|
198
|
+
elif method == "tools/call":
|
199
|
+
tool_name = params.get("name")
|
200
|
+
arguments = params.get("arguments", {})
|
201
|
+
result = await self.tools.call_tool(context, tool_name, arguments)
|
202
|
+
elif method == "resources/list":
|
203
|
+
result = await self.resources.list_resources(context)
|
204
|
+
elif method == "resources/read":
|
205
|
+
uri = params.get("uri")
|
206
|
+
result = await self.resources.read_resource(context, uri)
|
207
|
+
elif method == "prompts/list":
|
208
|
+
result = await self.prompts.list_prompts(context)
|
209
|
+
elif method == "prompts/get":
|
210
|
+
name = params.get("name")
|
211
|
+
arguments = params.get("arguments", {})
|
212
|
+
result = await self.prompts.get_prompt(context, name, arguments)
|
213
|
+
else:
|
214
|
+
raise ValueError(f"Unknown method: {method}")
|
215
|
+
|
216
|
+
return {
|
217
|
+
"jsonrpc": "2.0",
|
218
|
+
"id": request_id,
|
219
|
+
"result": result
|
220
|
+
}
|
221
|
+
|
222
|
+
except Exception as e:
|
223
|
+
logger.error(f"Error handling request {request_id}: {e}")
|
224
|
+
return {
|
225
|
+
"jsonrpc": "2.0",
|
226
|
+
"id": request_id,
|
227
|
+
"error": {
|
228
|
+
"code": -32603, # Internal error
|
229
|
+
"message": str(e)
|
230
|
+
}
|
231
|
+
}
|
232
|
+
|
233
|
+
finally:
|
234
|
+
if request_id:
|
235
|
+
self.finish_request(request_id)
|
236
|
+
|
237
|
+
async def _handle_notification(self, method: str, params: Dict[str, Any]) -> None:
|
238
|
+
"""Handle notification messages (no response expected)."""
|
239
|
+
logger.info(f"Received notification: {method}")
|
240
|
+
|
241
|
+
if method == "notifications/cancelled":
|
242
|
+
# Handle cancellation notification
|
243
|
+
request_id = params.get("requestId")
|
244
|
+
if request_id:
|
245
|
+
await self.cancel_request(request_id)
|
246
|
+
|
247
|
+
elif method == "notifications/initialized":
|
248
|
+
# MCP initialization complete
|
249
|
+
logger.info("MCP client initialization complete")
|
250
|
+
|
251
|
+
else:
|
252
|
+
logger.warning(f"Unknown notification method: {method}")
|
253
|
+
|
254
|
+
|
255
|
+
def create_server(
|
256
|
+
name: str = "RMCP MCP Server",
|
257
|
+
version: str = "0.3.2",
|
258
|
+
description: str = "R-based statistical analysis MCP server",
|
259
|
+
) -> MCPServer:
|
260
|
+
"""Create and return a new MCP server instance."""
|
261
|
+
return MCPServer(name=name, version=version, description=description)
|
rmcp/r_integration.py
ADDED
@@ -0,0 +1,112 @@
|
|
1
|
+
"""
|
2
|
+
Clean R integration for statistical analysis.
|
3
|
+
|
4
|
+
Extracted working R execution functionality without dependencies.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import os
|
8
|
+
import json
|
9
|
+
import tempfile
|
10
|
+
import subprocess
|
11
|
+
import logging
|
12
|
+
from typing import Dict, Any
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
class RExecutionError(Exception):
|
18
|
+
"""Exception raised when R script execution fails."""
|
19
|
+
|
20
|
+
def __init__(self, message: str, stdout: str = "", stderr: str = "", returncode: int = None):
|
21
|
+
super().__init__(message)
|
22
|
+
self.stdout = stdout
|
23
|
+
self.stderr = stderr
|
24
|
+
self.returncode = returncode
|
25
|
+
|
26
|
+
|
27
|
+
def execute_r_script(script: str, args: Dict[str, Any]) -> Dict[str, Any]:
|
28
|
+
"""
|
29
|
+
Execute an R script with the given arguments and return the results.
|
30
|
+
|
31
|
+
Args:
|
32
|
+
script: R script code to execute
|
33
|
+
args: Dictionary of arguments to pass to the R script
|
34
|
+
|
35
|
+
Returns:
|
36
|
+
Dictionary containing the results from R script execution
|
37
|
+
|
38
|
+
Raises:
|
39
|
+
RExecutionError: If R script execution fails
|
40
|
+
FileNotFoundError: If R is not installed or not in PATH
|
41
|
+
json.JSONDecodeError: If R script returns invalid JSON
|
42
|
+
"""
|
43
|
+
with tempfile.NamedTemporaryFile(suffix='.R', delete=False, mode='w') as script_file, \
|
44
|
+
tempfile.NamedTemporaryFile(suffix='.json', delete=False, mode='w') as args_file, \
|
45
|
+
tempfile.NamedTemporaryFile(suffix='.json', delete=False) as result_file:
|
46
|
+
|
47
|
+
script_path = script_file.name
|
48
|
+
args_path = args_file.name
|
49
|
+
result_path = result_file.name
|
50
|
+
|
51
|
+
try:
|
52
|
+
# Write arguments to JSON file
|
53
|
+
json.dump(args, args_file, default=str)
|
54
|
+
args_file.flush()
|
55
|
+
|
56
|
+
# Create complete R script
|
57
|
+
full_script = f'''
|
58
|
+
# Load required libraries
|
59
|
+
library(jsonlite)
|
60
|
+
|
61
|
+
# Load arguments
|
62
|
+
args <- fromJSON("{args_path}")
|
63
|
+
|
64
|
+
# User script
|
65
|
+
{script}
|
66
|
+
|
67
|
+
# Write result
|
68
|
+
write_json(result, "{result_path}", auto_unbox = TRUE)
|
69
|
+
'''
|
70
|
+
|
71
|
+
script_file.write(full_script)
|
72
|
+
script_file.flush()
|
73
|
+
|
74
|
+
logger.debug(f"Executing R script with args: {args}")
|
75
|
+
|
76
|
+
# Execute R script
|
77
|
+
process = subprocess.run(
|
78
|
+
['R', '--slave', '--no-restore', '--file=' + script_path],
|
79
|
+
capture_output=True,
|
80
|
+
text=True,
|
81
|
+
timeout=120
|
82
|
+
)
|
83
|
+
|
84
|
+
if process.returncode != 0:
|
85
|
+
error_msg = f"R script failed with return code {process.returncode}"
|
86
|
+
logger.error(f"{error_msg}\\nStderr: {process.stderr}")
|
87
|
+
raise RExecutionError(
|
88
|
+
error_msg,
|
89
|
+
stdout=process.stdout,
|
90
|
+
stderr=process.stderr,
|
91
|
+
returncode=process.returncode
|
92
|
+
)
|
93
|
+
|
94
|
+
# Read results
|
95
|
+
try:
|
96
|
+
with open(result_path, 'r') as f:
|
97
|
+
result = json.load(f)
|
98
|
+
logger.debug(f"R script executed successfully, result: {result}")
|
99
|
+
return result
|
100
|
+
except FileNotFoundError:
|
101
|
+
raise RExecutionError("R script did not produce output file")
|
102
|
+
except json.JSONDecodeError as e:
|
103
|
+
raise RExecutionError(f"R script produced invalid JSON: {e}")
|
104
|
+
|
105
|
+
finally:
|
106
|
+
# Cleanup temporary files
|
107
|
+
for temp_path in [script_path, args_path, result_path]:
|
108
|
+
try:
|
109
|
+
os.unlink(temp_path)
|
110
|
+
logger.debug(f"Cleaned up temporary file: {temp_path}")
|
111
|
+
except OSError:
|
112
|
+
pass
|
@@ -0,0 +1,26 @@
|
|
1
|
+
"""
|
2
|
+
Registry system for MCP server capabilities.
|
3
|
+
|
4
|
+
The registry pattern provides clean separation between:
|
5
|
+
- Protocol concerns (MCP message handling)
|
6
|
+
- Registry concerns (capability discovery and dispatch)
|
7
|
+
- Domain concerns (actual tool/resource/prompt logic)
|
8
|
+
|
9
|
+
This enables:
|
10
|
+
- Independent testing of domain logic
|
11
|
+
- Clean capability declaration
|
12
|
+
- Type-safe interfaces
|
13
|
+
"""
|
14
|
+
|
15
|
+
from .tools import ToolsRegistry, tool
|
16
|
+
from .resources import ResourcesRegistry, resource
|
17
|
+
from .prompts import PromptsRegistry, prompt
|
18
|
+
|
19
|
+
__all__ = [
|
20
|
+
"ToolsRegistry",
|
21
|
+
"ResourcesRegistry",
|
22
|
+
"PromptsRegistry",
|
23
|
+
"tool",
|
24
|
+
"resource",
|
25
|
+
"prompt",
|
26
|
+
]
|