fix(mcp): keep local skill search lightweight

This commit is contained in:
Pengyi Peng 2026-08-26 01:33:48 +08:00
parent 38277815ed
commit 11b3e817ae
3 changed files with 128 additions and 15 deletions

5
.gitignore vendored
View file

@ -62,6 +62,11 @@ tests/skill_engine/evolution/*
!tests/cloud/
tests/cloud/*
!tests/cloud/test_upload_trust.py
!tests/entrypoints/
tests/entrypoints/*
!tests/entrypoints/mcp/
tests/entrypoints/mcp/*
!tests/entrypoints/mcp/test_server_search.py
scripts/
# Local agent/project memory

View file

@ -231,6 +231,46 @@ async def _get_runtime_store(*, required: bool = True):
return None
async def _get_local_search_context():
"""Return local skills without booting the full agent runtime for search.
``search_skills`` is a discovery-only MCP tool. Initializing OpenSpace here
also initializes grounding providers, memory, evolution, and model config,
which can take longer than a normal MCP tool timeout on Windows. Reuse the
runtime registry when it already exists; otherwise build the same canonical
registry directly and omit optional quality-store enrichment.
"""
if _openspace_instance is not None and _openspace_instance.is_initialized():
host_skill_dirs_raw = os.environ.get("OPENSPACE_HOST_SKILL_DIRS", "")
if host_skill_dirs_raw:
env_dirs = [
directory.strip()
for directory in host_skill_dirs_raw.split(",")
if directory.strip()
]
if env_dirs:
await _auto_register_skill_dirs(env_dirs)
registry = _openspace_instance.get_skill_registry()
if not registry:
return None, None
store = _openspace_instance.get_skill_store()
if store and getattr(store, "_closed", False):
store = None
return registry.list_skills(), store
from openspace.runtime.skill_registry import build_skill_registry
registry = await asyncio.to_thread(
build_skill_registry,
workspace_dir=os.environ.get("OPENSPACE_WORKSPACE"),
)
if not registry:
return None, None
return registry.list_skills(), None
async def _get_cloud_mapping_store():
from openspace.cloud.local_mapping import CloudLocalMappingStore
@ -818,21 +858,7 @@ async def search_skills(
if not q:
return _json_ok({"results": [], "count": 0})
# Re-scan host skill directories so newly created skills are searchable.
local_skills = None
store = None
openspace = await _get_openspace()
host_skill_dirs_raw = os.environ.get("OPENSPACE_HOST_SKILL_DIRS", "")
if host_skill_dirs_raw:
env_dirs = [d.strip() for d in host_skill_dirs_raw.split(",") if d.strip()]
if env_dirs:
await _auto_register_skill_dirs(env_dirs)
registry = openspace.get_skill_registry()
if registry:
local_skills = registry.list_skills()
store = await _get_runtime_store(required=False)
local_skills, store = await _get_local_search_context()
results = await hybrid_search_skills(
query=q,

View file

@ -0,0 +1,82 @@
from __future__ import annotations
from types import SimpleNamespace
import pytest
from openspace.entrypoints.mcp import server
class _Registry:
def __init__(self, skills):
self._skills = skills
def list_skills(self):
return self._skills
@pytest.mark.asyncio
async def test_local_search_context_builds_registry_without_full_runtime(
monkeypatch,
):
expected = [SimpleNamespace(skill_id="skill-1")]
calls = []
def build_registry(*, workspace_dir=None):
calls.append(workspace_dir)
return _Registry(expected)
async def fail_if_full_runtime_starts():
raise AssertionError(
"local skill search must not initialize OpenSpace"
)
monkeypatch.setattr(server, "_openspace_instance", None)
monkeypatch.setattr(server, "_get_openspace", fail_if_full_runtime_starts)
monkeypatch.setenv("OPENSPACE_WORKSPACE", "test-workspace")
monkeypatch.setattr(
"openspace.runtime.skill_registry.build_skill_registry",
build_registry,
)
skills, store = await server._get_local_search_context()
assert skills == expected
assert store is None
assert calls == ["test-workspace"]
@pytest.mark.asyncio
async def test_local_search_context_reuses_initialized_runtime(monkeypatch):
expected = [SimpleNamespace(skill_id="skill-1")]
store = SimpleNamespace(_closed=False)
runtime = SimpleNamespace(
is_initialized=lambda: True,
get_skill_registry=lambda: _Registry(expected),
get_skill_store=lambda: store,
)
monkeypatch.setattr(server, "_openspace_instance", runtime)
monkeypatch.delenv("OPENSPACE_HOST_SKILL_DIRS", raising=False)
skills, actual_store = await server._get_local_search_context()
assert skills == expected
assert actual_store is store
@pytest.mark.asyncio
async def test_local_search_context_ignores_closed_runtime_store(monkeypatch):
runtime = SimpleNamespace(
is_initialized=lambda: True,
get_skill_registry=lambda: _Registry([]),
get_skill_store=lambda: SimpleNamespace(_closed=True),
)
monkeypatch.setattr(server, "_openspace_instance", runtime)
monkeypatch.delenv("OPENSPACE_HOST_SKILL_DIRS", raising=False)
skills, store = await server._get_local_search_context()
assert skills == []
assert store is None