Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for MCP servers #7620

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions frontend/src/i18n/declaration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ export enum I18nKey {
OBSERVATION_MESSAGE$EDIT = "OBSERVATION_MESSAGE$EDIT",
OBSERVATION_MESSAGE$WRITE = "OBSERVATION_MESSAGE$WRITE",
OBSERVATION_MESSAGE$BROWSE = "OBSERVATION_MESSAGE$BROWSE",
OBSERVATION_MESSAGE$MCP_CALL_TOOL = "OBSERVATION_MESSAGE$MCP_CALL_TOOL",
EXPANDABLE_MESSAGE$SHOW_DETAILS = "EXPANDABLE_MESSAGE$SHOW_DETAILS",
EXPANDABLE_MESSAGE$HIDE_DETAILS = "EXPANDABLE_MESSAGE$HIDE_DETAILS",
AI_SETTINGS$TITLE = "AI_SETTINGS$TITLE",
Expand Down
15 changes: 15 additions & 0 deletions frontend/src/i18n/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -4313,6 +4313,21 @@
"es": "Navegación completada",
"tr": "Gezinme tamamlandı"
},
"OBSERVATION_MESSAGE$MCP_CALL_TOOL": {
"en": "Called an MCP tool",
"zh-CN": "调用了 MCP 工具",
"zh-TW": "調用了 MCP 工具",
"ko-KR": "MCP 도구 호출됨",
"ja": "MCPツールを呼び出しました",
"no": "Kalt et MCP-verktøy",
"ar": "تم استدعاء أداة MCP",
"de": "Ein MCP-Tool aufgerufen",
"fr": "Outil MCP appelé",
"it": "Chiamato uno strumento MCP",
"pt": "Ferramenta MCP chamada",
"es": "Se ha llamado a una herramienta MCP",
"tr": "Bir MCP aracı çağrıldı"
},
"EXPANDABLE_MESSAGE$SHOW_DETAILS": {
"en": "Show details",
"zh-CN": "显示详情",
Expand Down
16 changes: 16 additions & 0 deletions frontend/src/services/observations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export function handleObservationMessage(message: ObservationMessage) {
case ObservationType.READ:
case ObservationType.EDIT:
case ObservationType.THINK:
case ObservationType.MCP_CALL_TOOL:
case ObservationType.NULL:
break; // We don't display the default message for these observations
default:
Expand Down Expand Up @@ -125,6 +126,21 @@ export function handleObservationMessage(message: ObservationMessage) {
}),
);
break;
case "mcp_call_tool":
store.dispatch(
addAssistantObservation({
...baseObservation,
observation: "mcp_call_tool" as const,
extras: {
tool_name: String(message.extras.tool_name || ""),
kwargs:
typeof message.extras.kwargs === "object"
? (message.extras.kwargs as Record<string, unknown>)
: {},
},
}),
);
break;
case "delegate":
store.dispatch(
addAssistantObservation({
Expand Down
19 changes: 19 additions & 0 deletions frontend/src/state/chat-slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ const HANDLED_ACTIONS: OpenHandsEventType[] = [
"browse",
"browse_interactive",
"edit",
"mcp_call_tool",
];

function getRiskText(risk: ActionSecurityRisk) {
Expand Down Expand Up @@ -112,6 +113,12 @@ export const chatSlice = createSlice({
} else if (actionID === "browse_interactive") {
// Include the browser_actions in the content
text = `**Action:**\n\n\`\`\`python\n${action.payload.args.browser_actions}\n\`\`\``;
} else if (actionID === "mcp_call_tool") {
text = `Tool name: \`${action.payload.args.tool_name}\`\n\nArguments:\n\`\`\`python\n${JSON.stringify(
action.payload.args.kwargs,
null,
2,
)}\n\`\`\``;
}
if (actionID === "run" || actionID === "run_ipython") {
if (
Expand Down Expand Up @@ -193,6 +200,18 @@ export const chatSlice = createSlice({
} else {
causeMessage.content = observation.payload.content;
}
} else if (observationID === "mcp_call_tool") {
let { content } = observation.payload;
if (content.length > MAX_CONTENT_LENGTH) {
content = `${content.slice(0, MAX_CONTENT_LENGTH)}...`;
}
const toolName = observation.payload.extras.tool_name;
const toolKwargs = JSON.stringify(
observation.payload.extras.kwargs,
null,
2,
);
causeMessage.content = `Tool name: \`${toolName}\`\n\nArguments:\n\`\`\`python\n${toolKwargs}\n\`\`\`\n\nOutput:\n\`\`\`python\n${content.trim()}\n\`\`\``;
} else if (observationID === "browse") {
let content = `**URL:** ${observation.payload.extras.url}\n`;
if (observation.payload.extras.error) {
Expand Down
3 changes: 3 additions & 0 deletions frontend/src/types/action-type.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ enum ActionType {
// Logs a thought.
THINK = "think",

// Calls an MCP tool
MCP_CALL_TOOL = "mcp_call_tool",

// If you're absolutely certain that you've completed your task and have tested your work,
// use the finish action to stop working.
FINISH = "finish",
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/types/core/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ export interface FinishAction extends OpenHandsActionEvent<"finish"> {
};
}

export interface MCPCallToolAction
extends OpenHandsActionEvent<"mcp_call_tool"> {
source: "agent";
args: {
thought: string;
tool_name: string;
kwargs: Record<string, unknown>;
};
}

export interface DelegateAction extends OpenHandsActionEvent<"delegate"> {
source: "agent";
timeout: number;
Expand Down Expand Up @@ -146,4 +156,5 @@ export type OpenHandsAction =
| FileReadAction
| FileEditAction
| FileWriteAction
| MCPCallToolAction
| RejectAction;
1 change: 1 addition & 0 deletions frontend/src/types/core/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type OpenHandsEventType =
| "reject"
| "think"
| "finish"
| "mcp_call_tool"
| "error";

interface OpenHandsBaseEvent {
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/types/core/observations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ export interface AgentThinkObservation
};
}

export interface MCPCallToolObservation
extends OpenHandsObservationEvent<"mcp_call_tool"> {
source: "agent";
extras: {
tool_name: string;
kwargs?: Record<string, unknown>;
};
}

export type OpenHandsObservation =
| AgentStateChangeObservation
| AgentThinkObservation
Expand All @@ -120,4 +129,5 @@ export type OpenHandsObservation =
| WriteObservation
| ReadObservation
| EditObservation
| MCPCallToolObservation
| ErrorObservation;
3 changes: 3 additions & 0 deletions frontend/src/types/observation-type.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ enum ObservationType {
// A response to the agent's thought (usually a static message)
THINK = "think",

// MCP tool call result
MCP_CALL_TOOL = "mcp_call_tool",

// A no-op observation
NULL = "null",
}
Expand Down
17 changes: 17 additions & 0 deletions openhands/agenthub/codeact_agent/function_calling.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
FinishTool,
IPythonTool,
LLMBasedFileEditTool,
MCPCallTool,
ThinkTool,
WebReadTool,
create_cmd_run_tool,
Expand All @@ -35,6 +36,7 @@
FileEditAction,
FileReadAction,
IPythonRunCellAction,
MCPCallToolAction,
MessageAction,
)
from openhands.events.event import FileEditSource, FileReadSource
Expand Down Expand Up @@ -191,6 +193,20 @@ def response_to_actions(response: ModelResponse) -> list[Action]:
f'Missing required argument "url" in tool call {tool_call.function.name}'
)
action = BrowseURLAction(url=arguments['url'])

# ================================================
# MCPCallTool (MCP)
# ================================================
elif tool_call.function.name == MCPCallTool['function']['name']:
if 'tool_name' not in arguments:
raise FunctionCallValidationError(
f'Missing required argument "tool_name" in tool call {tool_call.function.name}'
)
action = MCPCallToolAction(
tool_name=arguments['tool_name'],
kwargs=arguments['kwargs'] if 'kwargs' in arguments else None,
)

else:
raise FunctionCallNotExistsError(
f'Tool {tool_call.function.name} is not registered. (arguments: {arguments}). Please check the tool name and retry with an existing tool.'
Expand Down Expand Up @@ -245,6 +261,7 @@ def get_tools(
create_cmd_run_tool(use_simplified_description=use_simplified_tool_desc),
ThinkTool,
FinishTool,
MCPCallTool,
]
if codeact_enable_browsing:
tools.append(WebReadTool)
Expand Down
2 changes: 2 additions & 0 deletions openhands/agenthub/codeact_agent/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .finish import FinishTool
from .ipython import IPythonTool
from .llm_based_edit import LLMBasedFileEditTool
from .mcp import MCPCallTool
from .str_replace_editor import create_str_replace_editor_tool
from .think import ThinkTool
from .web_read import WebReadTool
Expand All @@ -16,4 +17,5 @@
'create_str_replace_editor_tool',
'WebReadTool',
'ThinkTool',
'MCPCallTool',
]
33 changes: 33 additions & 0 deletions openhands/agenthub/codeact_agent/tools/mcp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
from litellm import ChatCompletionToolParam, ChatCompletionToolParamFunctionChunk

_MCP_CALL_DESCRIPTION = """Custom tool to call other tools exposed by MCP (Model Context Protocol) servers.

For example, if you see a tool in the format: \
"Tool(name='tool_name', description='Tool description', inputSchema={'type': 'object', 'properties':
{'param1': {'type': 'string'}}})"

You can call it by setting `tool_name` to `tool_name` and `kwargs` to \
`{'param1': 'value'}`. \
"""

MCPCallTool = ChatCompletionToolParam(
type='function',
function=ChatCompletionToolParamFunctionChunk(
name='mcp_call_tool',
description=_MCP_CALL_DESCRIPTION,
parameters={
'type': 'object',
'properties': {
'tool_name': {
'type': 'string',
'description': 'The name of the tool to call.',
},
'kwargs': {
'type': 'object',
'description': 'The optional arguments dict to pass to the tool call.',
},
},
'required': ['tool_name'],
},
),
)
3 changes: 3 additions & 0 deletions openhands/core/schema/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,6 @@ class ActionType(str, Enum):

CONDENSATION = 'condensation'
"""Condenses a list of events into a summary."""

MCP_CALL_TOOL = 'mcp_call_tool'
"""Calls a MCP tool."""
3 changes: 3 additions & 0 deletions openhands/core/schema/observation.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,6 @@ class ObservationType(str, Enum):

RECALL = 'recall'
"""Result of a recall operation. This can be the workspace context, a microagent, or other types of information."""

MCP_CALL_TOOL = 'mcp_call_tool'
"""Result of a tool call to the MCP server."""
7 changes: 7 additions & 0 deletions openhands/core/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,13 @@ def create_memory(
)
memory.load_user_workspace_microagents(microagents)

# get MCP tools from the runtime
mcp_tools_definition = runtime.get_mcp_tool_definitions(
memory.get_mcp_configs()
)
logger.info(f'All MCP tools: {mcp_tools_definition}')
memory.populate_mcp_tool_definitions(mcp_tools_definition)

if selected_repository and repo_directory:
memory.set_repository_info(selected_repository, repo_directory)

Expand Down
2 changes: 2 additions & 0 deletions openhands/events/action/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
FileReadAction,
FileWriteAction,
)
from openhands.events.action.mcp import MCPCallToolAction
from openhands.events.action.message import MessageAction

__all__ = [
Expand All @@ -35,4 +36,5 @@
'ActionConfirmationStatus',
'AgentThinkAction',
'RecallAction',
'MCPCallToolAction',
]
19 changes: 19 additions & 0 deletions openhands/events/action/mcp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from dataclasses import dataclass
from typing import Any, ClassVar

from openhands.core.schema import ActionType
from openhands.events.action.action import Action


@dataclass
class MCPCallToolAction(Action):
runnable: ClassVar[bool] = True
thought: str = ''
tool_name: str = ''
kwargs: dict[str, Any] | None = None
action: str = ActionType.MCP_CALL_TOOL

@property
def message(self) -> str:
msg: str = f'Calling MCP tool `{self.tool_name}` with arguments: {self.kwargs}'
return msg
2 changes: 2 additions & 0 deletions openhands/events/observation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
FileReadObservation,
FileWriteObservation,
)
from openhands.events.observation.mcp import MCPCallToolObservation
from openhands.events.observation.observation import Observation
from openhands.events.observation.reject import UserRejectObservation
from openhands.events.observation.success import SuccessObservation
Expand All @@ -44,4 +45,5 @@
'AgentCondensationObservation',
'RecallObservation',
'RecallType',
'MCPCallToolObservation',
]
16 changes: 16 additions & 0 deletions openhands/events/observation/mcp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from dataclasses import dataclass
from typing import Any

from openhands.core.schema import ObservationType
from openhands.events.observation.observation import Observation


@dataclass
class MCPCallToolObservation(Observation):
observation: str = ObservationType.MCP_CALL_TOOL
tool_name: str = ''
kwargs: dict[str, Any] | None = None

@property
def message(self) -> str:
return self.content
2 changes: 2 additions & 0 deletions openhands/events/serialization/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
FileReadAction,
FileWriteAction,
)
from openhands.events.action.mcp import MCPCallToolAction
from openhands.events.action.message import MessageAction

actions = (
Expand All @@ -41,6 +42,7 @@
ChangeAgentStateAction,
MessageAction,
CondensationAction,
MCPCallToolAction,
)

ACTION_TYPE_TO_CLASS = {action_class.action: action_class for action_class in actions} # type: ignore[attr-defined]
Expand Down
2 changes: 2 additions & 0 deletions openhands/events/serialization/observation.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
FileReadObservation,
FileWriteObservation,
)
from openhands.events.observation.mcp import MCPCallToolObservation
from openhands.events.observation.observation import Observation
from openhands.events.observation.reject import UserRejectObservation
from openhands.events.observation.success import SuccessObservation
Expand All @@ -45,6 +46,7 @@
AgentCondensationObservation,
AgentThinkObservation,
RecallObservation,
MCPCallToolObservation,
)

OBSERVATION_TYPE_TO_CLASS = {
Expand Down
Loading
Loading