mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
[Feat]: Git mcp server to open PRs (#8348)
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: Xingyao Wang <xingyao@all-hands.dev> Co-authored-by: Robert Brennan <accounts@rbren.io>
This commit is contained in:
parent
7305c8fb31
commit
890796cc9d
@ -14,13 +14,15 @@ the GitHub API.
|
||||
You can use `curl` with the `GITHUB_TOKEN` to interact with GitHub's API.
|
||||
ALWAYS use the GitHub API for operations instead of a web browser.
|
||||
|
||||
To open a pull request, always use the `create_pr` tool
|
||||
|
||||
If you encounter authentication issues when pushing to GitHub (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://${GITHUB_TOKEN}@github.com/username/repo.git`
|
||||
|
||||
Here are some instructions for pushing, but ONLY do this if the user asks you to:
|
||||
* NEVER push directly to the `main` or `master` branch
|
||||
* Git config (username and email) is pre-set. Do not modify.
|
||||
* You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.
|
||||
* Use the GitHub API to create a pull request, if you haven't already
|
||||
* Use the `create_pr` tool to create a pull request, if you haven't already
|
||||
* Once you've created your own branch or a pull request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.
|
||||
* Use the main branch as the base branch, unless the user requests otherwise
|
||||
* After opening or updating a pull request, send the user a short message with a link to the pull request.
|
||||
@ -30,7 +32,5 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
|
||||
```bash
|
||||
git remote -v && git branch # to find the current org, repo and branch
|
||||
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
|
||||
curl -X POST "https://api.github.com/repos/$ORG_NAME/$REPO_NAME/pulls" \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
-d '{"title":"Create widget","head":"create-widget","base":"openhands-workspace"}'
|
||||
# Then use the MCP tool to create the PR instead of directly using the GitHub API
|
||||
```
|
||||
|
||||
@ -14,13 +14,15 @@ the GitLab API.
|
||||
You can use `curl` with the `GITLAB_TOKEN` to interact with GitLab's API.
|
||||
ALWAYS use the GitLab API for operations instead of a web browser.
|
||||
|
||||
To open a merge request, always use the `create_mr` tool
|
||||
|
||||
If you encounter authentication issues when pushing to GitLab (such as password prompts or permission errors), the old token may have expired. In such case, update the remote URL to include the current token: `git remote set-url origin https://oauth2:${GITLAB_TOKEN}@gitlab.com/username/repo.git`
|
||||
|
||||
Here are some instructions for pushing, but ONLY do this if the user asks you to:
|
||||
* NEVER push directly to the `main` or `master` branch
|
||||
* Git config (username and email) is pre-set. Do not modify.
|
||||
* You may already be on a branch starting with `openhands-workspace`. Create a new branch with a better name before pushing.
|
||||
* Use the GitLab API to create a merge request, if you haven't already
|
||||
* Use the `create_mr` tool to create a merge request, if you haven't already
|
||||
* Once you've created your own branch or a merge request, continue to update it. Do NOT create a new one unless you are explicitly asked to. Update the PR title and description as necessary, but don't change the branch name.
|
||||
* Use the main branch as the base branch, unless the user requests otherwise
|
||||
* After opening or updating a merge request, send the user a short message with a link to the merge request.
|
||||
@ -29,7 +31,5 @@ Here are some instructions for pushing, but ONLY do this if the user asks you to
|
||||
```bash
|
||||
git remote -v && git branch # to find the current org, repo and branch
|
||||
git checkout -b create-widget && git add . && git commit -m "Create widget" && git push -u origin create-widget
|
||||
curl -X POST "https://gitlab.com/api/v4/projects/$PROJECT_ID/merge_requests" \
|
||||
-H "Authorization: Bearer $GITLAB_TOKEN" \
|
||||
-d '{"source_branch": "create-widget", "target_branch": "openhands-workspace", "title": "Create widget"}'
|
||||
# Then use the MCP tool to create the MR instead of directly using the GitLab API
|
||||
```
|
||||
|
||||
@ -48,6 +48,7 @@ class AppConfig(BaseModel):
|
||||
file_uploads_allowed_extensions: Allowed file extensions. `['.*']` allows all.
|
||||
cli_multiline_input: Whether to enable multiline input in CLI. When disabled,
|
||||
input is read line by line. When enabled, input continues until /exit command.
|
||||
mcp_host: Host for OpenHands' default MCP server
|
||||
mcp: MCP configuration settings.
|
||||
"""
|
||||
|
||||
@ -92,6 +93,7 @@ class AppConfig(BaseModel):
|
||||
max_concurrent_conversations: int = Field(
|
||||
default=3
|
||||
) # Maximum number of concurrent agent loops allowed per user
|
||||
mcp_host: str = Field(default='localhost:3000')
|
||||
mcp: MCPConfig = Field(default_factory=MCPConfig)
|
||||
|
||||
defaults_dict: ClassVar[dict] = {}
|
||||
@ -141,5 +143,6 @@ class AppConfig(BaseModel):
|
||||
def model_post_init(self, __context: Any) -> None:
|
||||
"""Post-initialization hook, called when the instance is created with only default values."""
|
||||
super().model_post_init(__context)
|
||||
|
||||
if not AppConfig.defaults_dict: # Only set defaults_dict if it's empty
|
||||
AppConfig.defaults_dict = model_defaults_to_dict(self)
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import os
|
||||
from urllib.parse import urlparse
|
||||
|
||||
from pydantic import BaseModel, Field, ValidationError, model_validator
|
||||
from openhands.utils.import_utils import get_impl
|
||||
|
||||
|
||||
class MCPSSEServerConfig(BaseModel):
|
||||
@ -120,3 +122,31 @@ class MCPConfig(BaseModel):
|
||||
except ValidationError as e:
|
||||
raise ValueError(f'Invalid MCP configuration: {e}')
|
||||
return mcp_mapping
|
||||
|
||||
|
||||
|
||||
class OpenHandsMCPConfig:
|
||||
@staticmethod
|
||||
def create_default_mcp_server_config(host: str, user_id: str | None = None) -> MCPSSEServerConfig | None:
|
||||
"""
|
||||
Create a default MCP server configuration.
|
||||
|
||||
Args:
|
||||
host: Host string
|
||||
|
||||
Returns:
|
||||
MCPSSEServerConfig: A default SSE server configuration
|
||||
"""
|
||||
|
||||
return MCPSSEServerConfig(url=f'http://{host}/mcp/sse', api_key=None)
|
||||
|
||||
|
||||
|
||||
openhands_mcp_config_cls = os.environ.get(
|
||||
'OPENHANDS_MCP_CONFIG_CLS',
|
||||
'openhands.core.config.mcp_config.OpenHandsMCPConfig',
|
||||
)
|
||||
|
||||
OpenHandsMCPConfigImpl = get_impl(
|
||||
OpenHandsMCPConfig, openhands_mcp_config_cls
|
||||
)
|
||||
@ -448,6 +448,60 @@ class GitHubService(BaseGitService, GitService):
|
||||
|
||||
return all_branches
|
||||
|
||||
async def create_pr(
|
||||
self,
|
||||
repo_name: str,
|
||||
source_branch: str,
|
||||
target_branch: str,
|
||||
title: str,
|
||||
body: str | None = None,
|
||||
draft: bool = True,
|
||||
) -> str:
|
||||
"""
|
||||
Creates a PR using user credentials
|
||||
|
||||
Args:
|
||||
repo_name: The full name of the repository (owner/repo)
|
||||
source_branch: The name of the branch where your changes are implemented
|
||||
target_branch: The name of the branch you want the changes pulled into
|
||||
title: The title of the pull request (optional, defaults to a generic title)
|
||||
body: The body/description of the pull request (optional)
|
||||
draft: Whether to create the PR as a draft (optional, defaults to False)
|
||||
|
||||
Returns:
|
||||
- PR URL when successful
|
||||
- Error message when unsuccessful
|
||||
"""
|
||||
try:
|
||||
url = f'{self.BASE_URL}/repos/{repo_name}/pulls'
|
||||
|
||||
# Set default body if none provided
|
||||
if not body:
|
||||
body = f'Merging changes from {source_branch} into {target_branch}'
|
||||
|
||||
# Prepare the request payload
|
||||
payload = {
|
||||
'title': title,
|
||||
'head': source_branch,
|
||||
'base': target_branch,
|
||||
'body': body,
|
||||
'draft': draft,
|
||||
}
|
||||
|
||||
# Make the POST request to create the PR
|
||||
response, _ = await self._make_request(
|
||||
url=url, params=payload, method=RequestMethod.POST
|
||||
)
|
||||
|
||||
# Return the HTML URL of the created PR
|
||||
if 'html_url' in response:
|
||||
return response['html_url']
|
||||
else:
|
||||
return f'PR created but URL not found in response: {response}'
|
||||
|
||||
except Exception as e:
|
||||
return f'Error creating pull request: {str(e)}'
|
||||
|
||||
|
||||
github_service_cls = os.environ.get(
|
||||
'OPENHANDS_GITHUB_SERVICE_CLS',
|
||||
|
||||
@ -438,6 +438,61 @@ class GitLabService(BaseGitService, GitService):
|
||||
|
||||
return all_branches
|
||||
|
||||
async def create_mr(
|
||||
self,
|
||||
id: int | str,
|
||||
source_branch: str,
|
||||
target_branch: str,
|
||||
title: str,
|
||||
description: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
Creates a merge request in GitLab
|
||||
|
||||
Args:
|
||||
id: The ID or URL-encoded path of the project
|
||||
source_branch: The name of the branch where your changes are implemented
|
||||
target_branch: The name of the branch you want the changes merged into
|
||||
title: The title of the merge request (optional, defaults to a generic title)
|
||||
description: The description of the merge request (optional)
|
||||
draft: Whether to create the MR as a draft (optional, defaults to False)
|
||||
|
||||
Returns:
|
||||
- MR URL when successful
|
||||
- Error message when unsuccessful
|
||||
"""
|
||||
try:
|
||||
# Convert string ID to URL-encoded path if needed
|
||||
project_id = str(id).replace('/', '%2F') if isinstance(id, str) else id
|
||||
url = f'{self.BASE_URL}/projects/{project_id}/merge_requests'
|
||||
|
||||
# Set default description if none provided
|
||||
if not description:
|
||||
description = f'Merging changes from {source_branch} into {target_branch}'
|
||||
|
||||
# Prepare the request payload
|
||||
payload = {
|
||||
'source_branch': source_branch,
|
||||
'target_branch': target_branch,
|
||||
'title': title,
|
||||
'description': description,
|
||||
}
|
||||
|
||||
|
||||
# Make the POST request to create the MR
|
||||
response, _ = await self._make_request(
|
||||
url=url, params=payload, method=RequestMethod.POST
|
||||
)
|
||||
|
||||
# Return the web URL of the created MR
|
||||
if 'web_url' in response:
|
||||
return response['web_url']
|
||||
else:
|
||||
return f'MR created but URL not found in response: {response}'
|
||||
|
||||
except Exception as e:
|
||||
return f'Error creating merge request: {str(e)}'
|
||||
|
||||
|
||||
gitlab_service_cls = os.environ.get(
|
||||
'OPENHANDS_GITLAB_SERVICE_CLS',
|
||||
|
||||
@ -25,7 +25,11 @@ class MCPClient(BaseModel):
|
||||
arbitrary_types_allowed = True
|
||||
|
||||
async def connect_sse(
|
||||
self, server_url: str, api_key: str | None = None, timeout: float = 30.0
|
||||
self,
|
||||
server_url: str,
|
||||
api_key: str | None = None,
|
||||
conversation_id: str | None = None,
|
||||
timeout: float = 30.0,
|
||||
) -> None:
|
||||
"""Connect to an MCP server using SSE transport.
|
||||
|
||||
@ -41,9 +45,14 @@ class MCPClient(BaseModel):
|
||||
try:
|
||||
# Use asyncio.wait_for to enforce the timeout
|
||||
async def connect_with_timeout():
|
||||
headers = {'Authorization': f'Bearer {api_key}'} if api_key else {}
|
||||
|
||||
if conversation_id:
|
||||
headers['X-OpenHands-Conversation-ID'] = conversation_id
|
||||
|
||||
streams_context = sse_client(
|
||||
url=server_url,
|
||||
headers={'Authorization': f'Bearer {api_key}'} if api_key else None,
|
||||
headers=headers if headers else None,
|
||||
timeout=timeout,
|
||||
)
|
||||
streams = await self.exit_stack.enter_async_context(streams_context)
|
||||
|
||||
@ -44,7 +44,7 @@ def convert_mcp_clients_to_tools(mcp_clients: list[MCPClient] | None) -> list[di
|
||||
|
||||
|
||||
async def create_mcp_clients(
|
||||
sse_servers: list[MCPSSEServerConfig],
|
||||
sse_servers: list[MCPSSEServerConfig], conversation_id: str | None = None
|
||||
) -> list[MCPClient]:
|
||||
mcp_clients: list[MCPClient] = []
|
||||
# Initialize SSE connections
|
||||
@ -56,7 +56,11 @@ async def create_mcp_clients(
|
||||
|
||||
client = MCPClient()
|
||||
try:
|
||||
await client.connect_sse(server_url.url, api_key=server_url.api_key)
|
||||
await client.connect_sse(
|
||||
server_url.url,
|
||||
api_key=server_url.api_key,
|
||||
conversation_id=conversation_id,
|
||||
)
|
||||
# Only add the client to the list after a successful connection
|
||||
mcp_clients.append(client)
|
||||
logger.info(f'Connected to MCP server {server_url} via SSE')
|
||||
@ -155,6 +159,7 @@ async def add_mcp_tools_to_agent(
|
||||
"""
|
||||
Add MCP tools to an agent.
|
||||
"""
|
||||
|
||||
from openhands.runtime.impl.action_execution.action_execution_client import (
|
||||
ActionExecutionClient, # inline import to avoid circular import
|
||||
)
|
||||
|
||||
@ -356,6 +356,7 @@ class ActionExecutionClient(Runtime):
|
||||
) -> MCPConfig:
|
||||
# Add the runtime as another MCP server
|
||||
updated_mcp_config = self.config.mcp.model_copy()
|
||||
|
||||
# Send a request to the action execution server to updated MCP config
|
||||
stdio_tools = [
|
||||
server.model_dump(mode='json')
|
||||
@ -408,7 +409,7 @@ class ActionExecutionClient(Runtime):
|
||||
)
|
||||
|
||||
# Create clients for this specific operation
|
||||
mcp_clients = await create_mcp_clients(updated_mcp_config.sse_servers)
|
||||
mcp_clients = await create_mcp_clients(updated_mcp_config.sse_servers, self.sid)
|
||||
|
||||
# Call the tool and return the result
|
||||
# No need for try/finally since disconnect() is now just resetting state
|
||||
|
||||
@ -2,6 +2,8 @@ import warnings
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncIterator
|
||||
|
||||
from fastapi.routing import Mount
|
||||
|
||||
with warnings.catch_warnings():
|
||||
warnings.simplefilter('ignore')
|
||||
|
||||
@ -18,6 +20,7 @@ from openhands.server.routes.git import app as git_api_router
|
||||
from openhands.server.routes.manage_conversations import (
|
||||
app as manage_conversation_api_router,
|
||||
)
|
||||
from openhands.server.routes.mcp import mcp_server
|
||||
from openhands.server.routes.public import app as public_api_router
|
||||
from openhands.server.routes.secrets import app as secrets_router
|
||||
from openhands.server.routes.security import app as security_api_router
|
||||
@ -37,6 +40,7 @@ app = FastAPI(
|
||||
description='OpenHands: Code Less, Make More',
|
||||
version=__version__,
|
||||
lifespan=_lifespan,
|
||||
routes=[Mount(path='/mcp', app=mcp_server.sse_app())],
|
||||
)
|
||||
|
||||
|
||||
|
||||
141
openhands/server/routes/mcp.py
Normal file
141
openhands/server/routes/mcp.py
Normal file
@ -0,0 +1,141 @@
|
||||
import re
|
||||
from typing import Annotated
|
||||
from pydantic import Field
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.dependencies import get_http_request
|
||||
from openhands.integrations.github.github_service import GithubServiceImpl
|
||||
from openhands.integrations.gitlab.gitlab_service import GitLabServiceImpl
|
||||
from openhands.integrations.provider import ProviderToken
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
from openhands.server.shared import ConversationStoreImpl, config
|
||||
from openhands.server.user_auth import get_access_token, get_provider_tokens, get_user_id
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.storage.data_models.conversation_metadata import ConversationMetadata
|
||||
|
||||
mcp_server = FastMCP('mcp')
|
||||
|
||||
|
||||
async def save_pr_metadata(user_id: str, conversation_id: str, tool_result: str) -> None:
|
||||
conversation_store = await ConversationStoreImpl.get_instance(config, user_id)
|
||||
conversation: ConversationMetadata = await conversation_store.get_metadata(conversation_id)
|
||||
|
||||
|
||||
pull_pattern = r"pull/(\d+)"
|
||||
merge_request_pattern = r"merge_requests/(\d+)"
|
||||
|
||||
# Check if the tool_result contains the PR number
|
||||
pr_number = None
|
||||
match_pull = re.search(pull_pattern, tool_result)
|
||||
match_merge_request = re.search(merge_request_pattern, tool_result)
|
||||
|
||||
if match_pull:
|
||||
pr_number = int(match_pull.group(1))
|
||||
elif match_merge_request:
|
||||
pr_number = int(match_merge_request.group(1))
|
||||
|
||||
|
||||
if pr_number:
|
||||
conversation.pr_number.append(pr_number)
|
||||
await conversation_store.save_metadata(conversation)
|
||||
|
||||
@mcp_server.tool()
|
||||
async def create_pr(
|
||||
repo_name: Annotated[
|
||||
str, Field(description='GitHub repository ({{owner}}/{{repo}})')
|
||||
],
|
||||
source_branch: Annotated[str, Field(description='Source branch on repo')],
|
||||
target_branch: Annotated[str, Field(description='Target branch on repo')],
|
||||
title: Annotated[str, Field(description='PR Title')],
|
||||
body: Annotated[str | None, Field(description='PR body')]
|
||||
) -> str:
|
||||
"""Open a draft PR in GitHub"""
|
||||
|
||||
logger.info('Calling OpenHands MCP create_pr')
|
||||
|
||||
request = get_http_request()
|
||||
headers = request.headers
|
||||
conversation_id = headers.get('X-OpenHands-Conversation-ID', None)
|
||||
|
||||
provider_tokens = await get_provider_tokens(request)
|
||||
access_token = await get_access_token(request)
|
||||
user_id = await get_user_id(request)
|
||||
|
||||
github_token = provider_tokens.get(ProviderType.GITHUB, ProviderToken()) if provider_tokens else ProviderToken()
|
||||
|
||||
github_service = GithubServiceImpl(
|
||||
user_id=github_token.user_id,
|
||||
external_auth_id=user_id,
|
||||
external_auth_token=access_token,
|
||||
token=github_token.token,
|
||||
base_domain=github_token.host
|
||||
)
|
||||
|
||||
try:
|
||||
response = await github_service.create_pr(
|
||||
repo_name=repo_name,
|
||||
source_branch=source_branch,
|
||||
target_branch=target_branch,
|
||||
title=title,
|
||||
body=body
|
||||
)
|
||||
|
||||
if conversation_id and user_id:
|
||||
await save_pr_metadata(user_id, conversation_id, response)
|
||||
|
||||
except Exception as e:
|
||||
response = str(e)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
|
||||
@mcp_server.tool()
|
||||
async def create_mr(
|
||||
id: Annotated[
|
||||
int | str, Field(description='GitLab repository (ID or URL-encoded path of the project)')
|
||||
],
|
||||
source_branch: Annotated[str, Field(description='Source branch on repo')],
|
||||
target_branch: Annotated[str, Field(description='Target branch on repo')],
|
||||
title: Annotated[str, Field(description='MR Title')],
|
||||
description: Annotated[str | None, Field(description='MR description')]
|
||||
) -> str:
|
||||
"""Open a draft MR in GitLab"""
|
||||
|
||||
logger.info('Calling OpenHands MCP create_mr')
|
||||
|
||||
request = get_http_request()
|
||||
headers = request.headers
|
||||
conversation_id = headers.get('X-OpenHands-Conversation-ID', None)
|
||||
|
||||
provider_tokens = await get_provider_tokens(request)
|
||||
access_token = await get_access_token(request)
|
||||
user_id = await get_user_id(request)
|
||||
|
||||
github_token = provider_tokens.get(ProviderType.GITLAB, ProviderToken()) if provider_tokens else ProviderToken()
|
||||
|
||||
github_service = GitLabServiceImpl(
|
||||
user_id=github_token.user_id,
|
||||
external_auth_id=user_id,
|
||||
external_auth_token=access_token,
|
||||
token=github_token.token,
|
||||
base_domain=github_token.host
|
||||
)
|
||||
|
||||
try:
|
||||
response = await github_service.create_mr(
|
||||
id=id,
|
||||
source_branch=source_branch,
|
||||
target_branch=target_branch,
|
||||
title=title,
|
||||
description=description,
|
||||
)
|
||||
|
||||
if conversation_id and user_id:
|
||||
await save_pr_metadata(user_id, conversation_id, response)
|
||||
|
||||
except Exception as e:
|
||||
response = str(e)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@ from openhands.events.action import ChangeAgentStateAction, MessageAction
|
||||
from openhands.events.event import Event, EventSource
|
||||
from openhands.events.stream import EventStream
|
||||
from openhands.integrations.provider import CUSTOM_SECRETS_TYPE, PROVIDER_TOKEN_TYPE, ProviderHandler
|
||||
from openhands.integrations.service_types import ProviderType
|
||||
from openhands.mcp import add_mcp_tools_to_agent
|
||||
from openhands.memory.memory import Memory
|
||||
from openhands.microagent.microagent import BaseMicroagent
|
||||
@ -270,6 +271,23 @@ class AgentSession:
|
||||
security_analyzer, SecurityAnalyzer
|
||||
)(self.event_stream)
|
||||
|
||||
|
||||
def override_provider_tokens_with_custom_secret(
|
||||
self,
|
||||
git_provider_tokens: PROVIDER_TOKEN_TYPE | None,
|
||||
custom_secrets: CUSTOM_SECRETS_TYPE | None
|
||||
):
|
||||
if git_provider_tokens and custom_secrets:
|
||||
tokens = dict(git_provider_tokens)
|
||||
for provider, _ in tokens.items():
|
||||
token_name = ProviderHandler.get_provider_env_key(provider)
|
||||
if token_name in custom_secrets or token_name.upper() in custom_secrets:
|
||||
del tokens[provider]
|
||||
|
||||
return MappingProxyType(tokens)
|
||||
return git_provider_tokens
|
||||
|
||||
|
||||
async def _create_runtime(
|
||||
self,
|
||||
runtime_name: str,
|
||||
@ -299,7 +317,11 @@ class AgentSession:
|
||||
|
||||
self.logger.debug(f'Initializing runtime `{runtime_name}` now...')
|
||||
runtime_cls = get_runtime_cls(runtime_name)
|
||||
if runtime_cls == RemoteRuntime:
|
||||
if runtime_cls == RemoteRuntime:
|
||||
# If provider tokens is passed in custom secrets, then remove provider from provider tokens
|
||||
# We prioritize provider tokens set in custom secrets
|
||||
provider_tokens_without_gitlab = self.override_provider_tokens_with_custom_secret(git_provider_tokens, custom_secrets)
|
||||
|
||||
self.runtime = runtime_cls(
|
||||
config=config,
|
||||
event_stream=self.event_stream,
|
||||
@ -308,7 +330,7 @@ class AgentSession:
|
||||
status_callback=self._status_callback,
|
||||
headless_mode=False,
|
||||
attach_to_existing=False,
|
||||
git_provider_tokens=git_provider_tokens,
|
||||
git_provider_tokens=provider_tokens_without_gitlab,
|
||||
env_vars=env_vars,
|
||||
user_id=self.user_id,
|
||||
)
|
||||
|
||||
@ -6,12 +6,13 @@ from logging import LoggerAdapter
|
||||
import socketio
|
||||
|
||||
from openhands.controller.agent import Agent
|
||||
from openhands.core.config import AppConfig, MCPConfig
|
||||
from openhands.core.config import AppConfig
|
||||
from openhands.core.config.condenser_config import (
|
||||
BrowserOutputCondenserConfig,
|
||||
CondenserPipelineConfig,
|
||||
LLMSummarizingCondenserConfig,
|
||||
)
|
||||
from openhands.core.config.mcp_config import OpenHandsMCPConfigImpl
|
||||
from openhands.core.exceptions import MicroagentValidationError
|
||||
from openhands.core.logger import OpenHandsLoggerAdapter
|
||||
from openhands.core.schema import AgentState
|
||||
@ -115,7 +116,11 @@ class Session:
|
||||
or settings.sandbox_runtime_container_image
|
||||
else self.config.sandbox.runtime_container_image
|
||||
)
|
||||
self.config.mcp = settings.mcp_config or MCPConfig()
|
||||
self.config.mcp = settings.mcp_config
|
||||
# Add OpenHands' MCP server by default
|
||||
openhands_mcp_server = OpenHandsMCPConfigImpl.create_default_mcp_server_config(self.config.mcp_host, self.user_id)
|
||||
if openhands_mcp_server:
|
||||
self.config.mcp.sse_servers.append(openhands_mcp_server)
|
||||
max_iterations = settings.max_iterations or self.config.max_iterations
|
||||
|
||||
# This is a shallow copy of the default LLM config, so changes here will
|
||||
|
||||
@ -20,6 +20,7 @@ class ConversationMetadata:
|
||||
title: str | None = None
|
||||
last_updated_at: datetime | None = None
|
||||
trigger: ConversationTrigger | None = None
|
||||
pr_number: list[int] = field(default_factory=list)
|
||||
created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
|
||||
# Cost and token metrics
|
||||
accumulated_cost: float = 0.0
|
||||
|
||||
@ -38,7 +38,8 @@ class Settings(BaseModel):
|
||||
user_consents_to_analytics: bool | None = None
|
||||
sandbox_base_container_image: str | None = None
|
||||
sandbox_runtime_container_image: str | None = None
|
||||
mcp_config: MCPConfig | None = None
|
||||
mcp_config: MCPConfig = Field(default_factory=MCPConfig)
|
||||
|
||||
|
||||
model_config = {
|
||||
'validate_assignment': True,
|
||||
|
||||
4448
poetry.lock
generated
4448
poetry.lock
generated
File diff suppressed because one or more lines are too long
@ -83,7 +83,7 @@ prompt-toolkit = "^3.0.50"
|
||||
poetry = "^2.1.2"
|
||||
anyio = "4.9.0"
|
||||
pythonnet = "*"
|
||||
mcp = "1.9.0"
|
||||
fastmcp = "^2.3.3"
|
||||
mcpm = "1.12.0"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
||||
@ -40,10 +40,10 @@ async def test_create_mcp_clients_success(mock_mcp_client):
|
||||
|
||||
# Check that connect_sse was called with correct parameters
|
||||
mock_client_instance.connect_sse.assert_any_call(
|
||||
'http://server1:8080', api_key=None
|
||||
'http://server1:8080', api_key=None, conversation_id=None
|
||||
)
|
||||
mock_client_instance.connect_sse.assert_any_call(
|
||||
'http://server2:8080', api_key='test-key'
|
||||
'http://server2:8080', api_key='test-key', conversation_id=None
|
||||
)
|
||||
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user