feat(skills): MCP-served Agent Skills per the skills-over-MCP SEP (WG PoC)#1
Draft
feat(skills): MCP-served Agent Skills per the skills-over-MCP SEP (WG PoC)#1
Conversation
olaservo
added a commit
that referenced
this pull request
Apr 22, 2026
… reuses running server New `examples/skills_e2e_scenario.rs` drives Scenario #1 end-to-end against the shared YAML contract at `experiments/scenarios/pr-review-input-validation.yaml`. Takes `--scenario <path>` (positional also accepted); connects to the MCP server already running at the YAML's `mcp_server.endpoint` rather than spawning one. Evaluates the same five criteria fast-agent's runner does (skill-read-before-write, create-pending, add-comment(s), submit-pending-with-verdict, no-single-shot-bypass) with Goose's criterion 1 keyed on `load_skill(name="pull-requests")`. Writes `<scenario-dir>/../results/<iso>-<id>-goose-<model>.json`. `tests/mcp_skills_e2e.rs` trimmed back to just `mcp_skills_discovery_and_load_against_real_server` — the mechanical, sub-second discovery + load round-trip. LLM-in-the-loop activation now lives in the example binary so the scenario path can be a real CLI arg. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1fb583c to
14acf6d
Compare
… SEP Implement the reference behavior defined in the skills-over-MCP SEP: a Goose session can now discover and load Agent Skills hosted as MCP resources under a server's `skill://index.json`, in addition to the existing filesystem-backed skills. Core pieces: - `platform_extensions::mcp_skills` parses a server's skill index, caches `McpSkillEntry` records per-extension at connect time, and resolves supporting files by composing relative refs against each entry's `base_uri` with `..` / leading `/` rejection. - `ExtensionManager` threads `session_id: Option<&str>` through `add_client` and `get_extensions_info` so MCP-skill discovery is scoped to the current session and skipped when no session exists. Cache population is factored into `populate_mcp_skills_cache` with the single-session-lock rationale living in one place. - `platform_extensions::skills::load_skill` resolves literal names first, then composes supporting files; multi-text `read_resource` responses log a warn! with server/uri/text_count rather than silently dropping entries. - `platform_extensions::looks_like_uri` centralizes RFC-3986 scheme detection; `developer::edit::reject_uri_path` reuses it so the filesystem edit/write/read tools refuse `skill://...` (and other URI schemes) instead of misinterpreting them as paths. - Client advertises `io.modelcontextprotocol/skills` capability so servers can gate skill-related resources on the handshake. - Desktop UI surfaces MCP-sourced skills with an "MCP - <origin>" badge via a new `origin` field on `SlashCommand`. Known limitation: the per-extension MCP-skills cache is populated at connect time and not invalidated on `notifications/resources/list_changed` — see TODO at `extension_manager.rs`. Left as follow-up; not expanding scope here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add `crates/goose/tests/mcp_skills_e2e.rs` exercising MCP-served skill discovery and loading against a real `github-mcp-server` instance. Gates on `GITHUB_TOKEN` and the server binary being on PATH, picks an ephemeral port, and cleans up the subprocess via Drop on a `ServerHandle` guard. The mechanical round-trip test (`mcp_skills_discovery_and_load_against_real_server`) is sub-second and runs without a model in the loop — suitable for upstream CI once a `GITHUB_TOKEN` secret is wired up. LLM-in-the-loop scenario runs live out-of-tree at `skills-over-mcp-ig/clients/goose-harness/`, mirroring the `clients/gemini-cli-harness/` pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
14acf6d to
13e5dbb
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft — Not for merge into this fork's
main. Exists as a reviewable diff for the Skills-over-MCP SEP WG.Host-side implementation of the pre-submission Skills-over-MCP SEP (
io.modelcontextprotocol/skills). Skills served by connected MCP servers underskill://— or any URI scheme the server chooses, per the SEP's SHOULD-not-MUST stance — are discovered at extension-connect time, surfaced into the per-turn system prompt alongside filesystem skills, and loaded on demand through Goose's existingload_skilltool.Reference server: olaservo/github-mcp-server#add-agent-skills.
What the model sees
Goose already emits a "You have these skills..." bullet list from
SkillsClient's staticInitializeResult.instructions. This PR appends a second, parallel section per-turn via a newMcpClientTrait::get_dynamic_instructionshook:On FS-vs-MCP collision, FS wins and the MCP entry re-renders as
<server>__<name>. MCP-vs-MCP collisions also render prefixed.No new model-facing tool. The existing
load_skill(name)handler handles both FS and MCP skills:load_skill("pull-requests")— cache lookup, dispatches toExtensionManager::read_resource.load_skill("github__pull-requests")— disambiguation on collision; the model never supplies a server name.load_skill("<skill>/references/GUIDE.md")— supporting-file load, scheme-agnostic composition against the cachedbase_uri.skill://foo/SKILL.md) is rejected with a redirect to the pre-existingread_resourcetool.Discovery is not gated on the server advertising the capability —
resources/read skill://index.jsonis attempted unconditionally; a not-found / transport error yields an empty cache entry and a debug log. The client also advertisesio.modelcontextprotocol/skills: {}in its ownClientCapabilities.extensions.The
developerextension's file tools now reject<scheme>://input with a redirect toread_resource— prevents a generic file reader fromPath()-resolvingskill://.../SKILL.mdinto a bogus filesystem path. The guard is scheme-agnostic and shares the RFC-3986 detector withload_skill's URI rejection.Skill content from MCP servers is treated as untrusted model input per SEP §Security. Frontmatter parsing extracts only
nameanddescription— no hook / pre-post / shell-execution fields are honored.Code tour
The discovery module is
crates/goose/src/agents/platform_extensions/mcp_skills.rs— parses each server'sskill://index.jsoninto cachedMcpSkillEntry { server, name, description, base_uri, uri }, 5s fetch timeout, never propagates errors. The cache lives onExtension.mcp_skillsinextension_manager.rsso it's scoped to the owning server's lifecycle;aggregated_mcp_skills()is the per-turn accessor.SkillsClientgrows aWeak<ExtensionManager>and a newMcpClientTrait::get_dynamic_instructions(session_id)hook (default-None) that renders the MCP section per reply with a 10s-TTL FS-names cache for collision detection.developer's file tools andload_skill's URI-rejection share an RFC-3986 detector inplatform_extensions::mod.rsso the two guards can't drift apart. The client advertisesio.modelcontextprotocol/skills: {}inClientCapabilities.extensionsviaGooseClient::get_info. Server-side:SlashCommand.origin+SlashCommandsQuery.session_idon/config/slash_commandssurface MCP skills to the desktop;SkillsView.tsxadds theMCP · <server>pill. The alternativeui/goose2/surface is not updated.The
extensionmanager::read_resourcetool is deliberately unchanged — the SEP's illustrativeread_resource(server, uri)signature already matched.Bugs surfaced by e2e validation
Empty-session-id permanent client lock. The first draft of
ExtensionManager::add_extension/add_clientcalledfetch_server_skills(..., session_id: "", ...)unconditionally.McpClientasserts a single session id across its lifetime — so the empty-string "first session" permanently locked the client, and every subsequent real tool call panicked. Fix: thread the existingsession_id: Option<&str>into the fetch call; platform extensions that don't hit a real transport passNone(empty cache, repopulated on the next real-session call).Platform extensions aren't auto-enabled in ad-hoc Agent builds. The desktop wires up the skills platform extension via config migrations. Ad-hoc
Agent::with_config(...)builds — including e2e / scenario runners — do not auto-enable it. Without an explicitadd_extension(ExtensionConfig::Platform { name: "skills", ... }), the cache populates butload_skillis never exposed; activation silently fails with the model calling whatever tool is available instead. Fix (in the harness, not production): add the platform extension explicitly after agent construction.Running it locally
LLM-in-the-loop e2e lives out-of-tree at
skills-over-mcp-ig/clients/goose-harness/— a sibling cargo crate that path-deps../goose/crates/goose. Keeps this PR's diff narrow. Harness stands up a realAgentwith an Anthropic provider and drivespr-review-input-validation.yaml.Conformance vs SEP host requirements
InitializeResult.capabilities.extensionsand recognizeio.modelcontextprotocol/skillsserver_declares_skills_capabilityinmcp_skills.rsskill://resources fromskill://index.jsonfetch_server_skillsskill://URI schemes on indexed entriesbase_uriderived by stripping/SKILL.md; scheme never inspecteddebug!/warn!; registration still succeedsresources/readboth index and SKILL.mdSkillsClient::get_dynamic_instructionsload_skill, unified FS + MCPload_skill("<skill>/<rel>")composes againstbase_uriname+descriptionMCP · <server>pill in SkillsViewdeveloperfile tools reject<scheme>://GooseClient::get_infoinsertsio.modelcontextprotocol/skills: {}Out of scope (follow-ups)
type: "mcp-resource-template"entries + MCP completion-API integration for parameterized skills.enable_skillstoggle onExtensionConfig::StreamableHttp/Stdio.notifications/resources/list_changed→ cache invalidation.ui/goose2/.🤖 Generated with Claude Code