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:
advancer-young
2026-06-30 19:16:30 +08:00
committed by GitHub
parent 2618e06492
commit 096ec1a8ce
39 changed files with 4103 additions and 201 deletions
@@ -90,6 +90,56 @@ class TestMCPServiceGetRuntimeInfo:
assert result is None
class TestMCPServiceResources:
"""Tests for MCP resource helpers."""
async def test_get_resource_templates_delegates_to_loader(self):
ap = SimpleNamespace()
ap.tool_mgr = SimpleNamespace()
ap.tool_mgr.mcp_tool_loader = SimpleNamespace()
ap.tool_mgr.mcp_tool_loader.get_resource_templates = AsyncMock(
return_value=[{'uri_template': 'file:///{path}', 'name': 'files'}]
)
service = MCPService(ap)
result = await service.get_mcp_server_resource_templates('docs')
assert result == [{'uri_template': 'file:///{path}', 'name': 'files'}]
ap.tool_mgr.mcp_tool_loader.get_resource_templates.assert_awaited_once_with('docs')
async def test_read_resource_envelope_uses_ui_preview_source(self):
ap = SimpleNamespace()
ap.tool_mgr = SimpleNamespace()
ap.tool_mgr.mcp_tool_loader = SimpleNamespace()
ap.tool_mgr.mcp_tool_loader.read_resource_envelope = AsyncMock(
return_value={
'server_name': 'docs',
'uri': 'file:///README.md',
'contents': [],
'source': 'ui_preview',
}
)
service = MCPService(ap)
result = await service.read_mcp_server_resource_envelope(
'docs',
'file:///README.md',
max_bytes=4096,
include_blob=True,
)
assert result['source'] == 'ui_preview'
ap.tool_mgr.mcp_tool_loader.read_resource_envelope.assert_awaited_once_with(
'docs',
'file:///README.md',
include_blob=True,
source='ui_preview',
max_bytes=4096,
)
class TestMCPServiceGetMCPServers:
"""Tests for get_mcp_servers method."""
@@ -348,6 +348,8 @@ class TestPipelineServiceCreatePipeline:
'enable_all_mcp_servers': True,
'plugins': [],
'mcp_servers': [],
'mcp_resources': [],
'mcp_resource_agent_read_enabled': True,
}
@@ -814,6 +816,47 @@ class TestPipelineServiceUpdatePipelineExtensions:
# Verify - persistence was called
ap.persistence_mgr.execute_async.assert_called()
async def test_update_extensions_preserves_mcp_resource_agent_read_when_omitted(self):
"""Does not reset mcp_resource_agent_read_enabled when omitted by older clients."""
ap = SimpleNamespace()
ap.persistence_mgr = SimpleNamespace()
ap.pipeline_mgr = SimpleNamespace()
ap.pipeline_mgr.remove_pipeline = AsyncMock()
ap.pipeline_mgr.load_pipeline = AsyncMock()
original_pipeline = _create_mock_pipeline(
extensions_preferences={
'enable_all_plugins': True,
'enable_all_mcp_servers': True,
'plugins': [],
'mcp_servers': [],
'mcp_resources': [{'server_uuid': 'srv-1', 'uri': 'file:///README.md'}],
'mcp_resource_agent_read_enabled': False,
}
)
call_count = 0
async def mock_execute(query):
nonlocal call_count
call_count += 1
if call_count == 1:
return _create_mock_result(first_item=original_pipeline)
return Mock()
ap.persistence_mgr.execute_async = AsyncMock(side_effect=mock_execute)
ap.persistence_mgr.serialize_model = Mock(return_value={'uuid': 'test-uuid'})
service = PipelineService(ap)
service.get_pipeline = AsyncMock(return_value={'uuid': 'test-uuid'})
await service.update_pipeline_extensions('test-uuid', bound_plugins=[])
assert original_pipeline.extensions_preferences['mcp_resource_agent_read_enabled'] is False
assert original_pipeline.extensions_preferences['mcp_resources'] == [
{'server_uuid': 'srv-1', 'uri': 'file:///README.md'}
]
class TestDefaultStageOrder:
"""Tests for default_stage_order constant."""