import os import re from typing import Annotated from fastmcp import FastMCP from fastmcp.exceptions import ToolError from fastmcp.server.dependencies import get_http_request from pydantic import Field from openhands.core.logger import openhands_logger as logger 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 GitService, ProviderType from openhands.server.dependencies import get_dependencies from openhands.server.shared import ConversationStoreImpl, config, server_config from openhands.server.types import AppMode from openhands.server.user_auth import ( get_access_token, get_provider_tokens, get_user_id, ) from openhands.storage.data_models.conversation_metadata import ConversationMetadata mcp_server = FastMCP( 'mcp', stateless_http=True, dependencies=get_dependencies(), mask_error_details=True ) HOST = f'https://{os.getenv("WEB_HOST", "app.all-hands.dev").strip()}' CONVO_URL = HOST + '/{}' async def get_convo_link(service: GitService, conversation_id: str, body: str) -> str: """ Appends a followup link, in the PR body, to the OpenHands conversation that opened the PR """ if server_config.app_mode != AppMode.SAAS: return body user = await service.get_user() username = user.login convo_url = CONVO_URL.format(conversation_id) convo_link = ( f'@{username} can click here to [continue refining the PR]({convo_url})' ) body += f'\n\n{convo_link}' return body async def save_pr_metadata( user_id: str | None, 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: logger.info(f'Saving PR number: {pr_number} for convo {conversation_id}') conversation.pr_number.append(pr_number) else: logger.warning(f'Failed to extract PR number for convo {conversation_id}') 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 PR in GitHub""" logger.info('Calling OpenHands MCP create_pr') request = get_http_request() headers = request.headers conversation_id = headers.get('X-OpenHands-ServerConversation-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: body = await get_convo_link(github_service, conversation_id, body or '') except Exception as e: logger.warning(f'Failed to append convo link: {e}') 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: await save_pr_metadata(user_id, conversation_id, response) except Exception as e: error = f'Error creating pull request: {e}' raise ToolError(str(error)) 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 MR in GitLab""" logger.info('Calling OpenHands MCP create_mr') request = get_http_request() headers = request.headers conversation_id = headers.get('X-OpenHands-ServerConversation-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() ) gitlab_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: description = await get_convo_link( gitlab_service, conversation_id, description or '' ) except Exception as e: logger.warning(f'Failed to append convo link: {e}') try: response = await gitlab_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: error = f'Error creating merge request: {e}' raise ToolError(str(error)) return response