Skip to content

Commit 64f1d81

Browse files
committed
refactor(mcp): Refactor the MCP service module by splitting the monolithic file into a standard service package
1 parent d5b5485 commit 64f1d81

15 files changed

Lines changed: 1860 additions & 1898 deletions
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# MCP Implementation Alignment Plan
2+
3+
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement.
4+
5+
**Goal:** Restructure Ripperdoc's MCP implementation to strictly mirror Claude Code's architecture.
6+
7+
**Architecture:** Split the monolithic `utils/mcp/__init__.py` (1550 lines) into `services/mcp/` package with modules mirroring the reference's `services/mcp/*.ts`. Restructure `tools/mcp/` from a single `__init__.py` into individual tool directories matching `tools/*Mcp*/`. Add missing modules (normalization, envExpansion, headersHelper, oauthPort). Deduplicate `mcp/_tool.py` (404 lines dead code).
8+
9+
**Tech Stack:** Python 3.10+, asyncio, pydantic
10+
11+
---
12+
13+
### Task 1: Create `ripperdoc/services/mcp/` package structure
14+
15+
**Files:**
16+
- Create: `ripperdoc/services/mcp/__init__.py`
17+
- Create: `ripperdoc/services/mcp/types.py`
18+
- Create: `ripperdoc/services/mcp/normalization.py`
19+
- Create: `ripperdoc/services/mcp/mcp_string_utils.py`
20+
- Create: `ripperdoc/services/mcp/env_expansion.py`
21+
- Create: `ripperdoc/services/mcp/config.py`
22+
- Create: `ripperdoc/services/mcp/client.py`
23+
- Create: `ripperdoc/services/mcp/utils.py`
24+
- Modify: `ripperdoc/utils/mcp/__init__.py` → re-export shim
25+
26+
**Step 1: Create `__init__.py`**
27+
28+
```python
29+
"""MCP service layer — connection management, config loading, auth."""
30+
```
31+
32+
**Step 2: Create `types.py`**
33+
34+
Mirror `mcp/types.ts`:
35+
- `ConfigScope` enum (local, user, project, dynamic, enterprise, claudeai, managed)
36+
- `TransportType` enum (stdio, sse, sse-ide, http, ws, sdk)
37+
- `McpToolInfo` dataclass
38+
- `McpResourceInfo` dataclass
39+
- `McpServerInfo` dataclass (with scope, headers, instructions, capabilities, etc.)
40+
- `StdioServerConfig`, `SSEServerConfig`, `HTTPServerConfig`, etc. (typed configs)
41+
42+
**Step 3: Create `normalization.py`**
43+
44+
Mirror `mcp/normalization.ts`:
45+
- `normalize_name_for_mcp(name: str) -> str`
46+
47+
**Step 4: Create `mcp_string_utils.py`**
48+
49+
Mirror `mcp/mcpStringUtils.ts`:
50+
- `mcp_info_from_string(tool_string: str) -> Optional[dict]`
51+
- `build_mcp_tool_name(server_name: str, tool_name: str) -> str`
52+
- `get_mcp_prefix(server_name: str) -> str`
53+
54+
**Step 5: Create `env_expansion.py`**
55+
56+
Mirror `mcp/envExpansion.ts`:
57+
- `expand_env_vars_in_string(value: str) -> ExpandedResult`
58+
59+
**Step 6: Create `config.py`**
60+
61+
Mirror `mcp/config.ts`:
62+
- `load_json_file(path: Path) -> Dict`
63+
- `normalize_command(command, args) -> tuple`
64+
- `parse_server(name, raw) -> McpServerInfo`
65+
- `parse_servers(data) -> Dict[str, McpServerInfo]`
66+
- `load_server_configs(project_path) -> Dict[str, McpServerInfo]`
67+
- `load_mcp_server_configs(project_path) -> Dict[str, McpServerInfo]`
68+
- `parse_mcp_server_configs(raw) -> Dict[str, McpServerInfo]`
69+
- `project_scope_key(project_path) -> str`
70+
- `set_mcp_runtime_overrides(...)`
71+
- `clear_mcp_runtime_overrides(...)`
72+
73+
**Step 7: Create `client.py`**
74+
75+
Mirror `mcp/client.ts` (core connection logic):
76+
- `_SdkMcpSession` (minimal SDK client)
77+
- `McpRuntime` class — the main connection manager
78+
- `McpCircuitState` and circuit breaker logic
79+
- `connect`, `_connect_server`, `_connect_server_with_policy`, `aclose`
80+
- `server_snapshot()` method
81+
- stderr log management
82+
83+
**Step 8: Create `utils.py`**
84+
85+
Mirror `mcp/utils.ts`:
86+
- `format_mcp_instructions(servers) -> str`
87+
- `estimate_mcp_tokens(servers) -> int`
88+
- `find_mcp_resource(servers, server_name, uri) -> Optional[McpResourceInfo]`
89+
90+
**Step 9: Update `utils/mcp/__init__.py` → re-export shim**
91+
92+
Keep backward compatibility by re-exporting all public symbols from `services/mcp/`:
93+
```python
94+
from ripperdoc.services.mcp.types import (
95+
McpToolInfo, McpResourceInfo, McpServerInfo, ...
96+
)
97+
from ripperdoc.services.mcp.config import (
98+
load_mcp_server_configs, parse_mcp_server_configs, ...
99+
)
100+
from ripperdoc.services.mcp.client import (
101+
McpRuntime, ensure_mcp_runtime, shutdown_mcp_runtime, ...
102+
)
103+
from ripperdoc.services.mcp.utils import (
104+
format_mcp_instructions, estimate_mcp_tokens, find_mcp_resource, ...
105+
)
106+
from ripperdoc.services.mcp.mcp_string_utils import (
107+
mcp_info_from_string, build_mcp_tool_name, get_mcp_prefix, ...
108+
)
109+
from ripperdoc.services.mcp.normalization import (
110+
normalize_name_for_mcp, ...
111+
)
112+
from ripperdoc.services.mcp.env_expansion import (
113+
expand_env_vars_in_string, ...
114+
)
115+
```
116+
117+
### Task 2: Separate MCP tools into individual directories
118+
119+
**Files:**
120+
- Create: `ripperdoc/tools/mcp_tool/` (for MCPTool — dynamic tool invocation)
121+
- Create: `ripperdoc/tools/list_mcp_servers_tool/`
122+
- Create: `ripperdoc/tools/list_mcp_resources_tool/`
123+
- Create: `ripperdoc/tools/read_mcp_resource_tool/`
124+
- Keep: `ripperdoc/tools/mcp/dynamic_mcp.py` (DynamicMcpTool wrapper)
125+
- Keep: `ripperdoc/tools/mcp/mcp_output_limits.py`
126+
- Delete: `ripperdoc/tools/mcp/_tool.py` (dead code, 404 lines)
127+
- Update: `ripperdoc/tools/mcp/__init__.py` → re-export shim
128+
129+
### Task 3: Clean up and verify
130+
131+
- Remove the dead `_tool.py`
132+
- Run tests to verify nothing broke
133+
- Fix any import issues

ripperdoc/cli/ui/rich_ui/session.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,7 @@ def _auto_init_thinking_mode(self) -> None:
371371
372372
Runs once during __init__ so the rprompt shows the correct state
373373
before the first user message."""
374-
if not self._thinking_mode_manually_set and self._profile_supports_reasoning():
374+
if not getattr(self, "_thinking_mode_manually_set", False) and self._profile_supports_reasoning():
375375
self._thinking_mode_enabled = True
376376

377377
def _create_permission_checker(self) -> Any:
@@ -625,7 +625,13 @@ def _default_thinking_tokens_for_model(self) -> int:
625625
"""Return a default thinking token budget when model supports reasoning.
626626
627627
Priority: profile max_thinking_tokens > profile thinking_effort mapped > global default.
628+
Returns 0 if thinking_effort explicitly disables thinking.
628629
"""
630+
model_profile = get_profile_for_pointer(self.model)
631+
if model_profile is not None:
632+
effort = (getattr(model_profile, "thinking_effort", None) or "").strip().lower()
633+
if effort in {"none", "off", "disabled"}:
634+
return 0
629635
profile_tokens = self._resolve_profile_thinking_tokens()
630636
if profile_tokens > 0:
631637
return profile_tokens
@@ -654,7 +660,7 @@ def _get_thinking_tokens(self) -> int:
654660
# Auto-enable thinking when model supports reasoning, unless user
655661
# manually toggled it off. The budget is resolved from profile config
656662
# (max_thinking_tokens or thinking_effort) or global default_thinking_tokens.
657-
if not self._thinking_mode_manually_set and self._profile_supports_reasoning():
663+
if not getattr(self, "_thinking_mode_manually_set", False) and self._profile_supports_reasoning():
658664
self._thinking_mode_enabled = True
659665
return self._default_thinking_tokens_for_model()
660666
if not self._thinking_mode_enabled:

ripperdoc/services/mcp/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""MCP service layer — connection management, config loading, and tooling."""

0 commit comments

Comments
 (0)