[object Object]

← back to Cli Printing Press

feat(cli): apply PostHog agent-first learnings to MCP server generation (#160)

8354f5283a25461499eee5a8e5a4c605363a39aa · 2026-04-10 10:20:35 -0700 · Matt Van Horn

* feat(cli): replace thin about tool with rich context tool for MCP agents

Add DomainContext struct that captures resource taxonomy, domain archetype,
pagination patterns, query tips, and playbook entries from novel features.
Front-loads this context in the MCP server so agents understand the API
domain without wasting tokens on discovery.

* feat(cli): enrich MCP tool descriptions with response shapes and method hints

Add mcpDescriptionRich that appends response type/shape hints and method
context (destructive, partial update) to endpoint tool descriptions.
Also enriches sync/search/sql tool descriptions with usage guidance
and cross-references. Caps descriptions at 200 chars to prevent token bloat.

* feat(cli): add mcp_quality scorecard dimension

New Tier 1 dimension (0-10) that scores MCP server quality: checks for
context tool, high-level tools (sql/search/sync), response shape hints
in descriptions, no empty descriptions, and cross-reference guidance.
Tier 1 max moves from 120 to 130 with updated normalization.

* feat(cli): add archetype-specific agent playbook entries

Domain-specific insights embedded in the MCP context tool based on API
archetype (PM, communication, payments, CRM, developer-platform).
These are opinionated tips that prevent common agent mistakes — the kind
of knowledge PostHog calls 'skills that only humans can provide.'

* docs(cli): add PostHog agent-first learnings plan

---------

Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>

Files touched

Diff

commit 8354f5283a25461499eee5a8e5a4c605363a39aa
Author: Matt Van Horn <mvanhorn@users.noreply.github.com>
Date:   Fri Apr 10 10:20:35 2026 -0700

    feat(cli): apply PostHog agent-first learnings to MCP server generation (#160)
    
    * feat(cli): replace thin about tool with rich context tool for MCP agents
    
    Add DomainContext struct that captures resource taxonomy, domain archetype,
    pagination patterns, query tips, and playbook entries from novel features.
    Front-loads this context in the MCP server so agents understand the API
    domain without wasting tokens on discovery.
    
    * feat(cli): enrich MCP tool descriptions with response shapes and method hints
    
    Add mcpDescriptionRich that appends response type/shape hints and method
    context (destructive, partial update) to endpoint tool descriptions.
    Also enriches sync/search/sql tool descriptions with usage guidance
    and cross-references. Caps descriptions at 200 chars to prevent token bloat.
    
    * feat(cli): add mcp_quality scorecard dimension
    
    New Tier 1 dimension (0-10) that scores MCP server quality: checks for
    context tool, high-level tools (sql/search/sync), response shape hints
    in descriptions, no empty descriptions, and cross-reference guidance.
    Tier 1 max moves from 120 to 130 with updated normalization.
    
    * feat(cli): add archetype-specific agent playbook entries
    
    Domain-specific insights embedded in the MCP context tool based on API
    archetype (PM, communication, payments, CRM, developer-platform).
    These are opinionated tips that prevent common agent mistakes — the kind
    of knowledge PostHog calls 'skills that only humans can provide.'
    
    * docs(cli): add PostHog agent-first learnings plan
    
    ---------
    
    Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
---
 ...-001-feat-agent-first-posthog-learnings-plan.md | 312 +++++++++++++++++++++
 internal/generator/generator.go                    | 184 ++++++++++++
 internal/generator/templates/mcp_tools.go.tmpl     |  80 ++++--
 internal/pipeline/scorecard.go                     |  69 ++++-
 4 files changed, 619 insertions(+), 26 deletions(-)

diff --git a/docs/plans/2026-04-10-001-feat-agent-first-posthog-learnings-plan.md b/docs/plans/2026-04-10-001-feat-agent-first-posthog-learnings-plan.md
new file mode 100644
index 00000000..70ccb4ab
--- /dev/null
+++ b/docs/plans/2026-04-10-001-feat-agent-first-posthog-learnings-plan.md
@@ -0,0 +1,312 @@
+---
+title: "feat: Apply PostHog Agent-First Learnings to Printing Press"
+type: feat
+status: completed
+date: 2026-04-10
+deepened: 2026-04-10
+---
+
+# Apply PostHog Agent-First Learnings to Printing Press
+
+## Overview
+
+PostHog published their "golden rules of agent-first product engineering" based on two architecture overhauls of their MCP server (now 6K+ DAU). This plan maps their five rules against the Printing Press and identifies concrete, high-leverage improvements to the CLIs the machine prints.
+
+The core insight: PP already does several things PostHog learned the hard way, but has meaningful gaps in three areas - MCP tool richness, semantic abstraction, and agent feedback loops.
+
+## Problem Frame
+
+The Printing Press generates CLI + MCP server pairs from API specs. The CLIs are agent-native (--json, --compact, --stdin, typed exit codes). But the MCP servers are thin endpoint wrappers with no domain context, no composition hints, and no front-loaded knowledge. An agent connecting to a printed MCP server gets a flat list of 50+ tools named after URL paths, with no guidance on which matter or how they compose.
+
+PostHog made the same mistake in their v1 and rewrote twice. Their learnings are directly applicable.
+
+## PostHog Rules vs. Printing Press Today
+
+### Rule 1: Let agents do everything users can
+
+**PostHog's lesson:** Every user action must be agent-accessible. They auto-generate tools from typed endpoints with manual opt-in via YAML configs.
+
+**PP status: Strong.** The machine already generates dual interfaces (CLI for shell agents, MCP for IDE agents) from the same spec. Every endpoint becomes both a Cobra command and an MCP tool. Agent-native flags (--json, --compact, --select, --dry-run, --stdin, --yes) are standard. The scorecard checks for their presence.
+
+**Gap:** Parity between CLI and MCP is surface-level. The CLI has `sync`, `search`, `sql`, workflow commands, and NOI features. The MCP server registers raw endpoint tools but none of the higher-level CLI capabilities. An agent using the MCP can call `issues_list` but cannot call `stale` or `search`. The CLI surface is strictly richer than the MCP surface.
+
+### Rule 2: Meet agents at their level of abstraction
+
+**PostHog's lesson:** They replaced per-endpoint tools (get-insight, get-funnel) with a single `executeSql` tool. Agents already speak SQL fluently, so meeting them there eliminated 3 out of 4 tool calls and unlocked creative queries PostHog hadn't anticipated.
+
+**PP status: Partial.** The CLI has `sql` (raw SQLite queries on synced data), `search` (FTS5 full-text), and domain-specific workflow commands. These ARE the right abstraction for shell agents. But the MCP server doesn't expose them. It exposes the raw API endpoints only, forcing IDE agents to reason at the HTTP-path level.
+
+**Gap:** The MCP server is stuck at PostHog's v1 - one tool per endpoint. The higher-level abstractions (sql, search, stale, health, similar) that make the CLI powerful are invisible to MCP-connected agents. This is the single highest-leverage improvement.
+
+### Rule 3: Front-load universal context
+
+**PostHog's lesson:** Their v1 prompt was "Here are some tools, GLHF." Their v2 front-loads taxonomy, SQL syntax, and critical querying rules at session start. Everything else is deferred.
+
+**PP status: Weak.** The generated MCP server has an `about` tool that returns API name, tool count, and unique capabilities. But there's no rich front-loaded context. No taxonomy ("this API has projects, issues, cycles - here's how they relate"). No query patterns ("always filter by team_id first"). No critical rules ("rate limit is 100/min, batch operations where possible").
+
+**Gap:** Every MCP session starts cold. The agent wastes tokens discovering what the API is about, which tools matter, and how they relate. The research brief from Phase 1 already contains this knowledge but it never flows into the generated MCP server.
+
+### Rule 4: Writing skills is a human skill
+
+**PostHog's lesson:** Skills should contain only what an agent can't discover itself - idiosyncratic knowledge, edge cases, taste. "For retention events, use $pageview by default" - that's a PostHog Certified opinion that prevents agents from producing misleading analysis.
+
+**PP status: Mixed.** The NOI (Non-Obvious Insight) framework does embed taste - "Discord's real value is searchable knowledge base, not just message sending." But this taste lives in the research brief and README, not in the MCP tool descriptions or a generated skills file. An agent connecting via MCP has no access to the NOI insights or domain opinions.
+
+**Gap:** The research brief is rich with domain opinions and edge cases, but they're stranded in markdown files that MCP-connected agents never see. PostHog's approach of embedding opinions directly in tool descriptions and skill files would make printed CLIs meaningfully smarter for agents.
+
+### Rule 5: Treat agents like real users
+
+**PostHog's lesson:** Dogfood headlessly (CLI before UI). Do weekly trace reviews. Build evals from real agent sessions - both good and bad behaviors.
+
+**PP status: Partial.** The machine has dogfood (structural validation), verify (runtime testing), and scorecard (18-dimension quality assessment). The retro system analyzes generation runs for machine improvements. But there's no mechanism to observe how agents USE the generated CLIs after shipping.
+
+**Gap:** PP validates that the CLI builds and responds correctly, but never validates that agents can accomplish real tasks with it. The scorecard checks that --json flag exists but not that an agent can chain three commands to answer "why did signups drop?" No session tracing, no feedback mechanism, no eval loop on agent usage.
+
+## Scope Boundaries
+
+- This plan covers improvements to the Printing Press machine (templates, generator, skills)
+- All changes compound across every future printed CLI
+- Out of scope: retroactively updating already-published CLIs (espn, linear)
+- Out of scope: building a telemetry backend or analytics dashboard
+- Out of scope: changing the CLI side (Cobra commands) - focus is on MCP quality and agent context
+
+## Key Technical Decisions
+
+- **Enrich MCP templates rather than build a separate agent layer**: The MCP server template is the right place to add semantic tools, context, and composition hints. No new binaries needed.
+
+- **Flow research brief into generated code**: The Phase 1 research already captures domain taxonomy, critical rules, and edge cases. The generator should extract structured data from research and embed it in the MCP server's context tools.
+
+- **SQL/search as MCP tools**: PostHog's biggest win was exposing SQL. PP already has SQLite + FTS5. Exposing `sql` and `search` as MCP tools is the single highest-leverage change.
+
+- **Lightweight agent feedback over telemetry**: Rather than building a telemetry system, add a `report` MCP tool that writes structured issue files locally. The retro system can then analyze them.
+
+## Implementation Units
+
+- [ ] **Unit 1: Expose high-level CLI commands as MCP tools**
+
+**Goal:** Bridge the CLI/MCP parity gap. Make sync, search, sql, and workflow commands (stale, health, similar, orphans) available as MCP tools, not just CLI commands.
+
+**Requirements:** Rule 1 (let agents do everything), Rule 2 (right abstraction level)
+
+**Dependencies:** None
+
+**Files:**
+- Modify: `internal/generator/templates/mcp_tools.go.tmpl`
+- Modify: `internal/generator/generator.go`
+- Test: `internal/generator/generator_test.go`
+
+**Approach:**
+- Add a second section to the MCP tools template that registers high-level tools alongside the endpoint tools
+- `query_sql` - accepts SQL string, runs against local SQLite store, returns JSON results
+- `search` - accepts query string + optional resource filter, runs FTS5 search, returns matches
+- `sync` - triggers data sync for specified resources (or all)
+- Workflow tools registered dynamically based on domain archetype detection (same logic that generates CLI workflow commands)
+- These tools should have rich descriptions that explain what they're for, not just what they do
+
+**Patterns to follow:**
+- Existing `makeAPIHandler` pattern in mcp_tools.go.tmpl for tool registration
+- Existing domain archetype detection in generator.go for conditional workflow tool registration
+
+**Test scenarios:**
+- Happy path: Generated MCP server includes query_sql, search, sync tools when data layer is enabled
+- Happy path: Workflow tools (stale, health) registered when domain archetype matches
+- Edge case: MCP server with no data layer still works (sql/search tools omitted gracefully)
+- Edge case: Tool names don't collide with endpoint-derived tool names
+
+**Verification:**
+- A generated MCP server exposes sql, search, and sync tools alongside endpoint tools
+- An agent can run a SQL query through the MCP without touching the CLI
+
+---
+
+- [ ] **Unit 2: Front-load domain context in MCP server**
+
+**Goal:** Give MCP-connected agents immediate understanding of the API domain, taxonomy, critical rules, and tool composition patterns - without wasting tokens on discovery.
+
+**Requirements:** Rule 3 (front-load universal context)
+
+**Dependencies:** Unit 1 (high-level tools should exist before we describe them)
+
+**Files:**
+- Modify: `internal/generator/templates/mcp_tools.go.tmpl`
+- Modify: `internal/generator/generator.go`
+- Modify: `internal/pipeline/research.go` (to extract structured context from research brief)
+- Test: `internal/generator/generator_test.go`
+
+**Approach:**
+- Replace the thin `about` tool with a rich `context` tool (or enhance `about`) that returns structured domain knowledge
+- Content comes from two sources:
+  1. **Spec-derived**: resource taxonomy (what entities exist, how they relate), auth requirements, rate limits, pagination patterns
+  2. **Research-derived**: domain opinions, critical querying rules, common workflow patterns, edge cases
+- The generator should accept a structured "domain context" object (extracted from the research brief during generation) and embed it in the MCP server
+- Context is returned as structured JSON, not a wall of text - agents can parse and use what they need
+- Keep it concise: aim for under 2K tokens of context, not a novel
+
+**Patterns to follow:**
+- PostHog's approach: taxonomy + SQL syntax + critical rules, loaded at session start
+- Existing `aboutDescription` function in mcp_tools.go.tmpl
+
+**Test scenarios:**
+- Happy path: Generated MCP server's context tool returns API taxonomy, auth info, rate limit guidance
+- Happy path: Domain opinions from research brief appear in context output
+- Edge case: API with no research brief still generates useful context from spec alone
+- Edge case: Context stays under 2K tokens even for large APIs (50+ resources)
+
+**Verification:**
+- An agent connecting to the MCP server can call one tool and understand the API's domain model, key constraints, and recommended query patterns
+
+---
+
+- [ ] **Unit 3: Enrich MCP tool descriptions with agent-useful metadata**
+
+**Goal:** Transform thin endpoint descriptions into rich, agent-useful tool documentation that includes composition hints, expected output shape, and usage guidance.
+
+**Requirements:** Rule 4 (skills are human knowledge)
+
+**Dependencies:** None (can run parallel with Units 1-2)
+
+**Files:**
+- Modify: `internal/generator/templates/mcp_tools.go.tmpl`
+- Modify: `internal/generator/generator.go`
+- Test: `internal/generator/generator_test.go`
+
+**Approach:**
+- Enhance the tool description template to include:
+  - What the tool returns (shape hint: "Returns array of {id, name, status, assignee}")
+  - Common composition patterns ("Use with issues_list to find issues, then issues_get for detail")
+  - Critical constraints ("Rate limited to 100/min. Prefer batch endpoints for bulk operations")
+  - When NOT to use it ("For searching across resources, use the search tool instead of iterating list endpoints")
+- Keep descriptions concise - agents benefit from dense, factual context, not verbose prose
+- Generate these enrichments from OpenAPI response schemas and the research brief's domain knowledge
+- Cap description length to avoid bloating the tool list (PostHog found their v1 descriptions ate too many tokens)
+
+**Patterns to follow:**
+- PostHog's query-retention.md style: one-line opinions that prevent common mistakes
+- Existing `oneline()` filter in generator.go for description formatting
+
+**Test scenarios:**
+- Happy path: Tool descriptions include return shape hints derived from response schema
+- Happy path: Related tools cross-referenced in descriptions
+- Edge case: Tools with no response schema still get useful descriptions
+- Edge case: Total description token count stays reasonable (under 500 tokens per tool)
+
+**Verification:**
+- MCP tool descriptions are meaningfully richer than the raw OpenAPI endpoint summary
+- An agent reading tool descriptions can make informed choices without trial-and-error
+
+---
+
+- [ ] **Unit 4: Add agent_mcp_quality scorecard dimension**
+
+**Goal:** Score the quality of the generated MCP server, not just the CLI. Validate that MCP tools are rich, composable, and agent-usable.
+
+**Requirements:** Rule 5 (treat agents like real users)
+
+**Dependencies:** Units 1-3 (need the enrichments to exist before scoring them)
+
+**Files:**
+- Modify: `internal/pipeline/scorecard.go`
+- Test: `internal/pipeline/scorecard_test.go`
+
+**Approach:**
+- Add a new Tier 1 dimension: `mcp_quality` (replaces or supplements `agent_native`)
+- Check for:
+  - High-level tools present (sql, search, sync) when data layer exists
+  - Context/about tool returns structured domain knowledge
+  - Tool descriptions include return shape hints
+  - Tool descriptions include composition/cross-reference hints
+  - No empty description strings
+  - Total tool description token estimate is reasonable (not bloated, not skeletal)
+- Score 0-10 like other dimensions
+- This is still static analysis (code pattern matching), not dynamic agent testing
+
+**Patterns to follow:**
+- Existing scorecard dimension pattern in scorecard.go (presence checks + quality heuristics)
+
+**Test scenarios:**
+- Happy path: MCP server with all enrichments scores 8-10
+- Happy path: MCP server with only endpoint tools (no sql/search) scores lower
+- Edge case: MCP server without data layer doesn't get penalized for missing sql/search
+- Error path: Malformed mcp/tools.go file doesn't crash scorer
+
+**Verification:**
+- `printing-press scorecard` reports mcp_quality as a scored dimension
+- Gap report identifies specific MCP quality improvements when score is low
+
+---
+
+- [ ] **Unit 5: Generate agent playbook from research brief**
+
+**Goal:** Produce a generated skills/playbook file that MCP-connected agents can request, containing domain opinions, edge cases, and workflow recipes - the "idiosyncratic knowledge" PostHog says only humans can provide.
+
+**Requirements:** Rule 4 (skills are human knowledge), Rule 3 (front-load context)
+
+**Dependencies:** Unit 2 (context tool should exist to serve the playbook)
+
+**Files:**
+- Modify: `internal/generator/generator.go`
+- Create: `internal/generator/templates/agent_playbook.go.tmpl`
+- Test: `internal/generator/generator_test.go`
+
+**Approach:**
+- During generation, extract from the research brief:
+  - NOI rationale ("Discord's real value is searchable knowledge base")
+  - Domain edge cases ("For retention, use pageview events, not sign-in events")
+  - Common workflow sequences ("To investigate a spike: sync -> sql query -> drill into specific events")
+  - Things agents get wrong ("Don't paginate through all results when search would work")
+- Generate a structured playbook as a Go-embedded resource
+- Expose via MCP as a `get_playbook` tool or include in the context tool response
+- The playbook is generated once at build time, not dynamically - it's the research brief's domain knowledge crystallized into agent-consumable form
+
+**Patterns to follow:**
+- PostHog's skill files: one-liner opinions, not step-by-step manuals
+- Existing README.tmpl for extracting research brief content into generated output
+
+**Test scenarios:**
+- Happy path: Generated playbook contains NOI rationale and domain edge cases from research brief
+- Happy path: Playbook is accessible via MCP tool call
+- Edge case: CLI generated without research brief produces a minimal playbook from spec alone
+- Edge case: Playbook stays concise (under 3K tokens) even for complex APIs
+
+**Verification:**
+- An agent can request the playbook and receive actionable domain knowledge that wasn't obvious from tool descriptions alone
+
+## System-Wide Impact
+
+- **Interaction graph:** Changes to mcp_tools.go.tmpl affect every future printed MCP server. The generator pipeline flows: spec + research -> generator.go -> templates -> output. Changes are additive, not breaking.
+- **Error propagation:** New MCP tools (sql, search) that fail should return structured errors consistent with existing endpoint tools. No new error categories needed.
+- **State lifecycle risks:** The sql/search MCP tools depend on local SQLite state from sync. If sync hasn't run, these tools should return a clear "no data synced yet, run sync first" message, not cryptic errors.
+- **API surface parity:** This plan intentionally narrows the CLI/MCP parity gap. After implementation, MCP agents should be able to do ~80% of what CLI agents can (the remaining 20% being interactive CLI features like progress bars).
+- **Unchanged invariants:** The CLI interface (Cobra commands, flags, output formats) is unchanged. The MCP endpoint-derived tools are unchanged. All changes are additive MCP tools and enriched descriptions.
+
+## Risks & Dependencies
+
+| Risk | Mitigation |
+|------|------------|
+| MCP tool descriptions bloat token usage | Cap per-tool description length. PostHog found this was a real problem. Monitor total token estimate in scorecard. |
+| Research brief extraction is fragile (unstructured markdown -> structured data) | Define a structured "domain context" schema. Generator extracts what it can, falls back to spec-only context gracefully. |
+| sql/search MCP tools expose local data that may be stale | Always include sync timestamp in sql/search results. Agents can decide whether to re-sync. |
+| Playbook quality varies with research brief quality | Playbook is a best-effort enrichment. Scorecard checks for its presence but doesn't fail generation if research was thin. |
+
+## What PP Already Gets Right (PostHog Would Approve)
+
+For the record, these are areas where PP already embodies PostHog's rules:
+
+1. **Dual interface** (CLI + MCP) from one spec - PostHog's Rule 1
+2. **Agent-native flags** (--json, --compact, --stdin, --select, typed exit codes) - Rule 1
+3. **Local data layer** (SQLite + FTS5 + sql command) - Rule 2's abstraction principle
+4. **NOI framework** embeds domain taste - Rule 4
+5. **Dogfood/verify/scorecard** pipeline - Rule 5's quality loop
+6. **Retro system** feeds learnings back into the machine - Rule 5's eval loop
+7. **Reference files loaded on-demand** in skill instructions - Rule 3's "defer the rest"
+
+The improvements in this plan compound on these strengths rather than rebuilding.
+
+## Sources & References
+
+- PostHog blog post: "The golden rules of agent-first product engineering" (2026-04-09, @posthog on X)
+- Existing MCP template: `internal/generator/templates/mcp_tools.go.tmpl`
+- Existing scorecard: `internal/pipeline/scorecard.go`
+- Existing research pipeline: `internal/pipeline/research.go`
+- Prior agent-native plan: `docs/plans/2026-03-25-feat-agent-native-cli-audit-and-improvements-plan.md`
+- Prior MCP readiness plan: `docs/plans/2026-04-05-001-feat-mcp-readiness-layer-plan.md`
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index e60e1bd4..d4d14c18 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -41,6 +41,32 @@ type NovelFeature struct {
 	Rationale   string
 }
 
+// DomainContext holds structured domain knowledge for MCP-connected agents.
+// Front-loaded at session start so agents understand the API without discovery.
+type DomainContext struct {
+	APIName     string            `json:"api_name"`
+	Description string            `json:"description"`
+	Archetype   string            `json:"archetype"`
+	Resources   []ResourceSummary `json:"resources"`
+	QueryTips   []string          `json:"query_tips,omitempty"`
+	Playbook    []PlaybookEntry   `json:"playbook,omitempty"`
+}
+
+// ResourceSummary describes an API resource and its capabilities for agents.
+type ResourceSummary struct {
+	Name        string   `json:"name"`
+	Description string   `json:"description,omitempty"`
+	Endpoints   []string `json:"endpoints"`
+	Syncable    bool     `json:"syncable,omitempty"`
+	Searchable  bool     `json:"searchable,omitempty"`
+}
+
+// PlaybookEntry is a domain-specific insight for agents.
+type PlaybookEntry struct {
+	Topic   string `json:"topic"`
+	Insight string `json:"insight"`
+}
+
 type Generator struct {
 	Spec           *spec.APISpec
 	OutputDir      string
@@ -115,6 +141,7 @@ func New(s *spec.APISpec, outputDir string) *Generator {
 		"add":                    func(a, b int) int { return a + b },
 		"oneline":                oneline,
 		"mcpDescription":         mcpDescription,
+		"mcpDescriptionRich":     mcpDescriptionRich,
 		"flagName":               flagName,
 		"safeTypeName":           safeTypeName,
 		"hasNonScalarType": func(types map[string]spec.TypeDef) bool {
@@ -270,6 +297,116 @@ func (g *Generator) readmeData() *readmeTemplateData {
 	}
 }
 
+// buildDomainContext constructs structured domain knowledge for MCP agents
+// from the spec and profiler output. This is front-loaded context that prevents
+// agents from wasting tokens discovering what the API is about.
+func (g *Generator) buildDomainContext() DomainContext {
+	ctx := DomainContext{
+		APIName:     g.Spec.Name,
+		Description: oneline(g.Spec.Description),
+		Archetype:   string(profiler.ArchetypeGeneric),
+	}
+
+	if g.profile != nil {
+		ctx.Archetype = string(g.profile.Domain.Archetype)
+
+		// Build resource summaries with syncable/searchable annotations
+		syncSet := make(map[string]bool)
+		for _, sr := range g.profile.SyncableResources {
+			syncSet[sr.Name] = true
+		}
+
+		for rName, r := range g.Spec.Resources {
+			rs := ResourceSummary{
+				Name:        rName,
+				Description: oneline(r.Description),
+				Syncable:    syncSet[rName],
+				Searchable:  len(g.profile.SearchableFields[rName]) > 0,
+			}
+			for eName := range r.Endpoints {
+				rs.Endpoints = append(rs.Endpoints, eName)
+			}
+			sort.Strings(rs.Endpoints)
+			ctx.Resources = append(ctx.Resources, rs)
+		}
+		sort.Slice(ctx.Resources, func(i, j int) bool {
+			return ctx.Resources[i].Name < ctx.Resources[j].Name
+		})
+
+		// Add query tips based on pagination profile
+		if g.profile.Pagination.CursorParam != "" {
+			ctx.QueryTips = append(ctx.QueryTips,
+				fmt.Sprintf("Pagination uses cursor-based paging. Pass %s parameter for subsequent pages.", g.profile.Pagination.CursorParam))
+		}
+		if g.profile.Pagination.PageSizeParam != "" {
+			ctx.QueryTips = append(ctx.QueryTips,
+				fmt.Sprintf("Control page size with the %s parameter (default %d).", g.profile.Pagination.PageSizeParam, g.profile.Pagination.DefaultPageSize))
+		}
+		if g.profile.Pagination.SinceParam != "" {
+			ctx.QueryTips = append(ctx.QueryTips,
+				fmt.Sprintf("Use %s for incremental fetches (filter by modification time).", g.profile.Pagination.SinceParam))
+		}
+	}
+
+	// Add playbook entries from novel features
+	for _, nf := range g.NovelFeatures {
+		ctx.Playbook = append(ctx.Playbook, PlaybookEntry{
+			Topic:   nf.Name,
+			Insight: nf.Rationale,
+		})
+	}
+
+	// Add data layer tips when store is available
+	if g.VisionSet.Store {
+		ctx.QueryTips = append(ctx.QueryTips,
+			"Use the sql tool for ad-hoc analysis on synced data. Run sync first to populate the local database.",
+			"Use the search tool for full-text search across all synced resources. Faster than iterating list endpoints.",
+			"Prefer sql/search over repeated API calls when the data is already synced.")
+	}
+
+	// Add archetype-specific playbook entries — domain opinions that agents
+	// can't discover from the API spec alone (PostHog Rule 4: skills are human knowledge)
+	if g.profile != nil {
+		ctx.Playbook = append(ctx.Playbook, archetypePlaybook(g.profile.Domain.Archetype)...)
+	}
+
+	return ctx
+}
+
+// archetypePlaybook returns domain-specific insights based on API archetype.
+// These are opinionated tips that prevent common agent mistakes.
+func archetypePlaybook(arch profiler.DomainArchetype) []PlaybookEntry {
+	switch arch {
+	case profiler.ArchetypeProjectMgmt:
+		return []PlaybookEntry{
+			{Topic: "Finding stale work", Insight: "Use the stale command or sql query to find items not updated recently. More reliable than scanning list results manually."},
+			{Topic: "Load analysis", Insight: "When analyzing team workload, filter by assignee and status. Raw counts without status filtering are misleading."},
+			{Topic: "Bulk operations", Insight: "For bulk status changes, prefer update endpoints over delete+create. Most PM APIs track history on updates."},
+		}
+	case profiler.ArchetypeCommunication:
+		return []PlaybookEntry{
+			{Topic: "Message search", Insight: "Use the search tool on synced data rather than paginating through message history. Message APIs often have aggressive rate limits."},
+			{Topic: "Channel health", Insight: "When analyzing channel activity, use the channel-health command or sql aggregation on synced messages. Don't iterate individual messages via API."},
+		}
+	case profiler.ArchetypePayments:
+		return []PlaybookEntry{
+			{Topic: "Financial data", Insight: "Always use read-only operations for financial queries. Never use create/update tools for payment data without explicit user confirmation."},
+			{Topic: "Reconciliation", Insight: "For reconciliation tasks, sync first then use sql for cross-referencing. API pagination over financial records is slow and rate-limited."},
+		}
+	case profiler.ArchetypeCRM:
+		return []PlaybookEntry{
+			{Topic: "Contact lookup", Insight: "Use search for finding contacts by name/email. List endpoints return unsorted results and require pagination for large datasets."},
+			{Topic: "Activity tracking", Insight: "When checking deal activity, sync first and query locally. CRM APIs often throttle activity-log endpoints heavily."},
+		}
+	case profiler.ArchetypeDeveloperPlatform:
+		return []PlaybookEntry{
+			{Topic: "Resource discovery", Insight: "Use list commands to discover available resources before attempting operations. Developer platform APIs often have nested resource hierarchies."},
+		}
+	default:
+		return nil
+	}
+}
+
 func (g *Generator) Generate() error {
 	dirs := []string{
 		filepath.Join("cmd", naming.CLI(g.Spec.Name)),
@@ -623,6 +760,7 @@ func (g *Generator) Generate() error {
 	// Render MCP tools registration (needs VisionSet + store data + tool counts for annotations)
 	if g.VisionSet.MCP {
 		mcpTotal, mcpPublic := g.Spec.CountMCPTools()
+		domainCtx := g.buildDomainContext()
 		mcpData := struct {
 			*spec.APISpec
 			SyncableResources []profiler.SyncableResource
@@ -632,6 +770,7 @@ func (g *Generator) Generate() error {
 			MCPTotalCount     int
 			MCPPublicCount    int
 			NovelFeatures     []NovelFeature
+			DomainContext     DomainContext
 		}{
 			APISpec:           g.Spec,
 			SyncableResources: g.profile.SyncableResources,
@@ -641,6 +780,7 @@ func (g *Generator) Generate() error {
 			MCPTotalCount:     mcpTotal,
 			MCPPublicCount:    mcpPublic,
 			NovelFeatures:     g.NovelFeatures,
+			DomainContext:     domainCtx,
 		}
 		if err := g.renderTemplate("mcp_tools.go.tmpl", filepath.Join("internal", "mcp", "tools.go"), mcpData); err != nil {
 			return fmt.Errorf("rendering MCP tools: %w", err)
@@ -1142,6 +1282,50 @@ func mcpDescription(desc string, noAuth bool, authType string, publicCount, tota
 	return oneline(desc)
 }
 
+// mcpDescriptionRich builds an enriched MCP tool description that includes
+// the base description plus response shape hints and method context.
+// This gives agents enough information to choose the right tool without
+// trial-and-error. Total length is capped to prevent token bloat.
+func mcpDescriptionRich(desc string, noAuth bool, authType string, publicCount, totalCount int, method, respType, respItem string) string {
+	base := mcpDescription(desc, noAuth, authType, publicCount, totalCount)
+
+	var suffix string
+
+	// Add response shape hint
+	if respType == "array" && respItem != "" {
+		suffix = "Returns array of " + respItem + "."
+	} else if respType == "array" {
+		suffix = "Returns array."
+	} else if respType == "object" && respItem != "" {
+		suffix = "Returns " + respItem + "."
+	}
+
+	// Add method context for non-obvious cases
+	switch method {
+	case "DELETE":
+		if suffix != "" {
+			suffix += " Destructive."
+		} else {
+			suffix = "Destructive operation."
+		}
+	case "PATCH":
+		if suffix == "" {
+			suffix = "Partial update."
+		}
+	}
+
+	if suffix == "" {
+		return base
+	}
+
+	result := base + " " + suffix
+	// Cap at 200 chars to prevent token bloat (PostHog learned this the hard way)
+	if len(result) > 200 {
+		result = result[:197] + "..."
+	}
+	return result
+}
+
 func exampleValue(p spec.Param) string {
 	nameLower := strings.ToLower(p.Name)
 
diff --git a/internal/generator/templates/mcp_tools.go.tmpl b/internal/generator/templates/mcp_tools.go.tmpl
index a3e26be6..1ed7bd8d 100644
--- a/internal/generator/templates/mcp_tools.go.tmpl
+++ b/internal/generator/templates/mcp_tools.go.tmpl
@@ -64,7 +64,7 @@ func RegisterTools(s *server.MCPServer) {
 {{- range $eName, $endpoint := $resource.Endpoints}}
 	s.AddTool(
 		mcplib.NewTool("{{snake $name}}_{{snake $eName}}",
-			mcplib.WithDescription("{{mcpDescription $endpoint.Description $endpoint.NoAuth $.Auth.Type $.MCPPublicCount $.MCPTotalCount}}"),
+			mcplib.WithDescription("{{mcpDescriptionRich $endpoint.Description $endpoint.NoAuth $.Auth.Type $.MCPPublicCount $.MCPTotalCount $endpoint.Method $endpoint.Response.Type $endpoint.Response.Item}}"),
 {{- range $endpoint.Params}}
 {{- if eq .Type "integer"}}
 			mcplib.WithNumber("{{.Name}}"{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description("{{oneline .Description}}")),
@@ -82,7 +82,7 @@ func RegisterTools(s *server.MCPServer) {
 {{- range $eName, $endpoint := $subResource.Endpoints}}
 	s.AddTool(
 		mcplib.NewTool("{{snake $name}}_{{snake $subName}}_{{snake $eName}}",
-			mcplib.WithDescription("{{mcpDescription $endpoint.Description $endpoint.NoAuth $.Auth.Type $.MCPPublicCount $.MCPTotalCount}}"),
+			mcplib.WithDescription("{{mcpDescriptionRich $endpoint.Description $endpoint.NoAuth $.Auth.Type $.MCPPublicCount $.MCPTotalCount $endpoint.Method $endpoint.Response.Type $endpoint.Response.Item}}"),
 {{- range $endpoint.Params}}
 {{- if eq .Type "integer"}}
 			mcplib.WithNumber("{{.Name}}"{{if .Required}}, mcplib.Required(){{end}}, mcplib.Description("{{oneline .Description}}")),
@@ -100,12 +100,12 @@ func RegisterTools(s *server.MCPServer) {
 {{- end}}
 
 {{- if .VisionSet.Sync}}
-	// Sync tool
+	// Sync tool — populates local database for offline search and sql queries
 	s.AddTool(
 		mcplib.NewTool("sync",
-			mcplib.WithDescription("Sync API data to local SQLite for offline search and analysis"),
-			mcplib.WithString("resources", mcplib.Description("Comma-separated resource types to sync")),
-			mcplib.WithString("since", mcplib.Description("Incremental sync since duration (7d, 24h, 1w)")),
+			mcplib.WithDescription("Sync API data to local SQLite database. Run this before using search or sql tools. Supports incremental sync."),
+			mcplib.WithString("resources", mcplib.Description("Comma-separated resource types to sync (omit for all)")),
+			mcplib.WithString("since", mcplib.Description("Incremental sync since duration (e.g. 7d, 24h, 1w)")),
 			mcplib.WithBoolean("full", mcplib.Description("Full resync ignoring checkpoints")),
 		),
 		handleSync,
@@ -113,11 +113,11 @@ func RegisterTools(s *server.MCPServer) {
 {{- end}}
 
 {{- if .VisionSet.Search}}
-	// Search tool
+	// Search tool — faster than iterating list endpoints for finding specific items
 	s.AddTool(
 		mcplib.NewTool("search",
-			mcplib.WithDescription("Full-text search across synced data"),
-			mcplib.WithString("query", mcplib.Required(), mcplib.Description("Search query")),
+			mcplib.WithDescription("Full-text search across all synced data. Faster than paginating list endpoints. Requires sync first."),
+			mcplib.WithString("query", mcplib.Required(), mcplib.Description("Search query (supports FTS5 syntax: AND, OR, NOT, quotes for phrases)")),
 			mcplib.WithNumber("limit", mcplib.Description("Max results (default 25)")),
 		),
 		handleSearch,
@@ -125,22 +125,23 @@ func RegisterTools(s *server.MCPServer) {
 {{- end}}
 
 {{- if .VisionSet.Store}}
-	// SQL tool
+	// SQL tool — ad-hoc analysis on synced data without API calls
 	s.AddTool(
 		mcplib.NewTool("sql",
-			mcplib.WithDescription("Run read-only SQL query against local database"),
-			mcplib.WithString("query", mcplib.Required(), mcplib.Description("SQL query (SELECT only)")),
+			mcplib.WithDescription("Run read-only SQL against local database. Use for ad-hoc analysis, aggregations, and joins across synced resources. Requires sync first."),
+			mcplib.WithString("query", mcplib.Required(), mcplib.Description("SQL query (SELECT only). Tables match resource names.")),
 		),
 		handleSQL,
 	)
 {{- end}}
 
-	// About tool — self-describing metadata for this MCP server
+	// Context tool — front-loaded domain knowledge for agents.
+	// Call this first to understand the API taxonomy, query patterns, and capabilities.
 	s.AddTool(
-		mcplib.NewTool("about",
-			mcplib.WithDescription("Describe this MCP server's capabilities, auth requirements, and unique features"),
+		mcplib.NewTool("context",
+			mcplib.WithDescription("Get API domain context: resource taxonomy, auth requirements, query tips, and unique capabilities. Call this first."),
 		),
-		handleAbout,
+		handleContext,
 	)
 }
 
@@ -380,12 +381,12 @@ func handleSQL(ctx context.Context, req mcplib.CallToolRequest) (*mcplib.CallToo
 }
 {{- end}}
 
-func handleAbout(_ context.Context, _ mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
-	about := map[string]any{
-		"api":              "{{.Name}}",
-		"description":      "{{oneline .Description}}",
-		"tool_count":       {{.MCPTotalCount}},
-		"public_tool_count": {{.MCPPublicCount}},
+func handleContext(_ context.Context, _ mcplib.CallToolRequest) (*mcplib.CallToolResult, error) {
+	ctx := map[string]any{
+		"api":         "{{.DomainContext.APIName}}",
+		"description": "{{.DomainContext.Description}}",
+		"archetype":   "{{.DomainContext.Archetype}}",
+		"tool_count":  {{.MCPTotalCount}},
 {{- if and .Auth.Type (ne .Auth.Type "none")}}
 		"auth": map[string]any{
 			"type": "{{.Auth.Type}}",
@@ -396,15 +397,46 @@ func handleAbout(_ context.Context, _ mcplib.CallToolRequest) (*mcplib.CallToolR
 			"key_url": "{{.Auth.KeyURL}}",
 {{- end}}
 		},
+{{- end}}
+		"resources": []map[string]any{
+{{- range .DomainContext.Resources}}
+			{
+				"name": "{{.Name}}",
+{{- if .Description}}
+				"description": "{{.Description}}",
+{{- end}}
+				"endpoints": []string{ {{- range .Endpoints}}"{{.}}", {{end}} },
+{{- if .Syncable}}
+				"syncable": true,
+{{- end}}
+{{- if .Searchable}}
+				"searchable": true,
+{{- end}}
+			},
+{{- end}}
+		},
+{{- if .DomainContext.QueryTips}}
+		"query_tips": []string{
+{{- range .DomainContext.QueryTips}}
+			"{{.}}",
+{{- end}}
+		},
 {{- end}}
 {{- if .NovelFeatures}}
 		"unique_capabilities": []map[string]string{
 {{- range .NovelFeatures}}
-			{"name": "{{.Name}}", "command": "{{.Command}}", "description": "{{oneline .Description}}"},
+			{"name": "{{.Name}}", "command": "{{.Command}}", "description": "{{oneline .Description}}", "rationale": "{{oneline .Rationale}}"},
+{{- end}}
+		},
+{{- end}}
+{{- if .DomainContext.Playbook}}
+		"playbook": []map[string]string{
+{{- range .DomainContext.Playbook}}
+			{"topic": "{{.Topic}}", "insight": "{{.Insight}}"},
 {{- end}}
 		},
 {{- end}}
 	}
-	data, _ := json.MarshalIndent(about, "", "  ")
+	data, _ := json.MarshalIndent(ctx, "", "  ")
 	return mcplib.NewToolResultText(string(data)), nil
 }
diff --git a/internal/pipeline/scorecard.go b/internal/pipeline/scorecard.go
index e8645404..6b8f6550 100644
--- a/internal/pipeline/scorecard.go
+++ b/internal/pipeline/scorecard.go
@@ -45,6 +45,7 @@ type SteinerScore struct {
 	README        int `json:"readme"`         // 0-10
 	Doctor        int `json:"doctor"`         // 0-10
 	AgentNative   int `json:"agent_native"`   // 0-10
+	MCPQuality    int `json:"mcp_quality"`    // 0-10
 	LocalCache    int `json:"local_cache"`    // 0-10
 	Breadth       int `json:"breadth"`        // 0-10: how many commands (penalizes empty CLIs)
 	Vision        int `json:"vision"`         // 0-10
@@ -86,6 +87,7 @@ func RunScorecard(outputDir, pipelineDir, specPath string, verifyReport *VerifyR
 	sc.Steinberger.README = scoreREADME(outputDir)
 	sc.Steinberger.Doctor = scoreDoctor(outputDir)
 	sc.Steinberger.AgentNative = scoreAgentNative(outputDir)
+	sc.Steinberger.MCPQuality = scoreMCPQuality(outputDir)
 	sc.Steinberger.LocalCache = scoreLocalCache(outputDir)
 	sc.Steinberger.Breadth = scoreBreadth(outputDir)
 	sc.Steinberger.Vision = scoreVision(outputDir)
@@ -114,7 +116,7 @@ func RunScorecard(outputDir, pipelineDir, specPath string, verifyReport *VerifyR
 	sc.Steinberger.TypeFidelity = scoreTypeFidelity(outputDir)
 	sc.Steinberger.DeadCode = scoreDeadCode(outputDir)
 
-	// Tier 1: Infrastructure (string-matching, 120 max)
+	// Tier 1: Infrastructure (string-matching, 130 max)
 	tier1Raw := sc.Steinberger.OutputModes +
 		sc.Steinberger.Auth +
 		sc.Steinberger.ErrorHandling +
@@ -122,6 +124,7 @@ func RunScorecard(outputDir, pipelineDir, specPath string, verifyReport *VerifyR
 		sc.Steinberger.README +
 		sc.Steinberger.Doctor +
 		sc.Steinberger.AgentNative +
+		sc.Steinberger.MCPQuality +
 		sc.Steinberger.LocalCache +
 		sc.Steinberger.Breadth +
 		sc.Steinberger.Vision +
@@ -144,7 +147,7 @@ func RunScorecard(outputDir, pipelineDir, specPath string, verifyReport *VerifyR
 		sc.Steinberger.DeadCode
 
 	// Weighted composite: Tier 1 = 50%, Tier 2 = 50% of final 100-point scale
-	tier1Normalized := (tier1Raw * 50) / 120 // scale 0-120 to 0-50
+	tier1Normalized := (tier1Raw * 50) / 130 // scale 0-130 to 0-50
 	tier2Max := 50
 	if sc.IsDimensionUnscored("path_validity") {
 		tier2Max -= 10
@@ -543,6 +546,66 @@ func scoreAgentNative(dir string) int {
 	return score
 }
 
+func scoreMCPQuality(dir string) int {
+	mcpContent := readFileContent(filepath.Join(dir, "internal", "mcp", "tools.go"))
+	if mcpContent == "" {
+		return 0 // No MCP server generated
+	}
+
+	score := 0
+
+	// Presence: MCP tools.go exists and has RegisterTools
+	if strings.Contains(mcpContent, "RegisterTools") {
+		score += 2
+	}
+
+	// Context tool: has rich context/about tool with domain knowledge
+	if strings.Contains(mcpContent, `"context"`) || strings.Contains(mcpContent, "handleContext") {
+		score += 2
+	}
+
+	// High-level tools: sql, search, sync exposed to MCP (not just CLI)
+	highlevelCount := 0
+	if strings.Contains(mcpContent, `"sql"`) && strings.Contains(mcpContent, "handleSQL") {
+		highlevelCount++
+	}
+	if strings.Contains(mcpContent, `"search"`) && strings.Contains(mcpContent, "handleSearch") {
+		highlevelCount++
+	}
+	if strings.Contains(mcpContent, `"sync"`) && strings.Contains(mcpContent, "handleSync") {
+		highlevelCount++
+	}
+	if highlevelCount >= 2 {
+		score += 2
+	} else if highlevelCount >= 1 {
+		score++
+	}
+
+	// Description quality: response shape hints (Returns array, Returns object)
+	returnHints := strings.Count(mcpContent, "Returns array") + strings.Count(mcpContent, "Returns ")
+	if returnHints >= 3 {
+		score += 2
+	} else if returnHints >= 1 {
+		score++
+	}
+
+	// No empty descriptions
+	emptyDescs := strings.Count(mcpContent, `Description("")`)
+	if emptyDescs == 0 {
+		score++
+	}
+
+	// Description richness: tool descriptions reference other tools or provide usage guidance
+	if strings.Contains(mcpContent, "Requires sync") || strings.Contains(mcpContent, "Call this first") {
+		score++
+	}
+
+	if score > 10 {
+		score = 10
+	}
+	return score
+}
+
 func scoreLocalCache(dir string) int {
 	clientContent := readFileContent(filepath.Join(dir, "internal", "client", "client.go"))
 	score := 0
@@ -1769,6 +1832,7 @@ func buildGapReport(s SteinerScore, unscored []string) []string {
 		{"readme", s.README},
 		{"doctor", s.Doctor},
 		{"agent_native", s.AgentNative},
+		{"mcp_quality", s.MCPQuality},
 		{"local_cache", s.LocalCache},
 		{"breadth", s.Breadth},
 		{"vision", s.Vision},
@@ -1897,6 +1961,7 @@ func writeScorecardMD(sc *Scorecard, pipelineDir string) error {
 		{"README", "readme", s.README},
 		{"Doctor", "doctor", s.Doctor},
 		{"Agent Native", "agent_native", s.AgentNative},
+		{"MCP Quality", "mcp_quality", s.MCPQuality},
 		{"Local Cache", "local_cache", s.LocalCache},
 		{"Breadth", "breadth", s.Breadth},
 		{"Vision", "vision", s.Vision},

← 776d433c fix(skills): correct publish package flag name and staging w  ·  back to Cli Printing Press  ·  feat(cli): add printing-press-library plugin to marketplace cbcd67db →