Skip to content

feat(mcp): subscribe, read_acl/write_acl, call_remote_pod (closes #494 #495 #496)#497

Merged
melvincarvalho merged 1 commit into
gh-pagesfrom
mcp-followups
May 18, 2026
Merged

feat(mcp): subscribe, read_acl/write_acl, call_remote_pod (closes #494 #495 #496)#497
melvincarvalho merged 1 commit into
gh-pagesfrom
mcp-followups

Conversation

@melvincarvalho

Copy link
Copy Markdown
Contributor

Three follow-ups to the v0.0.200 MCP capstone (#490), bundled into one PR as discussed.

Closes #494 (subscribe), #495 (call_remote_pod), #496 (read_acl/write_acl).

What's in

subscribe (#494) — streaming change notifications

Streaming tool. Response switches to SSE; emits MCP notifications as resources change. WAC-filtered per event. Wraps the existing `resourceEvents` from `src/notifications/events.js`. Path scope: trailing slash = subtree, exact = single resource. Cleanup on client disconnect.

read_acl / write_acl (#496) — structured ACL editing

Structured form abstracts JSON-LD shape:

```json
{
"path": "/private/notes/",
"authorizations": [
{ "agents": ["did:nostr:abc..."], "modes": ["Read","Append"], "isDefault": true },
{ "agentClasses": ["acl:AuthenticatedAgent"], "modes": ["Read"] }
]
}
```

Round-trips via `src/wac/parser.js`. Requires `acl:Control` on the resource.

call_remote_pod (#495) — federation primitive

Design calls locked in as discussed:

  1. Gate path is derived from agent WebID — `/private/federation/`. Foreign or anonymous identities can't federate (no local path to gate against). Multi-pod chains compose by hopping, each gated locally.
  2. No pod-resident credentials — every call carries its own `auth` (bearer / custom header / anonymous). Credential vaulting is a separate security surface for a future PR.
  3. Depth cap — `MCP-Federation-Depth` header, hardcoded max 3.

Tool count

12 → 17. Still graspable in one read.

Tests

26 passing (was 18). New coverage:

  • read_acl returns owner authorizations on a fresh pod ACL
  • write_acl + read_acl round-trip with 3 agent classes
  • write_acl denied without Control
  • call_remote_pod denied without federation gate (anonymous)
  • call_remote_pod hits its own /mcp through the gate
  • call_remote_pod depth cap (header at 3 → next hop fails)
  • subscribe streams 'subscribed' init + resource_changed events
  • subscribe filters by scope (out-of-scope changes silent)

```
ℹ pass 26
ℹ fail 0
```

Test plan

  • All 26 MCP tests pass
  • Live-fire: subscribe against a pod, watch /forum/ updates land in real-time
  • Live-fire: write_acl to delegate, verify the granted agent can now access
  • Live-fire: call_remote_pod between two pods (would need two running pods)

…#495 #496)

Three follow-ups to the v0.0.200 MCP capstone (#490), bundled.

## subscribe (#494)

Streaming tool. Response switches to SSE (text/event-stream) and emits
MCP notifications as resources change. WAC-filtered per event so the
subscriber only sees resources they have Read access to. Wraps the
existing resourceEvents EventEmitter from src/notifications/events.js.

Path scope: trailing slash = subtree, exact = single resource, default
= whole pod (filtered by access). Cleanup on client disconnect.

## read_acl / write_acl (#496)

Structured ACL editing as first-class MCP tools. Eliminates hand-rolled
JSON-LD for the most common owner operation (delegating an agent
access). read_acl returns parsed authorizations (agents, agentClasses,
modes, isDefault). write_acl persists the same shape via
serializeAcl() in src/wac/parser.js.

Both require acl:Control on the resource.

## call_remote_pod (#495)

Federation primitive — invoke MCP tools on another pod by URL. Locked
in for v1 with conservative defaults:

  - Federation gate path is derived from the agent's WebID:
    <agent-pod>/private/federation/. Caller must have acl:Write there.
    Foreign or anonymous identities can't federate.
  - No pod-resident credential storage — every call carries its own
    auth (bearer / custom header / anonymous).
  - Depth cap via MCP-Federation-Depth header, hardcoded max 3.

Calls remote /mcp endpoint, forwards tools/call, returns the response.
30s timeout. Per-call audit via the existing log infrastructure.

## Tool count

12 → 17. Surface still graspable in one read. CRUD / Skills / Docs /
Introspect / ACL / Subscribe / Federation.

## Tests

26 passing (was 18). New coverage:

  - read_acl returns owner authorizations on a fresh pod ACL
  - write_acl + read_acl round-trip with 3 agent classes
  - write_acl denied without Control
  - call_remote_pod denied without federation gate (anonymous)
  - call_remote_pod hits its own /mcp through the gate
  - call_remote_pod depth cap enforcement (header at 3 → next hop fails)
  - subscribe streams a 'subscribed' init event + resource_changed events
  - subscribe filters by scope (out-of-scope changes are silent)

## Docs

docs/mcp.md expanded with sections for all three: structured ACL
ergonomics, subscribe SSE wire format with curl example, federation
worked example. 'What's not included' section pruned (these were the
items it deferred to).
@melvincarvalho
melvincarvalho merged commit beb0cfb into gh-pages May 18, 2026
1 check passed
@melvincarvalho
melvincarvalho deleted the mcp-followups branch May 18, 2026 08:06
melvincarvalho added a commit that referenced this pull request May 18, 2026
Live-fire smoke of #497 surfaced two real gotchas with the new
write_acl tool:

1. Relative WebID paths in 'agents' resolve against the .acl URL, not
   the pod root. ../profile/card.jsonld#me writes to a different
   absolute URI depending on where the .acl is — easy to lock yourself
   out.

2. There was no server-side check that the resulting ACL would keep
   the caller in control.

This commit:

- Adds a safety check in write_acl that parses the proposed ACL with
  its real URL (so relative agents resolve correctly), then checks
  whether any authorization grants Control to the caller. If not,
  refuses with an explanatory error pointing at the most common cause
  (relative WebIDs) and the workaround for legitimate ownership
  transfer.

- Adds a 'Footguns' section to docs/mcp.md documenting the relative-
  vs-absolute WebID rule with concrete right/wrong examples, the new
  safety check, and the SSE keep-alive caveat.

Tests: 27 passing (was 26). New test: write_acl refuses an ACL that
grants Control only to a foreign WebID.
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.

feat(mcp): subscribe tool — wrap WebSocket notifications as MCP SSE events

1 participant