mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-14 08:26:07 +00:00
feat(mcp): support mcp resources (#2215)
* feat(mcp): support mcp resources * feat(web): split MCP resources into tab * docs: add MCP resources PR review * feat(mcp): productionize resource support * feat(mcp): scope local agent tools and resources * fix(web): gate space embedding models behind login * fix(web): prevent clipped space model CTA * test: update preproc resource tool expectations * fix(web): expose skill authoring tools in selector --------- Co-authored-by: yang.xiang <yang.xiang@advancegroup.com> Co-authored-by: Hyu <chenhyu@proton.me> Co-authored-by: Junyan Qin <rockchinq@gmail.com>
This commit is contained in:
@@ -162,3 +162,46 @@ async def test_runtime_pipeline_execute(mock_app, sample_query):
|
||||
|
||||
# Verify stage was called
|
||||
mock_stage.process.assert_called_once()
|
||||
|
||||
|
||||
def test_runtime_pipeline_prefers_local_agent_mcp_resources(mock_app):
|
||||
"""Local Agent resource selection should override legacy extension prefs."""
|
||||
pipelinemgr = get_pipelinemgr_module()
|
||||
persistence_pipeline = get_persistence_pipeline_module()
|
||||
|
||||
pipeline_entity = Mock(spec=persistence_pipeline.LegacyPipeline)
|
||||
pipeline_entity.config = {
|
||||
'ai': {
|
||||
'local-agent': {
|
||||
'mcp-resources': [{'server_uuid': 'srv-new', 'uri': 'file:///new.md'}],
|
||||
'mcp-resource-agent-read-enabled': False,
|
||||
}
|
||||
}
|
||||
}
|
||||
pipeline_entity.extensions_preferences = {
|
||||
'mcp_resources': [{'server_uuid': 'srv-old', 'uri': 'file:///old.md'}],
|
||||
'mcp_resource_agent_read_enabled': True,
|
||||
}
|
||||
|
||||
runtime_pipeline = pipelinemgr.RuntimePipeline(mock_app, pipeline_entity, [])
|
||||
|
||||
assert runtime_pipeline.mcp_resource_attachments == [{'server_uuid': 'srv-new', 'uri': 'file:///new.md'}]
|
||||
assert runtime_pipeline.mcp_resource_agent_read_enabled is False
|
||||
|
||||
|
||||
def test_runtime_pipeline_falls_back_to_extension_mcp_resources(mock_app):
|
||||
"""Existing extension prefs remain compatible until a Local Agent value exists."""
|
||||
pipelinemgr = get_pipelinemgr_module()
|
||||
persistence_pipeline = get_persistence_pipeline_module()
|
||||
|
||||
pipeline_entity = Mock(spec=persistence_pipeline.LegacyPipeline)
|
||||
pipeline_entity.config = {'ai': {'local-agent': {}}}
|
||||
pipeline_entity.extensions_preferences = {
|
||||
'mcp_resources': [{'server_uuid': 'srv-old', 'uri': 'file:///old.md'}],
|
||||
'mcp_resource_agent_read_enabled': False,
|
||||
}
|
||||
|
||||
runtime_pipeline = pipelinemgr.RuntimePipeline(mock_app, pipeline_entity, [])
|
||||
|
||||
assert runtime_pipeline.mcp_resource_attachments == [{'server_uuid': 'srv-old', 'uri': 'file:///old.md'}]
|
||||
assert runtime_pipeline.mcp_resource_agent_read_enabled is False
|
||||
|
||||
@@ -14,6 +14,7 @@ from __future__ import annotations
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, Mock
|
||||
from importlib import import_module
|
||||
from types import SimpleNamespace
|
||||
|
||||
from tests.factories import (
|
||||
FakeApp,
|
||||
@@ -431,3 +432,60 @@ class TestPreProcessorVariables:
|
||||
variables = result.new_query.variables
|
||||
assert 'group_name' in variables
|
||||
assert 'sender_name' in variables
|
||||
|
||||
|
||||
class TestPreProcessorToolSelection:
|
||||
"""Tests for Local Agent tool selection."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_local_agent_filters_selected_tools(self):
|
||||
"""Only selected tools should be exposed when all-tools mode is off."""
|
||||
preproc = get_preproc_module()
|
||||
|
||||
app = FakeApp()
|
||||
mock_session = Mock()
|
||||
mock_session.launcher_type = Mock(value='person')
|
||||
mock_session.launcher_id = 12345
|
||||
app.sess_mgr.get_session = AsyncMock(return_value=mock_session)
|
||||
|
||||
mock_conversation = Mock()
|
||||
mock_conversation.prompt = Mock(messages=[])
|
||||
mock_conversation.prompt.copy = Mock(return_value=Mock(messages=[]))
|
||||
mock_conversation.messages = []
|
||||
mock_conversation.uuid = None
|
||||
app.sess_mgr.get_conversation = AsyncMock(return_value=mock_conversation)
|
||||
|
||||
mock_model = Mock()
|
||||
mock_model.model_entity = Mock(uuid='primary-model-uuid', abilities=['func_call'])
|
||||
app.model_mgr.get_model_by_uuid = AsyncMock(return_value=mock_model)
|
||||
app.tool_mgr.get_all_tools = AsyncMock(
|
||||
return_value=[
|
||||
SimpleNamespace(name='exec'),
|
||||
SimpleNamespace(name='plugin_tool'),
|
||||
SimpleNamespace(name='mcp_tool'),
|
||||
]
|
||||
)
|
||||
|
||||
mock_event_ctx = Mock()
|
||||
mock_event_ctx.event = Mock(default_prompt=[], prompt=[])
|
||||
app.plugin_connector.emit_event = AsyncMock(return_value=mock_event_ctx)
|
||||
|
||||
stage = preproc.PreProcessor(app)
|
||||
query = text_query('hello')
|
||||
query.pipeline_config = {
|
||||
'ai': {
|
||||
'runner': {'runner': 'local-agent'},
|
||||
'local-agent': {
|
||||
'model': {'primary': 'primary-model-uuid', 'fallbacks': []},
|
||||
'prompt': 'default',
|
||||
'enable-all-tools': False,
|
||||
'tools': ['plugin_tool'],
|
||||
},
|
||||
},
|
||||
'output': {'misc': {'at-sender': False}},
|
||||
'trigger': {'misc': {}},
|
||||
}
|
||||
|
||||
result = await stage.process(query, 'PreProcessor')
|
||||
|
||||
assert [tool.name for tool in result.new_query.use_funcs] == ['plugin_tool']
|
||||
|
||||
Reference in New Issue
Block a user