name: CI
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}/rmcp-ci
jobs:
# Fast checks that don't need R
lint-and-smoke:
name: Lint & Smoke Tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.11"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install black isort flake8 click jsonschema pytest pytest-asyncio
pip install -e .
- name: Run linting
run: |
black --check rmcp tests streamlit
isort --check-only rmcp tests streamlit
flake8 rmcp tests streamlit
- name: Quick smoke tests
run: |
# Test CLI works
rmcp --version
rmcp list-capabilities > /dev/null
# Test server can be created
python -c "
import sys
sys.path.insert(0, '.')
from rmcp.core.server import create_server
from rmcp.cli import _register_builtin_tools
server = create_server()
_register_builtin_tools(server)
print('✅ Server creation works')
"
# Test non-R dependent unit tests
python -m pytest tests/unit/test_server_basic.py -v
# Build Docker image with all R packages for faster testing
docker-build:
name: Build CI Docker Image
runs-on: ubuntu-latest
needs: lint-and-smoke
permissions:
contents: read
packages: write
attestations: write
id-token: write
outputs:
image: ${{ steps.image.outputs.image }}
digest: ${{ steps.build.outputs.digest }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
- name: Output image reference
id: image
run: echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITHUB_OUTPUT
# Full tests that need R (using freshly built Docker image)
full-tests:
name: Full Test Suite
runs-on: ubuntu-latest
needs: [lint-and-smoke, docker-build]
container:
image: ${{ needs.docker-build.outputs.image }}
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
# All R packages and Python dependencies are already installed in the Docker image
# Just install RMCP in development mode to make it importable
- name: Install RMCP in development mode
run: pip install -e .
- name: Run all unit tests
run: |
python -m pytest tests/unit/ -v
- name: Run integration tests
run: |
python tests/integration/test_mcp_interface.py
python tests/integration/test_direct_capabilities.py
python tests/integration/test_new_features_integration.py
- name: Run end-to-end tests
run: |
python tests/e2e/realistic_scenarios.py
python tests/e2e/test_claude_desktop_scenarios.py
# Feature verification (using freshly built Docker image)
feature-verification:
name: Feature Verification
runs-on: ubuntu-latest
needs: [lint-and-smoke, docker-build]
container:
image: ${{ needs.docker-build.outputs.image }}
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
steps:
- uses: actions/checkout@v4
# All dependencies pre-installed in Docker image - just install RMCP in development mode
- name: Install RMCP in development mode
run: pip install -e .
- name: Verify tool count
run: |
python -c "
import sys, asyncio
sys.path.insert(0, '.')
from rmcp.core.server import create_server
from rmcp.cli import _register_builtin_tools
server = create_server()
_register_builtin_tools(server)
async def count():
ctx = server.create_context('test', 'tools/list')
result = await server.tools.list_tools(ctx)
count = len(result['tools'])
print(f'Registered tools: {count}')
assert count >= 44, f'Expected at least 44 tools, got {count}'
print('✅ Tool count verification passed')
asyncio.run(count())
"
- name: Test new features
run: |
python -c "
import sys, asyncio, json
sys.path.insert(0, '.')
from rmcp.core.server import create_server
from rmcp.registries.tools import register_tool_functions
from rmcp.tools.formula_builder import build_formula
from rmcp.tools.helpers import suggest_fix, load_example
def extract_json_content(resp):
'''Extract JSON content from tool response'''
result = resp.get('result', {})
# Check structuredContent first (new MCP-compliant format)
structured = result.get('structuredContent')
if structured:
# Handle new object format (single item)
if isinstance(structured, dict):
if structured.get('type') == 'json':
return structured.get('json')
# Handle multi-item object format
elif 'items' in structured:
for item in structured['items']:
if item.get('type') == 'json':
return item.get('json')
# Handle legacy array format
elif isinstance(structured, list):
for item in structured:
if item.get('type') == 'json':
return item.get('json')
# Then check content with annotations (legacy format)
content_items = result.get('content', [])
for item in content_items:
if item.get('annotations', {}).get('mimeType') == 'application/json':
return json.loads(item['text'])
# Fallback: try to parse any text as JSON
for item in content_items:
if item.get('type') == 'text' and item.get('text'):
try:
return json.loads(item['text'])
except json.JSONDecodeError:
continue
raise ValueError('No JSON content found in response')
async def test():
server = create_server()
register_tool_functions(server.tools, build_formula, suggest_fix, load_example)
# Test formula building
req = {'jsonrpc': '2.0', 'id': 1, 'method': 'tools/call', 'params': {'name': 'build_formula', 'arguments': {'description': 'predict sales from marketing'}}}
resp = await server.handle_request(req)
result = extract_json_content(resp)
assert 'formula' in result
print(f'✅ Formula builder: {result[\"formula\"]}')
# Test error recovery
req = {'jsonrpc': '2.0', 'id': 2, 'method': 'tools/call', 'params': {'name': 'suggest_fix', 'arguments': {'error_message': 'there is no package called \"forecast\"'}}}
resp = await server.handle_request(req)
result = extract_json_content(resp)
assert result['error_type'] == 'missing_package'
print(f'✅ Error recovery: {result[\"error_type\"]}')
# Test example datasets
req = {'jsonrpc': '2.0', 'id': 3, 'method': 'tools/call', 'params': {'name': 'load_example', 'arguments': {'dataset_name': 'sales', 'size': 'small'}}}
resp = await server.handle_request(req)
result = extract_json_content(resp)
assert 'data' in result
print(f'✅ Example datasets: {result[\"metadata\"][\"rows\"]} rows loaded')
print('🎊 All new features working!')
asyncio.run(test())
"
- name: Verify package metadata consistency
run: |
python -c "
import rmcp
import sys
# Check version consistency between __init__.py and pyproject.toml
import tomllib
with open('pyproject.toml', 'rb') as f:
pyproject = tomllib.load(f)
init_version = rmcp.__version__
toml_version = pyproject['tool']['poetry']['version']
assert init_version == toml_version, \
f'Version mismatch: __init__.py={init_version} vs pyproject.toml={toml_version}'
print(f'✅ Version consistency verified: {init_version}')
"
- name: Verify tool registration and count
run: |
python -c "
from rmcp.core.server import create_server
from rmcp.cli import _register_builtin_tools
# Create server and register all tools
server = create_server()
_register_builtin_tools(server)
# Count registered tools
tool_count = len(server.tools._tools)
tool_names = set(server.tools._tools.keys())
print(f'✅ Registered {tool_count} tools')
# Verify minimum expected tool count
assert tool_count >= 44, f'Expected at least 44 tools, got {tool_count}'
# Verify key tools exist (sampling from each category)
required_tools = {
'linear_model', 'correlation_analysis', # Regression
'execute_r_analysis', 'list_allowed_r_packages', # Flexible R
'read_csv', 'write_excel', # File operations
'build_formula', 'suggest_fix', # Helpers
'arima_model', 'summary_stats' # Time series, descriptive
}
missing = required_tools - tool_names
assert not missing, f'Missing required tools: {missing}'
print('✅ All required tools registered and accessible')
print(f'✅ Tool registration verification passed ({tool_count} tools)')
"