Skip to content

Commit d3a829c

Browse files
jaredLundeclaude
andcommitted
agent: MCP Skills extension (SEP-2640) client support
Client side of `io.modelcontextprotocol/skills`: a declaring server's `skills/list` entries become `<server>:<name>` skills in a separate `<available_skills origin="mcp">` block, loaded (size, digest and frontmatter verified, never prefetched) through `mcp__<server>__skill__read`, invocable as `/skill:<server>:<name>`, listed by `get_commands`, re-listed on `ttlMs` expiry or list-changed, and cached in the manifest. Activation of a model-chosen skill and code execution while one is active need the user's approval (`run` denies unless `--approve-mcp-skills`); state is per session and restored only from host-authored transcript records. Rebased onto MCP Events (#133): `tools/mcp_stdio.rs` is the one stdio transport, and its `rescue`/`unwrap_rescued` is the one workaround for rmcp's untagged-result bug (rust-sdk#1197), for events and skills results alike. `tools/mcp_wire.rs` keeps only the streamable-HTTP client, which applies the same `rescue` to `skills/*` JSON bodies and SSE events. Rebased onto MCP Tasks (#130) too: every skills request (skills/list, skills/get, resources/read) goes through `mcp::request_tracked` (`track_call` under the calling session's host), so a nested request is attributed or refused, never sent to a shared connection's own host. A steered `/skill:` expands as its session (its command loop can answer); a re-list or expansion between runs runs under a client-less host, so a nested request is declined at once instead of stalling the prompt. Round-3 fixes: a steer waiting behind a `/skill:` when its run is cancelled is acked as not queued (the cancellation already dropped the run's lanes), and an expansion that finished but was never queued activates nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JimHGjsfk2Ktm5GxyZJKKk
1 parent 43bebb6 commit d3a829c

23 files changed

Lines changed: 7250 additions & 193 deletions

‎Cargo.lock‎

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎crates/agent/ARCHITECTURE.md‎

Lines changed: 182 additions & 48 deletions
Large diffs are not rendered by default.

‎crates/agent/Cargo.toml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,10 @@ http = "1"
192192
# for why that has to be explicit rather than inferred from crate features).
193193
rmcp = { version = "3.2.0", default-features = false, features = ["client", "macros", "transport-child-process", "transport-streamable-http-client-reqwest", "reqwest-tls-no-provider", "auth", "elicitation"] }
194194

195+
# Names the SSE event type in `tools::mcp_wire`'s `StreamableHttpClient` impl (rmcp re-exports its error
196+
# but not the event). Already resolved transitively at this exact version via rmcp.
197+
sse-stream = "0.2.5"
198+
195199
# `serve --listen`'s WebSocket transport (`serve_ws`): the server/`accept_async` side. Only
196200
# `handshake` is needed here (a plain-`ws://` loopback listener — the edge terminates TLS in front of
197201
# us, so no TLS feature). Same crate + exact version `agent-core` already depends on for its Codex

‎crates/agent/src/approval.rs‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,10 @@ pub struct ApprovalRequest {
9696
/// exactly what they are agreeing to for the rest of the session.
9797
pub scope_key: String,
9898
pub origin: ApprovalOrigin,
99+
/// Why this is being asked, when it is not just "this tool is gated": an MCP-served skill asking
100+
/// to be activated, or a code-execution call while one is active (see
101+
/// `tools::mcp_skills::SkillSession`). Sent to clients as the frame's `mcp_skill` object.
102+
pub context: Option<Value>,
99103
}
100104

101105
/// Whether a decision applies to this call only, or is remembered for the session.
@@ -377,6 +381,7 @@ pub async fn gated_before_tool_call(
377381
summary: summarize(input),
378382
scope_key: key,
379383
origin: origin.clone(),
384+
context: None,
380385
};
381386
match runtime.gate.request(request, cancel).await {
382387
Ok(decision) => {

0 commit comments

Comments
 (0)