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
@@ -0,0 +1,316 @@
|
|
1
|
+
"""
|
2
|
+
Prompts registry for MCP server.
|
3
|
+
|
4
|
+
Implements mature MCP patterns:
|
5
|
+
- Templated workflows that teach LLMs tool chaining
|
6
|
+
- Parameterized prompts with typed arguments
|
7
|
+
- Statistical analysis playbooks
|
8
|
+
|
9
|
+
Following the principle: "Ship prompts as workflows."
|
10
|
+
"""
|
11
|
+
|
12
|
+
from typing import Any, Dict, List, Optional, Callable
|
13
|
+
from dataclasses import dataclass
|
14
|
+
import logging
|
15
|
+
|
16
|
+
from ..core.context import Context
|
17
|
+
from ..core.schemas import validate_schema, SchemaError
|
18
|
+
|
19
|
+
logger = logging.getLogger(__name__)
|
20
|
+
|
21
|
+
|
22
|
+
@dataclass
|
23
|
+
class PromptDefinition:
|
24
|
+
"""Prompt template metadata and content."""
|
25
|
+
|
26
|
+
name: str
|
27
|
+
title: str
|
28
|
+
description: str
|
29
|
+
arguments_schema: Optional[Dict[str, Any]]
|
30
|
+
template: str
|
31
|
+
annotations: Optional[Dict[str, Any]] = None
|
32
|
+
|
33
|
+
|
34
|
+
class PromptsRegistry:
|
35
|
+
"""Registry for MCP prompts with templating support."""
|
36
|
+
|
37
|
+
def __init__(self):
|
38
|
+
self._prompts: Dict[str, PromptDefinition] = {}
|
39
|
+
|
40
|
+
def register(
|
41
|
+
self,
|
42
|
+
name: str,
|
43
|
+
title: str,
|
44
|
+
description: str,
|
45
|
+
template: str,
|
46
|
+
arguments_schema: Optional[Dict[str, Any]] = None,
|
47
|
+
annotations: Optional[Dict[str, Any]] = None,
|
48
|
+
) -> None:
|
49
|
+
"""Register a prompt template."""
|
50
|
+
|
51
|
+
if name in self._prompts:
|
52
|
+
logger.warning(f"Prompt '{name}' already registered, overwriting")
|
53
|
+
|
54
|
+
self._prompts[name] = PromptDefinition(
|
55
|
+
name=name,
|
56
|
+
title=title,
|
57
|
+
description=description,
|
58
|
+
template=template,
|
59
|
+
arguments_schema=arguments_schema,
|
60
|
+
annotations=annotations or {},
|
61
|
+
)
|
62
|
+
|
63
|
+
logger.debug(f"Registered prompt: {name}")
|
64
|
+
|
65
|
+
async def list_prompts(self, context: Context) -> Dict[str, Any]:
|
66
|
+
"""List available prompts for MCP prompts/list."""
|
67
|
+
|
68
|
+
prompts = []
|
69
|
+
for prompt_def in self._prompts.values():
|
70
|
+
prompt_info = {
|
71
|
+
"name": prompt_def.name,
|
72
|
+
"title": prompt_def.title,
|
73
|
+
"description": prompt_def.description,
|
74
|
+
}
|
75
|
+
|
76
|
+
if prompt_def.arguments_schema:
|
77
|
+
prompt_info["argumentsSchema"] = prompt_def.arguments_schema
|
78
|
+
|
79
|
+
if prompt_def.annotations:
|
80
|
+
prompt_info["annotations"] = prompt_def.annotations
|
81
|
+
|
82
|
+
prompts.append(prompt_info)
|
83
|
+
|
84
|
+
await context.info(f"Listed {len(prompts)} available prompts")
|
85
|
+
|
86
|
+
return {"prompts": prompts}
|
87
|
+
|
88
|
+
async def get_prompt(
|
89
|
+
self,
|
90
|
+
context: Context,
|
91
|
+
name: str,
|
92
|
+
arguments: Optional[Dict[str, Any]] = None
|
93
|
+
) -> Dict[str, Any]:
|
94
|
+
"""Get a rendered prompt for MCP prompts/get."""
|
95
|
+
|
96
|
+
if name not in self._prompts:
|
97
|
+
raise ValueError(f"Unknown prompt: {name}")
|
98
|
+
|
99
|
+
prompt_def = self._prompts[name]
|
100
|
+
arguments = arguments or {}
|
101
|
+
|
102
|
+
try:
|
103
|
+
# Validate arguments if schema provided
|
104
|
+
if prompt_def.arguments_schema:
|
105
|
+
validate_schema(
|
106
|
+
arguments,
|
107
|
+
prompt_def.arguments_schema,
|
108
|
+
f"prompt '{name}' arguments"
|
109
|
+
)
|
110
|
+
|
111
|
+
# Render template
|
112
|
+
rendered_content = self._render_template(prompt_def.template, arguments)
|
113
|
+
|
114
|
+
await context.info(f"Rendered prompt: {name}", arguments=arguments)
|
115
|
+
|
116
|
+
return {
|
117
|
+
"messages": [
|
118
|
+
{
|
119
|
+
"role": "user",
|
120
|
+
"content": {
|
121
|
+
"type": "text",
|
122
|
+
"text": rendered_content
|
123
|
+
}
|
124
|
+
}
|
125
|
+
]
|
126
|
+
}
|
127
|
+
|
128
|
+
except SchemaError as e:
|
129
|
+
await context.error(f"Schema validation failed for prompt '{name}': {e}")
|
130
|
+
raise
|
131
|
+
|
132
|
+
except Exception as e:
|
133
|
+
await context.error(f"Failed to render prompt '{name}': {e}")
|
134
|
+
raise
|
135
|
+
|
136
|
+
def _render_template(self, template: str, arguments: Dict[str, Any]) -> str:
|
137
|
+
"""Render template with arguments using simple string formatting."""
|
138
|
+
|
139
|
+
try:
|
140
|
+
# Use simple string formatting for now
|
141
|
+
# Could be enhanced with Jinja2 or similar
|
142
|
+
return template.format(**arguments)
|
143
|
+
except KeyError as e:
|
144
|
+
raise ValueError(f"Missing template argument: {e}")
|
145
|
+
except Exception as e:
|
146
|
+
raise ValueError(f"Template rendering error: {e}")
|
147
|
+
|
148
|
+
|
149
|
+
def prompt(
|
150
|
+
name: str,
|
151
|
+
title: str,
|
152
|
+
description: str,
|
153
|
+
arguments_schema: Optional[Dict[str, Any]] = None,
|
154
|
+
annotations: Optional[Dict[str, Any]] = None,
|
155
|
+
):
|
156
|
+
"""
|
157
|
+
Decorator to register a prompt template.
|
158
|
+
|
159
|
+
Usage:
|
160
|
+
@prompt(
|
161
|
+
name="analyze_workflow",
|
162
|
+
title="Statistical Analysis Workflow",
|
163
|
+
description="Guide for comprehensive statistical analysis",
|
164
|
+
arguments_schema={
|
165
|
+
"type": "object",
|
166
|
+
"properties": {
|
167
|
+
"dataset_name": {"type": "string"},
|
168
|
+
"analysis_type": {"type": "string", "enum": ["descriptive", "inferential", "predictive"]}
|
169
|
+
},
|
170
|
+
"required": ["dataset_name"]
|
171
|
+
}
|
172
|
+
)
|
173
|
+
def analyze_workflow():
|
174
|
+
return '''
|
175
|
+
I'll help you analyze the {dataset_name} dataset using {analysis_type} methods.
|
176
|
+
|
177
|
+
Let me start by examining the data structure and then proceed with the analysis.
|
178
|
+
'''
|
179
|
+
"""
|
180
|
+
|
181
|
+
def decorator(func: Callable[[], str]):
|
182
|
+
|
183
|
+
# Extract template content from function
|
184
|
+
template_content = func()
|
185
|
+
|
186
|
+
# Store prompt metadata on function
|
187
|
+
func._mcp_prompt_name = name
|
188
|
+
func._mcp_prompt_title = title
|
189
|
+
func._mcp_prompt_description = description
|
190
|
+
func._mcp_prompt_template = template_content
|
191
|
+
func._mcp_prompt_arguments_schema = arguments_schema
|
192
|
+
func._mcp_prompt_annotations = annotations
|
193
|
+
|
194
|
+
return func
|
195
|
+
|
196
|
+
return decorator
|
197
|
+
|
198
|
+
|
199
|
+
def register_prompt_functions(registry: PromptsRegistry, *functions) -> None:
|
200
|
+
"""Register multiple functions decorated with @prompt."""
|
201
|
+
|
202
|
+
for func in functions:
|
203
|
+
if hasattr(func, '_mcp_prompt_name'):
|
204
|
+
registry.register(
|
205
|
+
name=func._mcp_prompt_name,
|
206
|
+
title=func._mcp_prompt_title,
|
207
|
+
description=func._mcp_prompt_description,
|
208
|
+
template=func._mcp_prompt_template,
|
209
|
+
arguments_schema=func._mcp_prompt_arguments_schema,
|
210
|
+
annotations=func._mcp_prompt_annotations,
|
211
|
+
)
|
212
|
+
else:
|
213
|
+
logger.warning(f"Function {func.__name__} not decorated with @prompt, skipping")
|
214
|
+
|
215
|
+
|
216
|
+
# Built-in statistical analysis workflow prompts
|
217
|
+
|
218
|
+
@prompt(
|
219
|
+
name="statistical_workflow",
|
220
|
+
title="Statistical Analysis Workflow",
|
221
|
+
description="Comprehensive workflow for statistical data analysis",
|
222
|
+
arguments_schema={
|
223
|
+
"type": "object",
|
224
|
+
"properties": {
|
225
|
+
"dataset_name": {"type": "string"},
|
226
|
+
"analysis_goals": {"type": "string"},
|
227
|
+
"variables_of_interest": {"type": "array", "items": {"type": "string"}},
|
228
|
+
},
|
229
|
+
"required": ["dataset_name", "analysis_goals"]
|
230
|
+
}
|
231
|
+
)
|
232
|
+
def statistical_workflow_prompt():
|
233
|
+
return """I'll help you conduct a comprehensive statistical analysis of the {dataset_name} dataset.
|
234
|
+
|
235
|
+
Analysis Goals: {analysis_goals}
|
236
|
+
Variables of Interest: {variables_of_interest}
|
237
|
+
|
238
|
+
Let me guide you through a systematic analysis workflow:
|
239
|
+
|
240
|
+
**Phase 1: Data Exploration**
|
241
|
+
1. First, I'll examine the dataset structure and summary statistics
|
242
|
+
2. Check for missing values, outliers, and data quality issues
|
243
|
+
3. Visualize distributions and relationships between variables
|
244
|
+
|
245
|
+
**Phase 2: Analysis Selection**
|
246
|
+
Based on your goals and data characteristics, I'll recommend:
|
247
|
+
- Appropriate statistical tests or models
|
248
|
+
- Required assumptions and validation steps
|
249
|
+
- Visualization strategies
|
250
|
+
|
251
|
+
**Phase 3: Statistical Analysis**
|
252
|
+
I'll execute the analysis using appropriate tools and provide:
|
253
|
+
- Statistical results with interpretation
|
254
|
+
- Effect sizes and confidence intervals
|
255
|
+
- Diagnostic plots and assumption checking
|
256
|
+
|
257
|
+
**Phase 4: Results & Recommendations**
|
258
|
+
Finally, I'll summarize:
|
259
|
+
- Key findings and their practical significance
|
260
|
+
- Limitations and caveats
|
261
|
+
- Recommendations for next steps
|
262
|
+
|
263
|
+
Let's begin by examining your dataset. Please provide the data or specify how to access it."""
|
264
|
+
|
265
|
+
|
266
|
+
@prompt(
|
267
|
+
name="model_diagnostic_workflow",
|
268
|
+
title="Model Diagnostics Workflow",
|
269
|
+
description="Systematic model validation and diagnostics",
|
270
|
+
arguments_schema={
|
271
|
+
"type": "object",
|
272
|
+
"properties": {
|
273
|
+
"model_type": {"type": "string", "enum": ["linear", "logistic", "time_series", "ml"]},
|
274
|
+
"focus_areas": {"type": "array", "items": {"type": "string"}},
|
275
|
+
},
|
276
|
+
"required": ["model_type"]
|
277
|
+
}
|
278
|
+
)
|
279
|
+
def model_diagnostic_prompt():
|
280
|
+
return """I'll help you conduct thorough diagnostics for your {model_type} model.
|
281
|
+
|
282
|
+
Focus Areas: {focus_areas}
|
283
|
+
|
284
|
+
**Model Diagnostic Workflow**
|
285
|
+
|
286
|
+
**1. Residual Analysis**
|
287
|
+
- Plot residuals vs fitted values
|
288
|
+
- Check for patterns indicating model misspecification
|
289
|
+
- Assess homoscedasticity assumptions
|
290
|
+
|
291
|
+
**2. Distribution Checks**
|
292
|
+
- Q-Q plots for normality assessment
|
293
|
+
- Histogram of residuals
|
294
|
+
- Statistical tests for distributional assumptions
|
295
|
+
|
296
|
+
**3. Influence & Outliers**
|
297
|
+
- Identify high-leverage points
|
298
|
+
- Cook's distance for influential observations
|
299
|
+
- Studentized residuals analysis
|
300
|
+
|
301
|
+
**4. Model Assumptions**
|
302
|
+
- Linearity checks (for linear models)
|
303
|
+
- Independence verification
|
304
|
+
- Multicollinearity assessment (VIF)
|
305
|
+
|
306
|
+
**5. Predictive Performance**
|
307
|
+
- Cross-validation results
|
308
|
+
- Out-of-sample performance metrics
|
309
|
+
- Calibration plots (for probabilistic models)
|
310
|
+
|
311
|
+
**6. Interpretation & Validation**
|
312
|
+
- Coefficient stability
|
313
|
+
- Bootstrap confidence intervals
|
314
|
+
- Sensitivity analysis
|
315
|
+
|
316
|
+
Let's start the diagnostic process. Please provide your model results or specify how to access the fitted model."""
|
@@ -0,0 +1,266 @@
|
|
1
|
+
"""
|
2
|
+
Resources registry for MCP server.
|
3
|
+
|
4
|
+
Implements mature MCP patterns:
|
5
|
+
- Read-only endpoints for files and in-memory objects
|
6
|
+
- URI-based addressing (file://, mem://)
|
7
|
+
- Resource templates for parameterized access
|
8
|
+
- VFS integration for security
|
9
|
+
|
10
|
+
Following the principle: "Keeps data access explicit and auditable."
|
11
|
+
"""
|
12
|
+
|
13
|
+
from typing import Any, Dict, List, Optional, Union, Callable, Awaitable
|
14
|
+
from urllib.parse import urlparse, parse_qs
|
15
|
+
from pathlib import Path
|
16
|
+
import base64
|
17
|
+
import logging
|
18
|
+
|
19
|
+
from ..core.context import Context
|
20
|
+
from ..security.vfs import VFS, VFSError
|
21
|
+
|
22
|
+
logger = logging.getLogger(__name__)
|
23
|
+
|
24
|
+
|
25
|
+
class ResourcesRegistry:
|
26
|
+
"""Registry for MCP resources with VFS security."""
|
27
|
+
|
28
|
+
def __init__(self):
|
29
|
+
self._static_resources: Dict[str, Dict[str, Any]] = {}
|
30
|
+
self._memory_objects: Dict[str, Any] = {}
|
31
|
+
self._resource_templates: Dict[str, str] = {}
|
32
|
+
|
33
|
+
def register_static_resource(
|
34
|
+
self,
|
35
|
+
uri: str,
|
36
|
+
name: str,
|
37
|
+
description: Optional[str] = None,
|
38
|
+
mime_type: Optional[str] = None,
|
39
|
+
) -> None:
|
40
|
+
"""Register a static resource."""
|
41
|
+
|
42
|
+
self._static_resources[uri] = {
|
43
|
+
"uri": uri,
|
44
|
+
"name": name,
|
45
|
+
"description": description or f"Resource: {name}",
|
46
|
+
"mimeType": mime_type,
|
47
|
+
}
|
48
|
+
|
49
|
+
logger.debug(f"Registered static resource: {uri}")
|
50
|
+
|
51
|
+
def register_memory_object(
|
52
|
+
self,
|
53
|
+
name: str,
|
54
|
+
data: Any,
|
55
|
+
description: Optional[str] = None,
|
56
|
+
mime_type: str = "application/json",
|
57
|
+
) -> None:
|
58
|
+
"""Register an in-memory object as a resource."""
|
59
|
+
|
60
|
+
uri = f"mem://object/{name}"
|
61
|
+
self._memory_objects[name] = data
|
62
|
+
|
63
|
+
self._static_resources[uri] = {
|
64
|
+
"uri": uri,
|
65
|
+
"name": name,
|
66
|
+
"description": description or f"Memory object: {name}",
|
67
|
+
"mimeType": mime_type,
|
68
|
+
}
|
69
|
+
|
70
|
+
logger.debug(f"Registered memory object: {name}")
|
71
|
+
|
72
|
+
def register_resource_template(
|
73
|
+
self,
|
74
|
+
uri_template: str,
|
75
|
+
name: str,
|
76
|
+
description: Optional[str] = None,
|
77
|
+
) -> None:
|
78
|
+
"""Register a parameterized resource template."""
|
79
|
+
|
80
|
+
self._resource_templates[uri_template] = name
|
81
|
+
|
82
|
+
logger.debug(f"Registered resource template: {uri_template}")
|
83
|
+
|
84
|
+
async def list_resources(self, context: Context) -> Dict[str, Any]:
|
85
|
+
"""List available resources for MCP resources/list."""
|
86
|
+
|
87
|
+
resources = []
|
88
|
+
|
89
|
+
# Static resources
|
90
|
+
for resource_info in self._static_resources.values():
|
91
|
+
resources.append(resource_info)
|
92
|
+
|
93
|
+
# Resource templates
|
94
|
+
for uri_template, name in self._resource_templates.items():
|
95
|
+
resources.append({
|
96
|
+
"uri": uri_template,
|
97
|
+
"name": name,
|
98
|
+
"description": f"Template: {name}",
|
99
|
+
})
|
100
|
+
|
101
|
+
# File system resources (if VFS configured)
|
102
|
+
if hasattr(context.lifespan, 'vfs') and context.lifespan.vfs:
|
103
|
+
for mount_name, mount_path in context.lifespan.resource_mounts.items():
|
104
|
+
resources.append({
|
105
|
+
"uri": f"file://{mount_name}/",
|
106
|
+
"name": f"Files: {mount_name}",
|
107
|
+
"description": f"File system mount: {mount_path}",
|
108
|
+
})
|
109
|
+
|
110
|
+
await context.info(f"Listed {len(resources)} available resources")
|
111
|
+
|
112
|
+
return {"resources": resources}
|
113
|
+
|
114
|
+
async def read_resource(self, context: Context, uri: str) -> Dict[str, Any]:
|
115
|
+
"""Read a resource for MCP resources/read."""
|
116
|
+
|
117
|
+
try:
|
118
|
+
parsed_uri = urlparse(uri)
|
119
|
+
scheme = parsed_uri.scheme
|
120
|
+
|
121
|
+
if scheme == "file":
|
122
|
+
return await self._read_file_resource(context, parsed_uri)
|
123
|
+
elif scheme == "mem":
|
124
|
+
return await self._read_memory_resource(context, parsed_uri)
|
125
|
+
else:
|
126
|
+
# Check static resources
|
127
|
+
if uri in self._static_resources:
|
128
|
+
return await self._read_static_resource(context, uri)
|
129
|
+
else:
|
130
|
+
raise ValueError(f"Unsupported resource scheme or unknown URI: {uri}")
|
131
|
+
|
132
|
+
except Exception as e:
|
133
|
+
await context.error(f"Failed to read resource {uri}: {e}")
|
134
|
+
raise
|
135
|
+
|
136
|
+
async def _read_file_resource(self, context: Context, parsed_uri) -> Dict[str, Any]:
|
137
|
+
"""Read file:// resource using VFS."""
|
138
|
+
|
139
|
+
# Extract path from URI
|
140
|
+
file_path = Path(parsed_uri.path)
|
141
|
+
|
142
|
+
try:
|
143
|
+
# Use VFS for secure file access
|
144
|
+
if hasattr(context.lifespan, 'vfs') and context.lifespan.vfs:
|
145
|
+
vfs = context.lifespan.vfs
|
146
|
+
else:
|
147
|
+
# Fallback to direct path validation
|
148
|
+
context.require_path_access(file_path)
|
149
|
+
content = file_path.read_bytes()
|
150
|
+
mime_type = "application/octet-stream"
|
151
|
+
|
152
|
+
if 'vfs' in locals():
|
153
|
+
content = vfs.read_file(file_path)
|
154
|
+
file_info = vfs.file_info(file_path)
|
155
|
+
mime_type = file_info.get("mime_type", "application/octet-stream")
|
156
|
+
|
157
|
+
# Determine if content should be base64 encoded
|
158
|
+
is_text = mime_type and mime_type.startswith("text/")
|
159
|
+
|
160
|
+
if is_text:
|
161
|
+
try:
|
162
|
+
text_content = content.decode('utf-8')
|
163
|
+
return {
|
164
|
+
"contents": [
|
165
|
+
{
|
166
|
+
"uri": str(parsed_uri.geturl()),
|
167
|
+
"mimeType": mime_type,
|
168
|
+
"text": text_content,
|
169
|
+
}
|
170
|
+
]
|
171
|
+
}
|
172
|
+
except UnicodeDecodeError:
|
173
|
+
# Fall back to binary
|
174
|
+
pass
|
175
|
+
|
176
|
+
# Binary content
|
177
|
+
b64_content = base64.b64encode(content).decode('ascii')
|
178
|
+
return {
|
179
|
+
"contents": [
|
180
|
+
{
|
181
|
+
"uri": str(parsed_uri.geturl()),
|
182
|
+
"mimeType": mime_type,
|
183
|
+
"blob": b64_content,
|
184
|
+
}
|
185
|
+
]
|
186
|
+
}
|
187
|
+
|
188
|
+
except (VFSError, PermissionError, FileNotFoundError) as e:
|
189
|
+
raise ValueError(f"File access error: {e}")
|
190
|
+
|
191
|
+
async def _read_memory_resource(self, context: Context, parsed_uri) -> Dict[str, Any]:
|
192
|
+
"""Read mem:// resource from memory objects."""
|
193
|
+
|
194
|
+
# Extract object name from URI path
|
195
|
+
path_parts = parsed_uri.path.strip('/').split('/')
|
196
|
+
if len(path_parts) < 2 or path_parts[0] != "object":
|
197
|
+
raise ValueError(f"Invalid memory resource URI: {parsed_uri.geturl()}")
|
198
|
+
|
199
|
+
object_name = path_parts[1]
|
200
|
+
|
201
|
+
if object_name not in self._memory_objects:
|
202
|
+
raise ValueError(f"Memory object not found: {object_name}")
|
203
|
+
|
204
|
+
data = self._memory_objects[object_name]
|
205
|
+
|
206
|
+
# Serialize object to JSON text
|
207
|
+
import json
|
208
|
+
try:
|
209
|
+
text_content = json.dumps(data, indent=2, default=str)
|
210
|
+
return {
|
211
|
+
"contents": [
|
212
|
+
{
|
213
|
+
"uri": parsed_uri.geturl(),
|
214
|
+
"mimeType": "application/json",
|
215
|
+
"text": text_content,
|
216
|
+
}
|
217
|
+
]
|
218
|
+
}
|
219
|
+
except (TypeError, ValueError) as e:
|
220
|
+
raise ValueError(f"Failed to serialize memory object {object_name}: {e}")
|
221
|
+
|
222
|
+
async def _read_static_resource(self, context: Context, uri: str) -> Dict[str, Any]:
|
223
|
+
"""Read a pre-registered static resource."""
|
224
|
+
|
225
|
+
resource_info = self._static_resources[uri]
|
226
|
+
|
227
|
+
# Static resources would need their content defined somewhere
|
228
|
+
# For now, return placeholder
|
229
|
+
return {
|
230
|
+
"contents": [
|
231
|
+
{
|
232
|
+
"uri": uri,
|
233
|
+
"mimeType": resource_info.get("mimeType", "text/plain"),
|
234
|
+
"text": f"Static resource: {resource_info['name']}",
|
235
|
+
}
|
236
|
+
]
|
237
|
+
}
|
238
|
+
|
239
|
+
|
240
|
+
def resource(
|
241
|
+
uri: str,
|
242
|
+
name: str,
|
243
|
+
description: Optional[str] = None,
|
244
|
+
mime_type: Optional[str] = None,
|
245
|
+
):
|
246
|
+
"""
|
247
|
+
Decorator to register a static resource.
|
248
|
+
|
249
|
+
Usage:
|
250
|
+
@resource(
|
251
|
+
uri="static://example",
|
252
|
+
name="Example Resource",
|
253
|
+
description="An example static resource"
|
254
|
+
)
|
255
|
+
def example_resource():
|
256
|
+
return "resource content"
|
257
|
+
"""
|
258
|
+
|
259
|
+
def decorator(func):
|
260
|
+
func._mcp_resource_uri = uri
|
261
|
+
func._mcp_resource_name = name
|
262
|
+
func._mcp_resource_description = description
|
263
|
+
func._mcp_resource_mime_type = mime_type
|
264
|
+
return func
|
265
|
+
|
266
|
+
return decorator
|