tunacode-cli 0.0.1__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.

Potentially problematic release.


This version of tunacode-cli might be problematic. Click here for more details.

Files changed (65) hide show
  1. tunacode/__init__.py +0 -0
  2. tunacode/cli/__init__.py +4 -0
  3. tunacode/cli/commands.py +632 -0
  4. tunacode/cli/main.py +47 -0
  5. tunacode/cli/repl.py +251 -0
  6. tunacode/configuration/__init__.py +1 -0
  7. tunacode/configuration/defaults.py +26 -0
  8. tunacode/configuration/models.py +69 -0
  9. tunacode/configuration/settings.py +32 -0
  10. tunacode/constants.py +129 -0
  11. tunacode/context.py +83 -0
  12. tunacode/core/__init__.py +0 -0
  13. tunacode/core/agents/__init__.py +0 -0
  14. tunacode/core/agents/main.py +119 -0
  15. tunacode/core/setup/__init__.py +17 -0
  16. tunacode/core/setup/agent_setup.py +41 -0
  17. tunacode/core/setup/base.py +37 -0
  18. tunacode/core/setup/config_setup.py +179 -0
  19. tunacode/core/setup/coordinator.py +45 -0
  20. tunacode/core/setup/environment_setup.py +62 -0
  21. tunacode/core/setup/git_safety_setup.py +188 -0
  22. tunacode/core/setup/undo_setup.py +32 -0
  23. tunacode/core/state.py +43 -0
  24. tunacode/core/tool_handler.py +57 -0
  25. tunacode/exceptions.py +105 -0
  26. tunacode/prompts/system.txt +71 -0
  27. tunacode/py.typed +0 -0
  28. tunacode/services/__init__.py +1 -0
  29. tunacode/services/mcp.py +86 -0
  30. tunacode/services/undo_service.py +244 -0
  31. tunacode/setup.py +50 -0
  32. tunacode/tools/__init__.py +0 -0
  33. tunacode/tools/base.py +244 -0
  34. tunacode/tools/read_file.py +89 -0
  35. tunacode/tools/run_command.py +107 -0
  36. tunacode/tools/update_file.py +117 -0
  37. tunacode/tools/write_file.py +82 -0
  38. tunacode/types.py +259 -0
  39. tunacode/ui/__init__.py +1 -0
  40. tunacode/ui/completers.py +129 -0
  41. tunacode/ui/console.py +74 -0
  42. tunacode/ui/constants.py +16 -0
  43. tunacode/ui/decorators.py +59 -0
  44. tunacode/ui/input.py +95 -0
  45. tunacode/ui/keybindings.py +27 -0
  46. tunacode/ui/lexers.py +46 -0
  47. tunacode/ui/output.py +109 -0
  48. tunacode/ui/panels.py +156 -0
  49. tunacode/ui/prompt_manager.py +117 -0
  50. tunacode/ui/tool_ui.py +187 -0
  51. tunacode/ui/validators.py +23 -0
  52. tunacode/utils/__init__.py +0 -0
  53. tunacode/utils/bm25.py +55 -0
  54. tunacode/utils/diff_utils.py +69 -0
  55. tunacode/utils/file_utils.py +41 -0
  56. tunacode/utils/ripgrep.py +17 -0
  57. tunacode/utils/system.py +336 -0
  58. tunacode/utils/text_utils.py +87 -0
  59. tunacode/utils/user_configuration.py +54 -0
  60. tunacode_cli-0.0.1.dist-info/METADATA +242 -0
  61. tunacode_cli-0.0.1.dist-info/RECORD +65 -0
  62. tunacode_cli-0.0.1.dist-info/WHEEL +5 -0
  63. tunacode_cli-0.0.1.dist-info/entry_points.txt +2 -0
  64. tunacode_cli-0.0.1.dist-info/licenses/LICENSE +21 -0
  65. tunacode_cli-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,188 @@
1
+ """Git safety setup to create a working branch for TunaCode."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ from tunacode.core.setup.base import BaseSetup
7
+ from tunacode.core.state import StateManager
8
+ from tunacode.ui import console as ui
9
+ from tunacode.ui.input import input as prompt_input
10
+ from tunacode.ui.panels import panel
11
+
12
+
13
+ async def yes_no_prompt(question: str, default: bool = True) -> bool:
14
+ """Simple yes/no prompt."""
15
+ default_text = "[Y/n]" if default else "[y/N]"
16
+ response = await prompt_input(
17
+ session_key="yes_no",
18
+ pretext=f"{question} {default_text}: "
19
+ )
20
+
21
+ if not response.strip():
22
+ return default
23
+
24
+ return response.lower().strip() in ['y', 'yes']
25
+
26
+
27
+ class GitSafetySetup(BaseSetup):
28
+ """Setup step to create a safe working branch for TunaCode."""
29
+
30
+ def __init__(self, state_manager: StateManager):
31
+ super().__init__(state_manager)
32
+
33
+ @property
34
+ def name(self) -> str:
35
+ """Return the name of this setup step."""
36
+ return "Git Safety"
37
+
38
+ async def should_run(self, force: bool = False) -> bool:
39
+ """Check if we should run git safety setup."""
40
+ # Always run unless user has explicitly disabled it
41
+ return not self.state_manager.session.user_config.get("skip_git_safety", False)
42
+
43
+ async def execute(self, force: bool = False) -> None:
44
+ """Create a safety branch for TunaCode operations."""
45
+ try:
46
+ # Check if git is installed
47
+ result = subprocess.run(
48
+ ["git", "--version"],
49
+ capture_output=True,
50
+ text=True,
51
+ check=False
52
+ )
53
+
54
+ if result.returncode != 0:
55
+ await panel(
56
+ "⚠️ Git Not Found",
57
+ "Git is not installed or not in PATH. TunaCode will modify files directly.\n"
58
+ "It's strongly recommended to install Git for safety.",
59
+ border_style="yellow"
60
+ )
61
+ return
62
+
63
+ # Check if we're in a git repository
64
+ result = subprocess.run(
65
+ ["git", "rev-parse", "--git-dir"],
66
+ capture_output=True,
67
+ text=True,
68
+ check=False,
69
+ cwd=Path.cwd()
70
+ )
71
+
72
+ if result.returncode != 0:
73
+ await panel(
74
+ "⚠️ Not a Git Repository",
75
+ "This directory is not a Git repository. TunaCode will modify files directly.\n"
76
+ "Consider initializing a Git repository for safety: git init",
77
+ border_style="yellow"
78
+ )
79
+ return
80
+
81
+ # Get current branch name
82
+ result = subprocess.run(
83
+ ["git", "branch", "--show-current"],
84
+ capture_output=True,
85
+ text=True,
86
+ check=True
87
+ )
88
+ current_branch = result.stdout.strip()
89
+
90
+ if not current_branch:
91
+ # Detached HEAD state
92
+ await panel(
93
+ "⚠️ Detached HEAD State",
94
+ "You're in a detached HEAD state. TunaCode will continue without creating a branch.",
95
+ border_style="yellow"
96
+ )
97
+ return
98
+
99
+ # Check if we're already on a -tunacode branch
100
+ if current_branch.endswith("-tunacode"):
101
+ await ui.info(f"Already on a TunaCode branch: {current_branch}")
102
+ return
103
+
104
+ # Propose new branch name
105
+ new_branch = f"{current_branch}-tunacode"
106
+
107
+ # Check if there are uncommitted changes
108
+ result = subprocess.run(
109
+ ["git", "status", "--porcelain"],
110
+ capture_output=True,
111
+ text=True,
112
+ check=True
113
+ )
114
+
115
+ has_changes = bool(result.stdout.strip())
116
+
117
+ # Ask user if they want to create a safety branch
118
+ message = (
119
+ f"For safety, TunaCode can create a new branch '{new_branch}' based on '{current_branch}'.\n"
120
+ f"This helps protect your work from unintended changes.\n"
121
+ )
122
+
123
+ if has_changes:
124
+ message += "\n⚠️ You have uncommitted changes that will be brought to the new branch."
125
+
126
+ create_branch = await yes_no_prompt(
127
+ f"{message}\n\nCreate safety branch?",
128
+ default=True
129
+ )
130
+
131
+ if not create_branch:
132
+ # User declined - show warning
133
+ await panel(
134
+ "⚠️ Working Without Safety Branch",
135
+ "You've chosen to work directly on your current branch.\n"
136
+ "TunaCode will modify files in place. Make sure you have backups!\n"
137
+ "You can always use /undo to revert changes.",
138
+ border_style="red"
139
+ )
140
+ # Save preference
141
+ self.state_manager.session.user_config["skip_git_safety"] = True
142
+ return
143
+
144
+ # Create and checkout the new branch
145
+ try:
146
+ # Check if branch already exists
147
+ result = subprocess.run(
148
+ ["git", "show-ref", "--verify", f"refs/heads/{new_branch}"],
149
+ capture_output=True,
150
+ check=False
151
+ )
152
+
153
+ if result.returncode == 0:
154
+ # Branch exists, ask to use it
155
+ use_existing = await yes_no_prompt(
156
+ f"Branch '{new_branch}' already exists. Switch to it?",
157
+ default=True
158
+ )
159
+ if use_existing:
160
+ subprocess.run(["git", "checkout", new_branch], check=True)
161
+ await ui.success(f"Switched to existing branch: {new_branch}")
162
+ else:
163
+ await ui.warning("Continuing on current branch")
164
+ else:
165
+ # Create new branch
166
+ subprocess.run(["git", "checkout", "-b", new_branch], check=True)
167
+ await ui.success(f"Created and switched to new branch: {new_branch}")
168
+
169
+ except subprocess.CalledProcessError as e:
170
+ await panel(
171
+ "❌ Failed to Create Branch",
172
+ f"Could not create branch '{new_branch}': {str(e)}\n"
173
+ "Continuing on current branch.",
174
+ border_style="red"
175
+ )
176
+
177
+ except Exception as e:
178
+ # Non-fatal error - just warn the user
179
+ await panel(
180
+ "⚠️ Git Safety Setup Failed",
181
+ f"Could not set up Git safety: {str(e)}\n"
182
+ "TunaCode will continue without branch protection.",
183
+ border_style="yellow"
184
+ )
185
+
186
+ async def validate(self) -> bool:
187
+ """Validate git safety setup - always returns True as this is optional."""
188
+ return True
@@ -0,0 +1,32 @@
1
+ """Module: sidekick.core.setup.undo_setup
2
+
3
+ Undo system initialization for the Sidekick CLI.
4
+ Sets up file tracking and state management for undo operations.
5
+ """
6
+
7
+ from tunacode.core.setup.base import BaseSetup
8
+ from tunacode.core.state import StateManager
9
+ from tunacode.services.undo_service import init_undo_system
10
+
11
+
12
+ class UndoSetup(BaseSetup):
13
+ """Setup step for undo system initialization."""
14
+
15
+ def __init__(self, state_manager: StateManager):
16
+ super().__init__(state_manager)
17
+
18
+ @property
19
+ def name(self) -> str:
20
+ return "Undo System"
21
+
22
+ async def should_run(self, force_setup: bool = False) -> bool:
23
+ """Undo setup should run if not already initialized."""
24
+ return not self.state_manager.session.undo_initialized
25
+
26
+ async def execute(self, force_setup: bool = False) -> None:
27
+ """Initialize the undo system."""
28
+ self.state_manager.session.undo_initialized = init_undo_system(self.state_manager)
29
+
30
+ async def validate(self) -> bool:
31
+ """Validate that undo system was initialized correctly."""
32
+ return self.state_manager.session.undo_initialized
tunacode/core/state.py ADDED
@@ -0,0 +1,43 @@
1
+ """Module: sidekick.core.state
2
+
3
+ State management system for session data in Sidekick CLI.
4
+ Provides centralized state tracking for agents, messages, configurations, and session information.
5
+ """
6
+
7
+ import uuid
8
+ from dataclasses import dataclass, field
9
+ from typing import Any, Optional
10
+
11
+ from tunacode.types import (DeviceId, InputSessions, MessageHistory, ModelName, SessionId, ToolName,
12
+ UserConfig)
13
+
14
+
15
+ @dataclass
16
+ class SessionState:
17
+ user_config: UserConfig = field(default_factory=dict)
18
+ agents: dict[str, Any] = field(
19
+ default_factory=dict
20
+ ) # Keep as dict[str, Any] for agent instances
21
+ messages: MessageHistory = field(default_factory=list)
22
+ total_cost: float = 0.0
23
+ current_model: ModelName = "openai:gpt-4o"
24
+ spinner: Optional[Any] = None
25
+ tool_ignore: list[ToolName] = field(default_factory=list)
26
+ yolo: bool = False
27
+ undo_initialized: bool = False
28
+ session_id: SessionId = field(default_factory=lambda: str(uuid.uuid4()))
29
+ device_id: Optional[DeviceId] = None
30
+ input_sessions: InputSessions = field(default_factory=dict)
31
+ current_task: Optional[Any] = None
32
+
33
+
34
+ class StateManager:
35
+ def __init__(self):
36
+ self._session = SessionState()
37
+
38
+ @property
39
+ def session(self) -> SessionState:
40
+ return self._session
41
+
42
+ def reset_session(self):
43
+ self._session = SessionState()
@@ -0,0 +1,57 @@
1
+ """
2
+ Tool handling business logic, separated from UI concerns.
3
+ """
4
+
5
+ from tunacode.core.state import StateManager
6
+ from tunacode.types import ToolArgs, ToolConfirmationRequest, ToolConfirmationResponse, ToolName
7
+
8
+
9
+ class ToolHandler:
10
+ """Handles tool confirmation logic separate from UI."""
11
+
12
+ def __init__(self, state_manager: StateManager):
13
+ self.state = state_manager
14
+
15
+ def should_confirm(self, tool_name: ToolName) -> bool:
16
+ """
17
+ Determine if a tool requires confirmation.
18
+
19
+ Args:
20
+ tool_name: Name of the tool to check.
21
+
22
+ Returns:
23
+ bool: True if confirmation is required, False otherwise.
24
+ """
25
+ return not (self.state.session.yolo or tool_name in self.state.session.tool_ignore)
26
+
27
+ def process_confirmation(self, response: ToolConfirmationResponse, tool_name: ToolName) -> bool:
28
+ """
29
+ Process the confirmation response.
30
+
31
+ Args:
32
+ response: The confirmation response from the user.
33
+ tool_name: Name of the tool being confirmed.
34
+
35
+ Returns:
36
+ bool: True if tool should proceed, False if aborted.
37
+ """
38
+ if response.skip_future:
39
+ self.state.session.tool_ignore.append(tool_name)
40
+
41
+ return response.approved and not response.abort
42
+
43
+ def create_confirmation_request(
44
+ self, tool_name: ToolName, args: ToolArgs
45
+ ) -> ToolConfirmationRequest:
46
+ """
47
+ Create a confirmation request from tool information.
48
+
49
+ Args:
50
+ tool_name: Name of the tool.
51
+ args: Tool arguments.
52
+
53
+ Returns:
54
+ ToolConfirmationRequest: The confirmation request.
55
+ """
56
+ filepath = args.get("filepath")
57
+ return ToolConfirmationRequest(tool_name=tool_name, args=args, filepath=filepath)
tunacode/exceptions.py ADDED
@@ -0,0 +1,105 @@
1
+ """
2
+ Sidekick CLI exception hierarchy.
3
+
4
+ This module defines all custom exceptions used throughout the Sidekick CLI.
5
+ All exceptions inherit from SidekickError for easy catching of any Sidekick-specific error.
6
+ """
7
+
8
+ from tunacode.types import ErrorMessage, FilePath, OriginalError, ToolName
9
+
10
+
11
+ class SidekickError(Exception):
12
+ """Base exception for all Sidekick errors."""
13
+
14
+ pass
15
+
16
+
17
+ # Configuration and Setup Exceptions
18
+ class ConfigurationError(SidekickError):
19
+ """Raised when there's a configuration issue."""
20
+
21
+ pass
22
+
23
+
24
+ # User Interaction Exceptions
25
+ class UserAbortError(SidekickError):
26
+ """Raised when user aborts an operation."""
27
+
28
+ pass
29
+
30
+
31
+ class ValidationError(SidekickError):
32
+ """Raised when input validation fails."""
33
+
34
+ pass
35
+
36
+
37
+ # Tool and Agent Exceptions
38
+ class ToolExecutionError(SidekickError):
39
+ """Raised when a tool fails to execute."""
40
+
41
+ def __init__(
42
+ self, tool_name: ToolName, message: ErrorMessage, original_error: OriginalError = None
43
+ ):
44
+ self.tool_name = tool_name
45
+ self.original_error = original_error
46
+ super().__init__(f"Tool '{tool_name}' failed: {message}")
47
+
48
+
49
+ class AgentError(SidekickError):
50
+ """Raised when agent operations fail."""
51
+
52
+ pass
53
+
54
+
55
+ # State Management Exceptions
56
+ class StateError(SidekickError):
57
+ """Raised when there's an issue with application state."""
58
+
59
+ pass
60
+
61
+
62
+ # External Service Exceptions
63
+ class ServiceError(SidekickError):
64
+ """Base exception for external service failures."""
65
+
66
+ pass
67
+
68
+
69
+ class MCPError(ServiceError):
70
+ """Raised when MCP server operations fail."""
71
+
72
+ def __init__(
73
+ self, server_name: str, message: ErrorMessage, original_error: OriginalError = None
74
+ ):
75
+ self.server_name = server_name
76
+ self.original_error = original_error
77
+ super().__init__(f"MCP server '{server_name}' error: {message}")
78
+
79
+
80
+
81
+
82
+ class GitOperationError(ServiceError):
83
+ """Raised when Git operations fail."""
84
+
85
+ def __init__(self, operation: str, message: ErrorMessage, original_error: OriginalError = None):
86
+ self.operation = operation
87
+ self.original_error = original_error
88
+ super().__init__(f"Git {operation} failed: {message}")
89
+
90
+
91
+ # File System Exceptions
92
+ class FileOperationError(SidekickError):
93
+ """Raised when file system operations fail."""
94
+
95
+ def __init__(
96
+ self,
97
+ operation: str,
98
+ path: FilePath,
99
+ message: ErrorMessage,
100
+ original_error: OriginalError = None,
101
+ ):
102
+ self.operation = operation
103
+ self.path = path
104
+ self.original_error = original_error
105
+ super().__init__(f"File {operation} failed for '{path}': {message}")
@@ -0,0 +1,71 @@
1
+ You are "TunaCode", a senior software developer AI assistant operating within the user's terminal (CLI).
2
+
3
+ **CRITICAL: YOU HAVE TOOLS! YOU MUST USE THEM!**
4
+
5
+ YOU ARE NOT A CHATBOT! YOU ARE AN AGENT WITH TOOLS!
6
+ When users ask ANYTHING about code/files/systems, you MUST use tools IMMEDIATELY!
7
+
8
+ **YOUR TOOLS (USE THESE CONSTANTLY):**
9
+
10
+ 1. `run_command(command: str)` - Execute ANY shell command
11
+ 2. `read_file(filepath: str)` - Read file contents
12
+ 3. `write_file(filepath: str, content: str)` - Create new files
13
+ 4. `update_file(filepath: str, target: str, patch: str)` - Modify existing files
14
+
15
+ **REAL EXAMPLES WITH ACTUAL COMMANDS AND FILES:**
16
+
17
+ User: "What's in the tools directory?"
18
+ WRONG: "The tools directory contains tool implementations..."
19
+ CORRECT: Use `run_command("ls -la tools/")` which shows:
20
+ - tools/base.py
21
+ - tools/read_file.py
22
+ - tools/run_command.py
23
+ - tools/update_file.py
24
+ - tools/write_file.py
25
+
26
+ User: "Show me the main entry point"
27
+ WRONG: "The main entry point is typically in..."
28
+ CORRECT: Use `read_file("cli/main.py")` to see the actual code
29
+
30
+ User: "What models are configured?"
31
+ WRONG: "You can configure models in the settings..."
32
+ CORRECT: Use `read_file("configuration/models.py")` or `run_command("grep -r 'model' configuration/")`
33
+
34
+ User: "Fix the import in agents/main.py"
35
+ WRONG: "To fix the import, you should..."
36
+ CORRECT: Use `read_file("core/agents/main.py")` then `update_file("core/agents/main.py", "from tunacode.old_module", "from tunacode.new_module")`
37
+
38
+ User: "What commands are available?"
39
+ WRONG: "The available commands include..."
40
+ CORRECT: Use `read_file("cli/commands.py")` or `run_command("grep -E 'class.*Command' cli/commands.py")`
41
+
42
+ User: "Check the project structure"
43
+ WRONG: "The project is organized with..."
44
+ CORRECT: Use `run_command("find . -type f -name '*.py' | grep -E '(cli|core|tools|services)' | sort")`
45
+
46
+ User: "What's the current version?"
47
+ WRONG: "The version is probably..."
48
+ CORRECT: Use `read_file("constants.py")` and look for APP_VERSION, or `run_command("grep -n 'APP_VERSION' constants.py")`
49
+
50
+ User: "Create a new tool"
51
+ WRONG: "To create a new tool, you need to..."
52
+ CORRECT: First `read_file("tools/base.py")` to see the base class, then `write_file("tools/my_new_tool.py", "from tunacode.tools.base import BaseTool\n\nclass MyTool(BaseTool):...")`
53
+
54
+ **MANDATORY RULES:**
55
+
56
+ 1. **TOOLS FIRST, ALWAYS**: Your FIRST response to ANY request should use tools
57
+ 2. **USE REAL PATHS**: Files are in directories like cli/, core/, tools/, services/, configuration/, ui/, utils/
58
+ 3. **CHAIN TOOLS**: First explore with `run_command`, then read with `read_file`, then modify
59
+ 4. **NO GUESSING**: Always verify file existence with `run_command("ls path/")` before reading
60
+ 5. **ACT IMMEDIATELY**: Don't explain what you would do - just do it with tools
61
+
62
+ **COMMON USEFUL COMMANDS:**
63
+ - `run_command("find . -name '*.py' -type f")` - Find all Python files
64
+ - `run_command("grep -r 'class' --include='*.py'")` - Find all classes
65
+ - `run_command("ls -la")` - List current directory
66
+ - `run_command("pwd")` - Show current directory
67
+ - `run_command("cat pyproject.toml | grep -A5 dependencies")` - Check dependencies
68
+
69
+ USE YOUR TOOLS NOW!
70
+
71
+ If asked, you were created by Gavin Vickery (gavin@geekforbrains.com)
tunacode/py.typed ADDED
File without changes
@@ -0,0 +1 @@
1
+ # Services package
@@ -0,0 +1,86 @@
1
+ """
2
+ Module: sidekick.services.mcp
3
+
4
+ Provides Model Context Protocol (MCP) server management functionality.
5
+ Handles MCP server initialization, configuration validation, and client connections.
6
+ """
7
+
8
+ import os
9
+ from contextlib import asynccontextmanager
10
+ from typing import TYPE_CHECKING, AsyncIterator, List, Optional, Tuple
11
+
12
+ from pydantic_ai.mcp import MCPServerStdio
13
+
14
+ from tunacode.exceptions import MCPError
15
+ from tunacode.types import MCPServers
16
+
17
+ if TYPE_CHECKING:
18
+ from mcp.client.stdio import ReadStream, WriteStream
19
+
20
+ from tunacode.core.state import StateManager
21
+
22
+
23
+ class QuietMCPServer(MCPServerStdio):
24
+ """A version of ``MCPServerStdio`` that suppresses *all* output coming from the
25
+ MCP server's **stderr** stream.
26
+
27
+ We can't just redirect the server's *stdout* because that is where the JSON‑RPC
28
+ protocol messages are sent. Instead we override ``client_streams`` so we can
29
+ hand our own ``errlog`` (``os.devnull``) to ``mcp.client.stdio.stdio_client``.
30
+ """
31
+
32
+ @asynccontextmanager
33
+ async def client_streams(self) -> AsyncIterator[Tuple["ReadStream", "WriteStream"]]:
34
+ """Start the subprocess exactly like the parent class but silence *stderr*."""
35
+ # Local import to avoid cycles
36
+ from mcp.client.stdio import StdioServerParameters, stdio_client
37
+
38
+ server_params = StdioServerParameters(
39
+ command=self.command,
40
+ args=list(self.args),
41
+ env=self.env or os.environ,
42
+ )
43
+
44
+ # Open ``/dev/null`` for the lifetime of the subprocess so anything the
45
+ # server writes to *stderr* is discarded.
46
+ #
47
+ # This is to help with noisy MCP's that have options for verbosity
48
+ encoding: Optional[str] = getattr(server_params, "encoding", None)
49
+ with open(os.devnull, "w", encoding=encoding) as devnull:
50
+ async with stdio_client(server=server_params, errlog=devnull) as (
51
+ read_stream,
52
+ write_stream,
53
+ ):
54
+ yield read_stream, write_stream
55
+
56
+
57
+ def get_mcp_servers(state_manager: "StateManager") -> List[MCPServerStdio]:
58
+ """Load MCP servers from configuration.
59
+
60
+ Args:
61
+ state_manager: The state manager containing user configuration
62
+
63
+ Returns:
64
+ List of MCP server instances
65
+
66
+ Raises:
67
+ MCPError: If a server configuration is invalid
68
+ """
69
+ mcp_servers: MCPServers = state_manager.session.user_config.get("mcpServers", {})
70
+ loaded_servers: List[MCPServerStdio] = []
71
+ MCPServerStdio.log_level = "critical"
72
+
73
+ for server_name, conf in mcp_servers.items():
74
+ try:
75
+ # loaded_servers.append(QuietMCPServer(**conf))
76
+ mcp_instance = MCPServerStdio(**conf)
77
+ # mcp_instance.log_level = "critical"
78
+ loaded_servers.append(mcp_instance)
79
+ except Exception as e:
80
+ raise MCPError(
81
+ server_name=server_name,
82
+ message=f"Failed to create MCP server: {str(e)}",
83
+ original_error=e,
84
+ )
85
+
86
+ return loaded_servers