mcp-go's NewTool injects default annotations on every tool —
ReadOnlyHint:false, DestructiveHint:true, OpenWorldHint:true — and
buildToolFromDef never overrode them, so every Pad tool advertised
itself as destructive, including pure reads like pad_search and
pad_project. Hosts use destructiveHint to decide whether to prompt;
mislabeling reads trains users to click through prompts.
Derive the block in buildToolFromDef from the catalog's own knowledge
(readOnlyActions — the same single source the tool-surface serializer
uses — plus a new sibling additiveWriteActions allowlist):
- every action read-only → ReadOnlyHint:true, DestructiveHint:false,
IdempotentHint:true (pad_search, pad_project, pad_attachment,
pad_meta, pad_playbook);
- writes all purely ADDITIVE → ReadOnlyHint:false,
DestructiveHint:false (pad_workspace: invite/create/claim/restore;
pad_library: activate — codex round 1: marking additive writes
destructive reintroduces the prompt-training harm at tool level);
- any overwrite/delete-capable action → the conservative
ReadOnlyHint:false, DestructiveHint:true (pad_item, pad_collection,
pad_role) — unchanged on the wire from the old defaults;
- OpenWorldHint:false everywhere (pad tools are closed-world).
pad_set_workspace gets a hand-written block (write, non-destructive,
idempotent, closed-world).
Also adds the missing pad_item.history entry to readOnlyActions —
documented read-only since v0.14 but reported read_only:false on the
tool-surface descriptor.
ToolSurfaceVersion 0.19 → 0.20 (behavior bump, v0.9/v0.16 precedent):
no tool names, action enums, or param shapes changed. instructions.md
and README headings retitled per the drift tests. The changelog entry
describes only this change; BUG-2305 appends to it if it ships in the
same window (one bump total).
Tests: TestCatalogTools_AnnotationsExplicit pins a literal per-tool
read/additive/destructive table (deliberate second enumeration — a new
tool, or a write action added to an all-read or all-additive tool,
fails loudly until someone decides its class);
TestAdditiveWriteActions_NoStaleEntries guards the new allowlist
(real catalog pairs only, never overlapping readOnlyActions);
TestSetWorkspaceTool_AnnotationsExplicit covers both deployment
variants; pad_item.history joins the read spot-checks.
Mutation-verified both directions: destructive-polarity flip fails 10
tools; always-destructive fails the two additive rows.
Claude-Session: https://claude.ai/code/session_018qREYgDd6Ag1X1SDmqhyFM
Add read-only `ready` and `stale` actions to the pad_project MCP tool,
mirroring the existing CLI `pad project ready` / `pad project stale`.
`ready` returns the actionable backlog (query-oriented counterpart to
`next`); `stale` lists items needing attention. Both HTTP dispatchers
already existed; this wires them onto the catalog surface.
`pad project reconcile` stays CLI-only (shells out to `gh` for live PR
state — a local-git dependency MCP agents lack).
Bumps ToolSurfaceVersion 0.12 -> 0.13 across version.go, instructions.md,
README, CLAUDE.md; adds readOnlyActions entries, drift-guard test entries,
and a SKILL.md routing line.
TASK-2019
Claude-Session: https://claude.ai/code/session_019knGmnHcx5rrgWXQ8V8DZS
* feat(project): agent-accessible activity feed (pad project activity + MCP action)
Add a non-streaming, bounded activity query so agents can catch up on
what other agents/users changed since they last worked — the query
counterpart to the live `pad project watch` SSE stream.
- CLI: `pad project activity [--limit N] [--actor user|agent] [--since DATE]`
backed by the existing GET /workspaces/{ws}/activity feed.
- MCP: `pad_project.activity` action (passThrough) + cloud HTTP route.
- Extend the activity endpoint with a server-side `since` date filter
(handler parse + store SQL clause) so limit/actor/since behave
identically across CLI, stdio MCP, and cloud HTTP transports.
- Bump ToolSurfaceVersion 0.11 -> 0.12 (drift guard, README, CLAUDE.md,
instructions.md) and add a SKILL.md querying-guidance line.
Tests: store since-filter test, HTTP dispatch test, catalog action test.
Claude-Session: https://claude.ai/code/session_019knGmnHcx5rrgWXQ8V8DZS
* fix(mcp): mark pad_project.activity read-only in tool surface
Add activity to readOnlyActions so the serialized MCP tool surface emits
read_only:true (missing entries default to write). Spot-check it in
tool_surface_test.go to guard against regression.
Claude-Session: https://claude.ai/code/session_019knGmnHcx5rrgWXQ8V8DZS
Wire the existing attachment HTTP dispatchers onto the MCP catalog as a
new read-only pad_attachment tool with list/show actions, mirroring the
CLI `pad attachment list` / `pad attachment show`. Both dispatch paths
already existed (ExecDispatcher via passThrough, HTTPHandlerDispatcher
via dispatch_http_attachments.go) — this exposes them on the tool
surface.
- New tool rather than pad_item actions: an attachment is its own
workspace-scoped resource, not an item property; a dedicated tool
keeps pad_item's action enum focused.
- Read-only only: upload/download/view stay CLI-only (filesystem-bound),
matching the catalog's exclusion rules.
- Bumps ToolSurfaceVersion 0.10 -> 0.11; updates instructions.md,
README, CLAUDE.md, readOnlyActions, and the drift-guard fixtures.
- The base64 image RESOURCE for multimodal agents is deferred to
TASK-2076 (ResourceFetcher returns strings; no CLI base64-to-stdout
path exists — non-trivial, out of scope here).
TASK-2017
Claude-Session: https://claude.ai/code/session_019knGmnHcx5rrgWXQ8V8DZS
Expose the TASK-1972 CLI commands `pad workspace restore` /
`pad workspace deleted` on the MCP surface as two new pad_workspace
actions:
- `deleted` (read-only) — lists the caller's soft-deleted workspaces
still inside the 30-day restore window.
- `restore` (mutating, non-destructive, owner-only) — un-soft-deletes
a workspace by slug while it's still restorable.
Both passThrough to the CLI subcommands and reuse the existing `slug`
param (no new params). Non-interactive and non-destructive, so
MCP-appropriate.
- Mark `deleted` read-only in tool_surface.go readOnlyActions (restore
stays a write, the safe default).
- Wire both into the HTTP MCP route table (dispatch_http_routes.go) so
cloud/remote MCP dispatches to the existing /workspaces/deleted +
/workspaces/{slug}/restore endpoints instead of "not yet implemented
over HTTP transport" — mirroring TASK-1521's create/claim wiring.
- Bump ToolSurfaceVersion 0.7 -> 0.8 and document the addition in the
CLAUDE.md MCP stability contract.
- Extend catalog bijection/dispatch tests + cmdhelp fixture and add
route-table + HTTP-mapping tests for the new actions.
Claude-Session: https://claude.ai/code/session_01HxBkAMiFBtCRJ2tKSCt3ST
The cloud /mcp transport constructed a single process-global
WorkspaceState shared across every OAuth user and MCP session.
pad_set_workspace mutated it, and env.Dispatch injected the shared
value into any tool call without an explicit `workspace` — so one
session's selection bled into another's (cross-user, and across a
single user's concurrent sessions).
Not a cross-tenant write: BUG-1616's bearer-auth membership gate
already 403s a non-member. The real harm is workspace-slug leakage,
confusing "not a member of <someone else's ws>" errors, and
wrong-destination reads/writes among a user's OWN accessible
workspaces.
Fix: add NewSharedWorkspaceState() whose ResolveDefault() always
returns "". The cloud mount uses it, so the shared value is never
injected as a per-call default — resolution falls back to explicit
workspace= (or the per-user maybeInjectWorkspace default), never
cross-user shared memory. pad_set_workspace on a shared state no
longer persists and returns status=not_persisted. Local
`pad mcp serve` (single-user-per-process) is unchanged.
Also make the agent-facing surfaces honest per-deployment: the
pad_set_workspace tool description, the pad_item workspace param,
pad_meta tool-surface, and the embedded instructions.md no longer
promise session defaulting on multi-user/remote servers, and the
two "workspace is required" hints drop the stale pad_set_workspace
reference.
Regression guards in internal/mcp/bug1865_test.go. Full suite green.
Claude-Session: https://claude.ai/code/session_01HxBkAMiFBtCRJ2tKSCt3ST
Add GET /api/v1/mcp/tool-surface, a session/token-authenticated
same-origin endpoint that serves the MCP catalog descriptor JSON
(the nine env.Catalog tools, their actions, and input schemas) with a
new per-action read_only bool. Backs the Phase 3 browser-side WebMCP
layer (PLAN-1888): the client fetches once and derives readOnlyHint
from the read_only flags without re-deriving the read set in TS.
Wired via the SetMCPTransport injection pattern to avoid the import
cycle: internal/mcp already imports internal/server (dispatch_http.go),
so internal/server cannot import internal/mcp. internal/mcp exports a
cycle-free ToolSurfaceJSON() that builds from the package-global
Catalog plus a co-located readOnlyActions allowlist; cmd/pad/main.go
(which imports both) injects it via Server.SetToolSurfaceHandler before
setupRouter. The route mounts in the authed API group so it inherits
TokenAuth/SessionAuth/CSRFProtect/RequireAuth — NOT the bearer-gated
/mcp infra path — and is available on both cloud and self-host.
The existing actionMetaToolSurface (pad_meta action=tool-surface) now
shares the same serializer, so MCP and REST can't drift; it gains the
additive read_only flag too. No ToolSurfaceVersion bump (DR-7): adding
read_only is additive metadata; names/actions/params are unchanged.
Refs TASK-1891 / PLAN-1888
Claude-Session: https://claude.ai/code/session_01KmxkPxLksjf1pmrZDpsnTJ