mcp: keep local skill search lightweight

This commit is contained in:
who96 2026-03-26 08:52:20 +08:00 • committed by Dennis-yxchen
parent ccda314d34
commit f4451aa0ac
2 changed files with 113 additions and 1 deletions

View file

@ -119,6 +119,7 @@ mcp = FastMCP("OpenSpace", **_fastmcp_kwargs)
_openspace_instance = None
_openspace_lock = asyncio.Lock()
_standalone_store = None
_local_skill_registry = None
# Internal state: tracks bot skill directories already registered this session.
_registered_skill_dirs: set = set()
@ -198,6 +199,64 @@ def _get_store():
return _standalone_store
def _get_local_skill_registry():
"""Build a lightweight SkillRegistry for local-only skill search.
This avoids initializing the full OpenSpace engine when callers only
want to inspect local skills. It mirrors the skill directory discovery
order used by the full engine, but skips LLM / provider startup.
"""
global _local_skill_registry
if _local_skill_registry is not None:
return _local_skill_registry
from openspace.config import get_config
from openspace.skill_engine import SkillRegistry
skill_paths: List[Path] = []
host_dirs_raw = os.environ.get("OPENSPACE_HOST_SKILL_DIRS", "")
if host_dirs_raw:
for d in host_dirs_raw.split(","):
d = d.strip()
if not d:
continue
p = Path(d)
if p.exists():
skill_paths.append(p)
else:
logger.warning("Host skill dir does not exist: %s", d)
try:
skill_cfg = get_config().skills
except Exception as e:
logger.warning("Failed to load local skill config: %s", e)
skill_cfg = None
if skill_cfg and skill_cfg.skill_dirs:
for d in skill_cfg.skill_dirs:
p = Path(d)
if p in skill_paths:
continue
if p.exists():
skill_paths.append(p)
else:
logger.warning("Configured skill dir does not exist: %s", d)
builtin_skills = Path(__file__).resolve().parent / "skills"
if builtin_skills.exists():
skill_paths.append(builtin_skills)
if not skill_paths:
logger.debug("No local skill directories found")
return None
registry = SkillRegistry(skill_dirs=skill_paths)
registry.discover()
_local_skill_registry = registry
return registry
def _get_cloud_client():
"""Get a OpenSpaceClient instance (raises CloudError if not configured)."""
from openspace.cloud.auth import get_openspace_auth
@ -597,7 +656,11 @@ async def search_skills(
# Re-scan host skill directories so newly created skills are searchable.
local_skills = None
store = None
if source in ("all", "local"):
if source == "local":
registry = _get_local_skill_registry()
if registry:
local_skills = registry.list_skills()
elif source == "all":
openspace = await _get_openspace()
host_skill_dirs_raw = os.environ.get("OPENSPACE_HOST_SKILL_DIRS", "")

View file

@ -0,0 +1,49 @@
import json
import pytest
from openspace import mcp_server
@pytest.mark.asyncio
async def test_local_search_skills_does_not_initialize_openspace(monkeypatch, tmp_path):
skill_root = tmp_path / "skills"
skill_dir = skill_root / "demo-local-skill"
skill_dir.mkdir(parents=True)
(skill_dir / "SKILL.md").write_text(
"""---
name: Demo Local Skill
description: Local regression test skill
---
Find me when querying demo local skill.
""",
encoding="utf-8",
)
async def forbidden_get_openspace():
pytest.fail("_get_openspace should not run for source='local'")
monkeypatch.setenv("OPENSPACE_HOST_SKILL_DIRS", str(skill_root))
monkeypatch.delenv("OPENROUTER_API_KEY", raising=False)
monkeypatch.delenv("OPENAI_API_KEY", raising=False)
monkeypatch.delenv("OPENAI_BASE_URL", raising=False)
monkeypatch.setattr(mcp_server, "_get_openspace", forbidden_get_openspace)
monkeypatch.setattr(
"openspace.cloud.embedding.generate_embedding",
lambda text, api_key=None: None,
)
response = await mcp_server.search_skills(
query="demo local skill",
source="local",
limit=5,
auto_import=False,
)
payload = json.loads(response)
assert payload["count"] >= 1
assert any(
item["name"] == "Demo Local Skill"
for item in payload["results"]
)