From eb896e4469ab4ff6e016d312264306e5aaa952ac Mon Sep 17 00:00:00 2001 From: xarmian Date: Fri, 1 May 2026 18:11:38 -0400 Subject: [PATCH] feat(mcp): server-level instructions advertised in initialize handshake (TASK-971) (#355) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a top-level `instructions` string to the MCP initialize response so agents know WHEN to reach for pad without having to guess from tool descriptions alone. The Svelte MCP server in the dogfooding session that triggered PLAN-969 does this; pad now does too. Implementation: - internal/mcp/instructions.md (new) — embedded source content. MCP-aware adaptation of skills/pad/SKILL.md's opener: what pad is, when to reach for it, the v0.2 tool catalog summary, resource cheatsheet, workspace resolution order, ref convention, update flow, conventions hint, and the four prompts. - internal/mcp/instructions.go (new) — //go:embed wrapper exposing the content as the Instructions package var. - internal/mcp/server.go — pass server.WithInstructions(Instructions) into NewMCPServer. Single source of truth: the same string ships in both the local stdio handshake AND PLAN-943's HTTPHandlerDispatcher (when remote /mcp mounts in TASK-950 it will reuse the same constant — no docs drift between local and remote surfaces). Test: TestServer_InitializeAdvertisesInstructions drives a real initialize round-trip and asserts the response's Instructions field equals the embedded source. Sanity-checks the embed didn't truncate. Parent: TASK-971 → PLAN-969. --- internal/mcp/instructions.go | 24 +++++++++++++ internal/mcp/instructions.md | 66 ++++++++++++++++++++++++++++++++++++ internal/mcp/server.go | 7 ++++ internal/mcp/server_test.go | 39 +++++++++++++++++++++ 4 files changed, 136 insertions(+) create mode 100644 internal/mcp/instructions.go create mode 100644 internal/mcp/instructions.md diff --git a/internal/mcp/instructions.go b/internal/mcp/instructions.go new file mode 100644 index 00000000..f353ce0e --- /dev/null +++ b/internal/mcp/instructions.go @@ -0,0 +1,24 @@ +package mcp + +import _ "embed" + +// Instructions is the server-level instructions string advertised to +// MCP clients in the initialize response. Tells agents WHEN to reach +// for pad and gives a quick orientation to the tool surface and +// resources — same role the description text plays in tools/list, +// but at the server level so a host can show it before the first +// tool call. +// +// The Svelte MCP server in the dogfooding session that triggered +// PLAN-969 explicitly told the model "use this whenever Svelte +// development is involved." Pad does the same: a short, MCP-aware +// adaptation of skills/pad/SKILL.md's opener, embedded at build +// time so there's a single source of truth. +// +// Both ExecDispatcher (stdio) and HTTPHandlerDispatcher (HTTP) read +// this same string — the local handshake passes it via +// server.WithInstructions in NewServer (server.go); the future +// remote handshake in PLAN-943 will do the same. +// +//go:embed instructions.md +var Instructions string diff --git a/internal/mcp/instructions.md b/internal/mcp/instructions.md new file mode 100644 index 00000000..13c83f4c --- /dev/null +++ b/internal/mcp/instructions.md @@ -0,0 +1,66 @@ +Pad is a project tracker for developers and AI agents — issues (TASK, BUG), plans (PLAN), ideas (IDEA), docs (DOC), conventions, comments, and dependencies. Use this server when a user mentions: + +- Issue refs like `TASK-5`, `BUG-12`, `PLAN-3`, `IDEA-8` — they are stable, human-readable IDs and the canonical way to address items. +- Tasks / issues / items / plans / progress / "what's on my plate" / "what to work on next" / standup / changelog / retrospective. +- Project conventions, decision records, or "how should this team do X." + +If the user is asking general code questions with no project-management thread, you don't need this server. + +## Tool surface (v0.2) + +Eight tools, each with an `action` enum: + +- `pad_item` — Items: create / update / delete / get / list / move / link / unlink / deps / star / unstar / starred / comment / list-comments / bulk-update / note / decide. +- `pad_workspace` — Workspaces: list / members / invite / storage / audit-log. +- `pad_collection` — Collections: list / create. +- `pad_project` — Project intelligence: dashboard / next / standup / changelog. +- `pad_role` — Agent roles: list / create / delete. +- `pad_search` — Full-text search across items: query. +- `pad_meta` — Server introspection: server-info / version / tool-surface. +- `pad_set_workspace` — Pin a session-default workspace for subsequent calls. + +Always pass `action` as a top-level field. Per-action required parameters are documented in each tool's description. + +## Resources are cheaper than tool calls + +Read these directly when you need workspace state: + +- `pad://workspace/{ws}/dashboard` — computed project overview (active items, plans, attention, suggested next). +- `pad://workspace/{ws}/collections` — collection types + schemas. +- `pad://workspace/{ws}/items` — list of all items (use `pad_item.action: list` for filtering). +- `pad://workspace/{ws}/items/{ref}` — single item rendered as markdown. +- `pad://_meta/version` — server version + stability tiers. + +Resources support host-side prefetch — if the host can fetch them once at session start, you don't pay per turn. + +## Workspace context + +Every action that operates within a workspace accepts an optional `workspace` parameter. Resolution order: + +1. Explicit `workspace` argument on the call (highest priority). +2. Session default set via `pad_set_workspace`. +3. CWD-linked workspace from `.pad.toml` (when running locally). + +If none resolves, the action returns a structured `no_workspace` error with `available_workspaces`. Pass `workspace` explicitly when working across multiple workspaces in one session. + +## Always use issue refs + +Items have refs like `TASK-5`, `IDEA-12`, `PLAN-3`. Use those — never slugs. Refs are short, stable, human-readable, and what appears in audit trails and PR titles. + +## Update flow: read first, then patch + +For `pad_item.action: update`, the server merges your patch with the item's current state. Pass only the fields you want to change. When changing `status`, ALWAYS include a `comment` explaining why — it builds the audit trail that helps the team understand history. + +## Project conventions + +Workspaces can declare conventions (e.g. "run `make test` before PR", "use conventional commit format"). Before performing meaningful work, you may want to read active conventions: + +``` +pad_item.action: list, collection: "conventions", status: "active" +``` + +Filter by trigger (`always`, `on-implement`, `on-task-complete`, etc.) when relevant. + +## Multi-step workflows + +Four prompts ship with the server: `pad_plan`, `pad_ideate`, `pad_retro`, `pad_onboard`. Use them when the user wants help planning, brainstorming, retrospecting, or onboarding into a workspace — they encode the multi-step Pad-aware playbook for each. diff --git a/internal/mcp/server.go b/internal/mcp/server.go index 7ac87006..9d394507 100644 --- a/internal/mcp/server.go +++ b/internal/mcp/server.go @@ -63,6 +63,13 @@ func NewServer(opts Options) *Server { // capabilities.experimental.padCmdhelp, so external agents can // detect compatibility without reading pad://_meta/version first. server.WithExperimental(experimentalCapabilities()), + // Server-level instructions (TASK-971). Tells agents WHEN to + // reach for pad and orients them to the tool surface + + // resources before they make their first call. Embedded at + // build time from instructions.md so the source of truth is + // versioned with the binary; HTTPHandlerDispatcher (PLAN-943) + // advertises the same string. + server.WithInstructions(Instructions), ) return &Server{mcp: mcp, debug: opts.Debug} } diff --git a/internal/mcp/server_test.go b/internal/mcp/server_test.go index 7dd203a3..0528927a 100644 --- a/internal/mcp/server_test.go +++ b/internal/mcp/server_test.go @@ -97,6 +97,45 @@ func assertExperimentalNamespace(t *testing.T, exp map[string]any, key, wantVers } } +// TestServer_InitializeAdvertisesInstructions locks the wire-level +// invariant that the initialize response carries the server-level +// instructions string TASK-971 introduced. Empty / missing +// instructions would put pad back in the "model has to guess when to +// reach for it" state that triggered PLAN-969. +// +// Asserts the response embeds the constant defined in instructions.go +// rather than a hardcoded literal — keeps the test stable as the +// instructions text evolves. +func TestServer_InitializeAdvertisesInstructions(t *testing.T) { + srv := NewServer(Options{Version: "instr-test"}) + res, cleanup := runHandshake(t, srv) + defer cleanup() + + if res.Instructions == "" { + t.Fatalf("initialize response has empty instructions; want non-empty") + } + if res.Instructions != Instructions { + t.Errorf("initialize response instructions diverge from embedded source") + } + // Sanity check on the embedded content — confirms instructions.md + // shipped with the build (otherwise the file might be empty after + // a botched embed). + if !strings.Contains(Instructions, "Pad is a project tracker") { + t.Errorf("embedded instructions look truncated or wrong; first 80 chars: %q", + truncate(Instructions, 80)) + } +} + +// truncate is a small helper for failure messages — keeps long +// instructions text from spilling into terminal output when an +// assertion fires. +func truncate(s string, n int) string { + if len(s) <= n { + return s + } + return s[:n] + "…" +} + // TestServer_FallbackVersion locks the wire-level invariant that // serverInfo.version is NEVER empty, even when the caller built the // server with Options{}. Empty values would confuse some clients that