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,50 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: mseep-rmcp
|
3
|
+
Version: 0.3.3
|
4
|
+
Summary: Comprehensive Model Context Protocol server with 33 statistical analysis tools across 8 categories
|
5
|
+
Author-email: mseep <support@skydeck.ai>
|
6
|
+
License: MIT
|
7
|
+
Project-URL: Homepage, https://github.com/gojiplus/rmcp
|
8
|
+
Project-URL: Repository, https://github.com/gojiplus/rmcp
|
9
|
+
Project-URL: Documentation, https://github.com/gojiplus/rmcp#readme
|
10
|
+
Project-URL: Issues, https://github.com/gojiplus/rmcp/issues
|
11
|
+
Keywords: mcp,r,statistics,econometrics,model-context-protocol,data-analysis
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
13
|
+
Classifier: Intended Audience :: Developers
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
16
|
+
Classifier: Operating System :: OS Independent
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
18
|
+
Classifier: Programming Language :: Python :: 3.8
|
19
|
+
Classifier: Programming Language :: Python :: 3.9
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
23
|
+
Classifier: Programming Language :: R
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
26
|
+
Classifier: Topic :: System :: Distributed Computing
|
27
|
+
Requires-Python: >=3.8
|
28
|
+
Description-Content-Type: text/plain
|
29
|
+
License-File: LICENSE
|
30
|
+
Requires-Dist: click>=8.1.0
|
31
|
+
Requires-Dist: jsonschema>=4.0.0
|
32
|
+
Provides-Extra: dev
|
33
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
34
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
35
|
+
Requires-Dist: pytest-mock>=3.12.0; extra == "dev"
|
36
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
37
|
+
Requires-Dist: black>=23.0.0; extra == "dev"
|
38
|
+
Requires-Dist: isort>=5.12.0; extra == "dev"
|
39
|
+
Requires-Dist: flake8>=6.0.0; extra == "dev"
|
40
|
+
Requires-Dist: mypy>=1.5.0; extra == "dev"
|
41
|
+
Requires-Dist: pre-commit>=3.0.0; extra == "dev"
|
42
|
+
Provides-Extra: http
|
43
|
+
Requires-Dist: fastapi>=0.100.0; extra == "http"
|
44
|
+
Requires-Dist: uvicorn>=0.20.0; extra == "http"
|
45
|
+
Requires-Dist: sse-starlette>=1.6.0; extra == "http"
|
46
|
+
Provides-Extra: r
|
47
|
+
Requires-Dist: subprocess32>=3.5.0; python_version < "3.3" and extra == "r"
|
48
|
+
Dynamic: license-file
|
49
|
+
|
50
|
+
Package managed by MseeP.ai
|
@@ -0,0 +1,34 @@
|
|
1
|
+
mseep_rmcp-0.3.3.dist-info/licenses/LICENSE,sha256=9vFQ7IesB0PIsHbqsP3NFa2Hoq1dzbavqbN7rpF-RcE,1062
|
2
|
+
rmcp/__init__.py,sha256=AitIbW6uioNUS5HvPoKy3lAeMPz9yyoXbiBz-KswMiY,937
|
3
|
+
rmcp/cli.py,sha256=_Gwsn8_big6E7KFBwENBJVjK0hOoMB7QZ7bS_g0_JtE,9413
|
4
|
+
rmcp/r_integration.py,sha256=I9OdB5Q0jnMEXjrUfsgiN4YfnB_Lgw7aWkz0mFzAuMc,3665
|
5
|
+
rmcp/core/__init__.py,sha256=oQPSq-6PQwpdClzIrIYA2lgUGYFRCy66lj_mrxtgKek,419
|
6
|
+
rmcp/core/context.py,sha256=HRK1CuH40hq921cHBAU13yDqcX1BKoT5UVuu_oV9PUI,4877
|
7
|
+
rmcp/core/schemas.py,sha256=dwcd-yunU49CFM7LPn0_m49Z-BlfqdciVsW3fWttKKw,4383
|
8
|
+
rmcp/core/server.py,sha256=0vYNoIa8Q9mraOVhamqq6Fdauz714ucfFjwU_ulDses,9110
|
9
|
+
rmcp/r_assets/__init__.py,sha256=TMIhwzg5zTPgIFW-sEui4c8sJ79RmWTqXvjJomlwV0Y,155
|
10
|
+
rmcp/registries/__init__.py,sha256=Y099uvTx4bMlKTttxWH5XKRCZghgNkCLGrVhEaSBOtQ,636
|
11
|
+
rmcp/registries/prompts.py,sha256=WqvBRw3bap-1etBUui5Xn6WflpLFbM9bHjXqWN_8BjQ,10182
|
12
|
+
rmcp/registries/resources.py,sha256=D5AB8xmpdNaOT5tX_aHRuaJleDuM9X4X_8SgRLIXvTA,9027
|
13
|
+
rmcp/registries/tools.py,sha256=hVx8joLUUDWk4uQVnYeBbfcWcLrFuhj2hiCHvoPmUds,7188
|
14
|
+
rmcp/scripts/__init__.py,sha256=i5_HI3cpEb4afuLt-KSHdsEXYo5aEtEz8y1jSJasJtY,152
|
15
|
+
rmcp/security/__init__.py,sha256=IP5q8qpOA7b_ZtVg3zyLweO918SP05_QEF3t04OimoA,313
|
16
|
+
rmcp/security/vfs.py,sha256=0VRiKKh9_YX7wpmO-tZem1gWQv5DOf2dNOGpjtt3bWE,8323
|
17
|
+
rmcp/tools/descriptive.py,sha256=ge0dIeuXB6Dhb9dxUbV8MBE5RqZ1Pcza8EhkYugXjrM,8749
|
18
|
+
rmcp/tools/econometrics.py,sha256=iU8MncE8TkmO4x089lQ10q_ecFzfBAiguzpx-cFz74o,8285
|
19
|
+
rmcp/tools/fileops.py,sha256=wvM9uI8pWZVu4QS36XLUBsioRuq7_ZyHF0LGqJU3sGo,9944
|
20
|
+
rmcp/tools/machine_learning.py,sha256=PFSi63RFhx7ouTLYCW3s89iH2f5AMn_gTrBGqCvEbVg,9645
|
21
|
+
rmcp/tools/regression.py,sha256=4WzF-9Z9dQ-r6u6eXmd2qhykJYxPmuec4Uqiktw-fx8,9768
|
22
|
+
rmcp/tools/statistical_tests.py,sha256=PIkt2GWbi3zLvJgoKDxzQCLBpI1lFzR2QAbe-0ATMKE,11190
|
23
|
+
rmcp/tools/timeseries.py,sha256=wKUntaCPSxSAaRdIZrw1J6ZwrNryFxB3YaW05GKIs0s,7480
|
24
|
+
rmcp/tools/transforms.py,sha256=aqHV-rqWSTOr2eDg1uwgmNxzLNqYu-lvyHaS9cdhMdQ,9156
|
25
|
+
rmcp/tools/visualization.py,sha256=7AVReAY23zEsvFI7I1nTgRoNzAAKaVDkaQDA8yz-tfg,20149
|
26
|
+
rmcp/transport/__init__.py,sha256=ssvYT6JSBl6Iaas0MnFJfvVb-kwOAIFBmJHkO7xJp6Q,515
|
27
|
+
rmcp/transport/base.py,sha256=aWr6qK9iawcpo8SVSqmTUBiciiCnp5QEXIoPCWMxhdQ,4208
|
28
|
+
rmcp/transport/jsonrpc.py,sha256=4I-gyXZ_gV6B9BxyPD5OxBTVD14gsMe_M_OuoHed27E,7833
|
29
|
+
rmcp/transport/stdio.py,sha256=SkZ2wl0GJolwRp125IL1QNkQPGJ-jj9ziIvKB5-8vto,7571
|
30
|
+
mseep_rmcp-0.3.3.dist-info/METADATA,sha256=_xczBCyFykjkTUjX-B94zgTCyoC5k5nINP-9XujUBXI,2209
|
31
|
+
mseep_rmcp-0.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
32
|
+
mseep_rmcp-0.3.3.dist-info/entry_points.txt,sha256=GLl2t_fBiIuN5st0RjbPyaLIf6jpeIYbyy7qglOn-Oo,38
|
33
|
+
mseep_rmcp-0.3.3.dist-info/top_level.txt,sha256=cQtbyZqL6sY1EKAAp_yOm010hAc6PPMtP1oxyGt5BQc,5
|
34
|
+
mseep_rmcp-0.3.3.dist-info/RECORD,,
|
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 goji+
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
@@ -0,0 +1 @@
|
|
1
|
+
rmcp
|
rmcp/__init__.py
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
"""
|
2
|
+
RMCP MCP Server - A Model Context Protocol server for R-based statistical analysis.
|
3
|
+
|
4
|
+
This package implements a production-ready MCP server following established patterns:
|
5
|
+
- Spec correctness by construction using official SDK
|
6
|
+
- Clean separation of concerns (protocol/registries/domain)
|
7
|
+
- Security by default (VFS, allowlists, sandboxing)
|
8
|
+
- Transport-agnostic design (stdio primary, HTTP optional)
|
9
|
+
- Explicit schemas and typed context objects
|
10
|
+
"""
|
11
|
+
|
12
|
+
from .core.context import Context
|
13
|
+
from .core.server import create_server
|
14
|
+
from .registries.tools import ToolsRegistry, tool
|
15
|
+
from .registries.resources import ResourcesRegistry, resource
|
16
|
+
from .registries.prompts import PromptsRegistry, prompt
|
17
|
+
|
18
|
+
__version__ = "0.3.2"
|
19
|
+
__author__ = "Gaurav Sood"
|
20
|
+
__email__ = "gsood07@gmail.com"
|
21
|
+
|
22
|
+
__all__ = [
|
23
|
+
"Context",
|
24
|
+
"create_server",
|
25
|
+
"ToolsRegistry",
|
26
|
+
"ResourcesRegistry",
|
27
|
+
"PromptsRegistry",
|
28
|
+
"tool",
|
29
|
+
"resource",
|
30
|
+
"prompt",
|
31
|
+
]
|
rmcp/cli.py
ADDED
@@ -0,0 +1,317 @@
|
|
1
|
+
"""
|
2
|
+
Command-line interface for RMCP MCP Server.
|
3
|
+
|
4
|
+
Provides entry points for running the server with different transports
|
5
|
+
and configurations, following the principle of "multiple deployment targets."
|
6
|
+
"""
|
7
|
+
|
8
|
+
import asyncio
|
9
|
+
import click
|
10
|
+
import logging
|
11
|
+
import sys
|
12
|
+
from pathlib import Path
|
13
|
+
from typing import List, Optional
|
14
|
+
|
15
|
+
from .core.server import create_server
|
16
|
+
from .transport.stdio import StdioTransport
|
17
|
+
from .registries.tools import register_tool_functions
|
18
|
+
from .registries.resources import ResourcesRegistry
|
19
|
+
from .registries.prompts import register_prompt_functions, statistical_workflow_prompt, model_diagnostic_prompt
|
20
|
+
|
21
|
+
# Configure logging to stderr only
|
22
|
+
logging.basicConfig(
|
23
|
+
level=logging.INFO,
|
24
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
25
|
+
stream=sys.stderr
|
26
|
+
)
|
27
|
+
|
28
|
+
logger = logging.getLogger(__name__)
|
29
|
+
|
30
|
+
|
31
|
+
@click.group()
|
32
|
+
@click.version_option(version="0.3.2")
|
33
|
+
def cli():
|
34
|
+
"""RMCP MCP Server - Comprehensive statistical analysis with 33 tools across 8 categories."""
|
35
|
+
pass
|
36
|
+
|
37
|
+
|
38
|
+
@cli.command()
|
39
|
+
@click.option(
|
40
|
+
"--log-level",
|
41
|
+
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"]),
|
42
|
+
default="INFO",
|
43
|
+
help="Logging level"
|
44
|
+
)
|
45
|
+
def start(log_level: str):
|
46
|
+
"""Start RMCP MCP server (default stdio transport)."""
|
47
|
+
|
48
|
+
# Set logging level
|
49
|
+
logging.getLogger().setLevel(getattr(logging, log_level))
|
50
|
+
|
51
|
+
logger.info("Starting RMCP MCP Server")
|
52
|
+
|
53
|
+
try:
|
54
|
+
# Create and configure server
|
55
|
+
server = create_server()
|
56
|
+
config = {"allowed_paths": [str(Path.cwd())], "read_only": True}
|
57
|
+
server.configure(**config)
|
58
|
+
|
59
|
+
# Register built-in statistical tools
|
60
|
+
_register_builtin_tools(server)
|
61
|
+
|
62
|
+
# Register built-in prompts
|
63
|
+
register_prompt_functions(
|
64
|
+
server.prompts,
|
65
|
+
statistical_workflow_prompt,
|
66
|
+
model_diagnostic_prompt
|
67
|
+
)
|
68
|
+
|
69
|
+
# Set up stdio transport
|
70
|
+
transport = StdioTransport()
|
71
|
+
transport.set_message_handler(server.handle_request)
|
72
|
+
|
73
|
+
# Run the server
|
74
|
+
asyncio.run(transport.run())
|
75
|
+
|
76
|
+
except KeyboardInterrupt:
|
77
|
+
logger.info("Server interrupted by user")
|
78
|
+
except Exception as e:
|
79
|
+
logger.error(f"Server error: {e}")
|
80
|
+
sys.exit(1)
|
81
|
+
|
82
|
+
|
83
|
+
@cli.command()
|
84
|
+
@click.option(
|
85
|
+
"--allowed-paths",
|
86
|
+
multiple=True,
|
87
|
+
help="Allowed file system paths (can be specified multiple times)"
|
88
|
+
)
|
89
|
+
@click.option(
|
90
|
+
"--cache-root",
|
91
|
+
type=click.Path(),
|
92
|
+
help="Root directory for content caching"
|
93
|
+
)
|
94
|
+
@click.option(
|
95
|
+
"--read-only/--read-write",
|
96
|
+
default=True,
|
97
|
+
help="File system access mode (default: read-only)"
|
98
|
+
)
|
99
|
+
@click.option(
|
100
|
+
"--log-level",
|
101
|
+
type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"]),
|
102
|
+
default="INFO",
|
103
|
+
help="Logging level"
|
104
|
+
)
|
105
|
+
@click.option(
|
106
|
+
"--config-file",
|
107
|
+
type=click.Path(exists=True),
|
108
|
+
help="Configuration file path"
|
109
|
+
)
|
110
|
+
def serve(
|
111
|
+
allowed_paths: List[str],
|
112
|
+
cache_root: Optional[str],
|
113
|
+
read_only: bool,
|
114
|
+
log_level: str,
|
115
|
+
config_file: Optional[str],
|
116
|
+
):
|
117
|
+
"""Run MCP server with advanced configuration options."""
|
118
|
+
|
119
|
+
# Set logging level
|
120
|
+
logging.getLogger().setLevel(getattr(logging, log_level))
|
121
|
+
|
122
|
+
logger.info("Starting RMCP MCP Server")
|
123
|
+
|
124
|
+
try:
|
125
|
+
# Load configuration
|
126
|
+
config = _load_config(config_file) if config_file else {}
|
127
|
+
|
128
|
+
# Override with CLI options
|
129
|
+
if allowed_paths:
|
130
|
+
config["allowed_paths"] = list(allowed_paths)
|
131
|
+
if cache_root:
|
132
|
+
config["cache_root"] = cache_root
|
133
|
+
config["read_only"] = read_only
|
134
|
+
|
135
|
+
# Set defaults if not specified
|
136
|
+
if "allowed_paths" not in config:
|
137
|
+
config["allowed_paths"] = [str(Path.cwd())]
|
138
|
+
|
139
|
+
# Create and configure server
|
140
|
+
server = create_server()
|
141
|
+
server.configure(**config)
|
142
|
+
|
143
|
+
# Register built-in statistical tools
|
144
|
+
_register_builtin_tools(server)
|
145
|
+
|
146
|
+
# Register built-in prompts
|
147
|
+
register_prompt_functions(
|
148
|
+
server.prompts,
|
149
|
+
statistical_workflow_prompt,
|
150
|
+
model_diagnostic_prompt
|
151
|
+
)
|
152
|
+
|
153
|
+
# Set up stdio transport
|
154
|
+
transport = StdioTransport()
|
155
|
+
transport.set_message_handler(server.handle_request)
|
156
|
+
|
157
|
+
# Run the server
|
158
|
+
asyncio.run(transport.run())
|
159
|
+
|
160
|
+
except KeyboardInterrupt:
|
161
|
+
logger.info("Server interrupted by user")
|
162
|
+
except Exception as e:
|
163
|
+
logger.error(f"Server error: {e}")
|
164
|
+
sys.exit(1)
|
165
|
+
|
166
|
+
|
167
|
+
@cli.command()
|
168
|
+
@click.option("--host", default="localhost", help="Host to bind to")
|
169
|
+
@click.option("--port", default=8000, help="Port to bind to")
|
170
|
+
def serve_http(host: str, port: int):
|
171
|
+
"""Run MCP server over HTTP transport (requires fastapi extras)."""
|
172
|
+
try:
|
173
|
+
from .transport.http import HTTPTransport
|
174
|
+
except ImportError:
|
175
|
+
click.echo("HTTP transport requires 'fastapi' extras. Install with: pip install rmcp-mcp[http]")
|
176
|
+
sys.exit(1)
|
177
|
+
|
178
|
+
logger.info(f"HTTP transport not yet implemented")
|
179
|
+
# TODO: Implement HTTP transport
|
180
|
+
click.echo("HTTP transport coming soon!")
|
181
|
+
|
182
|
+
|
183
|
+
@cli.command()
|
184
|
+
@click.option(
|
185
|
+
"--allowed-paths",
|
186
|
+
multiple=True,
|
187
|
+
help="Allowed file system paths"
|
188
|
+
)
|
189
|
+
@click.option("--output", type=click.Path(), help="Output file for capabilities")
|
190
|
+
def list_capabilities(allowed_paths: List[str], output: Optional[str]):
|
191
|
+
"""List server capabilities (tools, resources, prompts)."""
|
192
|
+
|
193
|
+
# Create server to inspect capabilities
|
194
|
+
server = create_server()
|
195
|
+
if allowed_paths:
|
196
|
+
server.configure(allowed_paths=list(allowed_paths))
|
197
|
+
|
198
|
+
_register_builtin_tools(server)
|
199
|
+
register_prompt_functions(server.prompts, statistical_workflow_prompt, model_diagnostic_prompt)
|
200
|
+
|
201
|
+
async def _list():
|
202
|
+
from .core.context import Context, LifespanState
|
203
|
+
|
204
|
+
context = Context.create("list", "list", server.lifespan_state)
|
205
|
+
|
206
|
+
# Get capabilities
|
207
|
+
tools = await server.tools.list_tools(context)
|
208
|
+
resources = await server.resources.list_resources(context)
|
209
|
+
prompts = await server.prompts.list_prompts(context)
|
210
|
+
|
211
|
+
capabilities = {
|
212
|
+
"server": {
|
213
|
+
"name": server.name,
|
214
|
+
"version": server.version,
|
215
|
+
"description": server.description
|
216
|
+
},
|
217
|
+
"tools": tools,
|
218
|
+
"resources": resources,
|
219
|
+
"prompts": prompts
|
220
|
+
}
|
221
|
+
|
222
|
+
import json
|
223
|
+
json_output = json.dumps(capabilities, indent=2)
|
224
|
+
|
225
|
+
if output:
|
226
|
+
with open(output, 'w') as f:
|
227
|
+
f.write(json_output)
|
228
|
+
click.echo(f"Capabilities written to {output}")
|
229
|
+
else:
|
230
|
+
click.echo(json_output)
|
231
|
+
|
232
|
+
asyncio.run(_list())
|
233
|
+
|
234
|
+
|
235
|
+
@cli.command()
|
236
|
+
def validate_config():
|
237
|
+
"""Validate server configuration."""
|
238
|
+
click.echo("Configuration validation not yet implemented")
|
239
|
+
# TODO: Add config validation
|
240
|
+
|
241
|
+
|
242
|
+
def _load_config(config_file: str) -> dict:
|
243
|
+
"""Load configuration from file."""
|
244
|
+
import json
|
245
|
+
|
246
|
+
try:
|
247
|
+
with open(config_file, 'r') as f:
|
248
|
+
return json.load(f)
|
249
|
+
except Exception as e:
|
250
|
+
logger.error(f"Failed to load config file {config_file}: {e}")
|
251
|
+
return {}
|
252
|
+
|
253
|
+
|
254
|
+
def _register_builtin_tools(server):
|
255
|
+
"""Register built-in statistical tools."""
|
256
|
+
from .tools.regression import linear_model, correlation_analysis, logistic_regression
|
257
|
+
from .tools.timeseries import arima_model, decompose_timeseries, stationarity_test
|
258
|
+
from .tools.transforms import lag_lead, winsorize, difference, standardize
|
259
|
+
from .tools.statistical_tests import t_test, anova, chi_square_test, normality_test
|
260
|
+
from .tools.descriptive import summary_stats, outlier_detection, frequency_table
|
261
|
+
from .tools.fileops import read_csv, write_csv, data_info, filter_data
|
262
|
+
from .tools.econometrics import panel_regression, instrumental_variables, var_model
|
263
|
+
from .tools.machine_learning import kmeans_clustering, decision_tree, random_forest
|
264
|
+
from .tools.visualization import scatter_plot, histogram, boxplot, time_series_plot, correlation_heatmap, regression_plot
|
265
|
+
|
266
|
+
# Register all statistical tools
|
267
|
+
register_tool_functions(
|
268
|
+
server.tools,
|
269
|
+
# Original regression tools
|
270
|
+
linear_model,
|
271
|
+
correlation_analysis,
|
272
|
+
logistic_regression,
|
273
|
+
# Time series analysis
|
274
|
+
arima_model,
|
275
|
+
decompose_timeseries,
|
276
|
+
stationarity_test,
|
277
|
+
# Data transformations
|
278
|
+
lag_lead,
|
279
|
+
winsorize,
|
280
|
+
difference,
|
281
|
+
standardize,
|
282
|
+
# Statistical tests
|
283
|
+
t_test,
|
284
|
+
anova,
|
285
|
+
chi_square_test,
|
286
|
+
normality_test,
|
287
|
+
# Descriptive statistics
|
288
|
+
summary_stats,
|
289
|
+
outlier_detection,
|
290
|
+
frequency_table,
|
291
|
+
# File operations
|
292
|
+
read_csv,
|
293
|
+
write_csv,
|
294
|
+
data_info,
|
295
|
+
filter_data,
|
296
|
+
# Econometrics
|
297
|
+
panel_regression,
|
298
|
+
instrumental_variables,
|
299
|
+
var_model,
|
300
|
+
# Machine learning
|
301
|
+
kmeans_clustering,
|
302
|
+
decision_tree,
|
303
|
+
random_forest,
|
304
|
+
# Visualization
|
305
|
+
scatter_plot,
|
306
|
+
histogram,
|
307
|
+
boxplot,
|
308
|
+
time_series_plot,
|
309
|
+
correlation_heatmap,
|
310
|
+
regression_plot
|
311
|
+
)
|
312
|
+
|
313
|
+
logger.info("Registered comprehensive statistical analysis tools (30 total)")
|
314
|
+
|
315
|
+
|
316
|
+
if __name__ == "__main__":
|
317
|
+
cli()
|
rmcp/core/__init__.py
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
"""
|
2
|
+
Core MCP server components.
|
3
|
+
|
4
|
+
This module contains the fundamental building blocks:
|
5
|
+
- Context: Typed context for request + lifespan state
|
6
|
+
- Server: MCP app shell with lifecycle hooks
|
7
|
+
- Schemas: JSON Schema validation helpers
|
8
|
+
"""
|
9
|
+
|
10
|
+
from .context import Context
|
11
|
+
from .server import create_server
|
12
|
+
from .schemas import validate_schema, SchemaError
|
13
|
+
|
14
|
+
__all__ = ["Context", "create_server", "validate_schema", "SchemaError"]
|
rmcp/core/context.py
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
"""
|
2
|
+
Typed context object for MCP requests.
|
3
|
+
|
4
|
+
The Context object provides:
|
5
|
+
- Per-request state (request ID, progress token, cancellation)
|
6
|
+
- Lifespan state (settings, caches, resources)
|
7
|
+
- Cross-cutting features (logging, progress, security)
|
8
|
+
|
9
|
+
Following the principle: "Makes cross-cutting features universal without globals."
|
10
|
+
"""
|
11
|
+
|
12
|
+
from typing import Any, Dict, Optional, Callable, Awaitable
|
13
|
+
from dataclasses import dataclass, field
|
14
|
+
import asyncio
|
15
|
+
import logging
|
16
|
+
from pathlib import Path
|
17
|
+
|
18
|
+
|
19
|
+
@dataclass
|
20
|
+
class RequestState:
|
21
|
+
"""Per-request state passed to tool handlers."""
|
22
|
+
|
23
|
+
request_id: str
|
24
|
+
method: str
|
25
|
+
progress_token: Optional[str] = None
|
26
|
+
cancelled: bool = False
|
27
|
+
|
28
|
+
def is_cancelled(self) -> bool:
|
29
|
+
"""Check if request has been cancelled."""
|
30
|
+
return self.cancelled
|
31
|
+
|
32
|
+
def cancel(self) -> None:
|
33
|
+
"""Mark request as cancelled."""
|
34
|
+
self.cancelled = True
|
35
|
+
|
36
|
+
|
37
|
+
@dataclass
|
38
|
+
class LifespanState:
|
39
|
+
"""Lifespan state shared across requests."""
|
40
|
+
|
41
|
+
# Configuration
|
42
|
+
settings: Dict[str, Any] = field(default_factory=dict)
|
43
|
+
|
44
|
+
# Security
|
45
|
+
allowed_paths: list[Path] = field(default_factory=list)
|
46
|
+
read_only: bool = True
|
47
|
+
|
48
|
+
# Caching
|
49
|
+
cache_root: Optional[Path] = None
|
50
|
+
content_cache: Dict[str, Any] = field(default_factory=dict)
|
51
|
+
|
52
|
+
# Resources
|
53
|
+
resource_mounts: Dict[str, Path] = field(default_factory=dict)
|
54
|
+
|
55
|
+
|
56
|
+
@dataclass
|
57
|
+
class Context:
|
58
|
+
"""
|
59
|
+
Typed context passed to all tool handlers.
|
60
|
+
|
61
|
+
Provides both per-request state and shared lifespan state,
|
62
|
+
plus helpers for logging, progress, and cancellation.
|
63
|
+
"""
|
64
|
+
|
65
|
+
request: RequestState
|
66
|
+
lifespan: LifespanState
|
67
|
+
|
68
|
+
# Progress/logging callbacks
|
69
|
+
_progress_callback: Optional[Callable[[str, int, int], Awaitable[None]]] = None
|
70
|
+
_log_callback: Optional[Callable[[str, str, Dict[str, Any]], Awaitable[None]]] = None
|
71
|
+
|
72
|
+
@classmethod
|
73
|
+
def create(
|
74
|
+
cls,
|
75
|
+
request_id: str,
|
76
|
+
method: str,
|
77
|
+
lifespan_state: LifespanState,
|
78
|
+
progress_token: Optional[str] = None,
|
79
|
+
progress_callback: Optional[Callable[[str, int, int], Awaitable[None]]] = None,
|
80
|
+
log_callback: Optional[Callable[[str, str, Dict[str, Any]], Awaitable[None]]] = None,
|
81
|
+
) -> "Context":
|
82
|
+
"""Create a new context for a request."""
|
83
|
+
request_state = RequestState(
|
84
|
+
request_id=request_id,
|
85
|
+
method=method,
|
86
|
+
progress_token=progress_token
|
87
|
+
)
|
88
|
+
|
89
|
+
return cls(
|
90
|
+
request=request_state,
|
91
|
+
lifespan=lifespan_state,
|
92
|
+
_progress_callback=progress_callback,
|
93
|
+
_log_callback=log_callback,
|
94
|
+
)
|
95
|
+
|
96
|
+
# Cross-cutting feature helpers
|
97
|
+
|
98
|
+
async def progress(self, message: str, current: int, total: int) -> None:
|
99
|
+
"""Send progress notification if progress token is available."""
|
100
|
+
if self.request.progress_token and self._progress_callback:
|
101
|
+
await self._progress_callback(message, current, total)
|
102
|
+
|
103
|
+
async def log(self, level: str, message: str, **kwargs: Any) -> None:
|
104
|
+
"""Send structured log notification."""
|
105
|
+
if self._log_callback:
|
106
|
+
await self._log_callback(level, message, kwargs)
|
107
|
+
|
108
|
+
async def info(self, message: str, **kwargs: Any) -> None:
|
109
|
+
"""Log info message."""
|
110
|
+
await self.log("info", message, **kwargs)
|
111
|
+
|
112
|
+
async def warn(self, message: str, **kwargs: Any) -> None:
|
113
|
+
"""Log warning message."""
|
114
|
+
await self.log("warning", message, **kwargs)
|
115
|
+
|
116
|
+
async def error(self, message: str, **kwargs: Any) -> None:
|
117
|
+
"""Log error message."""
|
118
|
+
await self.log("error", message, **kwargs)
|
119
|
+
|
120
|
+
def check_cancellation(self) -> None:
|
121
|
+
"""Check if request has been cancelled, raise if so."""
|
122
|
+
if self.request.is_cancelled():
|
123
|
+
raise asyncio.CancelledError("Request was cancelled")
|
124
|
+
|
125
|
+
# Security helpers
|
126
|
+
|
127
|
+
def is_path_allowed(self, path: Path) -> bool:
|
128
|
+
"""Check if path access is allowed."""
|
129
|
+
try:
|
130
|
+
resolved_path = path.resolve()
|
131
|
+
return any(
|
132
|
+
resolved_path.is_relative_to(allowed_root.resolve())
|
133
|
+
for allowed_root in self.lifespan.allowed_paths
|
134
|
+
)
|
135
|
+
except (OSError, ValueError):
|
136
|
+
return False
|
137
|
+
|
138
|
+
def require_path_access(self, path: Path) -> None:
|
139
|
+
"""Require path access, raise if denied."""
|
140
|
+
if not self.is_path_allowed(path):
|
141
|
+
raise PermissionError(
|
142
|
+
f"Path access denied: {path}. "
|
143
|
+
f"Allowed roots: {[str(p) for p in self.lifespan.allowed_paths]}"
|
144
|
+
)
|
145
|
+
|
146
|
+
def get_cache_path(self, key: str) -> Optional[Path]:
|
147
|
+
"""Get cache path for key if caching is enabled."""
|
148
|
+
if self.lifespan.cache_root:
|
149
|
+
return self.lifespan.cache_root / key
|
150
|
+
return None
|