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.
@@ -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