mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-17 01:46:07 +00:00
158 lines
5.0 KiB
Python
158 lines
5.0 KiB
Python
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, Mock
|
|
|
|
import pytest
|
|
|
|
from langbot.pkg.provider.tools.loaders.mcp import (
|
|
MCP_TOOL_LIST_RESOURCES,
|
|
MCP_TOOL_READ_RESOURCE,
|
|
MCPLoader,
|
|
MCPSessionStatus,
|
|
)
|
|
from langbot.pkg.provider.tools.loaders.plugin import PluginToolLoader
|
|
from langbot_plugin.api.entities.builtin.resource.tool import LLMTool
|
|
|
|
|
|
def make_tool(name: str) -> LLMTool:
|
|
return LLMTool(
|
|
name=name,
|
|
human_desc=name,
|
|
description=name,
|
|
parameters={'type': 'object', 'properties': {}},
|
|
func=lambda parameters: parameters,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_two_mcp_servers_with_same_tool_route_to_authorized_server():
|
|
tool = make_tool('shared_tool')
|
|
first = SimpleNamespace(
|
|
server_uuid='mcp-a',
|
|
get_tools=Mock(return_value=[tool]),
|
|
invoke_mcp_tool=AsyncMock(return_value='from-a'),
|
|
)
|
|
second = SimpleNamespace(
|
|
server_uuid='mcp-b',
|
|
get_tools=Mock(return_value=[tool]),
|
|
invoke_mcp_tool=AsyncMock(return_value='from-b'),
|
|
)
|
|
loader = MCPLoader(SimpleNamespace(logger=Mock()))
|
|
loader.sessions = {'first': first, 'second': second}
|
|
query = SimpleNamespace(variables={})
|
|
|
|
result = await loader.invoke_tool(
|
|
'shared_tool',
|
|
{'value': 1},
|
|
query,
|
|
source_id='mcp-b',
|
|
)
|
|
|
|
assert result == 'from-b'
|
|
first.invoke_mcp_tool.assert_not_awaited()
|
|
second.invoke_mcp_tool.assert_awaited_once_with(
|
|
'shared_tool',
|
|
{'value': 1},
|
|
query=query,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize('reserved_name', [MCP_TOOL_LIST_RESOURCES, MCP_TOOL_READ_RESOURCE])
|
|
async def test_reserved_name_routes_to_real_mcp_tool_when_source_id_is_present(reserved_name):
|
|
real_tool = make_tool(reserved_name)
|
|
session = SimpleNamespace(
|
|
server_uuid='srv-real',
|
|
get_tools=Mock(return_value=[real_tool]),
|
|
invoke_mcp_tool=AsyncMock(return_value='real-result'),
|
|
)
|
|
loader = MCPLoader(SimpleNamespace(logger=Mock()))
|
|
loader.sessions = {'real': session}
|
|
query = SimpleNamespace(variables={})
|
|
|
|
assert await loader.has_tool(reserved_name, source_id='srv-real') is True
|
|
assert await loader.get_tool(reserved_name, source_id='srv-real') is real_tool
|
|
result = await loader.invoke_tool(reserved_name, {}, query, source_id='srv-real')
|
|
|
|
assert result == 'real-result'
|
|
session.invoke_mcp_tool.assert_awaited_once_with(reserved_name, {}, query=query)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
('reserved_name', 'invoke_method'),
|
|
[
|
|
(MCP_TOOL_LIST_RESOURCES, '_invoke_mcp_list_resources'),
|
|
(MCP_TOOL_READ_RESOURCE, '_invoke_mcp_read_resource'),
|
|
],
|
|
)
|
|
async def test_reserved_name_uses_host_synthetic_tool_when_source_id_is_none(
|
|
reserved_name,
|
|
invoke_method,
|
|
):
|
|
real_tool = make_tool(reserved_name)
|
|
session = SimpleNamespace(
|
|
server_uuid='srv-real',
|
|
server_name='real',
|
|
enable=True,
|
|
status=MCPSessionStatus.CONNECTED,
|
|
session=object(),
|
|
get_tools=Mock(return_value=[real_tool]),
|
|
has_resource_support=Mock(return_value=True),
|
|
invoke_mcp_tool=AsyncMock(return_value='real-result'),
|
|
)
|
|
loader = MCPLoader(SimpleNamespace(logger=Mock()))
|
|
loader.sessions = {'real': session}
|
|
synthetic_invoke = AsyncMock(return_value='synthetic-result')
|
|
setattr(loader, invoke_method, synthetic_invoke)
|
|
query = SimpleNamespace(variables={})
|
|
|
|
assert await loader.has_tool(reserved_name, source_id=None) is True
|
|
tool = await loader.get_tool(reserved_name, source_id=None)
|
|
result = await loader.invoke_tool(reserved_name, {}, query, source_id=None)
|
|
|
|
assert tool is not None
|
|
assert tool is not real_tool
|
|
assert result == 'synthetic-result'
|
|
synthetic_invoke.assert_awaited_once_with({}, query)
|
|
session.invoke_mcp_tool.assert_not_awaited()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_two_plugins_with_same_tool_forward_only_authorized_plugin():
|
|
manifest = SimpleNamespace(
|
|
owner='authorized/plugin',
|
|
metadata=SimpleNamespace(
|
|
name='shared_tool',
|
|
description=SimpleNamespace(en_US='Shared tool'),
|
|
),
|
|
spec={
|
|
'llm_prompt': 'Shared tool',
|
|
'parameters': {'type': 'object', 'properties': {}},
|
|
},
|
|
)
|
|
connector = SimpleNamespace(
|
|
list_tools=AsyncMock(return_value=[manifest]),
|
|
call_tool=AsyncMock(return_value='authorized-result'),
|
|
)
|
|
loader = PluginToolLoader(SimpleNamespace(plugin_connector=connector, logger=Mock()))
|
|
query = SimpleNamespace(session=SimpleNamespace(), query_id=7)
|
|
query.session.model_dump = Mock(return_value={})
|
|
|
|
result = await loader.invoke_tool(
|
|
'shared_tool',
|
|
{},
|
|
query,
|
|
source_id='authorized/plugin',
|
|
)
|
|
|
|
assert result == 'authorized-result'
|
|
connector.call_tool.assert_awaited_once_with(
|
|
'shared_tool',
|
|
{},
|
|
session=query.session,
|
|
query_id=7,
|
|
bound_plugins=['authorized/plugin'],
|
|
)
|