Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,11 @@ jobs:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,prefix={{branch}}-
type=ref,event=pr
# {{branch}} is only populated for branch pushes; on a pull_request
# it is empty and the tag becomes "-<sha>", which docker rejects.
type=sha,prefix={{branch}}-,enable=${{ github.event_name != 'pull_request' }}
type=sha,prefix=pr-,enable=${{ github.event_name == 'pull_request' }}
type=raw,value=latest,enable={{is_default_branch}}

- name: Extract metadata for production image
Expand All @@ -163,7 +167,9 @@ jobs:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch,suffix=-production
type=sha,prefix={{branch}}-production-
type=ref,event=pr,suffix=-production
type=sha,prefix={{branch}}-production-,enable=${{ github.event_name != 'pull_request' }}
type=sha,prefix=pr-production-,enable=${{ github.event_name == 'pull_request' }}
type=raw,value=production-latest,enable={{is_default_branch}}

- name: Build and push development Docker image
Expand Down
72 changes: 72 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,84 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.10.0] - 2026-07-27

### Security
- **`packages` was an injection vector**: `execute_r_analysis` interpolated
caller-supplied strings straight into `library(...)` lines, and
`validate_r_code` never saw them. A value like
`"stats); system('...'); library(utils"` executed arbitrary R, bypassing the
package allowlist, the dangerous-pattern block, and every approval category.
Entries are now required to be bare R identifiers and are checked against the
allowlist (or session approval) before use.
- **File writes are confined to the VFS roots**: `write_csv`/`write_excel`/
`write_json` and the six visualization tools validate their `file_path`
before handing it to R, via the new `VFS.validate_write_path()`. Previously
`VFS.write_file` had no production callers at all, so the VFS enforced
nothing. Fails open when no VFS is configured, so embedders are unaffected.
- **`approve_operation` no longer disables read-only globally**: it grants
write access to the requested directory via `VFS.grant_write()` instead of
setting `vfs.read_only = False` for the whole process.

### Added
- `approve_operation` and `approve_r_package` are registered and reachable.
They were decorated with `@tool` but never passed to `register_tool_functions`,
so `execute_r_analysis`'s "call approve_operation" instruction pointed at a
tool no client could see.
- `RConfig.max_concurrent` (env `RMCP_R_MAX_CONCURRENT`) to bound concurrent R
subprocesses; previously a hard-coded `4`.
- Guarded dependabot automation: weekly grouped patch/minor updates for uv
and GitHub Actions with a 7-day release cooldown; auto-approve + squash
auto-merge for patch/minor (majors stay open for review); `main` ruleset
requires CI checks for PR merges (admins bypass for direct pushes)

### Fixed
- **`rmcp serve` corrupted the stdio JSON-RPC stream**: it never configured
structlog, whose default factory writes to stdout.
- **R timeouts orphaned the subprocess**: `asyncio.wait_for` sat inside an
`asyncio.TaskGroup`, so the timeout surfaced as a `BaseExceptionGroup` that
the sibling `except TimeoutError` could not catch and `proc.kill()` never
ran. The timeout now wraps the whole group, and the message reports the
configured value rather than a hard-coded 120 seconds.
- **Concurrent R calls failed after the first event loop**: the module-level
`asyncio.Semaphore` bound itself to whichever loop first made a caller wait,
then raised "bound to a different event loop" everywhere else. Now one
limiter per running loop.
- **`load_example` masked every error it hit**: its fallback dict omitted
required output properties, so real failures surfaced as
`'statistics' is a required property`. It re-raises instead.
- `execute_r_analysis`'s `timeout_seconds` was declared and ignored.
- CORS was pinned to `*`: the configured `cors_origins` were never forwarded,
and their `http://localhost:*` wildcards are not matchable by Starlette's
`allow_origins` anyway. They are now compiled to `allow_origin_regex`.
- Approvals are stored on `LifespanState` rather than the per-request
`Context`, so they survive to the next request as documented.
- Oversized tool results are capped at 32 retained entries; an expired
`rmcp://data/{id}` link now explains it expired instead of raising `KeyError`.

### Changed
- **`tools/list` shrank ~66%**, from 103,939 to ~35,500 characters (~26k to
~9k tokens), while gaining two tools. `outputSchema` is no longer advertised
(it was 54% of the payload; results are still validated against it
server-side), and the 46 templated four-sentence descriptions are now
one-liners.
- The `initialize` instructions blob went from 2,789 to 922 characters and
stopped duplicating the tool catalogue. It was also defined twice verbatim.
- Tests that swallowed their own assertions now fail properly; several were
passing regardless of outcome. Fixing them surfaced a real wrong assertion
in `test_logistic_regression_separation_warning`.

### Removed
- `write_csv`'s `append` parameter: R's `write.csv` refuses it
("attempt to set 'append' ignored") and truncates, so the schema advertised
behaviour that never happened and silently lost data.
- ~1,700 lines of advertised-but-unwired subsystems: `package_tiers.py`,
`package_security.py` (the documented "4-tier security system", which was
never wired into any execution path), `discovery.py`, and `r_session.py`.
- Stale documentation: two environment variables and an approval API that do
not exist, a "Zero Skipped Tests" claim, and hard-coded tool counts that
disagreed with each other in four places.

## [0.9.1] - 2026-07-22

### Fixed
Expand Down
83 changes: 30 additions & 53 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ rmcp start # Full R integration available
**For production deployment (optimized):**
```bash
docker build --target production -t rmcp-production . # Multi-stage optimized build
docker run -p 8000:8000 rmcp-production rmcp http # Production HTTP server
docker run -p 8000:8000 rmcp-production rmcp serve-http # Production HTTP server
```

### Testing (Hybrid Strategy)
Expand Down Expand Up @@ -76,8 +76,8 @@ uv run sphinx-autogen docs/**/*.rst # Generate autosummary stubs
- **Transport Layer** (`rmcp/transport/`): Official MCP SDK transports (stdio + Streamable HTTP) via `rmcp/transport/sdk.py`; protocol bridge in `rmcp/core/sdk_adapter.py`
- **Core Server** (`rmcp/core/`): MCPServer, Context management, JSON-RPC protocol
- **Registries** (`rmcp/registries/`): Dynamic registration for tools, resources, prompts
- **Tools** (`rmcp/tools/`): 52 statistical analysis tools across 11 categories
- **Comprehensive Package Whitelist**: 429 R packages from CRAN task views with tiered security
- **Tools** (`rmcp/tools/`): 54 tools across 11 categories
- **Package Allowlist**: 429 R packages from CRAN task views
- **R Integration** (`rmcp/r_integration.py`): Python-R bridge via subprocess + JSON

### Adding New Statistical Tools
Expand Down Expand Up @@ -118,17 +118,17 @@ uv run sphinx-autogen docs/**/*.rst # Generate autosummary stubs
- `test_deployment_scenarios.py`: Docker environment validation, production builds, multi-platform testing

**Development Utilities**
- **`scripts/testing/run_comprehensive_tests.py`**: Comprehensive test runner for development (tests all 52 tools with real R)
- **`scripts/testing/run_comprehensive_tests.py`**: Comprehensive test runner for development (exercises every tool with real R)

**Complete Test Coverage**: Docker environment includes **all 240+ tests** with comprehensive coverage across all components:
- ✅ **R Integration**: 52 statistical tools with real R execution
- ✅ **R Integration**: statistical tools exercised against real R
- ✅ **HTTP Transport**: MCP SDK Streamable HTTP, uvicorn, bearer auth, session management
- ✅ **Core MCP Protocol**: JSON-RPC 2.0, tool calls, capabilities, error handling
- ✅ **Configuration System**: Environment variables, config files, hierarchical loading
- ✅ **Production Deployment**: Multi-stage Docker builds, security validation, size optimization
- ✅ **Scalability**: Concurrent request handling, load testing, performance validation
- ✅ **Cross-platform**: Multi-architecture support, numerical consistency, platform compatibility
- **Zero Skipped Tests**: All tests execute successfully with no dependency-related skips
- ⏭️ **Dependency-gated skips**: Docker and Claude Desktop scenarios skip when those aren't present

**Strategy**: Tests progress from basic functionality → protocol compliance → component integration → complete scenarios. Each tier builds on the previous, ensuring fast feedback for basic issues while providing comprehensive validation for complex workflows.

Expand Down Expand Up @@ -372,7 +372,7 @@ OPERATION_CATEGORIES = {
2. **User Notification**: Clear description of operation and security implications shown
3. **Approval Decision**: User accepts/denies with session-wide persistence
4. **Execution**: Approved operations proceed, denied operations are blocked
5. **Session Memory**: Decisions persist for the current session to avoid repetitive prompts
5. **Session Memory**: Decisions are stored on `LifespanState`, so they persist across requests for the life of the server process

### **Security Features**

Expand Down Expand Up @@ -421,32 +421,29 @@ OPERATION_CATEGORIES["database_operations"] = {
```python
# Unit tests in tests/unit/tools/test_operation_approval.py
def test_approval_required():
context = create_test_context()
code = 'ggsave("test.png")'
context = Context.create("test", "test", LifespanState())

# Should require approval
result = await validate_r_code(context, code)
assert result["requires_approval"] is True
assert result["operation_info"]["category"] == "file_operations"
# validate_r_code is synchronous and returns (is_safe, error)
is_safe, error = validate_r_code('ggsave("test.png")', context)
assert not is_safe
assert error == "OPERATION_APPROVAL_NEEDED:file_operations:ggsave"
```

### **Configuration Options**
### **Approval State**

**Environment Variables:**
```bash
# Disable approval system (for automation/testing)
export RMCP_DISABLE_OPERATION_APPROVAL=true

# Set default approval for specific categories
export RMCP_AUTO_APPROVE_FILE_OPERATIONS=true
```
Approvals are recorded on `LifespanState` (`rmcp/core/context.py`), not on the
per-request `Context` — a fresh `Context` is built for every MCP request, so
anything stored there would be forgotten before the next call:

**Session Configuration:**
```python
# Programmatic approval (for API integrations)
context.approval_state.approve_category("file_operations")
context.lifespan.approved_operations # {category: {operation: metadata}}
context.lifespan.approved_packages # set[str]
```

They persist for the life of the server process. There is no environment
variable to disable the approval system; the tools `approve_operation` and
`approve_r_package` are the only way to grant consent.

This system balances security with usability, ensuring sensitive operations require explicit user consent while maintaining smooth workflows for approved operations.

## Comprehensive R Package Management
Expand All @@ -465,36 +462,16 @@ RMCP v0.5.1 introduces a **systematic, evidence-based R package whitelist** with

**Additional Categories**: Spatial analysis, optimization, meta-analysis, clinical trials, robust statistics, missing data, NLP, experimental design, network analysis.

### **Tiered Security System**

**4-Tier Permission Model** balances functionality with security:

1. **Tier 1 - Auto-Approved (52 packages)**: Core statistical packages
- Base R, essential tidyverse, fundamental statistical packages
- Examples: `ggplot2`, `dplyr`, `MASS`, `survival`, `Matrix`

2. **Tier 2 - User Approval (56 packages)**: Extended functionality
- Popular ML, econometrics, time series packages
- Examples: `caret`, `randomForest`, `forecast`, `AER`, `rstan`

3. **Tier 3 - Admin Approval (75 packages)**: Specialized/development
- Advanced ML, development tools, web packages
- Examples: `xgboost`, `devtools`, `httr`, `tensorflow`

4. **Tier 4 - Blocked (26 packages)**: Security risks
- System access, external dependencies, compilation tools
- Examples: `rJava`, `RMySQL`, `processx`, `system` calls

### **Security Assessment**
### **Enforcement**

**Risk Categorization** by security impact:
- **System Access**: 4 packages (R.utils, unix, etc.)
- **Network Access**: 8 packages (curl, httr, etc.)
- **File Operations**: 8 packages (readr, openxlsx, etc.)
- **Code Execution**: 8 packages (Rcpp, devtools, etc.)
- **External Dependencies**: 7 packages (rJava, database drivers, etc.)
Package access is a single flat allowlist: a package is either in `ALLOWED_R_PACKAGES`
or it is refused. A refused package can be permitted for the session with
`approve_r_package`.

**Results**: 413 low-risk packages (96.3%), 4 medium-risk, 12 high-risk
Enforcement is regex over the R source text, so it catches honest mistakes but not
a determined caller — `do.call("library", list(pkg))` and `eval(parse(text=...))`
both evade it. Treat the allowlist as a guardrail, not a sandbox boundary; the real
isolation is the process and filesystem the server runs under.

### **Usage Examples**

Expand Down
31 changes: 15 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
[![Documentation](https://github.com/finite-sample/rmcp/actions/workflows/docs.yml/badge.svg)](https://finite-sample.github.io/rmcp/)
[![License](https://img.shields.io/github/license/finite-sample/rmcp)](https://github.com/finite-sample/rmcp/blob/main/LICENSE)

**Turn conversations into comprehensive statistical analysis** - A Model Context Protocol (MCP) server with **52 statistical analysis tools** across 11 categories and **429 R packages** from systematic CRAN task views. RMCP enables AI assistants to perform sophisticated statistical modeling, econometric analysis, machine learning, time series analysis, and data science tasks through natural conversation.
**Turn conversations into comprehensive statistical analysis** - A Model Context Protocol (MCP) server with **54 tools** across 11 categories and **429 R packages** from systematic CRAN task views. RMCP enables AI assistants to perform sophisticated statistical modeling, econometric analysis, machine learning, time series analysis, and data science tasks through natural conversation.

## 🚀 Quick Start (30 seconds)

Expand Down Expand Up @@ -83,7 +83,7 @@ Formula building, error recovery, example datasets → *"Help me build a regress
## 📦 Installation

### Prerequisites
- **Python 3.10+**
- **Python 3.11+**
- **R 4.4.0+** with **comprehensive package ecosystem**: RMCP uses a systematic 429-package whitelist from CRAN task views organized into 19+ categories:

```r
Expand All @@ -94,19 +94,20 @@ install.packages(c(
))

# Full ecosystem automatically available: Machine Learning (61 packages),
# Econometrics (55 packages), Time Series (48 packages),
# Bayesian Analysis (32 packages), and more
# Econometrics (55 packages), Time Series (57 packages),
# Bayesian Analysis (40 packages), and more
```

**Package Selection**: Evidence-based using CRAN task views, download statistics, and 4-tier security assessment
**Package Selection**: Evidence-based, using CRAN task views and download statistics

### Install RMCP

```bash
# Standard installation
pip install rmcp

# With HTTP transport support
# The Streamable HTTP transport ships in the base install.
# This extra adds pandas/openpyxl for Excel data handling.
pip install rmcp[http]

# Development installation
Expand Down Expand Up @@ -215,13 +216,13 @@ docker run -e RMCP_HTTP_HOST=0.0.0.0 -e RMCP_HTTP_PORT=8000 rmcp:latest
## 🔥 Key Features

- **🎯 Natural Conversation**: Ask questions in plain English, get statistical analysis
- **📚 Comprehensive Package Ecosystem**: 429 R packages from systematic CRAN task views with 4-tier security system
- **📚 Comprehensive Package Ecosystem**: 429 R packages from systematic CRAN task views
- **📊 Professional Output**: Formatted results with markdown tables and inline visualizations
- **🔒 Production Ready**: Official MCP SDK with stdio and Streamable HTTP transports, plus bearer-token auth for remote deployments
- **⚙️ Flexible Configuration**: Environment variables, config files, and CLI options
- **⚡ Fast & Reliable**: 100% test success rate across all scenarios
- **🌐 Multiple Transports**: stdio (Claude Desktop) and HTTP (web applications)
- **🛡️ Secure**: Evidence-based package selection with security-conscious permission tiers
- **🛡️ Guardrails**: Package allowlist, explicit user approval for file writes, package installs and system calls, and filesystem confinement for tool-written files. These guard against mistakes, not adversaries — RMCP executes R as the invoking user, so run it as a trusted local tool rather than an untrusted multi-tenant service.

## 📚 Documentation

Expand Down Expand Up @@ -252,15 +253,13 @@ cd rmcp
pip install -e ".[dev]"

# Run tests
python tests/unit/test_new_tools.py
python tests/e2e/test_claude_desktop_scenarios.py
uv run pytest tests/

# Format code
black rmcp/
# Lint and format
uv run ruff check --fix .
uv run ruff format .
```

See [CONTRIBUTING.md](CONTRIBUTING.md) for detailed guidelines.

## 📄 License

MIT License - see [LICENSE](LICENSE) file for details.
Expand All @@ -281,7 +280,8 @@ rmcp check-r-packages # Check what's missing

**MCP connection issues?**
```bash
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | rmcp start
rmcp list-capabilities # verify tools register without starting a session
rmcp --debug start # run the server with verbose logging on stderr
```

**📖 Need more help?** Check the [examples](examples/) directory for working code.
Expand All @@ -294,4 +294,3 @@ echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | rmcp start
---

**Ready to turn conversations into statistical insights?** Install RMCP and start analyzing data through AI assistants today! 🚀
# Test comment
2 changes: 1 addition & 1 deletion docs/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ Verify the installation works:
# Check version
rmcp --version

# List available tools (should show 53 tools)
# List available tools
rmcp list-capabilities

# Start the server
Expand Down
9 changes: 7 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[project]
name = "rmcp"
version = "0.9.1"
description = "Comprehensive Model Context Protocol server with 52 statistical analysis tools for Claude Desktop and Claude web, featuring HTTP transport, interactive documentation, and production deployment"
version = "0.10.0"
description = "Model Context Protocol server bringing R statistical analysis to Claude Desktop and Claude web, over stdio and Streamable HTTP"
authors = [{name = "Gaurav Sood", email = "gsood07@gmail.com"}]
license = {text = "MIT"}
readme = "README.md"
Expand Down Expand Up @@ -103,6 +103,11 @@ addopts = [
"--strict-markers",
"--strict-config",
]
filterwarnings = [
# A test that returns instead of asserting silently passes no matter what
# it computed. Make that a hard failure rather than a warning.
"error::pytest.PytestReturnNotNoneWarning",
]
asyncio_mode = "auto"
# asyncio_default_fixture_loop_scope removed for backwards compatibility with older pytest-asyncio versions
markers = [
Expand Down
5 changes: 1 addition & 4 deletions rmcp/bidirectional.py
Original file line number Diff line number Diff line change
Expand Up @@ -443,10 +443,7 @@ async def setup_r_bidirectional(
)
"""

# Execute setup in R session
setup_result = await context.execute_r_with_session(
setup_script, {}, use_session=True
)
setup_result = await context.execute_r(setup_script, {})

await context.info("R bidirectional communication set up successfully")

Expand Down
Loading
Loading