Skip to content

Commit 4ed15a3

Browse files
committed
move to uv, structured logging, us english
1 parent 0741c72 commit 4ed15a3

23 files changed

Lines changed: 1340 additions & 95 deletions

CLAUDE.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ export RMCP_R_TIMEOUT=300
319319
echo '{"debug": true, "logging": {"level": "DEBUG"}}' > ~/.rmcp/config.json
320320

321321
# CLI options override everything
322-
poetry run rmcp --debug --config custom.json start
322+
uv run rmcp --debug --config custom.json start
323323
```
324324

325325
### **Docker Configuration**
@@ -336,7 +336,7 @@ docker run -v $(pwd)/config.json:/etc/rmcp/config.json rmcp:latest
336336
- **Integration tests**: Configuration integration with HTTP/R/VFS components
337337
- **Environment tests**: Validation of all environment variable mappings
338338

339-
**📖 Complete documentation**: Auto-generated from code in `docs/configuration/` (build with `poetry run sphinx-build docs docs/_build`)
339+
**📖 Complete documentation**: Auto-generated from code in `docs/configuration/` (build with `uv run sphinx-build docs docs/_build`)
340340

341341
## Universal Operation Approval System
342342

@@ -545,13 +545,13 @@ Documentation is automatically generated from:
545545

546546
```bash
547547
# Build HTML documentation
548-
poetry run sphinx-build -b html docs docs/_build/html
548+
uv run sphinx-build -b html docs docs/_build/html
549549

550550
# Build with clean rebuild
551-
poetry run sphinx-build -E -a -b html docs docs/_build/html
551+
uv run sphinx-build -E -a -b html docs docs/_build/html
552552

553553
# Generate autosummary stubs
554-
poetry run sphinx-autogen docs/**/*.rst
554+
uv run sphinx-autogen docs/**/*.rst
555555

556556
# Serve documentation locally
557557
cd docs/_build/html && python -m http.server 8080

examples/claude_desktop_v0.5.0_examples.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ After running these examples with approvals, you should find:
228228
**Debug Commands:**
229229
```bash
230230
# Check RMCP status
231-
poetry run rmcp --debug start
231+
uv run rmcp --debug start
232232

233233
# Verify file permissions
234234
ls -la /tmp/

pyproject.toml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ dependencies = [
2727
"click>=8.1.0", # CLI interface
2828
"jsonschema>=4.0.0", # Schema validation
2929
"psutil>=5.0.0", # R process management
30+
"structlog>=23.1.0", # Structured logging for MCP observability
3031
# subprocess module is built-in for Python 3.8+
3132
]
3233

@@ -188,3 +189,21 @@ exclude_lines = [
188189
"class .*\\bProtocol\\):",
189190
"@(abc\\.)?abstractmethod",
190191
]
192+
193+
[tool.deptry]
194+
pep621_dev_dependency_groups = ["dev"]
195+
extend_exclude = [
196+
"scripts/", # Development scripts have their own dependency scope
197+
"streamlit/", # Streamlit app is optional component
198+
"tests/scenarios/test_deployment_scenarios.py", # Tests all optional deps
199+
]
200+
201+
[tool.deptry.per_rule_ignores]
202+
DEP002 = ["httpx", "pandas", "openpyxl"] # Test-only dependencies via pandas backend
203+
DEP003 = ["mcp", "requests", "anthropic"] # Transitive dependencies we import directly
204+
DEP001 = ["package_whitelist_comprehensive"] # Internal module
205+
DEP004 = ["pytest", "numpy"] # Dev dependencies in scripts/streamlit
206+
207+
[tool.deptry.package_module_name_map]
208+
structlog = "structlog"
209+
linkify-it-py = "linkify_it_py"

rmcp/cli.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from . import __version__
1717
from .config import get_config, load_config
1818
from .core.server import create_server
19+
from .logging_config import configure_structured_logging, get_logger
1920
from .registries.prompts import (
2021
model_diagnostic_prompt,
2122
panel_regression_prompt,
@@ -28,13 +29,8 @@
2829
from .transport.stdio import StdioTransport
2930

3031
# Modern Python 3.10+ syntax for type hints
31-
# Configure logging to stderr only
32-
logging.basicConfig(
33-
level=logging.INFO,
34-
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
35-
stream=sys.stderr,
36-
)
37-
logger = logging.getLogger(__name__)
32+
# Structured logging will be configured in CLI commands based on config
33+
logger = get_logger(__name__)
3834

3935

4036
async def _run_server_with_transport(server, transport) -> None:
@@ -57,8 +53,14 @@ async def _run_server_with_transport(server, transport) -> None:
5753
help="Path to configuration file",
5854
)
5955
@click.option("--debug", is_flag=True, help="Enable debug mode")
56+
@click.option(
57+
"--log-format",
58+
type=click.Choice(["structured", "pretty"]),
59+
default="structured",
60+
help="Log output format (structured=JSON, pretty=colored console)",
61+
)
6062
@click.pass_context
61-
def cli(ctx, config: Path, debug: bool):
63+
def cli(ctx, config: Path, debug: bool, log_format: str):
6264
"""RMCP MCP Server - Comprehensive statistical analysis with 44 tools across 11 categories."""
6365
# Ensure context object exists
6466
ctx.ensure_object(dict)
@@ -71,6 +73,7 @@ def cli(ctx, config: Path, debug: bool):
7173

7274
# Store config in context for subcommands
7375
ctx.obj["config"] = load_config(config_file=config, overrides=overrides)
76+
ctx.obj["log_format"] = log_format
7477

7578

7679
@cli.command()
@@ -84,10 +87,15 @@ def start(ctx, log_level: str):
8487
"""Start RMCP MCP server (default stdio transport)."""
8588
# Get configuration
8689
config = ctx.obj.get("config") or get_config()
90+
log_format = ctx.obj.get("log_format", "structured")
8791

88-
# Set logging level (CLI option overrides config)
92+
# Configure structured logging
8993
effective_log_level = log_level or config.logging.level
90-
logging.getLogger().setLevel(getattr(logging, effective_log_level.upper()))
94+
configure_structured_logging(
95+
level=effective_log_level,
96+
development_mode=(log_format == "pretty" or config.debug),
97+
enable_console=True,
98+
)
9199

92100
logger.info(f"Starting RMCP MCP Server v{__version__}")
93101
if config.debug:
@@ -291,6 +299,14 @@ def serve_http(
291299

292300
# Get configuration
293301
config = ctx.obj.get("config") or get_config()
302+
log_format = ctx.obj.get("log_format", "structured")
303+
304+
# Configure structured logging for HTTP transport
305+
configure_structured_logging(
306+
level=config.logging.level,
307+
development_mode=(log_format == "pretty" or config.debug),
308+
enable_console=True,
309+
)
294310

295311
# Use CLI options or fall back to config
296312
effective_host = host or config.http.host

rmcp/config/loader.py

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -221,25 +221,24 @@ def _load_environment_config(self) -> dict[str, Any]:
221221

222222
def _convert_env_value(self, env_var: str, value: str) -> Any:
223223
"""Convert environment variable string to appropriate type."""
224-
# Boolean conversion
225-
if env_var.endswith(("_READ_ONLY", "_DEBUG", "_STDERR_OUTPUT")):
226-
return value.lower() in ("true", "1", "yes", "on")
227-
228-
# Integer conversion
229-
if env_var.endswith(
230-
("_PORT", "_TIMEOUT", "_MAX_SESSIONS", "_MAX_WORKERS", "_MAX_FILE_SIZE")
231-
):
232-
try:
233-
return int(value)
234-
except ValueError:
235-
raise ConfigError(f"Invalid integer value for {env_var}: {value}")
236-
237-
# List conversion (comma-separated)
238-
if env_var.endswith(("_ORIGINS", "_PATHS", "_MIME_TYPES")):
239-
return [item.strip() for item in value.split(",") if item.strip()]
240-
241-
# String value
242-
return value
224+
match env_var:
225+
case var if var.endswith(("_READ_ONLY", "_DEBUG", "_STDERR_OUTPUT")):
226+
# Boolean conversion
227+
return value.lower() in ("true", "1", "yes", "on")
228+
case var if var.endswith(
229+
("_PORT", "_TIMEOUT", "_MAX_SESSIONS", "_MAX_WORKERS", "_MAX_FILE_SIZE")
230+
):
231+
# Integer conversion
232+
try:
233+
return int(value)
234+
except ValueError:
235+
raise ConfigError(f"Invalid integer value for {env_var}: {value}")
236+
case var if var.endswith(("_ORIGINS", "_PATHS", "_MIME_TYPES")):
237+
# List conversion (comma-separated)
238+
return [item.strip() for item in value.split(",") if item.strip()]
239+
case _:
240+
# String value
241+
return value
243242

244243
def _set_nested_value(self, config_dict: dict[str, Any], path: str, value: Any):
245244
"""Set a nested dictionary value using dot notation."""

rmcp/config/models.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
from dataclasses import dataclass, field
3434
from pathlib import Path
3535

36+
from ..types import CORSOrigin, LogLevel
37+
3638

3739
@dataclass(slots=True)
3840
class HTTPConfig:
@@ -87,7 +89,7 @@ class HTTPConfig:
8789
ssl_keyfile_password: str | None = None
8890
"""SSL private key password if the key file is encrypted."""
8991

90-
cors_origins: list[str] = field(
92+
cors_origins: list[CORSOrigin] = field(
9193
default_factory=lambda: [
9294
"http://localhost:*",
9395
"http://127.0.0.1:*",
@@ -283,7 +285,7 @@ class LoggingConfig:
283285
)
284286
"""
285287

286-
level: str = "INFO"
288+
level: LogLevel = "INFO"
287289
"""Logging level. Must be DEBUG, INFO, WARNING, ERROR, or CRITICAL."""
288290

289291
format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"

rmcp/core/server.py

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,16 +1060,17 @@ async def _handle_notification(self, method: str, params: dict[str, Any]) -> Non
10601060
- notifications/initialized: Client initialization complete
10611061
"""
10621062
logger.info(f"Received notification: {method}")
1063-
if method == "notifications/cancelled":
1064-
# Handle cancellation notification
1065-
request_id = params.get("requestId")
1066-
if request_id:
1067-
await self.cancel_request(request_id)
1068-
elif method == "notifications/initialized":
1069-
# MCP initialization complete
1070-
logger.info("MCP client initialization complete")
1071-
else:
1072-
logger.warning(f"Unknown notification method: {method}")
1063+
match method:
1064+
case "notifications/cancelled":
1065+
# Handle cancellation notification
1066+
request_id = params.get("requestId")
1067+
if request_id:
1068+
await self.cancel_request(request_id)
1069+
case "notifications/initialized":
1070+
# MCP initialization complete
1071+
logger.info("MCP client initialization complete")
1072+
case _:
1073+
logger.warning(f"Unknown notification method: {method}")
10731074

10741075

10751076
def create_server(

0 commit comments

Comments
 (0)