Skip to content

feat(mcp): serve distributed skills over MCP (SEP-2640) - #2022

Merged
phernandez merged 2 commits into
mainfrom
feat/skills-over-mcp
Nov 4, 2026
Merged

phernandez merged 2 commits into
mainfrom
feat/skills-over-mcp

Conversation

@phernandez

Copy link
Copy Markdown
Member

Summary

Serves the Basic Memory skills over MCP, using FastMCP's Skills extension support (SEP-2640). The pinned FastMCP 4.1.3 already includes it, so no upgrade is needed.

  • src/basic_memory/mcp/resources/skills.py serves the skills the wheel ships under basic_memory/data/skills through SkillsDirectoryProvider(experimental_skills=True) plus SkillsExtension.
    • Clients that negotiate io.modelcontextprotocol/skills get skills/list (description plus a digest manifest per skill) and skills/get.
    • Every other client can read skill://<name>/SKILL.md as an ordinary resource.
    • The skill set is the release set: pyproject's force-include list decides it, and tests/test_skills_dist.py already pins it. Internal skills such as memory-ci-capture are never offered.
  • set_project_delete_visibility now puts the Visibility mark on server.local_provider, not on the server. SkillsExtension raises on any server-level transform. The tools live on the local provider, so bm_project_delete behaves the same, and the existing visibility tests pass unchanged.

Client support (checked 2026-11-01)

  • Claude Code 2.1.316: the binary contains a skills/list / skills/get client for io.modelcontextprotocol/skills, but its enable check returns false. It is built and switched off.
  • Codex 0.165.0: no SEP-2640 client. Its skill:// handling is its own internal skill provider.
  • MCP Inspector 2.6+ and fast-agent: support the extension. Any client that reads resources can already read the skills.

Test plan

  • tests/mcp/test_skill_resources.py: skills/list offers exactly the shipped skills; skills/get sizes match the shipped files; a client without the extension can list and read skill://memory-notes/SKILL.md
  • just typecheck
  • affected MCP tests (skills, bm_man, bm_project, v1 registration): 70 passed with an isolated HOME
  • just fast-test -m "not benchmark": 5058 passed. The 8 failures were all Can't locate revision '0043' from a local home DB that another branch had migrated, and they reproduce without this change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CMYVL4TcbWdXhdvAzxfqjo

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-04T15:49:06.869178Z cf7ed10 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@tonydzi tonydzi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things from running this branch (head 819a804) locally: Python 3.12, pip install -e ., FastMCP 4.1.3. I did not run your full suite, only the probes below. The Visibility move to local_provider checks out: with BASIC_MEMORY_ENABLE_PROJECT_DELETE unset bm_project_delete is hidden, with =true it is listed, same as on main, and Client(mcp) lists all 19 shipped skills as skill:// resources.

1. skills.py runs distribution("basic-memory").locate_file(...) at import, and it sits in resources/__init__.py's import chain, so it can take down unrelated resources. resources/__init__.py now imports skills_provider, so from basic_memory.mcp.resources import note_resource executes distributed_skills_root(). I patched importlib.metadata.distribution to raise PackageNotFoundError for basic-memory (what a PYTHONPATH=src run, a vendored copy or a frozen build sees) and imported note_resource. On this branch that raises PackageNotFoundError: No package metadata was found for basic-memory; on main the same import succeeds. The comment above the RuntimeError says a missing skills dir should make "the server fail at import", but PackageNotFoundError is not caught, so the failure is wider than that and has a different message. It also takes out manual_index and project_info, which have nothing to do with skills. Possible fixes: catch PackageNotFoundError too and fall back to Path(__file__).parents[2] / "data" / "skills", or build the provider lazily in lifespan and skip registration with a warning when no skills dir exists.

2. In an editable install the served skills are a snapshot taken at install time, and the new tests cannot see drift. After pip install -e ., site-packages/basic_memory/data/skills/memory-notes/SKILL.md is a regular file, not a link. I appended a line to skills/memory-notes/SKILL.md in the checkout and read the installed copy back: the line was not there. So for a contributor editing skills/ and checking the result through skills/get or skill://, the server keeps serving the old text until the next reinstall. The docstring's "resolves to a real directory in every supported install" holds, but it doesn't say that directory can be stale.

test_skills_list_offers_every_shipped_skill and test_skill_get_returns_files_matching_the_shipped_bytes compare the served result with distributed_skills_root(), which is the directory being served. They would pass whatever it contains. A cheap guard: when the repo's skills/ is present, assert that each file under distributed_skills_root() has the same bytes as its skills/<name>/... source, so a stale editable install fails loudly. Alternatively say in skills/DEVELOPMENT.md that skill edits need just install (or whatever your reinstall target is) before they show up over MCP.

Sanity-checked, not a problem: the partial force-includes (memory-quest ships only SKILL.md, memory-onboarding skips evals/evals.json) leave out files that no shipped SKILL.md links to, so no served skill points at a missing file.

phernandez added a commit that referenced this pull request Nov 3, 2026
…an test

- Each xdist worker now drops and recreates its database once per
  session. Reusing a local server across runs left a schema from an
  interrupted run in a worker database (CREATE SCHEMA failed in
  test_postgres_physical_probe_uses_active_search_path). CI's service is
  fresh per job, but a session should never depend on that.
- test_conflict_check_is_one_indexed_query runs ANALYZE entity before
  EXPLAIN. With no statistics on the just-filled table, the planner chose
  among the project indexes arbitrarily, so the per-key index assertions
  flaked (also seen on #2022). 5/5 passes on Postgres after the change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMYVL4TcbWdXhdvAzxfqjo
Signed-off-by: phernandez <paul@basicmachines.co>
phernandez added a commit that referenced this pull request Nov 4, 2026
…an test

- Each xdist worker now drops and recreates its database once per
  session. Reusing a local server across runs left a schema from an
  interrupted run in a worker database (CREATE SCHEMA failed in
  test_postgres_physical_probe_uses_active_search_path). CI's service is
  fresh per job, but a session should never depend on that.
- test_conflict_check_is_one_indexed_query runs ANALYZE entity before
  EXPLAIN. With no statistics on the just-filled table, the planner chose
  among the project indexes arbitrarily, so the per-key index assertions
  flaked (also seen on #2022). 5/5 passes on Postgres after the change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMYVL4TcbWdXhdvAzxfqjo
Signed-off-by: phernandez <paul@basicmachines.co>
Serve the skills the wheel ships under basic_memory/data/skills as
skill://<name>/SKILL.md resources, and register FastMCP's SkillsExtension
so clients that negotiate io.modelcontextprotocol/skills can call
skills/list and skills/get. Clients without the extension can still read
the skills as ordinary resources.

The extension refuses servers with server-level transforms, so the
bm_project_delete Visibility mark moves to the local provider, which owns
the tools. Behavior is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMYVL4TcbWdXhdvAzxfqjo
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez
phernandez force-pushed the feat/skills-over-mcp branch from 819a804 to c1a9436 Compare November 4, 2026 15:10
Importing any resource ran the distribution lookup for the skills, so a
run without package metadata lost every resource. The server entrypoint
now imports the skills module itself, and a missing distribution fails
with the same message as a missing skills directory.

An editable install serves a copy of skills/ taken at install time. A
test compares the served bytes with skills/, and DEVELOPMENT.md names
the reinstall command.

Reported in review by tonydzi.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CMYVL4TcbWdXhdvAzxfqjo
Signed-off-by: phernandez <paul@basicmachines.co>
@phernandez

Copy link
Copy Markdown
Member Author

Thanks @tonydzi, both points were right. Fixed in cf7ed10.

1. Import chain. resources/__init__.py no longer imports the skills module. The server entrypoint (bm mcp) and the integration fixture import basic_memory.mcp.resources.skills directly. With your probe (distribution raising PackageNotFoundError for basic-memory), note_resource, manual_index and project_info now import fine. Importing the skills module raises RuntimeError: Basic Memory skills are missing: basic-memory is not installed, the same failure the comment describes.

I didn't add the Path(__file__) fallback. The skills exist only as force-included install files, so src/basic_memory/data/skills isn't there in a checkout, and the fallback would find nothing.

2. Stale editable installs. test_served_skills_match_the_repo_sources compares every served file with its skills/<name>/... source when the repo's skills/ is present. It fails after editing skills/memory-notes/SKILL.md without reinstalling, and passes after uv sync --reinstall-package basic-memory. skills/DEVELOPMENT.md now names that command.

@phernandez
phernandez merged commit c143040 into main Nov 4, 2026
58 checks passed
@phernandez
phernandez deleted the feat/skills-over-mcp branch November 4, 2026 16:24
phernandez added a commit that referenced this pull request Nov 4, 2026
Main's `test.yml` fails on the Windows SQLite unit shards (runs
41866893188, 41853960947, 41795319355), so Coverage is skipped.

- `test_cli_exit.py`, `test_fresh_install.py`, `test_chatgpt_graphs.py`:
`text=True` decodes the child's UTF-8 output with cp1252 on Windows. The
reader thread raises `UnicodeDecodeError` and `stdout` is `None`. Pass
`encoding="utf-8"`.
- `test_skill_resources.py` (from #2022): the expected text came from
`read_text()`, which turns a CRLF checkout into LF. The served text
keeps CRLF. Compare to the decoded bytes.
- `test_chatgpt_import_visits_each_mapping_key_once`: the 15s hang guard
timed out during startup on a loaded Windows runner. Raised to 60s.

Test-only change.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01CMYVL4TcbWdXhdvAzxfqjo

Signed-off-by: phernandez <paul@basicmachines.co>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants