Skip to content

Commit 2ccb24b

Browse files
committed
chore: add env mcp config check & fix lint python
1 parent 17b92f0 commit 2ccb24b

20 files changed

+210
-96
lines changed

frontend/src/i18n/declaration.ts

+1
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ export enum I18nKey {
301301
STATUS$ERROR_LLM_SERVICE_UNAVAILABLE = "STATUS$ERROR_LLM_SERVICE_UNAVAILABLE",
302302
STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR = "STATUS$ERROR_LLM_INTERNAL_SERVER_ERROR",
303303
STATUS$ERROR_LLM_OUT_OF_CREDITS = "STATUS$ERROR_LLM_OUT_OF_CREDITS",
304+
STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION = "STATUS$ERROR_LLM_CONTENT_POLICY_VIOLATION",
304305
STATUS$ERROR_RUNTIME_DISCONNECTED = "STATUS$ERROR_RUNTIME_DISCONNECTED",
305306
STATUS$LLM_RETRY = "STATUS$LLM_RETRY",
306307
AGENT_ERROR$BAD_ACTION = "AGENT_ERROR$BAD_ACTION",

openhands/agenthub/codeact_agent/codeact_agent.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import os
22
from collections import deque
33

4-
from litellm import ChatCompletionToolParam
5-
64
import openhands.agenthub.codeact_agent.function_calling as codeact_function_calling
75
from openhands.controller.agent import Agent
86
from openhands.controller.state.state import State
@@ -139,13 +137,13 @@ def step(self, state: State) -> Action:
139137
'messages': self.llm.format_messages_for_llm(messages),
140138
}
141139
params['tools'] = self.tools
142-
143-
140+
144141
if self.mcp_tools:
145142
# Only add tools with unique names
146143
existing_names = {tool['function']['name'] for tool in params['tools']}
147144
unique_mcp_tools = [
148-
tool for tool in self.mcp_tools
145+
tool
146+
for tool in self.mcp_tools
149147
if tool['function']['name'] not in existing_names
150148
]
151149
params['tools'] += unique_mcp_tools

openhands/core/config/app_config.py

-5
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,3 @@ def model_post_init(self, __context):
139139
super().model_post_init(__context)
140140
if not AppConfig.defaults_dict: # Only set defaults_dict if it's empty
141141
AppConfig.defaults_dict = model_defaults_to_dict(self)
142-
# Validate MCP configurations
143-
if self.mcp.sse.mcp_servers:
144-
self.mcp.sse.validate_servers()
145-
if self.mcp.stdio.commands:
146-
self.mcp.stdio.validate_stdio()

openhands/core/config/mcp_config.py

+23-2
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,17 @@ class MCPStdioConfig(BaseModel):
3737
Attributes:
3838
commands: List of commands to run.
3939
args: List of arguments for each command.
40+
envs: List of environment variable tuples for each command.
4041
"""
4142

4243
commands: List[str] = Field(default_factory=list)
4344
args: List[List[str]] = Field(default_factory=list)
4445
envs: List[List[tuple[str, str]]] = Field(default_factory=list)
45-
46+
4647
model_config = {'extra': 'forbid'}
4748

4849
def validate_stdio(self) -> None:
49-
"""Validate that commands and args are properly configured."""
50+
"""Validate that commands, args, and envs are properly configured."""
5051

5152
# Check if number of commands matches number of args lists
5253
if len(self.commands) != len(self.args):
@@ -55,6 +56,26 @@ def validate_stdio(self) -> None:
5556
f'number of args lists ({len(self.args)})'
5657
)
5758

59+
# Check if number of commands matches number of envs lists
60+
if len(self.commands) != len(self.envs):
61+
raise ValueError(
62+
f'Number of commands ({len(self.commands)}) does not match '
63+
f'number of envs lists ({len(self.envs)})'
64+
)
65+
66+
# Validate each environment variable tuple
67+
for i, env_list in enumerate(self.envs):
68+
for j, env_tuple in enumerate(env_list):
69+
if not isinstance(env_tuple, tuple) or len(env_tuple) != 2:
70+
raise ValueError(
71+
f'Environment variable at index {j} for command {i} must be a tuple of (key, value)'
72+
)
73+
key, value = env_tuple
74+
if not isinstance(key, str) or not isinstance(value, str):
75+
raise ValueError(
76+
f'Environment variable key and value at index {j} for command {i} must be strings'
77+
)
78+
5879

5980
class MCPConfig(BaseModel):
6081
"""Configuration for MCP (Message Control Protocol) settings.

openhands/core/setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import hashlib
22
import os
33
import uuid
4-
from typing import Callable, List, Tuple, Type
4+
from typing import Callable, Tuple, Type
55

66
from pydantic import SecretStr
77

openhands/integrations/github/github_service.py

-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@ async def get_user(self) -> User:
100100
email=response.get('email'),
101101
)
102102

103-
104103
async def _fetch_paginated_repos(
105104
self, url: str, params: dict, max_repos: int, extract_key: str | None = None
106105
) -> list[dict]:

openhands/integrations/gitlab/gitlab_service.py

-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import os
22
from typing import Any
3-
from urllib.parse import quote_plus
43

54
import httpx
65
from pydantic import SecretStr

openhands/io/json.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,14 @@ def dumps(obj, **kwargs):
3636
"""Serialize an object to str format"""
3737
if not kwargs:
3838
return _json_encoder.encode(obj)
39-
39+
4040
# Create a copy of the kwargs to avoid modifying the original
4141
encoder_kwargs = kwargs.copy()
42-
42+
4343
# If cls is specified, use it; otherwise use our custom encoder
4444
if 'cls' not in encoder_kwargs:
4545
encoder_kwargs['cls'] = OpenHandsJSONEncoder
46-
46+
4747
return json.dumps(obj, **encoder_kwargs)
4848

4949

openhands/mcp/__init__.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1-
from openhands.mcp.mcp import MCPClient, convert_mcp_clients_to_tools, create_mcp_clients, BaseTool
1+
from openhands.mcp.mcp import (
2+
BaseTool,
3+
MCPClient,
4+
convert_mcp_clients_to_tools,
5+
create_mcp_clients,
6+
)
27

3-
__all__ = ['MCPClient', 'convert_mcp_clients_to_tools', 'create_mcp_clients', 'BaseTool']
8+
__all__ = [
9+
'MCPClient',
10+
'convert_mcp_clients_to_tools',
11+
'create_mcp_clients',
12+
'BaseTool',
13+
]

openhands/mcp/mcp.py

+55-27
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from mcp.client.stdio import stdio_client
88
from mcp.types import CallToolResult, TextContent, Tool
99
from pydantic import BaseModel, Field
10-
from termcolor import colored
1110

1211
from openhands.core.config.mcp_config import MCPConfig
1312
from openhands.core.logger import openhands_logger as logger
@@ -76,7 +75,7 @@ class Config:
7675

7776
async def connect_sse(self, server_url: str, timeout: float = 30.0) -> None:
7877
"""Connect to an MCP server using SSE transport.
79-
78+
8079
Args:
8180
server_url: The URL of the SSE server to connect to.
8281
timeout: Connection timeout in seconds. Default is 30 seconds.
@@ -89,15 +88,19 @@ async def connect_sse(self, server_url: str, timeout: float = 30.0) -> None:
8988
try:
9089
import asyncio
9190
from asyncio import TimeoutError
92-
91+
9392
# Create a task for the connection
94-
connection_task = asyncio.create_task(self._connect_sse_internal(server_url))
95-
93+
connection_task = asyncio.create_task(
94+
self._connect_sse_internal(server_url)
95+
)
96+
9697
# Wait for the connection with timeout
9798
try:
9899
await asyncio.wait_for(connection_task, timeout=timeout)
99100
except TimeoutError:
100-
logger.error(f'Connection to {server_url} timed out after {timeout} seconds')
101+
logger.error(
102+
f'Connection to {server_url} timed out after {timeout} seconds'
103+
)
101104
# Cancel the connection task
102105
connection_task.cancel()
103106
try:
@@ -110,7 +113,7 @@ async def connect_sse(self, server_url: str, timeout: float = 30.0) -> None:
110113
async def _connect_sse_internal(self, server_url: str) -> None:
111114
"""Internal method to establish SSE connection."""
112115
streams_context = sse_client(
113-
url=server_url,
116+
url=server_url,
114117
)
115118
streams = await self.exit_stack.enter_async_context(streams_context)
116119
self.session = await self.exit_stack.enter_async_context(
@@ -119,9 +122,15 @@ async def _connect_sse_internal(self, server_url: str) -> None:
119122

120123
await self._initialize_and_list_tools()
121124

122-
async def connect_stdio(self, command: str, args: List[str], envs: List[tuple[str, str]], timeout: float = 30.0) -> None:
125+
async def connect_stdio(
126+
self,
127+
command: str,
128+
args: List[str],
129+
envs: List[tuple[str, str]],
130+
timeout: float = 30.0,
131+
) -> None:
123132
"""Connect to an MCP server using stdio transport.
124-
133+
125134
Args:
126135
command: The command to execute.
127136
args: The arguments to pass to the command.
@@ -136,15 +145,19 @@ async def connect_stdio(self, command: str, args: List[str], envs: List[tuple[st
136145
try:
137146
import asyncio
138147
from asyncio import TimeoutError
139-
148+
140149
# Create a task for the connection
141-
connection_task = asyncio.create_task(self._connect_stdio_internal(command, args, envs))
142-
150+
connection_task = asyncio.create_task(
151+
self._connect_stdio_internal(command, args, envs)
152+
)
153+
143154
# Wait for the connection with timeout
144155
try:
145156
await asyncio.wait_for(connection_task, timeout=timeout)
146157
except TimeoutError:
147-
logger.error(f'Connection to {command} timed out after {timeout} seconds')
158+
logger.error(
159+
f'Connection to {command} timed out after {timeout} seconds'
160+
)
148161
# Cancel the connection task
149162
connection_task.cancel()
150163
try:
@@ -154,7 +167,9 @@ async def connect_stdio(self, command: str, args: List[str], envs: List[tuple[st
154167
except Exception as e:
155168
logger.error(f'Error connecting to {command}: {str(e)}')
156169

157-
async def _connect_stdio_internal(self, command: str, args: List[str], envs: List[tuple[str, str]]) -> None:
170+
async def _connect_stdio_internal(
171+
self, command: str, args: List[str], envs: List[tuple[str, str]]
172+
) -> None:
158173
"""Internal method to establish stdio connection."""
159174
envs_dict: dict[str, str] = {}
160175
for env in envs:
@@ -249,7 +264,10 @@ def convert_mcp_clients_to_tools(mcp_clients: list[MCPClient] | None) -> list[di
249264

250265

251266
async def create_mcp_clients(
252-
sse_mcp_server: List[str], commands: List[str], args: List[List[str]], envs: List[List[tuple[str, str]]]
267+
sse_mcp_server: List[str],
268+
commands: List[str],
269+
args: List[List[str]],
270+
envs: List[List[tuple[str, str]]],
253271
) -> List[MCPClient]:
254272
mcp_clients: List[MCPClient] = []
255273
# Initialize SSE connections
@@ -271,11 +289,15 @@ async def create_mcp_clients(
271289
try:
272290
await client.disconnect()
273291
except Exception as disconnect_error:
274-
logger.error(f'Error during disconnect after failed connection: {str(disconnect_error)}')
292+
logger.error(
293+
f'Error during disconnect after failed connection: {str(disconnect_error)}'
294+
)
275295

276296
# Initialize stdio connections
277297
if commands:
278-
for i, (command, command_args, command_envs) in enumerate(zip(commands, args, envs)):
298+
for i, (command, command_args, command_envs) in enumerate(
299+
zip(commands, args, envs)
300+
):
279301
logger.info(
280302
f'Initializing MCP agent for {command} with stdio connection...'
281303
)
@@ -292,39 +314,45 @@ async def create_mcp_clients(
292314
try:
293315
await client.disconnect()
294316
except Exception as disconnect_error:
295-
logger.error(f'Error during disconnect after failed connection: {str(disconnect_error)}')
317+
logger.error(
318+
f'Error during disconnect after failed connection: {str(disconnect_error)}'
319+
)
296320

297321
return mcp_clients
298322

323+
299324
async def fetch_mcp_tools_from_config(mcp_config: MCPConfig) -> list[dict]:
300325
"""
301326
Retrieves the list of MCP tools from the MCP clients.
302-
327+
303328
Returns:
304329
A list of tool dictionaries. Returns an empty list if no connections could be established.
305330
"""
306331
mcp_clients = []
307332
mcp_tools = []
308-
333+
309334
try:
310335
mcp_clients = await create_mcp_clients(
311-
mcp_config.sse.mcp_servers, mcp_config.stdio.commands, mcp_config.stdio.args, mcp_config.stdio.envs
336+
mcp_config.sse.mcp_servers,
337+
mcp_config.stdio.commands,
338+
mcp_config.stdio.args,
339+
mcp_config.stdio.envs,
312340
)
313-
341+
314342
if not mcp_clients:
315-
logger.warning("No MCP clients were successfully connected")
343+
logger.warning('No MCP clients were successfully connected')
316344
return []
317-
345+
318346
mcp_tools = convert_mcp_clients_to_tools(mcp_clients)
319347
except Exception as e:
320-
logger.error(f"Error fetching MCP tools: {str(e)}")
348+
logger.error(f'Error fetching MCP tools: {str(e)}')
321349
return []
322350
finally:
323351
# Always disconnect clients to clean up resources
324352
for mcp_client in mcp_clients:
325353
try:
326354
await mcp_client.disconnect()
327355
except Exception as disconnect_error:
328-
logger.error(f"Error disconnecting MCP client: {str(disconnect_error)}")
329-
356+
logger.error(f'Error disconnecting MCP client: {str(disconnect_error)}')
357+
330358
return mcp_tools

openhands/runtime/action_execution_server.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@
7474
class ActionRequest(BaseModel):
7575
action: dict
7676
sse_mcp_config: Optional[list[str]] = None
77-
stdio_mcp_config: Optional[tuple[list[str], list[list[str]], list[list[tuple[str, str]]]]] = None
77+
stdio_mcp_config: Optional[
78+
tuple[list[str], list[list[str]], list[list[tuple[str, str]]]]
79+
] = None
7880

7981

8082
ROOT_GID = 0
@@ -190,7 +192,9 @@ def __init__(
190192
)
191193
self.memory_monitor.start_monitoring()
192194
self.sse_mcp_servers: list[str] = []
193-
self.stdio_mcp_config: tuple[list[str], list[list[str]], list[list[tuple[str, str]]]] = ([], [], [])
195+
self.stdio_mcp_config: tuple[
196+
list[str], list[list[str]], list[list[tuple[str, str]]]
197+
] = ([], [], [])
194198

195199
@property
196200
def initial_cwd(self):

openhands/server/routes/git.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from fastapi import APIRouter, Depends, status
22
from fastapi.responses import JSONResponse
33
from pydantic import SecretStr
4-
from openhands.server.shared import server_config
4+
55
from openhands.integrations.github.github_service import GithubServiceImpl
66
from openhands.integrations.provider import (
77
PROVIDER_TOKEN_TYPE,
@@ -16,7 +16,7 @@
1616
User,
1717
)
1818
from openhands.server.auth import get_access_token, get_provider_tokens
19-
from openhands.server.types import AppMode
19+
from openhands.server.shared import server_config
2020

2121
app = APIRouter(prefix='/api/user')
2222

@@ -33,7 +33,9 @@ async def get_user_repositories(
3333
)
3434

3535
try:
36-
repos: list[Repository] = await client.get_repositories(sort, server_config.app_mode)
36+
repos: list[Repository] = await client.get_repositories(
37+
sort, server_config.app_mode
38+
)
3739
return repos
3840

3941
except AuthenticationError as e:

openhands/server/routes/manage_conversations.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,9 @@ async def _create_new_conversation(
112112
title=conversation_title,
113113
user_id=user_id,
114114
github_user_id=None,
115-
selected_repository=selected_repository.full_name if selected_repository else selected_repository,
115+
selected_repository=selected_repository.full_name
116+
if selected_repository
117+
else selected_repository,
116118
selected_branch=selected_branch,
117119
)
118120
)

0 commit comments

Comments
 (0)