[object Object]

← back to Cli Printing Press

chore(cli)!: remove megamcp aggregate server and Composio plan (#363)

0e1b850854510005ce60ab2153a024e993c5fdea · 2026-04-28 09:50:36 -0700 · Trevin Chow

* chore(cli): remove megamcp aggregate server and Composio plan

The aggregate "all CLIs in one MCP server" surface no longer earns its
place. Per-CLI MCPB bundles serve Claude Desktop install; per-CLI
agent-skill packaging serves cross-client install. An aggregate server
with dynamic activation, meta-tools, and an auth-format substitution
layer added maintenance cost (auth.go has had multiple bugfixes in the
changelog) without giving agents anything they can't already do by
picking the right printed-CLI skill from descriptions.

Pre-launch, no users to migrate. Removes:

- internal/megamcp/ (handler, manifest, registry, metatools, activation,
  auth, security; ~5,000 LOC including tests)
- cmd/printing-press-mcp/ (the aggregate binary)
- docs/plans/2026-04-19-001-feat-composio-inspired-features-plan.md
  (forward-looking plan that built on megamcp)
- smithery.yaml (described printing-press-mcp; not relevant without it)
- printing-press-mcp build/archive blocks from .goreleaser.yaml
- Indirect go.mod deps that came from mcp-go: mark3labs/mcp-go,
  google/jsonschema-go, google/uuid, spf13/cast, yosida95/uritemplate

tools-manifest.json stays — auth-doctor and mcp-audit still consume it.
The "cli-only" MCP readiness label is now fully dead (megamcp was its
only consumer); comments in climanifest.go, mcpb_manifest.go, bundle.go,
and mcpb_bundle.go are tightened to drop the stale references.

Note: the printing-press-mcp aggregate binary is removed. Replacement
paths are per-CLI MCPB bundles (Claude Desktop) and per-CLI /pp-<api>
agent skills (cross-client). Pre-launch — no users affected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(cli): collapse computeMCPReady switch redundancy

The "full" cases (none, api_key, bearer_token) were unreachable
through the explicit case after the default branch already returned
"full" for everything not in cookie/composed. Drop the redundant case.

Truth table identical across all 8 known auth types (none, api_key,
bearer_token, oauth2, cookie, composed, empty, unknown):
  cookie/composed → "partial"
  everything else → "full"

Pure function, no callers affected. Tests + goldens unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(cli): purge megamcp from active forward-looking plans

Fallout from removing internal/megamcp/ in this PR.

Two active plans referenced megamcp:

- docs/plans/2026-04-19-002-feat-super-cli-run-namespace-plan.md was
  built explicitly on megamcp parity. The technical approach wrapped
  internal/megamcp/registry.go and lifted helpers from
  internal/megamcp/handler.go. With megamcp gone, the plan cannot be
  implemented as written and would need a clean-sheet rewrite.
  Deleted; if the run-namespace concept is still desired, write a
  fresh plan grounded in the per-CLI surface.

- docs/plans/2026-04-22-003-feat-mcp-production-readiness-plan.md
  mentioned megamcp in 7 supporting paragraphs (current-state framing,
  a "patterns to follow" pointer, an "unchanged invariants" item,
  source references). The plan itself is about per-CLI MCP improvements
  and stands without those references — surgical edits only.

Other plans that reference megamcp are completed/historical or
out-of-scope for this PR; surface them for separate cleanup if needed:
  - 2026-04-13-002-feat-cloudflare-cli-learnings-plan.md (active)
  - 2026-04-19-004-feat-auth-doctor-plan.md (active)
  - 2026-04-17-002-fix-private-library-registry-auth-plan.md (active)
  - 2026-04-19-003-feat-unified-auth-manager-plan.md (active)
  - 2026-04-05-001-feat-mcp-readiness-layer-plan.md (active)
  - docs/brainstorms/2026-04-06-mega-mcp-generic-proxy-requirements.md
  - docs/plans/2026-04-06-002-feat-mega-mcp-aggregate-server-plan.md
    (the original megamcp build plan — historical record, leave)
  - docs/plans/2026-04-06-003-feat-mega-mcp-generic-proxy-plan.md
    (historical record, leave)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(cli): sweep remaining megamcp references from plan dir

Continues the megamcp removal cleanup beyond the two plans handled in
the previous commit.

Deleted (4 docs — premise gone, no value as forward-looking artifacts):
- docs/plans/2026-04-06-002-feat-mega-mcp-aggregate-server-plan.md
  (the original megamcp build plan)
- docs/plans/2026-04-06-003-feat-mega-mcp-generic-proxy-plan.md
  (the megamcp generic-proxy build plan)
- docs/brainstorms/2026-04-06-mega-mcp-generic-proxy-requirements.md
  (the originating brainstorm)
- docs/plans/2026-04-17-002-fix-private-library-registry-auth-plan.md
  (a fix for megamcp's registry-fetch path; bug doesn't exist now)

Status flips (3 docs — work landed or was superseded; refs become
historical and accurate to that snapshot):
- 2026-04-19-004-feat-auth-doctor-plan.md → completed (auth-doctor
  is shipped; in AGENTS.md glossary)
- 2026-04-05-001-feat-mcp-readiness-layer-plan.md → completed (the
  MCPReady field, tools-manifest.json, NoAuth flag all shipped and
  remain in use)
- 2026-04-19-003-feat-unified-auth-manager-plan.md → superseded (the
  auth-doctor plan's overview explicitly notes this one was killed
  in favor of the lighter diagnostic)

Surgical edit (1 doc):
- 2026-04-13-002-feat-cloudflare-cli-learnings-plan.md drops "megamcp"
  from a comma-separated list of subsystems the plan does not touch.

After this sweep, all live megamcp references in docs/plans/ live in
completed/superseded plans where the references are historical record
of what was built at that time.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 0e1b850854510005ce60ab2153a024e993c5fdea
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Tue Apr 28 09:50:36 2026 -0700

    chore(cli)!: remove megamcp aggregate server and Composio plan (#363)
    
    * chore(cli): remove megamcp aggregate server and Composio plan
    
    The aggregate "all CLIs in one MCP server" surface no longer earns its
    place. Per-CLI MCPB bundles serve Claude Desktop install; per-CLI
    agent-skill packaging serves cross-client install. An aggregate server
    with dynamic activation, meta-tools, and an auth-format substitution
    layer added maintenance cost (auth.go has had multiple bugfixes in the
    changelog) without giving agents anything they can't already do by
    picking the right printed-CLI skill from descriptions.
    
    Pre-launch, no users to migrate. Removes:
    
    - internal/megamcp/ (handler, manifest, registry, metatools, activation,
      auth, security; ~5,000 LOC including tests)
    - cmd/printing-press-mcp/ (the aggregate binary)
    - docs/plans/2026-04-19-001-feat-composio-inspired-features-plan.md
      (forward-looking plan that built on megamcp)
    - smithery.yaml (described printing-press-mcp; not relevant without it)
    - printing-press-mcp build/archive blocks from .goreleaser.yaml
    - Indirect go.mod deps that came from mcp-go: mark3labs/mcp-go,
      google/jsonschema-go, google/uuid, spf13/cast, yosida95/uritemplate
    
    tools-manifest.json stays — auth-doctor and mcp-audit still consume it.
    The "cli-only" MCP readiness label is now fully dead (megamcp was its
    only consumer); comments in climanifest.go, mcpb_manifest.go, bundle.go,
    and mcpb_bundle.go are tightened to drop the stale references.
    
    Note: the printing-press-mcp aggregate binary is removed. Replacement
    paths are per-CLI MCPB bundles (Claude Desktop) and per-CLI /pp-<api>
    agent skills (cross-client). Pre-launch — no users affected.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * refactor(cli): collapse computeMCPReady switch redundancy
    
    The "full" cases (none, api_key, bearer_token) were unreachable
    through the explicit case after the default branch already returned
    "full" for everything not in cookie/composed. Drop the redundant case.
    
    Truth table identical across all 8 known auth types (none, api_key,
    bearer_token, oauth2, cookie, composed, empty, unknown):
      cookie/composed → "partial"
      everything else → "full"
    
    Pure function, no callers affected. Tests + goldens unchanged.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * docs(cli): purge megamcp from active forward-looking plans
    
    Fallout from removing internal/megamcp/ in this PR.
    
    Two active plans referenced megamcp:
    
    - docs/plans/2026-04-19-002-feat-super-cli-run-namespace-plan.md was
      built explicitly on megamcp parity. The technical approach wrapped
      internal/megamcp/registry.go and lifted helpers from
      internal/megamcp/handler.go. With megamcp gone, the plan cannot be
      implemented as written and would need a clean-sheet rewrite.
      Deleted; if the run-namespace concept is still desired, write a
      fresh plan grounded in the per-CLI surface.
    
    - docs/plans/2026-04-22-003-feat-mcp-production-readiness-plan.md
      mentioned megamcp in 7 supporting paragraphs (current-state framing,
      a "patterns to follow" pointer, an "unchanged invariants" item,
      source references). The plan itself is about per-CLI MCP improvements
      and stands without those references — surgical edits only.
    
    Other plans that reference megamcp are completed/historical or
    out-of-scope for this PR; surface them for separate cleanup if needed:
      - 2026-04-13-002-feat-cloudflare-cli-learnings-plan.md (active)
      - 2026-04-19-004-feat-auth-doctor-plan.md (active)
      - 2026-04-17-002-fix-private-library-registry-auth-plan.md (active)
      - 2026-04-19-003-feat-unified-auth-manager-plan.md (active)
      - 2026-04-05-001-feat-mcp-readiness-layer-plan.md (active)
      - docs/brainstorms/2026-04-06-mega-mcp-generic-proxy-requirements.md
      - docs/plans/2026-04-06-002-feat-mega-mcp-aggregate-server-plan.md
        (the original megamcp build plan — historical record, leave)
      - docs/plans/2026-04-06-003-feat-mega-mcp-generic-proxy-plan.md
        (historical record, leave)
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    * docs(cli): sweep remaining megamcp references from plan dir
    
    Continues the megamcp removal cleanup beyond the two plans handled in
    the previous commit.
    
    Deleted (4 docs — premise gone, no value as forward-looking artifacts):
    - docs/plans/2026-04-06-002-feat-mega-mcp-aggregate-server-plan.md
      (the original megamcp build plan)
    - docs/plans/2026-04-06-003-feat-mega-mcp-generic-proxy-plan.md
      (the megamcp generic-proxy build plan)
    - docs/brainstorms/2026-04-06-mega-mcp-generic-proxy-requirements.md
      (the originating brainstorm)
    - docs/plans/2026-04-17-002-fix-private-library-registry-auth-plan.md
      (a fix for megamcp's registry-fetch path; bug doesn't exist now)
    
    Status flips (3 docs — work landed or was superseded; refs become
    historical and accurate to that snapshot):
    - 2026-04-19-004-feat-auth-doctor-plan.md → completed (auth-doctor
      is shipped; in AGENTS.md glossary)
    - 2026-04-05-001-feat-mcp-readiness-layer-plan.md → completed (the
      MCPReady field, tools-manifest.json, NoAuth flag all shipped and
      remain in use)
    - 2026-04-19-003-feat-unified-auth-manager-plan.md → superseded (the
      auth-doctor plan's overview explicitly notes this one was killed
      in favor of the lighter diagnostic)
    
    Surgical edit (1 doc):
    - 2026-04-13-002-feat-cloudflare-cli-learnings-plan.md drops "megamcp"
      from a comma-separated list of subsystems the plan does not touch.
    
    After this sweep, all live megamcp references in docs/plans/ live in
    completed/superseded plans where the references are historical record
    of what was built at that time.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
    
    ---------
    
    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 .goreleaser.yaml                                   |  29 -
 cmd/printing-press-mcp/main.go                     |  77 --
 ...26-04-06-mega-mcp-generic-proxy-requirements.md | 115 ---
 ...2026-04-05-001-feat-mcp-readiness-layer-plan.md |   2 +-
 ...4-06-002-feat-mega-mcp-aggregate-server-plan.md | 544 --------------
 ...6-04-06-003-feat-mega-mcp-generic-proxy-plan.md | 460 ------------
 ...04-13-002-feat-cloudflare-cli-learnings-plan.md |   2 +-
 ...7-002-fix-private-library-registry-auth-plan.md |  72 --
 ...-19-001-feat-composio-inspired-features-plan.md | 625 ---------------
 ...-04-19-002-feat-super-cli-run-namespace-plan.md | 395 ----------
 ...026-04-19-003-feat-unified-auth-manager-plan.md |   2 +-
 docs/plans/2026-04-19-004-feat-auth-doctor-plan.md |   2 +-
 ...04-22-003-feat-mcp-production-readiness-plan.md |  15 +-
 go.mod                                             |   6 +-
 go.sum                                             |  12 -
 internal/authdoctor/scan.go                        |   6 +-
 internal/cli/bundle.go                             |   2 +-
 internal/megamcp/activation.go                     | 290 -------
 internal/megamcp/activation_test.go                | 315 --------
 internal/megamcp/auth.go                           | 198 -----
 internal/megamcp/auth_test.go                      | 358 ---------
 internal/megamcp/handler.go                        | 241 ------
 internal/megamcp/handler_test.go                   | 835 ---------------------
 internal/megamcp/manifest.go                       | 358 ---------
 internal/megamcp/manifest_test.go                  | 445 -----------
 internal/megamcp/metatools.go                      | 463 ------------
 internal/megamcp/metatools_test.go                 | 524 -------------
 internal/megamcp/registry.go                       |  50 --
 internal/megamcp/registry_test.go                  | 124 ---
 internal/megamcp/security.go                       | 194 -----
 internal/megamcp/security_test.go                  | 330 --------
 internal/megamcp/types.go                          |  52 --
 internal/pipeline/climanifest.go                   |  13 +-
 internal/pipeline/mcpb_bundle.go                   |   4 +-
 internal/pipeline/mcpb_manifest.go                 |   8 +-
 internal/pipeline/publish.go                       |   5 +-
 internal/pipeline/toolsmanifest.go                 |   8 +-
 smithery.yaml                                      |   8 -
 38 files changed, 28 insertions(+), 7161 deletions(-)

diff --git a/.goreleaser.yaml b/.goreleaser.yaml
index 2f1084cf..b0821c8a 100644
--- a/.goreleaser.yaml
+++ b/.goreleaser.yaml
@@ -19,24 +19,6 @@ builds:
       - -s -w -X github.com/mvanhorn/cli-printing-press/v2/internal/version.Version={{.Version}}
     mod_timestamp: "{{ .CommitTimestamp }}"
 
-  - id: printing-press-mcp
-    main: ./cmd/printing-press-mcp
-    binary: printing-press-mcp
-    env:
-      - CGO_ENABLED=0
-    goos:
-      - linux
-      - darwin
-      - windows
-    goarch:
-      - amd64
-      - arm64
-    flags:
-      - -trimpath
-    ldflags:
-      - -s -w -X github.com/mvanhorn/cli-printing-press/v2/internal/version.Version={{.Version}}
-    mod_timestamp: "{{ .CommitTimestamp }}"
-
 archives:
   - id: printing-press
     builds:
@@ -49,17 +31,6 @@ archives:
         formats:
           - zip
 
-  - id: printing-press-mcp
-    builds:
-      - printing-press-mcp
-    formats:
-      - tar.gz
-    name_template: "printing-press-mcp_{{ .Os }}_{{ .Arch }}"
-    format_overrides:
-      - goos: windows
-        formats:
-          - zip
-
 checksum:
   name_template: checksums.txt
 
diff --git a/cmd/printing-press-mcp/main.go b/cmd/printing-press-mcp/main.go
deleted file mode 100644
index 3474fc96..00000000
--- a/cmd/printing-press-mcp/main.go
+++ /dev/null
@@ -1,77 +0,0 @@
-package main
-
-import (
-	"fmt"
-	"os"
-	"os/signal"
-	"path/filepath"
-	"syscall"
-
-	"github.com/mark3labs/mcp-go/server"
-	"github.com/mvanhorn/cli-printing-press/v2/internal/megamcp"
-	"github.com/mvanhorn/cli-printing-press/v2/internal/version"
-)
-
-const (
-	defaultBaseURL = "https://raw.githubusercontent.com/mvanhorn/printing-press-library/main"
-)
-
-func main() {
-	baseURL := os.Getenv("PRINTING_PRESS_MCP_BASE_URL")
-	if baseURL == "" {
-		baseURL = defaultBaseURL
-	}
-
-	cacheDir := os.Getenv("PRINTING_PRESS_MCP_CACHE_DIR")
-	if cacheDir == "" {
-		home, err := os.UserHomeDir()
-		if err != nil {
-			fmt.Fprintf(os.Stderr, "error: cannot determine home directory: %v\n", err)
-			os.Exit(1)
-		}
-		cacheDir = filepath.Join(home, ".cache", "printing-press-mcp")
-	}
-
-	// Fetch registry from GitHub (or cache).
-	registry, err := megamcp.FetchRegistry(baseURL)
-	if err != nil {
-		fmt.Fprintf(os.Stderr, "warning: could not fetch registry: %v\n", err)
-		// Continue with empty entries — LoadManifests handles nil gracefully,
-		// and cached manifests from a previous successful run will still be used.
-	}
-
-	// Load and cache tools manifests.
-	var registryEntries []megamcp.RegistryEntry
-	if registry != nil {
-		registryEntries = registry.Entries
-	}
-	entries, warnings := megamcp.LoadManifests(registryEntries, cacheDir, baseURL)
-	for _, w := range warnings {
-		fmt.Fprintf(os.Stderr, "warning: %s\n", w)
-	}
-
-	// Create the MCP server with dynamic tool capabilities.
-	s := server.NewMCPServer("printing-press-mcp", version.Version,
-		server.WithToolCapabilities(true))
-
-	// Set up activation manager and register meta-tools.
-	am := megamcp.NewActivationManager(s, entries)
-	megamcp.RegisterMetaTools(s, am)
-
-	fmt.Fprintf(os.Stderr, "printing-press-mcp %s: %d APIs loaded, serving on stdio\n",
-		version.Version, len(entries))
-
-	// Handle graceful shutdown.
-	sigCh := make(chan os.Signal, 1)
-	signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
-	go func() {
-		<-sigCh
-		fmt.Fprintf(os.Stderr, "\nshutting down...\n")
-		os.Exit(0)
-	}()
-
-	if err := server.ServeStdio(s); err != nil {
-		fmt.Fprintf(os.Stderr, "MCP server error: %v\n", err)
-		os.Exit(1)
-	}
-}
diff --git a/docs/brainstorms/2026-04-06-mega-mcp-generic-proxy-requirements.md b/docs/brainstorms/2026-04-06-mega-mcp-generic-proxy-requirements.md
deleted file mode 100644
index 4ec351cb..00000000
--- a/docs/brainstorms/2026-04-06-mega-mcp-generic-proxy-requirements.md
+++ /dev/null
@@ -1,115 +0,0 @@
----
-date: 2026-04-06
-topic: mega-mcp-generic-proxy
----
-
-# Mega MCP: Generic HTTP Proxy for the Printing Press Library
-
-## Problem Frame
-
-Every printed CLI ships an MCP binary, but using them requires installing each one individually — `go install` of a long GitHub path, Go toolchain required, one `claude mcp add` per API. With 6 CLIs today and a growing catalog, this friction prevents adoption. Agents have no way to discover available APIs without the user manually configuring each MCP connection.
-
-Step 1 (PR #145) built the data layer: MCP metadata in manifests and registry, per-endpoint auth awareness, self-describing `about` tools. This plan builds the access layer: a single MCP server binary that gives agents access to every API in the library through one install.
-
-**The key insight:** the per-API MCP servers' core handler (`makeAPIHandler`) is a generic HTTP client — method, path template, positional params, query params. The mega MCP replicates this pattern at runtime using pre-computed tool manifests from the publish pipeline, rather than requiring pre-generated code or local binaries. The mega MCP is a **discovery and demo layer** — it covers the 95% CRUD case. Users who want the full experience (sync, search, SQL, adaptive rate limiting, cookie auth) install the per-API CLI/MCP individually.
-
-## Requirements
-
-**Distribution & Installation**
-
-- R1. Single binary (`printing-press-mcp`) installable via `go install` and Homebrew. No local printing press library required. No Go toolchain required at runtime for Homebrew users.
-- R2. One-command setup for Claude Code (`claude mcp add printing-press -- printing-press-mcp`) and Claude Desktop (single JSON config entry).
-- R3. Marketplace-ready: the mega MCP includes `smithery.yaml` and documentation sufficient for listing on Smithery and the Claude marketplace as a single entry covering all library APIs. Exact marketplace requirements TBD in planning.
-
-**Discovery & Activation**
-
-- R4. At startup, fetch `registry.json` from the public library repo (`mvanhorn/printing-press-library`) on GitHub to discover available APIs and their metadata.
-- R5. For each API, fetch and cache the **tools manifest** (`tools-manifest.json`) from the API's path in the public library repo. The manifest is generated at publish time and contains tool names, descriptions, parameter schemas (with location: path/query/body), auth config, base URL, and required headers. Cache at `~/.cache/printing-press-mcp/` with a 24h TTL. Verify cached manifests against the registry's `spec_checksum` field on each startup — re-fetch on mismatch. Graceful degradation: use cache if fetch fails; on first run with no cache and no network, start with zero APIs and surface the error through `library_info`.
-- R6. Expose a `library_info` meta-tool listing all available APIs with: name, description, tool count, auth type, auth status (configured or not), novel features, and whether a richer per-API MCP exists locally. When a per-API MCP is available, include the install command. Does NOT expose specific env var names in the response — agents use `library_info` for discovery, and the `setup_guide` meta-tool for auth configuration details.
-- R7. Expose a `setup_guide` meta-tool that returns auth configuration instructions for a specific API: required env vars, key URL, and example `claude mcp add --env` commands. Separated from `library_info` so credential-related metadata is behind a deliberate introspection step, not broadcast in the catalog listing.
-- R8. Expose an `activate_api` meta-tool that dynamically registers all tools for a specific API by slug. By default, only meta-tools are registered at startup. The agent calls `library_info` → discovers APIs → calls `activate_api("espn")` → ESPN tools become available. This keeps the default tool count at ~4 (meta-tools only) and scales to any library size. Uses the MCP `tools/list_changed` notification to inform the client. A `deactivate_api` meta-tool unregisters tools for an API.
-- R9. Expose a `search_tools` meta-tool that searches across all APIs' tool names and descriptions by keyword, returning matching tools with their API slug — without requiring activation first. Lets the agent find the right API for a task before activating it.
-
-**Tool Registration & Routing**
-
-- R10. Register MCP tools from the tools manifest (not by parsing OpenAPI specs at runtime). Tool names follow the existing `{snake_resource}_{snake_endpoint}` convention including sub-resource endpoints, prefixed with `{normalized_slug}__` (double underscore separator, slug hyphens → underscores). Registration-time collision detection: reject duplicate prefixed names with a warning.
-- R11. When a tool is called, make the HTTP request directly to the API using a generic HTTP client. The handler classifies each parameter by location (from the manifest): **path** params for URL template substitution, **query** params appended to the URL, **body** params serialized as the JSON request body. Path and query params are excluded from the body. Attach RequiredHeaders (API version headers like `cal-api-version`, `Stripe-Version`) and per-endpoint HeaderOverrides from the manifest.
-- R12. Startup latency: the mega MCP initializes meta-tools and loads manifests in under 5 seconds with cached data (parallel loading). First-run startup (no cache) targets <30 seconds.
-
-**Authentication**
-
-- R13. Auth handled via env vars configured through `claude mcp add --env` or Claude Desktop config. The mega MCP reads each API's auth config from the tools manifest — auth type, header name, format string (e.g., `Bearer {TOKEN}`), header-vs-query placement (`In` field), and env var names. At runtime: read the env var value, apply the format string substitution, set the result on the correct header or query param. Validate format strings against a safe pattern set (header/query substitution only, never path segment) before accepting them from the manifest.
-- R14. Supported auth types: `api_key` (header or query), `bearer_token` (Authorization header), `oauth2` (same treatment as `bearer_token` — env var holds the token; interactive OAuth flows are out of scope), `none`. For `cookie`/`composed` auth types, only endpoints marked `NoAuth` in the manifest are registered (public endpoints work, auth-required endpoints are excluded with a description noting the limitation).
-- R15. Credential isolation: env vars for one API are not used in requests to another API. Each API's HTTP client reads only its own `AuthEnvVars`. Startup-time collision check: if two APIs declare the same env var name, warn the user and require the API filter to separate them.
-- R16. Auth-aware error handling: 401/403 responses return a generic error ("authentication not configured for this API — call `setup_guide` for instructions"). 429 responses surface the error body. Credentials never appear in error output or logs.
-
-**Integrity & Security**
-
-- R17. Spec integrity: verify fetched tools manifests against the registry's `spec_checksum` before accepting them. Reject manifests that fail verification and fall back to cache. Require all fetch URLs to use HTTPS.
-- R18. Server URL validation: before registering tools from a manifest, validate that the API's base URL is HTTPS, not a private IP range (RFC 1918, loopback, link-local, cloud metadata), and resolves to a public address. Reject APIs with invalid base URLs with a warning.
-- R19. Sanitize all manifest-derived text (tool descriptions, parameter descriptions) before registering as MCP tool metadata — strip control characters and length-limit to prevent prompt injection via malicious manifest content.
-
-**Scaling & Filtering**
-
-- R20. API filter: `PRINTING_PRESS_APIS=espn,dub` env var limits which APIs are available for activation. Default is all APIs in the registry.
-- R21. The activation model (R8) keeps the default tool count at ~4-5 meta-tools regardless of library size. Tool count only grows when the agent explicitly activates APIs. This scales to any number of APIs without degrading agent tool selection.
-
-## Success Criteria
-
-- A user installs `printing-press-mcp` and runs `claude mcp add printing-press -- printing-press-mcp` with no other setup. Agent calls `library_info` and sees all available APIs.
-- Agent calls `activate_api("espn")` → ESPN tools appear → agent calls `espn__scores_get` (no auth) and receives real ESPN data.
-- Agent calls `search_tools("pizza")` → finds `pagliacci_pizza__stores_list` → calls `activate_api("pagliacci-pizza")` → calls the tool and gets store data.
-- Agent calls `setup_guide("dub")` → gets auth instructions → user configures `DUB_TOKEN` → agent calls `dub__links_list` and receives real Dub data.
-- Auth-required Pagliacci endpoints are not registered. Tool descriptions note "install pagliacci-pizza-pp-mcp for full access including ordering."
-- The mega MCP works identically in Claude Code and Claude Desktop.
-- Newly published APIs appear on restart without rebuilding the binary (consequence of R4's startup-time fetch).
-
-## Scope Boundaries
-
-- **In scope:** Generic HTTP proxy for all APIs in the public library registry. Marketplace metadata (`smithery.yaml`) for the mega MCP itself. Activation-based tool registration for agent-controlled scaling.
-- **Out of scope:** Subprocess/hybrid mode (spawning per-API MCP binaries for richer features). This is a follow-up — the generic proxy is the v1.
-- **Out of scope:** Per-API MCP binary distribution (goreleaser per API). The mega MCP IS the distribution strategy.
-- **Out of scope:** Directory restructure of `~/printing-press/library/`. The mega MCP reads from GitHub, not local library.
-- **Out of scope:** Novel features (sync, search, SQL), adaptive rate limiting, response caching — these require the per-API MCP binary. The mega MCP is a discovery/demo layer; tool descriptions surface when a richer per-API MCP exists with install instructions.
-- **Out of scope:** Hot-reloading. A restart picks up registry changes.
-- **Out of scope:** Interactive OAuth2 flows, cookie/composed auth beyond public endpoints.
-- **Known limitation:** Cookie/composed auth APIs (Pagliacci) only expose public endpoints (~17% of tools). Tool descriptions note this and include upgrade instructions.
-
-## Key Decisions
-
-- **Tools manifest over runtime spec parsing:** The publish pipeline generates a `tools-manifest.json` per API containing pre-computed tool schemas (names, descriptions, parameters with location classification, auth config, base URL, required headers). The mega MCP reads these lightweight JSON files instead of fetching and parsing raw OpenAPI/internal/GraphQL specs at runtime. This eliminates: runtime kin-openapi dependency, spec-format concerns (OpenAPI vs internal YAML vs GraphQL), parser global mutable state, auth format re-derivation, and startup performance issues. The manifest is format-agnostic — it's always the output of whichever parser was used at generation time.
-- **Activation model over all-at-once registration:** Only meta-tools (`library_info`, `setup_guide`, `activate_api`, `deactivate_api`, `search_tools`, `about`) are registered at startup. API tools are registered on demand via `activate_api`. This keeps the default tool count at ~6 regardless of library size, preventing agent tool selection degradation. Uses MCP's `tools/list_changed` notification to inform clients.
-- **Discovery/demo layer with upgrade path:** The mega MCP is explicitly positioned as a discovery and demo layer — not the full per-API MCP experience. It covers CRUD operations via generic HTTP proxy. Tool descriptions surface when a richer per-API MCP exists locally (with install commands). `library_info` shows upgrade availability. This is honest framing that drives adoption of the full printing press stack.
-- **Generic HTTP proxy over subprocess architecture:** The original plan (Step 2) used per-API MCP subprocesses. This required a local library with pre-built binaries, Go at runtime, and complex subprocess management. The generic proxy eliminates all of that — one binary, no local library, no runtime Go dependency for binary-distribution users.
-- **Fetch from GitHub, our copies:** Tool manifests and registry live in the public library repo under our control. Spec URLs point to our copies (not third-party vendor URLs), ensuring stability and known formats. Integrity verified via checksums.
-- **Cookie/composed auth: public endpoints only:** Rather than excluding cookie-auth APIs entirely, register their public endpoints. Pagliacci's store finder and menu browser work without auth. Tool descriptions note the limitation and include the per-API MCP install command for full access.
-- **Mega MCP as the marketplace listing:** One Smithery/Claude marketplace entry for the aggregator, not one per API. Individual per-API `smithery.yaml` files still exist for standalone discovery, but the mega MCP is the primary install path.
-- **Hybrid mode deferred:** The subprocess-based hybrid mode (detect local per-API binary → route through it for richer features) is explicitly deferred. v1 is pure generic proxy. The tool handler interface is designed so hybrid can slot in later: each API's handler is a function that takes a tool call and returns a result — swapping the generic HTTP handler for a subprocess handler is a per-API config change, not an architectural change.
-
-## Prerequisites (must ship before or alongside the mega MCP)
-
-- **Tools manifest generation in the publish pipeline:** The publish pipeline must generate `tools-manifest.json` per API at publish time, containing: tool names, descriptions, parameter schemas with location (path/query/body), auth config (type, header, format, in, env vars), base URL, required headers, and per-endpoint header overrides. This is a machine change to the printing press generator/publish flow.
-- **Registry schema extension:** Add `spec_checksum`, `spec_format`, and `manifest_url` (pointing to `tools-manifest.json` in the repo) to `registry.json` entries. Update the publish skill to populate these from the CLI manifest.
-- **Re-publish existing CLIs:** The 6 existing CLIs need to be re-published with tools manifests. This can be done as part of the prerequisite work.
-
-## Dependencies / Assumptions
-
-- The public library repo (`mvanhorn/printing-press-library`) has a `registry.json` with MCP metadata (added in Step 1, PR #145). The registry will be extended with `manifest_url` and `spec_checksum` per the prerequisites above.
-- The tools manifest format is generated by the printing press publish pipeline and is format-agnostic (works for OpenAPI, internal YAML, GraphQL, and sniffed specs).
-- Tool manifests live in the public library repo at the path indicated by each registry entry's `path` field: `{path}/tools-manifest.json`.
-- The `mcp-go` library (`github.com/mark3labs/mcp-go`) must be added as a direct dependency in `go.mod`. It supports `tools/list_changed` notifications for dynamic tool registration.
-- MCP marketplace listing (Smithery) accepts a `smithery.yaml` at the repo root level, not just per-directory.
-- The deployment model is single-user local: the MCP server runs as a subprocess of Claude Code/Desktop, which is the only process that can reach its stdio. Multi-user or network-exposed deployments are out of scope.
-
-## Outstanding Questions
-
-### Deferred to Planning
-
-- [Affects R3][Needs research] What are the exact requirements for Smithery marketplace listing? Does it need a Docker image, or can it reference a binary?
-- [Affects R10][Technical] The `__` separator could theoretically collide with tool names containing double underscores. Validate against current APIs during implementation and confirm registration-time collision detection catches any issues.
-- [Affects R20][Technical] Should the API filter support category-based filtering (e.g., `PRINTING_PRESS_CATEGORIES=sports,developer-tools`) in addition to slug-based?
-- [Affects R8][Technical] Does `mcp-go` support the `tools/list_changed` notification? If not, the activation model may require the client to re-fetch tools manually (e.g., agent instructs user to "refresh tools" after activation).
-
-## Next Steps
-
--> `ce:plan` for structured implementation planning. The old subprocess plan (`docs/plans/2026-04-06-002-feat-mega-mcp-aggregate-server-plan.md`) is superseded by this requirements doc. It remains as reference material for the deferred hybrid mode.
diff --git a/docs/plans/2026-04-05-001-feat-mcp-readiness-layer-plan.md b/docs/plans/2026-04-05-001-feat-mcp-readiness-layer-plan.md
index c5fa5722..e020ab3d 100644
--- a/docs/plans/2026-04-05-001-feat-mcp-readiness-layer-plan.md
+++ b/docs/plans/2026-04-05-001-feat-mcp-readiness-layer-plan.md
@@ -1,7 +1,7 @@
 ---
 title: "feat: MCP Readiness Layer — Per-Endpoint Auth Awareness and Public Library MCP Catalog"
 type: feat
-status: active
+status: completed
 date: 2026-04-05
 ---
 
diff --git a/docs/plans/2026-04-06-002-feat-mega-mcp-aggregate-server-plan.md b/docs/plans/2026-04-06-002-feat-mega-mcp-aggregate-server-plan.md
deleted file mode 100644
index 13358341..00000000
--- a/docs/plans/2026-04-06-002-feat-mega-mcp-aggregate-server-plan.md
+++ /dev/null
@@ -1,544 +0,0 @@
----
-title: "feat: Mega MCP — Library-Wide Aggregate MCP Server with Catalog Discovery and Tool Passthrough"
-type: feat
-status: active
-date: 2026-04-06
-deepened: 2026-04-06
----
-
-# feat: Mega MCP — Library-Wide Aggregate MCP Server with Catalog Discovery and Tool Passthrough
-
-## Overview
-
-**Step 2 of 2** toward a Printing Press Mega MCP. Step 1 (PR #145) built the data layer: per-endpoint auth awareness, MCP metadata in manifests, self-describing `about` tools, minority-side auth annotations, smithery.yaml, and registry.json MCP fields.
-
-This plan builds the access layer: a single MCP server binary that aggregates every API in the local library into one installable MCP connection. An agent adds one MCP server and gets access to every printed CLI's tools — Dub links, ESPN scores, Steam achievements, Pagliacci menus — with catalog discovery, auth awareness, and passthrough execution to the per-API MCP binaries.
-
-The plan has two phases:
-1. **Directory restructure** — library directories keyed by API slug (`dub/`) instead of CLI name (`dub-pp-cli/`). Prerequisite for clean mega MCP discovery and an overdue alignment with manuscripts (already slug-keyed).
-2. **Mega MCP server** — new binary that scans the library, discovers tools from per-API MCP servers, and routes tool calls through.
-
-## Problem Frame
-
-Every printed CLI already ships an MCP binary — but using them requires adding each one individually to Claude Code or Claude Desktop. With 6 CLIs today and a growing catalog, this is friction that compounds. Agents have no way to discover what APIs are available without the user manually configuring each MCP connection.
-
-The data layer from Step 1 makes each MCP server self-describing. This plan makes them collectively accessible: one `claude mcp add` gives agents access to every API in the library.
-
-A prerequisite directory restructure is included because the current library layout (`dub-pp-cli/`) names directories after the CLI binary, which misrepresents the contents (each directory contains both CLI and MCP binaries) and creates awkward mapping for the mega MCP's catalog-style discovery. Manuscripts are already keyed by API slug; the library should match.
-
-## Requirements Trace
-
-- R1. Library directories keyed by API slug, not CLI name
-- R2. Backward-compatible discovery: accept both old (`dub-pp-cli/`) and new (`dub/`) directory layouts during transition
-- R3. Mega MCP binary discovers all MCP-ready CLIs in `~/printing-press/library/` via manifest scanning
-- R4. Mega MCP exposes a `library_info` tool listing all available APIs, tool counts, auth status, and readiness
-- R5. Mega MCP registers all per-API tools with namespaced names and routes calls to per-API MCP subprocesses
-- R6. Auth-aware: surfaces missing credentials and forwards per-API env vars to subprocesses with credential isolation (each subprocess receives only its own API's env vars)
-- R7. Extensible: picks up newly published CLIs on restart without rebuilding the mega MCP binary
-- R8. Installable via `claude mcp add` with a single command
-- R9. Publish pipeline and skills updated for slug-keyed library paths
-
-## Scope Boundaries
-
-- **In scope:** Local library aggregation only. The mega MCP reads `~/printing-press/library/`, not the public library repo.
-- **In scope:** Startup-time probing of per-API MCP binaries via `tools/list` to discover full tool schemas. This avoids a new generation artifact and uses the MCP protocol as designed.
-- **In scope:** `mcp-go` as a direct dependency in the printing-press `go.mod` — the mega MCP is a first-class MCP server.
-- **Out of scope:** Public library repo directory migration — that's a separate PR in `mvanhorn/printing-press-library` after the machine changes land.
-- **Out of scope:** MCP marketplace submission for the mega MCP itself. We build the binary; marketplace listing is a separate effort.
-- **Out of scope:** Hot-reloading of the library while the mega MCP is running. A restart picks up changes. Live watch can be added later.
-- **Out of scope:** Cross-API workflows or tool composition. The mega MCP is a multiplexer, not an orchestrator.
-- **Out of scope:** Changing binary names (`dub-pp-cli`, `dub-pp-mcp`) — these stay as-is. Only the directory key changes.
-- **Known limitation:** CLIs without MCP metadata in their manifest (e.g., generated via `--docs` before Step 1) will not appear in the mega MCP. They need to be regenerated or manually have MCP metadata added.
-
-## Context & Research
-
-### Relevant Code and Patterns
-
-- **Library scanner:** `scanLibrary()` at `internal/cli/library.go:86-144` already iterates `~/printing-press/library/`, reads `.printing-press.json` manifests, and returns structured entries. Inclusion criteria: `naming.IsCLIDirName(dirName) || entry.APIName != ""`. The `APIName != ""` branch means manifest-bearing directories are accepted regardless of naming — this is the migration bridge.
-- **Path construction:** `DefaultOutputDir(apiName)` at `internal/pipeline/pipeline.go:16` returns `filepath.Join(PublishedLibraryRoot(), naming.CLI(apiName))`. This is the single point that determines library directory names.
-- **Naming functions:** `internal/naming/naming.go` has `CLI()`, `MCP()`, `TrimCLISuffix()`, `IsCLIDirName()`. The restructure needs a counterpart for slug-keyed dirs.
-- **CLIManifest MCP fields:** `MCPBinary`, `MCPToolCount`, `MCPPublicToolCount`, `MCPReady`, `AuthType`, `AuthEnvVars`, `NovelFeatures` — all added in Step 1. The mega MCP reads these directly.
-- **Per-API MCP server template:** `main_mcp.go.tmpl` creates an `mcp-go` server via `server.NewMCPServer()` + `server.ServeStdio()`. Tool names follow `{resource}_{endpoint}` pattern (e.g., `links_list`, `links_create`). The `about` tool returns self-describing metadata.
-- **MCP Go SDK (`mark3labs/mcp-go`):** Used in generated templates for per-API MCP servers. Currently NOT a direct dependency of the printing-press binary. Supports `server.ServeStdio()` for server-side. Client-side capabilities (for probing sub-MCPs) need to be verified during implementation.
-- **Publish pipeline:** `PublishWorkingCLI()` at `internal/pipeline/publish.go:83` copies working dir to library and writes manifest. Uses `DefaultOutputDir()` for the target path.
-- **`ClaimOutputDir()`:** Atomic directory claiming with `-2`, `-3` suffixes for reruns. Works on any base path — slug-keyed dirs will work without changes to this function.
-- **`RenameCLI()`** at `internal/pipeline/renamecli.go` already handles MCP directory renaming alongside CLI renaming. Needs update for slug-keyed outer directories.
-- **Goreleaser config:** `internal/generator/templates/goreleaser.yaml.tmpl` builds both CLI and MCP binaries. The printing-press binary's own `.goreleaser.yaml` would need a new entry for the mega MCP binary.
-- **Test patterns:** `PRINTING_PRESS_HOME` env var override for isolation. Table-driven tests with `testify/assert`. `writeTestManifest` helper in pipeline tests.
-
-### Institutional Learnings
-
-- **Layout contract** (`docs/solutions/best-practices/checkout-scoped-printing-press-output-layout`): Published CLIs live at `~/printing-press/library/<dir>/`. The manifest is the source of truth for identity, not the directory name. Rerun suffixes (`-pp-cli-2`) are valid — the actual command entrypoint lives inside `cmd/<api>-pp-cli/`.
-- **Filepath traversal protection** (`docs/solutions/security-issues/filepath-join-traversal-with-user-input`): Any MCP tool name or argument that feeds into `filepath.Join` is an attack surface. Belt-and-suspenders: reject `..`, `/`, `\` in input AND verify resolved path is under library root with `strings.HasPrefix(absResult, absRoot + string(filepath.Separator))`.
-- **Validation must not mutate source directory** (`docs/solutions/best-practices/validation-must-not-mutate-source-directory`): The mega MCP must treat `~/printing-press/library/` as read-only. If it needs to build MCP binaries, use temp directories.
-- **Independent source discovery** (`docs/solutions/best-practices/multi-source-api-discovery-design`): Use `errgroup.Group` (not `WithContext`) when probing CLIs. A broken CLI binary should degrade gracefully (log warning, exclude), not crash the server.
-
-## Key Technical Decisions
-
-- **Separate binary, not a subcommand:** The mega MCP lives at `cmd/printing-press-mcp/main.go` as its own binary. MCP servers are long-running processes invoked by `claude mcp add`; they should not depend on invoking the full printing-press generator binary. The printing-press binary is for code generation; the mega MCP binary is for runtime tool serving. Goreleaser builds both.
-
-- **API-slug directory key with backward compat:** `DefaultOutputDir` changes from `naming.CLI(apiName)` to just `apiName`. The library scanner accepts both layouts — manifest presence (`APIName != ""`) is the inclusion criterion, not directory name suffix. This means existing `dub-pp-cli/` directories continue to work alongside new `dub/` directories. A `library migrate` command is deferred to a follow-up since it's cosmetic — backward-compat discovery makes it unnecessary for functionality.
-
-- **Double-underscore tool namespacing (`dub__links_list`):** Per-API tool names are prefixed with `{normalized-slug}__` (double underscore). Single underscore is already used within tool names (`links_list`), so double underscore is the unambiguous separator. The mega MCP strips the prefix to route to the correct subprocess. Examples: `dub__links_list`, `espn__get_scores`, `steam_web__get_player_summaries`. **Slug-to-prefix normalization:** API slugs are kebab-case (`steam-web`), but MCP tool names cannot contain hyphens. Slugs are normalized to snake_case for the prefix: hyphens → underscores, consecutive underscores collapsed to single (`steam--web` → `steam_web`, not `steam__web`). The normalizer rejects any slug that produces a prefix containing `__` after conversion (would be ambiguous with the separator). In practice this cannot happen with `cleanSpecName()`'s output, but the guard prevents future regressions.
-
-- **Persistent per-API MCP subprocesses:** The mega MCP starts each per-API MCP binary as a long-running subprocess at startup, rather than spawning ephemeral processes per tool call. Rationale: (1) amortizes Go binary startup time, (2) the per-API MCP server may hold state (config, connection pools), (3) the MCP protocol is designed for persistent connections. The subprocess manager restarts crashed processes on the next tool call.
-
-- **Startup-time tool discovery via `tools/list`:** At startup, the mega MCP sends a `tools/list` JSON-RPC request to each running sub-MCP to discover the full tool schema (names, descriptions, parameters). This avoids a new generation artifact and uses the MCP protocol as designed. Discovery results are cached in memory. A `library_info` meta-tool surfaces the catalog for agents.
-
-- **Require pre-built MCP binaries (no build-on-demand):** The mega MCP expects each per-API MCP binary to be pre-built and present in the library directory. It looks for the binary at `cmd/{mcp-binary-name}/{mcp-binary-name}` (the standard `go build` output location) or the Makefile's output path. If the binary doesn't exist, the API is excluded with a clear error message telling the user to run `cd ~/printing-press/library/{slug} && go build -o ./cmd/{name}-pp-mcp/{name}-pp-mcp ./cmd/{name}-pp-mcp`. This avoids the complexity of a build cache, eliminates the runtime Go toolchain requirement for users who receive pre-built binaries, and keeps the mega MCP's startup fast. A follow-up can add build-on-demand for convenience.
-
-- **Credential isolation via env var subtraction:** Each subprocess receives the full parent environment **minus other APIs' `AuthEnvVars`**. For example, the Dub subprocess inherits `DUB_TOKEN` (its own) but NOT `STEAM_WEB_API_KEY` (Steam's). This is implemented by starting from `os.Environ()`, collecting all `AuthEnvVars` from ALL discovered APIs, then removing everything except the current API's vars from that set. System vars (`PATH`, `HOME`, `TMPDIR`, proxy vars like `HTTPS_PROXY`/`HTTP_PROXY`/`NO_PROXY`, TLS vars like `SSL_CERT_FILE`/`SSL_CERT_DIR`, locale vars like `LANG`) are preserved — stripping these would silently break HTTP requests in corporate/non-default environments (Go's `exec.Cmd.Env`, when non-nil, replaces the entire environment). This prevents credential leakage across APIs while preserving all networking and system functionality.
-
-- **`mcp-go` as direct dependency:** Added to the printing-press `go.mod`. The mega MCP uses both server-side (for its own MCP interface) and client-side (for probing sub-MCPs) capabilities. Verify `mcp-go` client support (`client.NewStdioMCPClient` or equivalent) at the start of Unit 5. If `mcp-go` lacks client-side support, implement a JSON-RPC client that handles the MCP initialization handshake (`initialize` → response → `initialized` notification) before sending `tools/list` or `tools/call`. Budget ~200-300 lines for a proper client with handshake, request-response correlation, and timeout handling — not the ~100 lines originally estimated.
-
-- **Printing-press MCP binary naming:** The mega MCP binary is called `printing-press-mcp` (no API-slug prefix, no `-pp-` infix). It represents the printing press itself, not a single API. This distinguishes it from per-API MCP servers (`dub-pp-mcp`, `espn-pp-mcp`).
-
-## Open Questions
-
-### Resolved During Planning
-
-- **Should the directory restructure be in this plan or separate?** → In this plan as Phase 1. The restructure is a prerequisite that makes the mega MCP's discovery cleaner, and the user presented both together.
-- **Separate binary or subcommand?** → Separate binary (`printing-press-mcp`). Clean separation between generation-time and runtime concerns. MCP servers are long-running; coupling with the generator binary adds unnecessary weight.
-- **How to namespace tool names?** → Double-underscore prefix: `{api-slug}__{tool-name}`. Unambiguous since single underscore is already used within tool names. Easy to split for routing.
-- **Ephemeral or persistent subprocesses?** → Persistent. Amortizes startup, supports stateful per-API servers, matches MCP protocol's session model.
-- **Where does tool schema come from?** → Probed from running sub-MCPs via `tools/list` at startup. No new generation artifact needed.
-- **How does auth work?** → Environment inheritance. User configures env vars in `claude mcp add --env`. Mega MCP passes them through to subprocesses.
-
-### Deferred to Implementation
-
-- **`mcp-go` client-side capabilities** — verify whether `mark3labs/mcp-go` supports MCP client operations (`initialize`, `tools/list`, `tools/call` over stdio). If not, implement a JSON-RPC client with MCP handshake support (~200-300 lines). Check at the start of Unit 5 since it affects subprocess manager design.
-- **Tool name collision across APIs** — the prefix + `__` + original name scheme could theoretically produce collisions. Unit 5 validates uniqueness at registration time. Verify with the current 6 CLIs during implementation.
-- **MCP proxy alternatives** — the MCP specification supports native proxy/aggregation patterns, and tools like `mcp-proxy` exist. The custom approach is chosen for: control over tool naming, `library_info` integration, auth-aware credential isolation, and library-specific discovery. If implementation complexity exceeds expectations, evaluate off-the-shelf alternatives before building more than Unit 4.
-
-### Resolved During Document Review
-
-- **Subprocess restart strategy** — resolved: restart once on crash (not 3 retries). Log crash events visibly. Don't retry the call that caused the crash. Exclude on failed restart.
-- **RenameCLI adaptation for slug-keyed directories** — resolved: option (a). Modify `validateRenameInputs()` to accept names that pass either `IsCLIDirName()` or `IsLibraryDir()`.
-- **Serialized vs multiplexed subprocess communication** — resolved: multiplexed using JSON-RPC `id` correlation. Head-of-line blocking from serialized model would degrade UX, and multiplexing is ~50 lines beyond what serialized needs.
-- **Credential isolation** — resolved: subtraction model. Each subprocess gets full parent env minus other APIs' AuthEnvVars. Preserves networking/TLS/proxy vars. Safer than allowlist approach which would silently break HTTP in corporate environments.
-
-## High-Level Technical Design
-
-> *This illustrates the intended approach and is directional guidance for review, not implementation specification. The implementing agent should treat it as context, not code to reproduce.*
-
-```
-                              ┌─────────────────────────┐
-                              │     Agent (Claude)       │
-                              └──────────┬──────────────┘
-                                         │ MCP (stdio)
-                              ┌──────────▼──────────────┐
-                              │   printing-press-mcp     │
-                              │   (Mega MCP Server)      │
-                              │                          │
-                              │  ┌───────────────────┐   │
-                              │  │   Tool Router      │   │
-                              │  │   dub__*  → dub    │   │
-                              │  │   espn__* → espn   │   │
-                              │  │   steam_web__* → … │   │
-                              │  └───────┬───────────┘   │
-                              │          │               │
-                              │  ┌───────▼───────────┐   │
-                              │  │ Subprocess Manager │   │
-                              │  │ (per-API MCP bins) │   │
-                              │  └───────┬───────────┘   │
-                              └──────────┼──────────────┘
-                       ┌─────────────────┼──────────────────┐
-                       │                 │                   │
-              ┌────────▼──────┐  ┌───────▼───────┐  ┌───────▼───────┐
-              │  dub-pp-mcp   │  │ espn-pp-mcp   │  │steam-web-pp-  │
-              │  (subprocess) │  │ (subprocess)   │  │mcp (subproc)  │
-              └───────────────┘  └───────────────┘  └───────────────┘
-```
-
-**Startup flow:**
-1. Scan `~/printing-press/library/` for directories with `.printing-press.json`
-2. Filter to `MCPReady != ""` entries with pre-built MCP binaries
-3. For each: start MCP binary as subprocess, perform MCP handshake (`initialize` → response → `initialized`)
-4. Send `tools/list` to each initialized subprocess, collect tool schemas
-5. Register all tools in the mega MCP with `{normalized-slug}__` prefix
-6. Register `library_info` and `about` meta-tools
-7. Begin serving on stdio
-
-**Tool call flow:**
-1. Agent calls `dub__links_list` with arguments
-2. Mega MCP extracts prefix `dub`, tool name `links_list`
-3. Routes to the `dub` subprocess: sends `tools/call` with original tool name and arguments
-4. Returns the subprocess response to the agent
-
-## Implementation Units
-
-### Phase 1: Directory Restructure
-
-- [ ] **Unit 1: Change library directory key from CLI name to API slug**
-
-  **Goal:** New CLIs land in `~/printing-press/library/{api-slug}/` instead of `~/printing-press/library/{api-slug}-pp-cli/`. Existing directories with either naming continue to be discovered.
-
-  **Requirements:** R1, R2
-
-  **Dependencies:** None
-
-  **Files:**
-  - Modify: `internal/pipeline/pipeline.go` (`DefaultOutputDir`)
-  - Modify: `internal/cli/root.go` (lines 325-338: the `generate --spec` path renames output dir to `naming.CLI(apiSpec.Name)` — this would undo the slug-keyed change. Update `derivedDir` to use API slug instead of `naming.CLI()`)
-  - Modify: `internal/cli/library.go` (`scanLibrary` inclusion criteria)
-  - Modify: `internal/cli/emboss.go` (lines 196-211: library CLI resolution tries exact match then `naming.CLI(target)` — add `naming.TrimCLISuffix(target)` fallback for slug-keyed lookup)
-  - Modify: `internal/naming/naming.go` (add `IsLibraryDir()` that accepts slug-keyed dirs)
-  - Test: `internal/pipeline/pipeline_test.go`
-  - Test: `internal/cli/library_test.go`
-  - Test: `internal/naming/naming_test.go`
-
-  **Not modified:** `internal/pipeline/paths.go` (`WorkingCLIDir`) — this constructs ephemeral working directories during generation runs, not published library paths. The working dir naming (`naming.CLI()`) is unchanged. Also not modified: `internal/pipeline/dogfood.go` and `internal/pipeline/runtime.go` — their `IsCLIDirName` calls scan `cmd/` subdirectories within CLIs (binary names), not library directories.
-
-  **Approach:**
-  `DefaultOutputDir` changes from `filepath.Join(PublishedLibraryRoot(), naming.CLI(apiName))` to `filepath.Join(PublishedLibraryRoot(), apiName)`. `scanLibrary` already has the `entry.APIName != ""` branch that accepts manifest-bearing directories regardless of naming — this becomes the primary inclusion path. `IsCLIDirName` check stays as a fallback for legacy directories without manifests (unlikely but safe). Add `IsLibraryDir(name)` to naming package that returns true for valid API slugs (no path separators, no `..`, non-empty).
-
-  **This unit does NOT rename existing directories.** Old `dub-pp-cli/` directories continue to be discovered via backward-compat logic. Only newly published CLIs get slug-keyed directories. A `library migrate` command is deferred to a follow-up since backward-compat discovery means migration is cosmetic, not functional.
-
-  **Critical: `generate --spec` rename logic.** At `internal/cli/root.go:325-338`, the `--spec` code path explicitly renames the output directory to `naming.CLI(apiSpec.Name)` after generation. This would undo the `DefaultOutputDir` change for every `--spec` run without explicit `--output`. Update `derivedDir` to use the API slug directly instead of `naming.CLI()`. The `--docs` code path at lines 148-200 does NOT have this rename logic and works correctly as-is.
-
-  **Emboss library resolution.** At `internal/cli/emboss.go:196-211`, the emboss command resolves library CLIs by trying the exact target name, then `naming.CLI(target)`. After restructuring, `emboss dub-pp-cli` would try `naming.CLI("dub-pp-cli")` = `"dub-pp-cli-pp-cli"` (double-suffixed). Add a fallback: try `naming.TrimCLISuffix(target)` to resolve the slug.
-
-  **Patterns to follow:**
-  - `naming.IsCLIDirName()` pattern for the new `IsLibraryDir()`
-  - `scanLibrary()` manifest-first discovery pattern
-  - `PRINTING_PRESS_HOME` env var override in tests
-
-  **Test scenarios:**
-  - Happy path: `DefaultOutputDir("dub")` returns `~/printing-press/library/dub` (not `dub-pp-cli`)
-  - Happy path: `scanLibrary` finds a CLI at `library/dub/` with a valid manifest
-  - Happy path: `scanLibrary` finds a CLI at `library/dub-pp-cli/` with a valid manifest (backward compat)
-  - Happy path: `IsLibraryDir("dub")` returns true, `IsLibraryDir("dub-pp-cli")` returns true
-  - Edge case: `scanLibrary` ignores directories without manifests AND without CLI suffix (e.g., `library/.DS_Store/`)
-  - Edge case: `IsLibraryDir("")` returns false, `IsLibraryDir("../etc")` returns false
-  - Edge case: Rerun suffix directories (`library/dub-2/`) are discovered when they have manifests
-  - Edge case: `ClaimOutputDir` works correctly with slug-keyed base path (append `-2`, `-3`)
-
-  **Verification:** `go test ./internal/pipeline/... ./internal/cli/... ./internal/naming/...` passes. New CLIs published during a fullrun land in slug-keyed directories.
-
-- [ ] **Unit 2: Update publish pipeline for slug-keyed paths**
-
-  **Goal:** The publish pipeline writes to slug-keyed directories and updates module paths, branch names, and collision detection accordingly.
-
-  **Requirements:** R9
-
-  **Dependencies:** Unit 1
-
-  **Files:**
-  - Modify: `internal/pipeline/publish.go` (if any path construction uses `naming.CLI` for library dir)
-  - Modify: `internal/pipeline/renamecli.go` (directory rename logic — now renames API slug as directory, binary names as content)
-  - Modify: `internal/cli/publish.go` (the `publish package` command constructs `outCLIDir` using `cliName` — must use API slug from `manifest.APIName` instead; also `stashExistingCLI` search, `RewriteModulePath`, and module path construction)
-
-  **Not modified in this plan:** `skills/printing-press-publish/SKILL.md` — the skill's branch naming (`feat/dub-pp-cli` → `feat/dub`) and registry entry paths reference the public library repo, which is out of scope. Updating the skill before the public repo accepts slug-keyed directories would cause the skill to produce broken PRs. The skill update belongs in the public library migration PR.
-
-  **Not modified in this plan:** `internal/generator/templates/readme.md.tmpl` — the README template's `go install` path points at the public library repo, which is out of scope for this plan. Changing the template now would generate install paths referencing a directory layout (`library/<cat>/dub/...`) that doesn't exist in the public repo until the separate migration PR lands. The template change belongs in that PR.
-  - Test: `internal/pipeline/renamecli_test.go`
-
-  **Approach:**
-  `PublishWorkingCLI` already delegates to `DefaultOutputDir` (updated in Unit 1), so the local publish path changes automatically. `RenameCLI` needs two changes: (1) **Validation:** Modify `validateRenameInputs()` at `renamecli.go:162-165` to accept names that pass either `IsCLIDirName()` or `IsLibraryDir()` (from Unit 1). Currently it strictly requires `-pp-cli`/`-cli` suffix and would reject slug-keyed names like `dub`. (2) **Inner logic:** `RenameCLI`'s inner logic derives `cmd/` subdirectory names from `naming.TrimCLISuffix(oldCLIName)`. When the outer directory is slug-keyed (e.g., `dub`), the function must still receive the CLI name (e.g., `dub-pp-cli`) as the `oldCLIName` parameter so inner `cmd/` directory resolution works correctly. The slug is the directory key; the CLI name is the rename parameter.
-
-  **`internal/cli/publish.go` changes:** The `publish package` command at line ~290 constructs `outCLIDir = filepath.Join(dest, "library", category, cliName)` — this must change to use the API slug (from the manifest's `APIName` field) instead of `cliName`. The `stashExistingCLI` function searches by CLI name across categories — update to search by API slug. The `RewriteModulePath` call uses the directory path which now contains the slug. The `modulePath` flag's default also needs updating.
-
-  The publish skill's collision detection and branch naming are deferred to the public library migration PR (see "Not modified" note above).
-
-  **Patterns to follow:**
-  - Existing `RenameCLI` test patterns in `renamecli_test.go`
-  - README template auth-conditional blocks
-
-  **Test scenarios:**
-  - Happy path: `RenameCLI` renames a slug-keyed directory (e.g., `dub/` to `newname/`) and updates CLI/MCP binary names in content
-  - Happy path: `RenameCLI` with slug-keyed directory correctly renames inner `cmd/` subdirectories when passed the CLI name as the rename parameter
-  - Edge case: `RenameCLI` correctly handles the case where the old directory is CLI-name-keyed (backward compat during transition)
-
-  **Verification:** `go test ./internal/pipeline/...` passes.
-
-### Phase 2: Mega MCP Server
-
-- [ ] **Unit 3: Add `mcp-go` dependency and create `internal/megamcp/` package with discovery**
-
-  **Goal:** The mega MCP can scan the local library and build an in-memory catalog of all MCP-ready APIs with their metadata.
-
-  **Requirements:** R3, R7
-
-  **Dependencies:** Units 1-2 (slug-keyed paths make discovery cleaner, but the scanner works with both layouts)
-
-  **Files:**
-  - Modify: `go.mod` (add `github.com/mark3labs/mcp-go`)
-  - Create: `internal/megamcp/discovery.go`
-  - Create: `internal/megamcp/discovery_test.go`
-
-  **Approach:**
-  Add `mcp-go` as a direct dependency. Create the `internal/megamcp/` package with a `DiscoverAPIs(libraryRoot string) ([]APIEntry, error)` function. `APIEntry` contains:
-  - `Slug string` — API slug derived from the manifest's `APIName` field (e.g., `"dub"`)
-  - `Dir string` — absolute path to the library directory
-  - `Manifest pipeline.CLIManifest` — the full parsed manifest
-  - `MCPBinaryPath string` — absolute path to the pre-built MCP binary (resolved in Unit 5)
-  - `NormalizedPrefix string` — slug normalized to snake_case for tool namespacing (e.g., `"steam_web"`)
-
-  The `Manifest.MCPBinary` field (e.g., `"dub-pp-mcp"`) is set by `naming.MCP(apiName)` during generation. It matches the `cmd/{name}-pp-mcp/` source directory name exactly. This is guaranteed by the generator — both `cmd/` directory creation and manifest population use `naming.MCP()`.
-
-  Discovery iterates `libraryRoot`, reads `.printing-press.json` from each directory (reusing `pipeline.CLIManifest` struct), and filters to entries where `MCPReady` is non-empty and `MCPBinary` is non-empty. Uses `errgroup.Group` (not `WithContext`) so a broken manifest doesn't block discovery of other CLIs. Applies path traversal validation from the institutional learning: reject directory names containing `..`, `/`, or `\`.
-
-  `NormalizedPrefix` is computed from the slug: hyphens → underscores, consecutive underscores collapsed, validated to not contain `__` (which would collide with the tool name separator).
-
-  **Binary resolution (merged from former Unit 4):** After manifest parsing, `ResolveMCPBinary(entry *APIEntry) error` searches for the pre-built MCP binary within the library directory. Search order: (1) `{dir}/cmd/{mcp-binary}/{mcp-binary}` (standard `go build` output), (2) `{dir}/{mcp-binary}` (Makefile output to project root). The `mcp-binary` name comes from `entry.Manifest.MCPBinary` (e.g., `dub-pp-mcp`), which is set by `naming.MCP(apiName)` during generation (Step 1, PR #145). This field is guaranteed to match the `cmd/{name}-pp-mcp/` directory name.
-
-  `FilterReady(entries []APIEntry) (ready []APIEntry, warnings []string)` returns only entries with resolvable binaries. For each entry without a binary, it returns a warning message telling the user exactly how to build it: `cd ~/printing-press/library/{slug} && go build -o ./cmd/{name}-pp-mcp/{name}-pp-mcp ./cmd/{name}-pp-mcp`. This is deliberately minimal — no build cache, no `go build` invocation, no Go toolchain dependency.
-
-  **Patterns to follow:**
-  - `scanLibrary()` in `internal/cli/library.go` — directory iteration and manifest parsing pattern
-  - `errgroup.Group` without context per multi-source discovery learning
-  - `os.Stat` for binary existence checking
-  - `PRINTING_PRESS_HOME` env var in tests
-
-  **Test scenarios:**
-  - Happy path: Library with 3 CLIs (full, partial, cli-only readiness) → returns 2 entries (full + partial), excludes cli-only
-  - Happy path: CLI in slug-keyed directory (`library/dub/`) discovered correctly
-  - Happy path: CLI in CLI-name-keyed directory (`library/dub-pp-cli/`) discovered correctly
-  - Happy path: API slug `steam-web` → NormalizedPrefix `steam_web`
-  - Happy path: Binary exists at `cmd/{name}-pp-mcp/{name}-pp-mcp` → resolved
-  - Happy path: Binary exists at project root `{name}-pp-mcp` → resolved
-  - Edge case: Empty library → returns empty slice, no error
-  - Edge case: Library with a directory missing `.printing-press.json` → skipped with no error
-  - Edge case: Library with a corrupt manifest JSON → skipped with warning, other CLIs still discovered
-  - Edge case: Directory name contains `..` → rejected (traversal protection)
-  - Edge case: Binary missing → excluded with actionable warning message
-  - Edge case: Binary exists but is not executable → excluded with warning
-
-  **Verification:** `go test ./internal/megamcp/...` passes. `go mod tidy` succeeds.
-
-- [ ] **Unit 4: Subprocess manager**
-
-  **Goal:** The mega MCP can start, monitor, and communicate with per-API MCP subprocesses over stdio.
-
-  **Requirements:** R5, R6
-
-  **Dependencies:** Unit 3
-
-  **Files:**
-  - Create: `internal/megamcp/subprocess.go`
-  - Create: `internal/megamcp/subprocess_test.go`
-
-  **Approach:**
-  `SubprocessManager` manages the lifecycle of per-API MCP subprocesses. Each subprocess is a `*exec.Cmd` running the pre-built MCP binary with stdin/stdout piped for JSON-RPC communication.
-
-  **First task in this unit:** Verify `mcp-go` client support. Check whether `mark3labs/mcp-go` provides `client.NewStdioMCPClient` or equivalent. If it does, use it — it will handle the MCP initialization handshake, request-response correlation, and JSON-RPC framing. If not, implement a lightweight MCP client (~200-300 lines) that handles: (a) the three-message initialization handshake (`initialize` request → server response → `initialized` notification), (b) JSON-RPC 2.0 request/response correlation via `id` field, (c) timeout per request.
-
-  `Start(entry APIEntry) (*ManagedProcess, error)` starts the MCP binary with a credential-isolated environment (full parent env minus other APIs' `AuthEnvVars` per R6), then performs the MCP initialization handshake. The subprocess is not considered ready until the handshake completes successfully. `Stop(slug string)` sends SIGTERM and waits. `StopAll()` stops all subprocesses (called at mega MCP shutdown).
-
-  `SendRequest(slug string, method string, params json.RawMessage) (json.RawMessage, error)` sends a JSON-RPC request to a subprocess and reads the response. Used for both `tools/list` (at startup) and `tools/call` (at runtime). Includes a timeout to prevent hanging on unresponsive subprocesses.
-
-  **Concurrency model:** Per-subprocess communication is **multiplexed** using JSON-RPC `id` correlation. The `SubprocessManager` maintains a `map[int]chan json.RawMessage` of pending requests and a goroutine per subprocess reading stdout responses and dispatching them to the correct channel by `id`. This allows concurrent tool calls to the same API (~50 lines beyond what a serialized approach would need). Multiplexing is chosen over serialization because: (1) the mega MCP aggregates N APIs behind one connection — head-of-line blocking from a slow upstream API would stall all subsequent calls to that API, (2) the JSON-RPC id correlation is already needed for the MCP handshake, and (3) retrofitting multiplexing later requires rewriting the core I/O path.
-
-  **Stdout reader dispatch logic:** The reader goroutine must handle three message types: (1) JSON-RPC responses with `id` field → dispatch to matching pending request channel, (2) JSON-RPC notifications without `id` field (MCP servers may send progress, logging, resource update notifications at any time) → discard or route to a notification handler, (3) non-JSON lines (Go panics, diagnostic output) → skip and log. Restart is protected by a per-subprocess mutex so concurrent crash detection results in exactly one restart attempt.
-
-  If a subprocess has died when a tool call arrives, the manager attempts one restart (including re-running the MCP handshake). If the restart fails, the subprocess is excluded until the next mega MCP restart. Crash events are logged visibly to stderr with the subprocess name and timestamp. The manager does NOT silently retry the tool call that caused the crash — it returns an error to the agent. The next distinct tool call triggers the restart attempt.
-
-  **Subprocess stderr:** Explicitly set `cmd.Stderr` to a per-subprocess log buffer or file — do NOT inherit the mega MCP's stderr (which would leak per-API diagnostic messages, potentially including credential-shaped strings, into the mega MCP's output stream). The stdout reader must handle non-JSON lines gracefully (skip and log) since Go panics and diagnostic output may appear on stdout despite best efforts.
-
-  **Patterns to follow:**
-  - `exec.Command` with `StdinPipe()` and `StdoutPipe()` for bidirectional communication
-  - JSON-RPC 2.0 message format matching `mcp-go`'s wire protocol — verify framing (newline-delimited JSON vs LSP-style Content-Length headers) by reading `mcp-go`'s `ServeStdio` source at the start of this unit
-  - MCP specification initialization handshake: `initialize` (client→server), response (server→client), `initialized` notification (client→server)
-
-  **Test scenarios:**
-  - Happy path: Start a subprocess, MCP handshake completes, send `tools/list`, receive tool list response
-  - Happy path: Send `tools/call` with arguments, receive tool result
-  - Happy path: Subprocess receives its own AuthEnvVars but NOT other APIs' AuthEnvVars (DUB_TOKEN present for Dub, STEAM_WEB_API_KEY absent; system/networking vars like PATH, HTTPS_PROXY preserved)
-  - Happy path: Concurrent tool calls to same API are multiplexed (both in-flight simultaneously, responses matched by id)
-  - Edge case: MCP handshake fails (subprocess returns error) → subprocess excluded with warning
-  - Edge case: Subprocess crashes → crash logged to stderr, error returned to agent, next tool call triggers one restart attempt
-  - Edge case: Subprocess unresponsive → request times out with error
-  - Edge case: Stop all subprocesses → all processes terminated, no zombies
-  - Edge case: Subprocess fails to restart → excluded from routing with warning
-  - Integration: Full round-trip with a real (simple) MCP binary — verify initialization handshake + tool call correctness
-
-  **Verification:** `go test ./internal/megamcp/...` passes. No zombie processes after test cleanup.
-
-- [ ] **Unit 5: Tool aggregation and routing**
-
-  **Goal:** The mega MCP discovers tools from all sub-MCPs, registers them with namespaced names, and routes incoming tool calls to the correct subprocess.
-
-  **Requirements:** R5
-
-  **Dependencies:** Unit 4
-
-  **Files:**
-  - Create: `internal/megamcp/router.go`
-  - Create: `internal/megamcp/router_test.go`
-
-  **Approach:**
-  `ToolRouter` is initialized at startup. For each running subprocess, it sends `tools/list` and collects the tool schemas. Each tool is re-registered in the mega MCP with the name `{normalized-prefix}__{original-name}` (double underscore separator). The router maintains a mapping from prefixed tool name → (normalized prefix, original tool name).
-
-  When a tool call arrives:
-  1. Parse the prefix: split on `__` to extract the normalized prefix and original tool name
-  2. Validate the prefix exists in the router
-  3. Forward the call to the correct subprocess via `SendRequest("tools/call", ...)`
-  4. Return the subprocess response to the agent
-
-  **Tool name collision detection:** At registration time, validate that no prefixed tool name is ambiguous. The `__` separator could theoretically collide if an original tool name starts with a known prefix + `_` (e.g., API slug `user` + tool `accounts_list` → `user__accounts_list`, but tool `user_accounts_list` from a different API with slug `user` would produce the same prefixed name). The router validates at registration time that all prefixed names are unique and logs a warning for any collision. If a tool name itself contains `__`, the router splits on the first occurrence only.
-
-  **Tool count awareness:** With 6 CLIs and ~270 tools, MCP clients may experience degraded tool selection. The plan ships with all tools registered at startup. If tool count becomes a UX problem at scale, a follow-up can implement lazy tool registration (register only meta-tools initially, dynamically register per-API tools on agent intent via a `use_api` meta-tool). This is documented as a known tradeoff.
-
-  Traversal protection: the normalized prefix extracted from the tool name is validated against the known set of prefixes — it never flows into path construction.
-
-  **Patterns to follow:**
-  - `mcp-go` tool registration via `server.AddTool()`
-  - Existing `makeAPIHandler` pattern in `mcp_tools.go.tmpl` for handler function signatures
-
-  **Test scenarios:**
-  - Happy path: Tools from 3 APIs registered with correct prefixes → `dub__links_list`, `espn__get_scores`, etc.
-  - Happy path: Tool call `dub__links_list` routed to dub subprocess with original name `links_list`
-  - Happy path: Tool call arguments forwarded correctly to subprocess
-  - Edge case: Tool call with unknown prefix → returns MCP error "unknown API: xyz"
-  - Edge case: Tool call with unknown tool name for a known API → forwards to subprocess (let the sub-MCP handle the error)
-  - Edge case: Tool name containing `__` within the original name → split on first occurrence only
-  - Edge case: Subprocess died → router returns error to agent, next distinct call triggers restart attempt
-
-  **Verification:** `go test ./internal/megamcp/...` passes. End-to-end tool call routing works with mock subprocess.
-
-- [ ] **Unit 6: `library_info` and `about` meta-tools**
-
-  **Goal:** The mega MCP exposes discovery tools that let agents learn what APIs are available, their capabilities, and auth requirements.
-
-  **Requirements:** R4, R6
-
-  **Dependencies:** Unit 5
-
-  **Files:**
-  - Create: `internal/megamcp/metatools.go`
-  - Create: `internal/megamcp/metatools_test.go`
-
-  **Approach:**
-  Two meta-tools registered directly in the mega MCP (not routed to subprocesses):
-
-  **`library_info`**: Returns a JSON object listing all discovered APIs:
-  ```
-  {
-    "apis": [
-      {
-        "slug": "dub",
-        "name": "Dub",
-        "description": "Short link management",
-        "tool_count": 53,
-        "public_tool_count": 0,
-        "auth_type": "api_key",
-        "auth_env_vars": ["DUB_TOKEN"],
-        "mcp_ready": "full",
-        "auth_configured": true,
-        "novel_features": [...]
-      },
-      ...
-    ],
-    "total_tools": 270,
-    "total_apis": 6
-  }
-  ```
-  The `auth_configured` field checks `os.Getenv()` for each API's env vars — returning a boolean "ready" status per API without naming which specific env vars are or aren't set. This prevents `library_info` from serving as a credential reconnaissance tool if a prompt injection reaches the agent. The `auth_env_vars` array lists what's needed (from the manifest); `auth_configured` says whether it's satisfied.
-
-  **`about`**: Returns metadata about the mega MCP itself — version, library root, total API count, total tool count. Matches the pattern of per-API `about` tools from Step 1.
-
-  **Patterns to follow:**
-  - Per-API `about` tool pattern from `mcp_tools.go.tmpl`
-  - `CLIManifest` field access patterns
-
-  **Test scenarios:**
-  - Happy path: `library_info` returns all discovered APIs with correct metadata
-  - Happy path: `library_info` shows `auth_configured: true` when env var is set
-  - Happy path: `library_info` shows `auth_configured: false` when env var is missing
-  - Happy path: `about` returns mega MCP version and summary stats
-  - Edge case: No APIs discovered → `library_info` returns empty `apis` array with `total_tools: 0`
-
-  **Verification:** `go test ./internal/megamcp/...` passes. Meta-tool responses are valid JSON.
-
-- [ ] **Unit 7: Entry point, distribution, and setup**
-
-  **Goal:** The mega MCP is buildable, distributable, and installable with a single `claude mcp add` command.
-
-  **Requirements:** R8
-
-  **Dependencies:** Unit 6
-
-  **Files:**
-  - Create: `cmd/printing-press-mcp/main.go`
-  - Modify: `internal/version/version.go` (reuse for mega MCP version reporting)
-
-  **Approach:**
-  `main.go` creates an `mcp-go` server, runs the discovery + subprocess startup pipeline, registers the aggregated tools and meta-tools, and serves on stdio via `server.ServeStdio()`. Graceful shutdown on SIGINT/SIGTERM stops all subprocesses.
-
-  **Initial distribution: `go install` only.** Do NOT add a goreleaser entry yet. The mega MCP requires per-API MCP binaries to be pre-built in the library, which only works when the user has Go installed (the `go install` path). Adding goreleaser would distribute a standalone binary to users who may not have Go, resulting in a mega MCP that silently excludes all APIs. Goreleaser distribution is a follow-up that depends on pre-building MCP binaries during the publish pipeline.
-
-  Install and usage:
-  ```
-  # Install
-  go install github.com/mvanhorn/cli-printing-press/cmd/printing-press-mcp@latest
-
-  # Add to Claude Code
-  claude mcp add printing-press -- printing-press-mcp
-
-  # Add to Claude Desktop (config.json)
-  { "mcpServers": { "printing-press": { "command": "printing-press-mcp" } } }
-  ```
-
-  The mega MCP reuses `internal/version.Version` for its version string so it stays in sync with the printing-press binary across releases.
-
-  **Patterns to follow:**
-  - `cmd/printing-press/main.go` entry point pattern
-  - `main_mcp.go.tmpl` for `mcp-go` server setup
-
-  **Test scenarios:**
-  - Happy path: `go build ./cmd/printing-press-mcp` succeeds
-  - Happy path: `printing-press-mcp --help` or version flag works (if supported by mcp-go server)
-  - Integration: Start mega MCP with a test library containing 2 CLIs → `tools/list` returns all tools prefixed, `library_info` returns catalog
-
-  **Verification:** `go build -o ./printing-press-mcp ./cmd/printing-press-mcp` succeeds. Binary starts and serves MCP on stdio.
-
-## System-Wide Impact
-
-- **Interaction graph:** The mega MCP introduces a new process tree: `printing-press-mcp` → N per-API MCP subprocesses. Claude Desktop/Code communicates with the mega MCP via stdio; the mega MCP communicates with sub-MCPs via stdio. The per-API MCP servers make HTTP requests to their respective APIs (unchanged).
-- **Error propagation:** Subprocess errors (crashed process, build failure, timeout) are caught at the mega MCP layer and returned as MCP tool errors. Individual API failures do not crash the mega MCP or affect other APIs.
-- **State lifecycle risks:** The subprocess manager holds process handles that must be cleaned up on shutdown. If the mega MCP crashes without cleanup, orphan processes may linger. Graceful shutdown handlers and process group management mitigate this.
-- **API surface parity:** The mega MCP's tool list should exactly match the union of all per-API MCP tool lists (plus meta-tools). Any discrepancy means the discovery or registration logic has a bug.
-- **Unchanged invariants:** Per-API MCP servers (`dub-pp-mcp`, `espn-pp-mcp`) continue to work independently. The mega MCP is additive — it does not modify or replace per-API MCP functionality. Users who prefer individual MCP connections can still use them.
-
-## Risks & Dependencies
-
-| Risk | Mitigation |
-|------|------------|
-| `mcp-go` lacks client-side support for communicating with sub-MCPs | Implement a JSON-RPC client with MCP handshake support (~200-300 lines). Verify `mcp-go` client support at start of Unit 6 before committing to custom implementation. |
-| Pre-built MCP binaries not present in library | Clear error messages with exact build commands. Initially scoped to `go install` users (who have Go). Follow-up: pre-build during publish pipeline to support binary-only distribution. |
-| Subprocess startup time for large libraries | With 6 CLIs, startup should be <10s. If the library grows to 50+, consider lazy subprocess startup (start on first tool call, not at boot). |
-| Orphan processes on ungraceful shutdown | Primary mitigation: sub-MCPs detect parent death via stdin EOF (mcp-go's `ServeStdio` exits when stdin closes, which happens when the parent dies). Secondary: SIGINT/SIGTERM handler calls `StopAll()`. Known limitation: SIGKILL leaves no cleanup window — document that orphan processes are possible after hard kills. macOS has no `PR_SET_PDEATHSIG`; stdin EOF is the cross-platform mechanism. |
-| Breaking `go install` for existing users after directory restructure | Old `go install .../dub-pp-cli/cmd/dub-pp-cli@latest` paths stop working. The migration PR in the public library repo must document this. Binary names are unchanged — only the directory component of the install path changes. |
-| Directory restructure conflicts with in-flight publish PRs | Coordinate: ensure no publish PRs are open when the public library migration PR lands. The 6 existing CLIs are stable per the user's input. |
-
-## Phased Delivery
-
-### Phase 1: Directory Restructure (Units 1-2)
-Can be shipped as its own PR. Unblocks Phase 2 but also stands alone as a correctness improvement (manuscripts already slug-keyed; library should match).
-
-### Phase 2: Mega MCP Server (Units 3-7)
-Depends on Phase 1 for clean discovery, but the mega MCP scanner works against either layout via backward-compat logic. Can be shipped as one PR or split into two (discovery/subprocess in one, routing/meta-tools/entry-point in another).
-
-**Prerequisite for testing:** Existing library CLIs were generated before Step 1 (PR #145) and lack MCP metadata. At least 2-3 CLIs must be regenerated (or embossed) with the MCP readiness layer before the mega MCP can be validated against real data. Integration tests in Unit 7 should use both synthetic test fixtures and at least one real regenerated CLI.
-
-### Follow-up (not in this plan)
-- `library migrate` command — rename existing `dub-pp-cli/` directories to `dub/` (cosmetic since backward-compat discovery works without it)
-- Public library repo migration (rename directories, update registry.json paths, update go.mod module paths)
-- Goreleaser distribution of `printing-press-mcp` (depends on pre-built MCP binaries)
-- Build MCP binaries at publish time (avoid runtime Go toolchain requirement, enable goreleaser distribution)
-- Build-on-demand cache for MCP binaries (convenience for users who modify library source)
-- MCP binary integrity verification — record SHA-256 hash in manifest at publish time, verify before execution at startup
-- Lazy tool registration — register only meta-tools at startup, dynamically register per-API tools on agent intent (addresses tool count scaling at 50+ APIs)
-- Lazy subprocess startup for large libraries (start on first tool call, idle timeout to reclaim memory)
-- Live library watch (detect newly published CLIs without restart)
-- Marketplace listing for the mega MCP
-
-## Sources & References
-
-- Related PR: mvanhorn/cli-printing-press#145 (Step 1: MCP readiness layer)
-- Related plan: `docs/plans/2026-04-05-001-feat-mcp-readiness-layer-plan.md`
-- Institutional learnings:
-  - `docs/solutions/best-practices/checkout-scoped-printing-press-output-layout` (layout contract)
-  - `docs/solutions/security-issues/filepath-join-traversal-with-user-input` (traversal protection)
-  - `docs/solutions/best-practices/validation-must-not-mutate-source-directory` (immutable source)
-  - `docs/solutions/best-practices/multi-source-api-discovery-design` (errgroup pattern)
diff --git a/docs/plans/2026-04-06-003-feat-mega-mcp-generic-proxy-plan.md b/docs/plans/2026-04-06-003-feat-mega-mcp-generic-proxy-plan.md
deleted file mode 100644
index 6419ab77..00000000
--- a/docs/plans/2026-04-06-003-feat-mega-mcp-generic-proxy-plan.md
+++ /dev/null
@@ -1,460 +0,0 @@
----
-title: "feat: Mega MCP — Generic HTTP Proxy with Activation Model"
-type: feat
-status: active
-date: 2026-04-06
-origin: docs/brainstorms/2026-04-06-mega-mcp-generic-proxy-requirements.md
----
-
-# feat: Mega MCP — Generic HTTP Proxy with Activation Model
-
-## Overview
-
-A single MCP server binary (`printing-press-mcp`) that gives agents access to every API in the printing press library through one install. The mega MCP fetches pre-computed tool manifests from the public library repo on GitHub, registers tools on demand via an activation model, and proxies HTTP requests directly to APIs — no local library, no subprocess management, no Go toolchain at runtime.
-
-This plan has two phases:
-1. **Publish pipeline prerequisites** — generate `tools-manifest.json` per API at publish time, extend the registry schema
-2. **Mega MCP binary** — new binary that fetches manifests, registers meta-tools, proxies HTTP calls
-
-## Problem Frame
-
-Every printed CLI ships an MCP binary, but each requires individual installation via `go install` of a long GitHub path. With 6 CLIs and a growing catalog, this friction prevents adoption. The mega MCP reduces setup to one `claude mcp add` command for all APIs. (see origin: `docs/brainstorms/2026-04-06-mega-mcp-generic-proxy-requirements.md`)
-
-## Requirements Trace
-
-**Distribution:** R1 (single binary), R2 (one-command setup), R3 (marketplace-ready)
-**Discovery:** R4 (registry fetch), R5 (manifest fetch + cache), R6 (library_info), R7 (setup_guide), R8 (activate_api), R9 (search_tools)
-**Routing:** R10 (tool registration from manifest), R11 (generic HTTP handler with param classification), R12 (startup latency)
-**Auth:** R13 (format string expansion), R14 (auth type support), R15 (credential isolation), R16 (error handling)
-**Security:** R17 (integrity verification), R18 (URL validation), R19 (text sanitization)
-**Scaling:** R20 (API filter), R21 (activation model scaling)
-
-## Scope Boundaries
-
-- **In scope:** Generic HTTP proxy, activation model, manifest generation, registry extension, marketplace metadata
-- **Out of scope:** Subprocess/hybrid mode, novel features (sync/search/SQL), directory restructure, hot-reloading, interactive OAuth, adaptive rate limiting
-- **Known limitation:** Cookie/composed auth APIs expose only public endpoints. Tool descriptions include per-API MCP install instructions for full access.
-
-## Context & Research
-
-### Relevant Code and Patterns
-
-- **Publish pipeline:** `internal/pipeline/publish.go` — `writeCLIManifestForPublish()` has access to the parsed spec and writes `.printing-press.json`. The new `writeToolsManifest()` goes alongside it at the same call sites: `PublishWorkingCLI()` and `PromoteWorkingCLI()`.
-- **CLIManifest:** `internal/pipeline/climanifest.go` — already has `SpecChecksum`, `SpecFormat`, `MCPReady`, `AuthType`, `AuthEnvVars`. `populateMCPMetadata()` at line 104 shows how to compute MCP fields from a parsed spec.
-- **MCP tool registration:** `internal/generator/templates/mcp_tools.go.tmpl` — `RegisterTools()` iterates `Resources/SubResources/Endpoints`, registers each via `s.AddTool()`. Tool names: `{{snake $name}}_{{snake $eName}}`. `makeAPIHandler()` at line 148 is the generic HTTP handler pattern.
-- **Auth config:** `internal/spec/spec.go` `AuthConfig` struct — `Type`, `Header`, `Format`, `In`, `EnvVars`, `KeyURL`. Generated `config.go.tmpl` expands format strings via `applyAuthFormat()`.
-- **RequiredHeaders:** `internal/spec/spec.go` `RequiredHeader` struct — `Name`, `Value`. Applied at `client.go.tmpl:368-370`. Per-endpoint `HeaderOverrides` at line 372.
-- **HTTP client:** `internal/generator/templates/client.go.tmpl` — `do()` method handles auth injection, required headers, retries, error classification (401/403/429/5xx).
-- **Parameter classification:** Implicit in templates — `Positional == true` means path param, others are query (GET) or body (POST/PUT/PATCH). `Endpoint.Params` vs `Endpoint.Body` in `spec.go:66-78`.
-- **Naming:** `internal/naming/naming.go` — `CLI()`, `MCP()`. `toSnakeCase()` at `internal/openapi/schema_builder.go:362` handles hyphens (needed for slug normalization).
-- **Registry schema:** `skills/printing-press-publish/SKILL.md` lines 566-587 — current schema has `name`, `category`, `api`, `description`, `path`, `mcp` block.
-- **Goreleaser:** `.goreleaser.yaml` — single binary build. Needs second entry for `printing-press-mcp`.
-- **Catalog embed:** `catalog/catalog.go` — `//go:embed *.yaml` pattern. Not needed for mega MCP (fetches at runtime) but informs the caching pattern.
-- **Test patterns:** `setPressTestEnv(t)` for isolation, `t.TempDir()` for filesystem tests, table-driven with `testify/assert`.
-
-### Institutional Learnings
-
-- **Filepath traversal:** Belt-and-suspenders — reject `..`/`/`/`\` in inputs AND verify resolved path is under expected root. Applies to cache path construction from API slugs. (`docs/solutions/security-issues/filepath-join-traversal-with-user-input`)
-- **Multi-source discovery:** Use `errgroup.Group` (not `WithContext`) for parallel manifest fetches. Injectable base URLs for testable HTTP clients. HTTPS-only enforcement. (`docs/solutions/best-practices/multi-source-api-discovery-design`)
-- **Immutable source:** Cache writes via temp-then-rename to prevent partial reads on restart. (`docs/solutions/best-practices/validation-must-not-mutate-source-directory`)
-
-## Key Technical Decisions
-
-- **Tools manifest generated at publish time, not runtime spec parsing:** The publish pipeline writes `tools-manifest.json` alongside `.printing-press.json`. Contains tool names, descriptions, parameter schemas with location (path/query/body), auth config, base URL, required headers. Format-agnostic — works for OpenAPI, internal YAML, GraphQL, and sniffed specs. Eliminates runtime kin-openapi dependency and parser global state concerns.
-
-- **Activation model with `tools/list_changed`:** Only ~6 meta-tools registered at startup. Agent calls `activate_api("espn")` → tools registered → `tools/list_changed` notification sent to client. If `mcp-go` v0.26+ doesn't support `tools/list_changed`, fall back to re-registering tools and documenting that the client may need to re-fetch. Verify at the start of Unit 5 implementation.
-
-- **Auth format string expansion at runtime:** The manifest contains `auth.format` (e.g., `Bearer {DUB_TOKEN}`), `auth.header` (e.g., `Authorization`), `auth.in` (header/query). At runtime: read env var value, substitute into format string, set on the correct header or query param. Validate format strings at manifest load time — reject patterns that would place credentials in URL path segments.
-
-- **Parameter classification from manifest, not guessed at runtime:** Each tool parameter in the manifest has an explicit `location` field: `path`, `query`, or `body`. The generic HTTP handler uses this directly — no heuristics needed. Path params substituted into URL template, query params appended to URL, body params serialized as JSON. This fixes the existing template bug where POST/PUT/PATCH sends path params in the body.
-
-- **Two different snake functions for two different jobs:** Tool name segments use `toSnake()` from `generator.go:794` (CamelCase → snake_case only, does NOT convert hyphens). API slug prefixes use `toSnakeCase()` from `schema_builder.go:362` (also converts hyphens → underscores). `WriteToolsManifest` must use `toSnake` for resource/endpoint names (matching the MCP template's `{{snake}}` function) and `toSnakeCase` behavior for the slug prefix. The slug normalizer lives in `internal/megamcp/` as a private function (no second consumer justifies exporting to the naming package yet). Collapse consecutive underscores, reject if result contains `__`.
-
-- **Credential isolation via separate HTTP clients:** Each API gets its own `http.Client` (or at minimum its own auth configuration). The generic handler reads only the env vars declared in that API's manifest `auth.env_vars`. Startup-time collision check warns if two APIs share an env var name.
-
-## Open Questions
-
-### Resolved During Planning
-
-- **Where do tools manifests live?** → In the public library repo at `{path}/tools-manifest.json`, fetched via GitHub raw content URL. Our copies, not third-party URLs.
-- **Which `toSnake` function for slug normalization?** → `toSnakeCase()` from `schema_builder.go:362` (handles hyphens). Export as `naming.SlugToToolPrefix()`.
-- **Where does `writeToolsManifest()` go?** → In `internal/pipeline/publish.go`, called from the same sites as `writeCLIManifestForPublish()`: `PublishWorkingCLI()` and `PromoteWorkingCLI()`.
-- **How does the mega MCP know param location (path/query/body)?** → Explicit `location` field in the tools manifest. Derived from `Param.Positional` (true = path) and `Endpoint.Body` (body params) at manifest generation time.
-
-### Deferred to Implementation
-
-- **`mcp-go` `tools/list_changed` support** — verify at start of Unit 5. If unsupported, the agent can instruct the user to refresh, or the mega MCP can return the tool list in the `activate_api` response text as a workaround.
-- **Smithery marketplace listing requirements** — needs research. Does it need Docker, or can it reference a Go binary?
-- **Category-based filtering** — whether `PRINTING_PRESS_CATEGORIES` is needed in addition to `PRINTING_PRESS_APIS`. Defer to user feedback.
-
-## High-Level Technical Design
-
-> *This illustrates the intended approach and is directional guidance for review, not implementation specification.*
-
-```
-User: claude mcp add printing-press -- printing-press-mcp
-                          │
-                          ▼
-              ┌──────────────────────┐
-              │  printing-press-mcp   │
-              │  (Mega MCP Server)    │
-              │                       │
-              │  Startup:             │
-              │  1. Fetch registry    │
-              │  2. Fetch manifests   │◄── GitHub raw content
-              │  3. Cache locally     │    (our copies)
-              │  4. Register 6 meta-  │
-              │     tools only        │
-              │                       │
-              │  Runtime:             │
-              │  library_info → catalog│
-              │  activate_api(espn) → │
-              │    register tools +   │
-              │    tools/list_changed  │
-              │  espn__scores_get →   │
-              │    generic HTTP GET   │──► https://site.api.espn.com/...
-              │    to API             │◄── JSON response
-              │    return to agent    │
-              └──────────────────────┘
-```
-
-**Activation flow:**
-1. Agent calls `library_info` → sees 6 APIs with descriptions and auth status
-2. Agent calls `activate_api("espn")` → loads ESPN manifest, registers ~3 tools with `espn__` prefix
-3. Agent calls `espn__scores_get` → mega MCP substitutes path params, makes GET request, returns response
-4. Agent calls `setup_guide("dub")` → gets "Set DUB_TOKEN via `claude mcp add --env DUB_TOKEN=xxx`"
-
-## Implementation Units
-
-### Phase 1: Publish Pipeline Prerequisites
-
-- [ ] **Unit 1: Generate tools-manifest.json at publish time**
-
-  **Goal:** The publish pipeline writes a `tools-manifest.json` per API containing everything the mega MCP needs to register and execute tools — without runtime spec parsing.
-
-  **Requirements:** R5, R10, R11, R13
-
-  **Dependencies:** None
-
-  **Files:**
-  - Create: `internal/pipeline/toolsmanifest.go`
-  - Modify: `internal/pipeline/publish.go` (call `WriteToolsManifest` from inside `writeCLIManifestForPublish` after spec parsing)
-  - Test: `internal/pipeline/toolsmanifest_test.go`
-
-  **Approach:**
-  `WriteToolsManifest(dir string, parsed *spec.APISpec) error` iterates the same `Resources/SubResources/Endpoints` hierarchy the MCP template uses, with map keys sorted alphabetically for deterministic JSON output (critical for stable `manifest_checksum` values across re-publishes). For each endpoint, emits a tool entry with: name (same `snake(resource)_snake(endpoint)` convention), description (with `mcpDescription` minority-side annotations), method, path template, `no_auth` flag, and parameters with explicit `location` field.
-
-  Parameter location is derived from existing spec data: `Param.Positional == true` → `"path"`, params from `Endpoint.Body` → `"body"`, all others → `"query"`. This makes explicit what the template currently leaves implicit.
-
-  API-level metadata includes: `api_name`, `base_url` (from `spec.BaseURL`), full `auth` config (type, header, format, in, env_vars, key_url), `required_headers`, and `mcp_ready`.
-
-  The function is called from a single site: inside `writeCLIManifestForPublish()` after the spec parsing block (after `publish.go:248`), where the `parsed` variable is available. Both `PublishWorkingCLI()` and `PromoteWorkingCLI()` flow through `writeCLIManifestForPublish`, so both paths get the tools manifest automatically. If `parsed` is nil (e.g., `--docs` mode, no spec.json), `WriteToolsManifest` is skipped — no manifest generated, and the API is invisible to the mega MCP.
-
-  `SlugToToolPrefix` in the naming package converts API slugs to tool name prefixes: hyphens → underscores, consecutive underscores collapsed, rejects if result contains `__`.
-
-  **Patterns to follow:**
-  - `writeCLIManifestForPublish()` at `publish.go:176` — manifest writing pattern
-  - `mcp_tools.go.tmpl` `RegisterTools()` — resource/endpoint iteration and tool naming
-  - `mcpDescription()` template function in `generator.go` — minority-side auth annotation logic
-  - `populateMCPMetadata()` at `climanifest.go:104` — spec-to-manifest data flow
-
-  **Test scenarios:**
-  - Happy path: OpenAPI spec with 3 resources, 10 endpoints → manifest has 10 tool entries with correct names, methods, paths
-  - Happy path: Sub-resource endpoints included (`guild_members_list`)
-  - Happy path: Each param has explicit `location` field — positional params marked `"path"`, body params marked `"body"`, others `"query"`
-  - Happy path: Auth config serialized completely (type, header, format, in, env_vars, key_url)
-  - Happy path: RequiredHeaders and per-endpoint HeaderOverrides included
-  - Happy path: NoAuth endpoints flagged correctly
-  - Happy path: `SlugToToolPrefix("steam-web")` → `"steam_web"`, `SlugToToolPrefix("dub")` → `"dub"`
-  - Edge case: API with no auth (type "none") → auth block present but minimal
-  - Edge case: Cookie/composed auth → only NoAuth endpoints in manifest tools list
-  - Edge case: `SlugToToolPrefix("steam--web")` → `"steam_web"` (consecutive underscores collapsed)
-  - Edge case: API with empty description → manifest still valid
-  - Edge case: Round-trip: write manifest, read back, all fields preserved
-
-  **Verification:** `go test ./internal/pipeline/... ./internal/naming/...` passes. A generated `tools-manifest.json` for the petstore test fixture contains all expected tools with correct parameter locations.
-
-- [ ] **Unit 2: Extend registry schema and publish skill**
-
-  **Goal:** Registry entries include `manifest_checksum`, `spec_format`, and `manifest_url` so the mega MCP can fetch and verify tools manifests.
-
-  **Requirements:** R4, R5, R17
-
-  **Dependencies:** Unit 1
-
-  **Files:**
-  - Modify: `skills/printing-press-publish/SKILL.md` (add fields to registry entry schema in Step 8)
-  - Modify: `internal/pipeline/climanifest.go` (ensure `SpecChecksum` and `SpecFormat` are always populated when a spec is available)
-
-  **Approach:**
-  The registry entry's `mcp` block gains three fields:
-  - `manifest_checksum`: SHA-256 hash of the `tools-manifest.json` file, computed at publish time. Distinct from `CLIManifest.SpecChecksum` which hashes the raw spec file — different artifacts, different checksums
-  - `spec_format`: from `CLIManifest.SpecFormat` (openapi3, graphql, internal)
-  - `manifest_url`: derived from the entry's `path` field: `{path}/tools-manifest.json`
-
-  The publish skill's Step 8 (registry entry construction) is updated to include these fields. The skill reads them from the CLI manifest which is already written by this point in the pipeline.
-
-  `manifest_url` is a relative path within the library repo, not a full URL. The mega MCP constructs the full GitHub raw content URL at runtime: `https://raw.githubusercontent.com/mvanhorn/printing-press-library/main/{manifest_url}`.
-
-  **Patterns to follow:**
-  - Existing registry schema extension pattern in `SKILL.md` Step 8
-  - `specChecksum()` at `publish.go` for checksum computation
-
-  **Test scenarios:**
-  - Happy path: Publish skill Step 8 includes `manifest_checksum`, `spec_format`, `manifest_url` in the registry entry
-  - Happy path: `manifest_url` is `library/<category>/<cli-name>/tools-manifest.json`
-  - Edge case: CLI without a spec (--docs mode) → `manifest_checksum` and `manifest_url` empty, mega MCP skips this API
-
-  **Verification:** A simulated publish produces a registry entry with all three new fields populated.
-
-### Phase 2: Mega MCP Binary
-
-- [ ] **Unit 3: Skeleton, registry fetching, and manifest caching**
-
-  **Goal:** The mega MCP binary starts, fetches the registry from GitHub, fetches and caches tools manifests, and verifies integrity.
-
-  **Requirements:** R1, R4, R5, R12, R17, R18, R19, R20
-
-  **Dependencies:** Units 1-2
-
-  **Files:**
-  - Modify: `go.mod` (add `github.com/mark3labs/mcp-go`)
-  - Create: `cmd/printing-press-mcp/main.go`
-  - Create: `internal/megamcp/registry.go` (registry fetcher)
-  - Create: `internal/megamcp/manifest.go` (manifest fetcher + cache)
-  - Create: `internal/megamcp/types.go` (ToolsManifest, ToolEntry, ParamEntry structs)
-  - Create: `internal/megamcp/security.go` (URL validation, text sanitization, checksum verification)
-  - Test: `internal/megamcp/registry_test.go`
-  - Test: `internal/megamcp/manifest_test.go`
-  - Test: `internal/megamcp/security_test.go`
-
-  **Approach:**
-  `FetchRegistry(baseURL string) ([]RegistryEntry, error)` fetches `registry.json` from GitHub raw content. `baseURL` is injectable for testing (per multi-source discovery learning). Filters to entries with non-empty `manifest_url` and `mcp_ready != "cli-only"`. Applies `PRINTING_PRESS_APIS` env var filter (R20).
-
-  `FetchManifest(entry RegistryEntry, cacheDir string) (*ToolsManifest, error)` fetches `tools-manifest.json` from the manifest_url. Cache logic: check cache → if cached, verify checksum against registry's `manifest_checksum` → if match, use cache → if mismatch or missing, fetch from GitHub → verify checksum → write to cache via temp-then-rename. Cache dir: `~/.cache/printing-press-mcp/manifests/{slug}/tools-manifest.json` with `0700` dir permissions, `0600` file permissions.
-
-  Security: `ValidateBaseURL(url string) error` rejects non-HTTPS, private IP ranges (10.x, 172.16-31.x, 192.168.x), loopback (127.x, ::1), link-local (169.254.x, fe80::), and cloud metadata (169.254.169.254). `SanitizeText(s string, maxLen int) string` strips control characters and length-limits manifest-derived descriptions. `VerifyChecksum(data []byte, expected string) error` compares SHA-256.
-
-  Path traversal protection on cache paths: reject API slugs containing `..`, `/`, `\` AND verify resolved cache path is under cache root.
-
-  Parallel manifest loading via `errgroup.Group` (not `WithContext`). Failed fetches log warnings and exclude the API — other APIs continue.
-
-  First run with no cache and no network: start with zero APIs, surface error through `library_info`.
-
-  **Patterns to follow:**
-  - `setPressTestEnv(t)` pattern for test isolation
-  - `httptest.NewServer` for mock GitHub responses
-  - `errgroup.Group` per multi-source discovery learning
-  - Temp-then-rename for cache writes per immutable source learning
-  - Belt-and-suspenders path traversal per filepath traversal learning
-
-  **Test scenarios:**
-  - Happy path: Fetch registry with 3 entries → filter to 2 (one is cli-only) → fetch 2 manifests → both cached
-  - Happy path: Cached manifest with matching checksum → no re-fetch
-  - Happy path: Cached manifest with mismatched checksum → re-fetched and updated
-  - Happy path: `PRINTING_PRESS_APIS=espn,dub` → only 2 APIs loaded
-  - Edge case: GitHub returns 404 for registry → start with zero APIs, library_info shows error
-  - Edge case: One manifest fetch fails, others succeed → failed API excluded with warning, others available
-  - Edge case: First run, no cache, no network → zero APIs, clear error in library_info
-  - Edge case: API slug with traversal characters (`../etc`) → rejected before path construction
-  - Edge case: Manifest base_url is private IP (10.0.0.1) → API rejected with warning
-  - Edge case: Manifest base_url is HTTP (not HTTPS) → API rejected with warning
-  - Edge case: Manifest description contains control characters → sanitized before registration
-  - Edge case: Cache directory doesn't exist → created with 0700 permissions
-
-  **Verification:** `go test ./internal/megamcp/...` passes. `go build ./cmd/printing-press-mcp` succeeds. Binary starts and loads manifests from a mock server.
-
-- [ ] **Unit 4: Generic HTTP handler with auth and param routing**
-
-  **Goal:** The mega MCP can make HTTP requests to any API using the tools manifest data — correct auth headers, proper parameter classification, required headers.
-
-  **Requirements:** R11, R13, R14, R15, R16
-
-  **Dependencies:** Unit 3
-
-  **Files:**
-  - Create: `internal/megamcp/handler.go`
-  - Create: `internal/megamcp/auth.go` (format string expansion, auth header construction)
-  - Test: `internal/megamcp/handler_test.go`
-  - Test: `internal/megamcp/auth_test.go`
-
-  **Approach:**
-  `MakeToolHandler(manifest *ToolsManifest, tool ToolEntry) server.ToolHandlerFunc` returns a handler that:
-  1. Builds the URL: substitute `location: "path"` params into the path template, append `location: "query"` params as query string
-  2. Builds the body: serialize `location: "body"` params as JSON (POST/PUT/PATCH only). Path and query params are excluded from the body.
-  3. Constructs auth: read env var from `manifest.Auth.EnvVars`, apply `manifest.Auth.Format` string substitution (e.g., `Bearer {DUB_TOKEN}` → `Bearer abc123`), set on `manifest.Auth.Header` or query param based on `manifest.Auth.In`
-  4. Attaches `RequiredHeaders` from manifest to every request
-  5. Attaches per-tool `HeaderOverrides` if present
-  6. **Fail-closed auth check:** Before making the request, if `auth.type` is not `"none"` and the required env var is absent or empty AND the tool's `no_auth` flag is false, return an MCP error immediately ("Authentication not configured — call `setup_guide` for instructions"). Only proceed without auth when `no_auth: true`.
-  7. **Post-assembly URL validation:** After substituting path params and appending query params, verify the assembled URL's host still matches the validated base_url host. Reject if the host changed (prevents path-param-based SSRF). URL-encode path param values before substitution. Use `url.Values` for query param construction (prevents `&`/`#` injection).
-  8. **Missing path param check:** Before making the request, verify all `{placeholder}` tokens in the path template were substituted. If any remain, return an MCP error listing the missing params.
-  9. Makes the HTTP request with a per-API `http.Client` (30s timeout, response body capped at 10MB via `io.LimitReader`)
-  10. Handles errors: 401/403 → generic "auth not configured" message (no env var names in error). 429 → surface response body. 4xx → return API error (with credential value redaction pass — replace known auth values with `[REDACTED]` before returning). 5xx/network → MCP error
-
-  `ApplyAuthFormat(format string, envVars []string) (string, error)` expands `{PLACEHOLDER}` in the format string with env var values. Validates that the format string only contains recognized placeholders and doesn't place credentials in URL paths.
-
-  Credential isolation: each API's handler reads only its own `Auth.EnvVars`. A per-API HTTP client is constructed at activation time (not shared across APIs).
-
-  **Patterns to follow:**
-  - `makeAPIHandler` in `mcp_tools.go.tmpl` — path substitution, query params, body construction, error handling
-  - `AuthHeader()` in `config.go.tmpl` — format string expansion
-  - `do()` in `client.go.tmpl` — required headers, error classification
-
-  **Test scenarios:**
-  - Happy path: GET request with path param → param substituted in URL, not in query or body
-  - Happy path: GET request with query params → appended to URL
-  - Happy path: POST request with body params → serialized as JSON body. Path params excluded from body
-  - Happy path: API key auth with `In: "header"` → auth header set correctly
-  - Happy path: API key auth with `In: "query"` → auth appended as query param
-  - Happy path: Bearer token auth → `Authorization: Bearer {value}` header set
-  - Happy path: RequiredHeaders attached to every request
-  - Happy path: Per-tool HeaderOverrides applied
-  - Happy path: No auth (type "none") → no auth header set
-  - Edge case: Missing env var for auth on non-NoAuth endpoint → MCP error returned before request ("call setup_guide")
-  - Edge case: Missing env var for auth on NoAuth endpoint → request proceeds without auth
-  - Edge case: 401 response → generic error message, no env var names, credential values redacted from response body
-  - Edge case: 429 response → error includes response body
-  - Edge case: Network timeout → MCP error
-  - Edge case: Response body exceeds 10MB → truncated with error
-  - Edge case: Format string with unrecognized placeholder → rejected at load time
-  - Edge case: Path param value containing `../` → URL-encoded before substitution, assembled URL host matches base_url
-  - Edge case: Required path param missing from arguments → MCP error listing missing params
-  - Edge case: Two APIs share an env var name → startup warning
-  - Integration: Round-trip against `httptest.NewServer` — GET with params, POST with body, auth header verified, assembled URL host matches
-
-  **Verification:** `go test ./internal/megamcp/...` passes. A test hitting a mock API server receives correctly formed requests with proper auth and parameters.
-
-- [ ] **Unit 5: Meta-tools and activation model**
-
-  **Goal:** The mega MCP exposes discovery and activation tools that let agents find APIs, get setup instructions, and dynamically register API tools.
-
-  **Requirements:** R6, R7, R8, R9, R21
-
-  **Dependencies:** Units 3-4
-
-  **Files:**
-  - Create: `internal/megamcp/metatools.go`
-  - Create: `internal/megamcp/activation.go` (tool registration/deregistration state)
-  - Test: `internal/megamcp/metatools_test.go`
-  - Test: `internal/megamcp/activation_test.go`
-
-  **Approach:**
-  Six meta-tools registered at startup:
-
-  **`library_info`**: Returns JSON listing all loaded APIs: name, description, tool count, auth type, auth status (checks `os.Getenv` for each API's env vars — returns boolean only, NOT env var names), `mcp_ready` level, novel features, and whether a local per-API MCP binary exists (checks `~/printing-press/library/{slug}/cmd/{mcp-binary}/` path). If local binary exists, includes install hint.
-
-  **`setup_guide(api_slug)`**: Returns auth configuration instructions for a specific API: required env vars, key URL, example `claude mcp add --env` command. This is the deliberate introspection step for credential metadata — separated from `library_info` to avoid broadcasting env var names in catalog listings.
-
-  **`activate_api(api_slug)`**: Loads the API's tools manifest (already cached from startup), registers all tools with `{normalized_slug}__` prefix via `s.AddTool()`. Sends `tools/list_changed` notification if `mcp-go` supports it. Returns confirmation with tool count.
-
-  **`deactivate_api(api_slug)`**: Removes all tools for an API. Sends `tools/list_changed`.
-
-  **`search_tools(query)`**: Searches across ALL loaded manifests' tool names and descriptions by keyword (case-insensitive substring match). Returns matching tools with API slug, tool name, and description — without requiring activation. Lets the agent find the right API before activating it.
-
-  **`about`**: Returns mega MCP version (from `internal/version.Version`), total API count, total tool count across all manifests, and library root path.
-
-  **First task in this unit:** Verify `mcp-go` support for `tools/list_changed` notification and dynamic tool add/remove. If `mcp-go` doesn't support removing tools after registration, the `deactivate_api` implementation may need to re-create the MCP server with the reduced tool set (heavier but functional).
-
-  **Patterns to follow:**
-  - Per-API `about` tool in `mcp_tools.go.tmpl` — handler pattern
-  - `mcp-go` `server.AddTool()` API for dynamic registration
-  - `version.Version` for version reporting
-
-  **Test scenarios:**
-  - Happy path: `library_info` returns all APIs with correct metadata, auth_configured reflects env var presence
-  - Happy path: `library_info` shows local upgrade available when per-API MCP binary exists
-  - Happy path: `setup_guide("dub")` returns env var names, key URL, example command
-  - Happy path: `activate_api("espn")` → 3 ESPN tools registered with `espn__` prefix
-  - Happy path: After activation, `espn__scores_get` is callable
-  - Happy path: `deactivate_api("espn")` → ESPN tools removed
-  - Happy path: `search_tools("scores")` → returns `espn__scores_get` match with description
-  - Happy path: `search_tools("pizza")` → returns Pagliacci tools even before activation
-  - Happy path: `about` returns version and counts
-  - Edge case: `activate_api` with unknown slug → error "API not found"
-  - Edge case: `activate_api` called twice for same API → idempotent (no duplicate tools)
-  - Edge case: `search_tools` with no matches → empty results array
-  - Edge case: `library_info` with no APIs loaded (network failure) → empty list with error message
-  - Edge case: `setup_guide` for no-auth API → "No authentication required"
-
-  **Verification:** `go test ./internal/megamcp/...` passes. Meta-tools return valid JSON responses.
-
-- [ ] **Unit 6: Entry point, goreleaser, and marketplace metadata**
-
-  **Goal:** The mega MCP binary is buildable, distributable via `go install` and goreleaser, and includes marketplace metadata.
-
-  **Requirements:** R1, R2, R3
-
-  **Dependencies:** Unit 5
-
-  **Files:**
-  - Modify: `cmd/printing-press-mcp/main.go` (wire everything together)
-  - Modify: `.goreleaser.yaml` (add second binary build)
-  - Create: `smithery.yaml` (repo-root level, for the mega MCP)
-  - Modify: `internal/version/version.go` (reuse for mega MCP)
-
-  **Approach:**
-  `main.go` creates an `mcp-go` server, runs the registry fetch → manifest load → cache pipeline, registers the 6 meta-tools, and serves on stdio. Graceful shutdown on SIGINT/SIGTERM.
-
-  Add `printing-press-mcp` as a second build in `.goreleaser.yaml` alongside the existing `printing-press` binary. Both distributed in the same release. The mega MCP binary reuses `internal/version.Version` for its version string.
-
-  `smithery.yaml` at repo root describes the mega MCP for Smithery marketplace listing. No per-API env vars are required (they're all optional). Description highlights the catalog: "270+ tools across 6 APIs — sports scores, link management, pizza ordering, and more."
-
-  Install and usage:
-  ```
-  go install github.com/mvanhorn/cli-printing-press/cmd/printing-press-mcp@latest
-  claude mcp add printing-press -- printing-press-mcp
-  ```
-
-  **Patterns to follow:**
-  - `cmd/printing-press/main.go` entry point pattern
-  - `.goreleaser.yaml` existing build configuration
-  - `main_mcp.go.tmpl` for `mcp-go` server setup (`server.NewMCPServer`, `server.ServeStdio`)
-  - `smithery.yaml` generation pattern from `writeSmitheryYAML` in `publish.go`
-
-  **Test scenarios:**
-  - Happy path: `go build ./cmd/printing-press-mcp` succeeds
-  - Integration: Start mega MCP with mock GitHub server → `library_info` returns API catalog → `activate_api` registers tools → tool call returns mock API response
-
-  **Verification:** Binary builds and starts. End-to-end flow works with mock server: library_info → activate → tool call → response.
-
-## System-Wide Impact
-
-- **Interaction graph:** The mega MCP binary is a new artifact in this repo. It imports `internal/megamcp/` (new), `internal/naming/` (modified), `internal/pipeline/` (types only), and `internal/version/`. The publish pipeline gains `writeToolsManifest()`. The publish skill gains registry fields. No existing behavior changes — all additions.
-- **Error propagation:** HTTP errors from APIs are caught in the handler and returned as MCP tool errors. Registry/manifest fetch failures are caught at startup and surfaced via `library_info`. Individual API failures do not affect other APIs.
-- **State lifecycle risks:** The manifest cache at `~/.cache/printing-press-mcp/` persists across restarts. Stale cache is mitigated by checksum verification against the registry. Cache corruption is mitigated by temp-then-rename writes.
-- **API surface parity:** Tool names match (both use `toSnake` on `spec.APISpec` keys). Tool parameter schemas intentionally diverge for POST/PUT/PATCH: the mega MCP registers body params separately (with `location: "body"`) and excludes path params from the request body. The per-API MCP template has a known bug where it sends path params in the body. This is an improvement, not a parity violation — a follow-up should fix the template to match. Agents switching from mega MCP to per-API MCP may notice different parameter lists for mutation endpoints.
-- **Unchanged invariants:** Per-API MCP servers continue to work independently. The mega MCP is additive. The publish pipeline still generates all existing artifacts (`.printing-press.json`, CLI binary, MCP binary). `tools-manifest.json` is a new addition.
-
-## Risks & Dependencies
-
-| Risk | Mitigation |
-|------|------------|
-| `mcp-go` doesn't support `tools/list_changed` or dynamic tool removal | Verify at start of Unit 5. Fallback: return tool list in activation response text; re-create server for deactivation. |
-| GitHub rate limits on raw content fetches | Cache aggressively (24h TTL + checksum-based invalidation). First-run fetches ~7 files (1 registry + 6 manifests). Well within anonymous rate limits. |
-| Manifest format drift between publish pipeline and mega MCP | Both are in the same repo. `ToolsManifest` struct is shared. Test fixtures verify round-trip compatibility. |
-| Tool naming mismatch between mega MCP and per-API MCP servers | Both derive tool names from `spec.APISpec` using the same naming convention. Unit 1 tests verify parity with template output. |
-| Existing CLIs need re-publishing with tools manifests | Documented as a prerequisite. Can be batched as a single publish run after Units 1-2 land. |
-
-## Phased Delivery
-
-### Phase 1: Publish Pipeline Prerequisites (Units 1-2)
-Ship as one PR. Additive only — doesn't change existing publish behavior, just generates an additional artifact. Can be validated by running a test publish and inspecting the `tools-manifest.json`.
-
-### Phase 2: Mega MCP Binary (Units 3-6)
-Ship as one or two PRs. Can be split: Units 3-4 (fetching + handler) in one PR, Units 5-6 (meta-tools + entry point) in another. The binary is usable only after all 4 units land.
-
-### Post-ship
-- Re-publish existing 6 CLIs with tools manifests (batch operation)
-- List mega MCP on Smithery marketplace
-- Announce via README update in the public library repo
-
-## Sources & References
-
-- **Origin document:** `docs/brainstorms/2026-04-06-mega-mcp-generic-proxy-requirements.md`
-- **Superseded plan:** `docs/plans/2026-04-06-002-feat-mega-mcp-aggregate-server-plan.md` (subprocess architecture — reference for deferred hybrid mode)
-- **Step 1 PR:** mvanhorn/cli-printing-press#145 (MCP readiness layer)
-- **Step 1 plan:** `docs/plans/2026-04-05-001-feat-mcp-readiness-layer-plan.md`
-- Institutional learnings:
-  - `docs/solutions/security-issues/filepath-join-traversal-with-user-input` (traversal protection)
-  - `docs/solutions/best-practices/multi-source-api-discovery-design` (errgroup, injectable URLs)
-  - `docs/solutions/best-practices/validation-must-not-mutate-source-directory` (temp-then-rename)
diff --git a/docs/plans/2026-04-13-002-feat-cloudflare-cli-learnings-plan.md b/docs/plans/2026-04-13-002-feat-cloudflare-cli-learnings-plan.md
index bf841fd6..10d9629c 100644
--- a/docs/plans/2026-04-13-002-feat-cloudflare-cli-learnings-plan.md
+++ b/docs/plans/2026-04-13-002-feat-cloudflare-cli-learnings-plan.md
@@ -214,7 +214,7 @@ Printed CLIs are one SKILL.md away from being genuinely agent-ready, but three i
 
 ## System-Wide Impact
 
-- **Interaction graph:** Only pipeline (dogfood, scorecard) and generator (templates, pipeline) are touched. No changes to `verify`, `emboss`, `publish`, `research`, `crowdsniff`, `megamcp`, or SKILL.md generation.
+- **Interaction graph:** Only pipeline (dogfood, scorecard) and generator (templates, pipeline) are touched. No changes to `verify`, `emboss`, `publish`, `research`, `crowdsniff`, or SKILL.md generation.
 - **Error propagation:** New dogfood check surfaces through existing `DogfoodReport` and `deriveDogfoodVerdict`. New scorecard dimension uses the existing `UnscoredDimensions` pattern for opt-outs.
 - **State lifecycle risks:** None - all changes are generation-time and static-analysis-time.
 - **API surface parity:** Agent-context subcommand is additive. SKILL.md (already generated) remains unchanged. Existing commands are untouched.
diff --git a/docs/plans/2026-04-17-002-fix-private-library-registry-auth-plan.md b/docs/plans/2026-04-17-002-fix-private-library-registry-auth-plan.md
deleted file mode 100644
index 01033548..00000000
--- a/docs/plans/2026-04-17-002-fix-private-library-registry-auth-plan.md
+++ /dev/null
@@ -1,72 +0,0 @@
----
-title: "fix: Private library registry auth for mega MCP"
-type: fix
-status: active
-date: 2026-04-17
----
-
-# fix: Private library registry auth for mega MCP
-
-## Overview
-
-The `printing-press-mcp` binary fetches `registry.json` and per-API `tools-manifest.json` files from `raw.githubusercontent.com/mvanhorn/printing-press-library/main/...` via unauthenticated HTTP. The library repo is private, so every fetch returns 404 and the mega MCP ships with an empty tool catalog. Fix: attach `Authorization: token $GITHUB_TOKEN` when the env var is set. Same URL, same response shape, just with auth.
-
-## Problem Frame
-
-Two `http.Get` calls need the header:
-
-- `FetchRegistry` in `internal/megamcp/registry.go:18`
-- `fetchManifestData` in `internal/megamcp/manifest.go:283`
-
-Single user (the maintainer), single private repo, token read from `GITHUB_TOKEN` the same way `internal/crowdsniff/github.go` already does it.
-
-## Scope Boundaries
-
-- **In scope:** Add `Authorization` header to the two fetch calls. One test that the header gets set when `GITHUB_TOKEN` is present.
-- **Out of scope:** Shared auth helper, upgraded error diagnostics, doc reconciliation, tests that the token never appears in error strings, a `library_status` meta-tool, flipping repo visibility.
-
-## Key Technical Decisions
-
-- **Env var is `GITHUB_TOKEN`.** Matches `internal/crowdsniff/github.go:19,43` convention. No new env var.
-- **Inline at each call site.** Two fetches, ~5 lines each. A shared helper adds more surface than it saves.
-- **Keep existing error shapes.** `HTTP 404` on 404 is fine. Maintainer reading their own repo knows what that means.
-
-## Implementation Units
-
-- [ ] **Unit 1: Attach GITHUB_TOKEN auth header to registry and manifest fetches**
-
-**Goal:** Both outbound GETs carry `Authorization: token <value>` when `GITHUB_TOKEN` is set. Nothing else changes.
-
-**Files:**
-- Modify: `internal/megamcp/registry.go`
-- Modify: `internal/megamcp/manifest.go`
-- Modify: `internal/megamcp/registry_test.go` (add one auth-header assertion)
-
-**Approach:**
-- In both files, replace `http.Get(url)` with a `http.NewRequest("GET", url, nil)` + `http.DefaultClient.Do(req)` pair, and set the `Authorization` header when `os.Getenv("GITHUB_TOKEN")` is non-empty.
-- Use `token <value>` format (matches the crowdsniff package, not `Bearer`).
-- Don't wrap in a helper - two call sites isn't enough to justify the indirection.
-
-**Patterns to follow:**
-- `internal/crowdsniff/github.go` for the token-read + header-set pattern.
-
-**Test scenarios:**
-- Happy path: with `GITHUB_TOKEN` set via `t.Setenv`, the `httptest.NewServer` handler observes `Authorization: token <value>` on the inbound request. (Extend an existing `registry_test.go` case - don't add a new file.)
-
-**Verification:**
-- `go test ./internal/megamcp/...` passes.
-- Rebuild (`go install ./cmd/printing-press-mcp`), restart Claude Code, `library_info` returns a non-empty API list.
-
-## Risks & Dependencies
-
-| Risk | Mitigation |
-|------|------------|
-| Token missing or scope-insufficient still returns 404, with no hint why | Accepted. Maintainer context - you know what 404 means on a private repo read. |
-| Future contributor adds a third fetch and forgets the header | Accepted for now. Revisit if a third fetch path lands. |
-
-## Sources & References
-
-- `internal/megamcp/registry.go:15-39`
-- `internal/megamcp/manifest.go:282-299`
-- `internal/crowdsniff/github.go:17-44`
-- Repo visibility: `gh repo view mvanhorn/printing-press-library --json visibility` -> `PRIVATE` as of 2026-04-17.
diff --git a/docs/plans/2026-04-19-001-feat-composio-inspired-features-plan.md b/docs/plans/2026-04-19-001-feat-composio-inspired-features-plan.md
deleted file mode 100644
index ec713844..00000000
--- a/docs/plans/2026-04-19-001-feat-composio-inspired-features-plan.md
+++ /dev/null
@@ -1,625 +0,0 @@
----
-title: "feat: Composio-inspired features for the Printing Press - stack-ranked absorption plan"
-type: feat
-status: active
-date: 2026-04-19
-deepened: 2026-04-19
----
-
-# feat: Composio-inspired features for the Printing Press - stack-ranked absorption plan
-
-## Overview
-
-Composio is a hosted agent-integration platform covering 1,000+ toolkits. Their CLI, MCP hosting, trigger system, and unified auth layer are the strongest reference points on the market for what a "super CLI" looks like in 2026. This plan does a deep feature inventory of Composio, cross-references it against what the Printing Press already ships (machine + library + megamcp), stack-ranks the absorbable features, and proposes implementation units for the top tier only.
-
-The plan is intentionally research-first and decision-forward. The stack ranking is the primary artifact. Implementation units are scoped to the S-tier and A-tier items; B and C tiers are deferred to Future Considerations so the user can approve the cut before we start cutting work.
-
-## Problem Frame
-
-The Printing Press is a CLI factory. For any API, it generates a printed CLI plus a matching MCP server with local SQLite, FTS5 search, compound commands, domain archetypes, verify/dogfood/scorecard, and optional sniff-gate discovery. The library ships 21 printed CLIs and an aggregate megamcp server with dynamic activation, setup_guide, and cross-API tool search.
-
-Composio solves a different shape of the same problem. Instead of generating one great local CLI per API, they host one platform that normalises auth, execution, discovery, and event delivery across thousands of APIs. A Composio user runs `composio login` once, runs `composio link linkedin`, and then every agent-framework (Claude Agent SDK, OpenAI Agents, LangChain, MCP clients) has authenticated LinkedIn access without per-API onboarding.
-
-The gap the user wants to close: which Composio features would move the Printing Press forward without breaking its local-first, agent-native, "two binaries per API + no backend" philosophy? That is the question this plan answers.
-
-## Requirements Trace
-
-- R1. Inventory Composio's CLI, MCP, triggers, auth, and ecosystem features in enough depth to make informed decisions.
-- R2. Inventory the Printing Press's current feature set (machine, library, megamcp) in enough depth to spot real gaps.
-- R3. Stack-rank Composio features by absorbability into the Printing Press using explicit criteria (impact, reach, effort, fit, moat).
-- R4. For the top ranked items, produce implementation units concrete enough to hand to `/ce:work` without another planning pass.
-- R5. Explicitly mark out-of-scope items so we do not drift into cloning the parts of Composio that fight PP's philosophy (hosted dashboard, Python sandbox, multi-tenant SaaS).
-
-## Scope Boundaries
-
-In scope:
-- Research synthesis and stack ranking of Composio features worth absorbing.
-- Implementation units for the S-tier and A-tier features.
-- Impact analysis across the machine, printed CLIs, and library/megamcp.
-
-Out of scope (explicit non-goals):
-- Building a hosted backend or dashboard.
-- Python sandbox or remote filesystem for tool execution.
-- Multi-tenant per-user MCP URLs.
-- Rewriting printed CLIs to route through a central server.
-- White-label or branded auth screens.
-- Copying Composio's intent-routing LLM layer (agents already search_tools via megamcp).
-
-## Context & Research
-
-### Composio feature inventory
-
-Collected from composio.dev, docs.composio.dev, their CLI reference page, MCP overview, triggers doc, and the ComposioHQ/skills repo.
-
-CLI surface (from `composio --help` / docs):
-
-| Command | What it does |
-|---------|--------------|
-| `composio login [-y]` | One-time auth with Composio backend. `-y` for CI. |
-| `composio whoami` | Show account + workspace. |
-| `composio link <toolkit>` | Kicks off OAuth for a specific toolkit (LinkedIn, Stripe, Slack, etc.). `--no-wait` prints auth URL and exits. |
-| `composio connected-accounts list --toolkits X` | Inventory of linked accounts. |
-| `composio tools list --toolkit X` | Tool catalog per toolkit. |
-| `composio tools info TOOL_SLUG` | Schema / params / description for one tool. |
-| `composio search "query" [--toolkits X] [--human]` | Cross-toolkit semantic search. JSON by default. |
-| `composio execute TOOL_SLUG -d <json\|@file\|->` | Run a tool. `--dry-run`, `--get-schema`, `--skip-connection-check`, `--parallel`. |
-| `composio proxy --toolkit X -X METHOD /path [-H ...] [-d ...]` | Authenticated raw HTTP using linked creds. |
-| `composio run --file workflow.ts` | Execute a TypeScript/JS workflow file. |
-| `composio generate ts [--toolkits X] [--compact] [--transpiled] [--type-tools]` | Typed SDK codegen. |
-| `composio generate py [--toolkits X]` | Python codegen. |
-| `composio dev init` | Initialise local dev context. |
-| `composio dev playground-execute --user-id X` | Test against playground user. |
-| `composio dev listen --toolkits X [--table]` | Live-tail triggers and logs. |
-| `composio dev logs tools` / `logs triggers` | Historical logs. |
-| `composio artifacts cwd` | Path to session artifacts. |
-| Env: `COMPOSIO_API_KEY`, `COMPOSIO_BASE_URL`, `COMPOSIO_CACHE_DIR`, `COMPOSIO_SESSION_DIR`, `COMPOSIO_LOG_LEVEL`, `COMPOSIO_DISABLE_TELEMETRY`, webhook secret. |
-
-MCP surface:
-- Server config created via dashboard or API, exposes a subset of toolkits and `allowed_tools`.
-- `composio.mcp.generate(user_id, mcp_config_id)` returns a per-user URL: `https://backend.composio.dev/v3/mcp/SERVER_ID?user_id=USER_ID`.
-- Requires `x-api-key` header when `require_mcp_api_key` is on.
-- Works with any MCP client (Claude Desktop, Cursor, OpenAI Agents, Windsurf, Cline).
-
-Triggers surface:
-- Trigger types per toolkit (e.g. `GITHUB_COMMIT_EVENT`, `SLACK_NEW_MESSAGE`, `GMAIL_NEW_EMAIL`).
-- Webhook delivery for apps with native webhooks; polling (15-min floor) otherwise.
-- Create trigger instance scoped to user + connected account + config params.
-- Webhook signature verification built-in.
-
-Agent-framework integrations: Claude Agent SDK, Anthropic SDK, OpenAI Agents, OpenAI, Gemini, Vercel AI, LangChain, LangGraph, CrewAI, LlamaIndex, Mastra, Cloudflare Workers. Python and TypeScript parity for most.
-
-Claude Code integration: `npx skills add composiohq/skills` installs skills for tool-router, auth, toolkits, triggers. Heavy emphasis on "identify user, create session, get tools" as the canonical flow.
-
-Security posture: SOC2 + ISO 27001:2022, bring-your-own-cloud, fine-grained data access controls, managed OAuth rotation, scoped inline authorization.
-
-### Printing Press feature inventory (current state, 2026-04-19)
-
-Machine (`cli-printing-press`):
-- Generates `<api>-pp-cli` (Cobra) plus `<api>-pp-mcp` (MCP server) from OpenAPI 3, GraphQL SDL, HAR, or internal YAML spec.
-- Agent-native flags: `--json`, `--select`, `--dry-run`, `--stdin`, `--csv`, `--compact`, `--quiet`, `--yes`, `--no-input`, `--no-cache`, `--no-color`. Auto-JSON when piped. Typed exits (0/2/3/4/5/7).
-- Domain archetypes (PM, Communication, Payments, Infrastructure, Content) auto-generate `stale`, `orphans`, `load`, `reconcile`, `health`, `similar`, `channel-health`.
-- Local-first data layer: domain-specific SQLite tables, FTS5, cursor sync, `sync`/`search`/`sql`/`tail`.
-- Quality pipeline: dogfood (structural), verify (runtime), scorecard (two-tier), shipcheck (combined), emboss, polish, retro.
-- Codex mode for token savings, sniff gate for no-spec APIs, crowd-sniff for ecosystem-absorb manifest.
-- 18 APIs in catalog (Asana, DigitalOcean, Discord, Front, GitHub, HubSpot, LaunchDarkly, Pipedrive, Plaid, Postman, SendGrid, Sentry, Square, Stripe, Stytch, Telegram, Twilio, Petstore).
-
-Library (`printing-press-library`):
-- 21 printed CLIs + 19 MCP servers + `/ppl` router skill + 21 `pp-*` focused skills.
-- Plugin marketplace for Claude Code.
-- Per-CLI auth today is env-var based: `ESPN_KEY`, `HUBSPOT_ACCESS_TOKEN`, `DUB_TOKEN`, `LINEAR_API_KEY`, `CAL_COM_TOKEN`, `KALSHI_API_KEY`, etc. No shared credential store.
-- Auth types present across the catalog: `none`, `api_key`, `bearer_token`, `composed` (cookie auth), OAuth implied for some (e.g. Linear). No unified OAuth helper.
-
-Megamcp (`internal/megamcp/`):
-- Aggregate MCP server that loads all printed MCP manifests and exposes 6 meta-tools: `library_info`, `setup_guide`, `activate_api`, `deactivate_api`, `search_tools`, `about`.
-- Dynamic tool registration/deregistration via `ActivationManager`.
-- Fail-closed auth via `hasAuthConfigured(manifest)` and `ApplyAuthFormat` (placeholder substitution from env vars).
-- Max 32KB response to agent, 10MB over the wire. Response classification for error telemetry.
-- Already solves a large chunk of the "Composio-style unified MCP server" problem on the MCP side.
-
-Gap summary (the interesting bit):
-- CLI side has no unified `pp execute`, `pp search`, `pp proxy`. Every tool lives behind its printed CLI binary (e.g. `espn-pp-cli scores`) or the `/pp-espn` skill. There is no cross-CLI execution entry point to parallel megamcp's MCP-side unification.
-- No central auth manager. Each printed CLI reads its own env vars. Users manage many tokens. No OAuth helper, no token refresh, no keychain story.
-- No triggers / event delivery. Polling is manual via `sync` commands. No webhook receiver.
-- No `--parallel` batch execution across tools.
-- No `allowed_tools` filter on MCP servers.
-- No per-CLI `llms.txt` artefact.
-
-### External references
-
-- Composio CLI reference: https://composio.dev/toolkits/linkedin/framework/cli
-- Composio docs: https://docs.composio.dev
-- Composio skills for Claude Code: https://github.com/ComposioHQ/skills
-- Composio monorepo: https://github.com/ComposioHQ/composio
-- Related prior PP plan: `docs/plans/2026-04-06-002-feat-mega-mcp-aggregate-server-plan.md` (already shipped).
-
-### Institutional learnings referenced
-
-- PP philosophy "absorb then transcend" (README L84-94): absorb every feature from every competitor, then compound with SQLite + agent-native layer. Composio absorption lives at the root of this philosophy.
-- `AGENTS.md` machine-vs-printed discipline: many Composio features are machine-level (generator changes that affect every future CLI); fewer are printed-CLI-level.
-- Glossary: megamcp shows the pattern for aggregate servers and meta-tools. It is the right place to extend for `allowed_tools` and parallel activation.
-
-## Key Technical Decisions
-
-KTD-1. Landing repo for the super-CLI entry point is `cli-printing-press` (not `printing-press-library`). Rationale: the command surface is a machine capability that must ship to every user the moment they install the press. Putting it in the library couples it to plugin install order. It belongs next to the generator binary so it travels together.
-
-KTD-2. The super-CLI takes the shape of subcommands on the existing `printing-press` binary: `printing-press execute`, `printing-press search`, `printing-press info`, `printing-press proxy`, `printing-press list`, `printing-press auth ...`, `printing-press trigger ...`. Rationale: one binary to install, one `/install mvanhorn/cli-printing-press` to get everything. A separate `pp` binary would duplicate distribution, release tooling, and updater concerns. A `/ppl` skill alias can still surface the same commands for humans.
-
-KTD-3. The unified auth layer is additive. Printed CLIs continue to read env vars as primary. A new `auth resolve <api>` helper is read by generator templates and returns the first available credential in order: process env -> `PP_AUTH_FILE` override -> shared secure store -> keychain. Rationale: every existing printed CLI keeps working with zero template changes on the hot path. Migration is pull, not push.
-
-KTD-4. The shared credential store is file-based at `~/.pp/credentials.json` with `chmod 600`, with a pluggable backend interface so macOS keychain / Linux Secret Service / Windows Credential Manager can be wired in later without changing the public API. Rationale: ship value now; keychain is a P2 follow-up once the interface is stable.
-
-KTD-5. OAuth flows are implemented per-API via a small `authproviders/` registry. Each provider is a Go file with `AuthURL`, `TokenExchange`, `Refresh`, and metadata (scopes, redirect URI handling). Rationale: we cannot reuse Composio's hosted OAuth broker and we will not build one. Per-provider code is honest work and stays under 150 lines per provider for most APIs.
-
-KTD-6. Triggers are polling-first. `printing-press trigger run` is a local daemon that schedules per-trigger polls using existing `sync` cursors from each printed CLI's store, then forwards diffs to the configured sink (webhook URL, stdout-as-JSONL, file). Webhook-receiver mode is a phase-two add for APIs with native webhooks. Rationale: PP already has cursor-based sync everywhere; polling triggers are a 200-line wrapper over that. Webhook receivers require a long-running HTTP server with signature verification and are a larger lift.
-
-KTD-7. `allowed_tools` and `denied_tools` are expressed as printed-MCP flags (`<api>-pp-mcp --allow "tool1,tool2"`) and as megamcp flags (`printing-press mega-mcp --allow espn:scores_get,espn:teams_list`). Rationale: symmetry between the per-API MCP and the aggregate server. Security and token economy both benefit.
-
-KTD-8. Parallel execution is exposed at two layers: `printing-press execute --parallel` accepts `-d @batch.json` where the batch is an array of `{api, tool, params}`, and megamcp adds a `batch_execute` meta-tool that accepts the same shape. Rationale: matches Composio's `--parallel` affordance without new infra.
-
-KTD-9. `printing-press execute` is a raw-HTTP-through-manifest path. It intentionally does NOT invoke local SQLite, compound commands (`stale`, `orphans`, `load`), domain archetypes, FTS5 search, or `--select`/`--compact` projection. When the user wants the local data layer or compound semantics, they must use `<api>-pp-cli` directly. The super-CLI is a discovery + batching + auth-unification surface, not a replacement for the printed CLIs. Rationale: without this boundary, `execute` silently diverges from the printed CLI for the same tool name, and the Unit 2 verification step generates false-positive parity bugs every time a printed CLI ships a new compound command. Adjust Unit 2 verification to parity-on-raw-HTTP-status-and-body only.
-
-KTD-10. Super-CLI verbs live under a dedicated `printing-press run` subcommand group to preserve the top-level namespace for generator-only commands. The surface becomes `printing-press run list`, `run info`, `run search`, `run execute`, `run proxy`. A compact alias `pp run ...` can ship later. Rationale: `list`, `search`, `info` are high-value English verbs that a future generator feature (catalog search, spec info) will want. Grouping under `run` both signals "this is the runtime role" and reserves the top-level space for the generator role.
-
-KTD-11. Manifest schema versioning is a blocking precondition, not a phase-five addition. `tools-manifest.json` gets a `schema_version: int` field (start at 1). The manifest reader in `internal/megamcp/manifest.go` refuses unknown major versions with an actionable error, and refuses older versions when reading features that require a newer version. Rationale: KTD-2 makes every future manifest field a backward-compat contract between two release trains (generator releases vs library-manifest regenerations). Without an explicit schema gate, Unit 5's new trigger-ready field and every other additive field becomes a silent-fail surface.
-
-KTD-12. Shared `internal/apihttp/` returns a transport-neutral `Response` type (`{StatusCode, Body, Headers}`), not an `mcp.CallToolResult`. MCP-side wraps the Response into `mcp.NewToolResultText/Error` and applies the 32KB `maxAgentResponse` truncation in that wrapper. CLI-side maps status codes to typed exits (0/2/3/4/5/7) in its own wrapper. Host validation and credential redaction stay in `apihttp` because both callers need them. Rationale: without this split the shared package grows "cli-mode flag" branches in every response path and the 32KB agent cap silently applies to CLI output.
-
-KTD-13. Auth-resolution read order makes the env-var-wins choice explicit and surfaces divergence rather than hiding it. Order: process env -> `PP_AUTH_FILE` override -> shared store -> keychain -> nil. `auth doctor` treats "env var and store both present with different values" as a yellow finding and names the shell file the env var came from where detectable (`~/.zshrc`, `~/.bashrc`, loaded profile). `auth link` prints a warning after a successful link if the corresponding env var is set in the current shell, with copy-paste `unset` guidance. `auth list` adds a "shadowed by env" column. Rationale: env-var-wins is simple but creates a silent-stale-token class of bugs. The detection surface must be first-class, not left to user intuition.
-
-## Feature Stack Ranking
-
-Scoring criteria, each 1-5:
-- Impact: agent/human experience delta.
-- Reach: how many printed CLIs it lights up.
-- Effort (inverted): lower effort -> higher score.
-- Fit: alignment with local-first, no-backend, agent-native philosophy.
-- Moat: how much harder it is for a thin-wrapper competitor to replicate.
-
-| Rank | Feature | Impact | Reach | Effort | Fit | Moat | Total | Tier |
-|------|---------|--------|-------|--------|-----|------|-------|------|
-| 1 | Unified super-CLI (`printing-press execute`/`search`/`info`/`proxy`/`list`) | 5 | 5 | 4 | 5 | 4 | 23 | S |
-| 2 | Unified auth manager (`auth login`/`auth link`/`auth status`/`auth doctor`) | 5 | 5 | 3 | 4 | 5 | 22 | S |
-| 3 | Local triggers system (`trigger add`/`trigger run`) | 5 | 4 | 3 | 4 | 5 | 21 | A |
-| 4 | `allowed_tools` + `denied_tools` on `<api>-pp-mcp` and megamcp | 3 | 5 | 5 | 5 | 2 | 20 | A |
-| 5 | `--parallel` batch execution across tools (CLI + megamcp) | 4 | 4 | 5 | 4 | 2 | 19 | A |
-| 6 | `printing-press run --file workflow.yaml` (scriptable chains) | 3 | 3 | 2 | 4 | 3 | 15 | B |
-| 7 | Per-CLI `llms.txt` / `llms-full.txt` artefact | 3 | 5 | 5 | 4 | 1 | 18 | B |
-| 8 | Typed SDK codegen (`printing-press generate ts\|py`) | 2 | 3 | 2 | 3 | 2 | 12 | B |
-| 9 | Browser-based playground for printed CLIs | 2 | 3 | 1 | 2 | 2 | 10 | C |
-| 10 | Per-user multi-tenant MCP URL hosting | 2 | 5 | 1 | 1 | 2 | 11 | C |
-| 11 | Python-sandboxed tool execution | 2 | 3 | 1 | 1 | 2 | 9 | C |
-| 12 | Hosted dashboard for connections/triggers/logs | 3 | 5 | 1 | 1 | 2 | 12 | C |
-| 13 | White-label auth screens | 1 | 2 | 2 | 2 | 1 | 8 | C |
-
-Stack-ranking commentary:
-
-S-tier is where PP wins back the ground Composio covers for one-command discovery and one-credential onboarding, without giving up local-first. These are the two features the user will feel in week one.
-
-A-tier adds event-driven workflows, safety (tool filtering), and throughput (parallel). Each is valuable independently; together they turn the printed MCPs into a production-grade agent substrate.
-
-B-tier is nice to have. llms.txt is basically free and should probably ship alongside S-tier. The workflow YAML and codegen can wait until there is user demand signal.
-
-C-tier is where we explicitly refuse to follow Composio. A hosted dashboard and multi-tenant MCP service would fork PP away from its shipped posture; Python sandboxing does not fit Go-binary distribution; white-label auth has no PP customer.
-
-## High-Level Technical Design
-
-> This illustrates the intended approach and is directional guidance for review, not implementation specification. The implementing agent should treat it as context, not code to reproduce.
-
-### Surface shape for the super-CLI
-
-Super-CLI verbs live under the reserved `run` namespace (KTD-10) to preserve the top-level namespace for generator-only commands.
-
-```
-printing-press                   # existing generator entry point
-  generate ...                   # existing
-  verify ...                     # existing
-  scorecard ...                  # existing
-  emboss ...                     # existing
-  dogfood ...                    # existing
-  mega-mcp [--allow ...] ...     # existing, extended (Unit 4)
-
-  run list                       # new: installed printed CLIs + auth status
-  run info <api> [<tool>]        # new: manifest / schema lookup
-  run search <query> [--api X]   # new: delegates to megamcp-style search across manifests
-  run execute <api> <tool> [-d .]# new: raw-HTTP-through-manifest (KTD-9); does NOT use local SQLite or compound commands
-          [--parallel -d @batch.json]
-          [--dry-run] [--get-schema] [--skip-auth-check]
-  run proxy --api X -X METHOD /path  # new: authenticated raw HTTP using unified auth
-          [-H ...] [-d ...]
-
-  auth login                     # new: no-op placeholder; future SSO
-       link <api>                # new: per-API OAuth (state+PKCE) / api-key capture (tty only)
-       list [--api X]            # new: linked creds + expiry + shadowed-by-env + env-differs columns
-       status                    # new: quick summary
-       doctor                    # new: probe creds + env divergence + perms + sync-path hygiene
-       revoke <api>
-       fix-perms                 # new: restore 0600/0700 on ~/.pp and contents
-       rotate-secret             # new: webhook sink secret rotation (Unit 5)
-
-  trigger add <api> <event>      # new: create a polling trigger
-          --sink webhook|stdout|file
-          --url https://...      # webhook only, https enforced unless --insecure
-          --secret <value>       # webhook only, auto-generated if omitted
-          --every 5m
-          list
-          run                    # daemon mode, HydrateForRequest only
-          logs
-          remove <id>
-          rotate-secret <id> [--overlap 10m]
-```
-
-### Credential resolution order (read path)
-
-```
-hasCred(api) =
-  os.Getenv(primaryEnvVar)          # printed CLI's current path, unchanged
-    || os.Getenv(PP_AUTH_FILE)      # operator override
-    || authstore.Read(api)           # shared JSON file at ~/.pp/credentials.json
-    || keychain.Read(api)            # P2 follow-up, same interface
-    || nil
-```
-
-Two hydration helpers (KTD-13, KTD-3):
-
-- `authload.HydrateForRequest(ctx, slug) -> string` returns the token for use directly in a request header. Token does not land in process env. Preferred for all new printed CLIs and mandatory for the trigger daemon (Unit 5).
-- `authload.Hydrate(slug, envVarName)` populates `os.Setenv` before a CLI's legacy auth code reads it. Kept for already-printed CLIs we do not want to re-emit. Documented as the less-safe path.
-
-Printed CLIs keep their existing env-var semantics. Env-var-wins over store is preserved, but `auth doctor` and `auth list` surface divergence so stale `.zshrc` entries are visible instead of silently shadowing a fresh link.
-
-### Trigger polling loop
-
-```
-for each trigger in store:
-  tick := schedule.Next(trigger)
-  wait until tick
-  cursor := store.GetCursor(trigger.id)
-  diff := printedCLI(trigger.api).Sync(--since cursor, --json)
-  if diff is empty: continue
-  emit(trigger.sink, trigger.event, diff)
-  store.SetCursor(trigger.id, diff.nextCursor)
-```
-
-Sinks: `webhook` POSTs signed JSON; `stdout` writes JSONL for the agent to tail; `file` appends to a rotated log.
-
-### Generator change for future CLIs
-
-Templates add one line: every printed CLI imports `cliutil/authload` and calls `authload.Hydrate("api-slug", "PRIMARY_ENV_VAR")` in root command `PersistentPreRun`. That is the entire surface area in the printed CLI. All the logic lives in the shared package.
-
-## Implementation Units
-
-- [ ] Unit 0: Manifest schema versioning (blocking precondition)
-
-  Goal: Add `schema_version` to `tools-manifest.json`, enforce it in the manifest reader, and establish the backward-compat contract before any new manifest fields ship.
-
-  Requirements: R3, R4, R5
-
-  Dependencies: None. Must land before Units 1, 2, or 5.
-
-  Files:
-  - Modify: `internal/megamcp/manifest.go` to add `SchemaVersion int` on `ToolsManifest` and refuse unknown major versions when reading.
-  - Modify: `internal/pipeline/toolsmanifest.go` to emit `schema_version: 1` on generation.
-  - Modify: the writer path for the `printing-press-library` registry to tolerate mixed-version manifests.
-  - Test: `internal/megamcp/manifest_test.go` extends to cover version-missing, version-current, version-future-major, version-future-minor.
-
-  Approach:
-  - Current version 1. A missing field is treated as version 1 to keep all shipped manifests valid.
-  - Future-major refusal is actionable: "manifest schema_version=2 is newer than this printing-press build; upgrade to at least vX.Y."
-  - Future-minor with unknown fields is accepted with a debug-level log ("ignoring unknown field X in manifest"). Never fail on forward-compat minor bumps.
-
-  Patterns to follow: `internal/megamcp/manifest.go` structure; existing manifest tests.
-
-  Test scenarios:
-  - Happy path: manifest with no `schema_version` loads as v1.
-  - Happy path: manifest with `schema_version: 1` and known fields loads.
-  - Edge case: manifest with `schema_version: 1` and an unknown field (forward-compat minor) loads with one warning log and no error.
-  - Error path: manifest with `schema_version: 2` returns a typed error and a named upgrade instruction; no partial registration.
-  - Integration: megamcp started against a library containing one v1 manifest and one v2 manifest registers only the v1 one and logs the v2 skip at warn level.
-
-  Verification: `printing-press run list` shows accepted and skipped manifests distinctly so users can diagnose version mismatches without reading logs.
-
-- [ ] Unit 1: Super-CLI skeleton - run list, run info, run search
-
-  Goal: Ship `printing-press run list`, `printing-press run info`, `printing-press run search` as thin readers over the existing megamcp registry code, under the reserved `run` namespace (KTD-10).
-
-  Requirements: R3, R4
-
-  Dependencies: Unit 0.
-
-  Files:
-  - Create: `internal/supercli/registry.go` (wrapper reusing `internal/megamcp/registry.go`)
-  - Create: `internal/supercli/list.go`, `internal/supercli/info.go`, `internal/supercli/search.go`
-  - Create: `internal/cli/supercli_cmd.go` (Cobra wiring)
-  - Modify: `cmd/printing-press/main.go` to register the subcommand group
-  - Test: `internal/supercli/registry_test.go`, `internal/supercli/list_test.go`, `internal/supercli/search_test.go`
-
-  Approach:
-  - Share the same manifest loader megamcp uses so a newly-installed printed CLI is discoverable without a re-registration step.
-  - `list` output: table in TTY, JSON when piped. Columns: api, cli-binary, mcp-binary, tool_count, auth_type, auth_status (green/red based on env + store).
-  - `info <api>` returns the full `tools-manifest.json`. `info <api> <tool>` returns a single tool with its parameters.
-  - `search <query>` reuses `makeSearchToolsHandler` logic from megamcp directly; result shape matches.
-
-  Patterns to follow: `internal/megamcp/metatools.go` handlers.
-
-  Test scenarios:
-  - Happy path: `list` returns all 21 library manifests when the library is installed.
-  - Happy path: `info espn` returns the ESPN manifest.
-  - Happy path: `search "scores"` ranks `espn:scores_get` above unrelated tools.
-  - Edge case: `info unknown-api` returns exit 3 and a typed error with suggested APIs.
-  - Edge case: empty library returns `[]` in JSON mode, not an error.
-
-  Verification: `list`, `info`, and `search` all pass `--json | jq` round-trips; auto-JSON triggers when piped.
-
-- [ ] Unit 2: Super-CLI execute + proxy
-
-  Goal: Ship `printing-press run execute <api> <tool>` and `printing-press run proxy --api X -X METHOD /path` under the reserved `run` namespace (KTD-10), explicitly as raw-HTTP-through-manifest paths that do NOT invoke local data layer or compound commands (KTD-9).
-
-  Requirements: R3, R4
-
-  Dependencies: Unit 0, Unit 1.
-
-  Files:
-  - Create: `internal/apihttp/execute.go` with the transport-neutral `Response` type (KTD-12).
-  - Create: `internal/apihttp/hosts.go` (lift host validation from `internal/megamcp/handler.go`).
-  - Create: `internal/apihttp/redact.go` (lift credential redaction).
-  - Modify: `internal/megamcp/handler.go` to wrap `apihttp.Execute` and own the 32KB truncation and MCP-specific result shaping.
-  - Create: `internal/supercli/execute.go`, `internal/supercli/proxy.go`, `internal/supercli/batch.go`.
-  - Modify: `internal/cli/supercli_cmd.go` to wire under `run execute` / `run proxy`.
-  - Test: `internal/apihttp/execute_test.go`, `internal/supercli/execute_test.go`, `internal/supercli/proxy_test.go`, `internal/supercli/batch_test.go`, plus `internal/megamcp/handler_test.go` regression coverage for the wrapper.
-
-  Approach:
-  - `apihttp.Execute(ctx, manifest, tool, args) -> (Response, error)` builds the URL with path-param substitution, applies auth via `internal/authstore` (Unit 3) with env-var fallback, enforces host allowlist, and returns `{StatusCode, Body, Headers}` without any transport-specific shaping.
-  - MCP wrapper applies the 32KB `maxAgentResponse` truncation and returns `mcp.CallToolResult`.
-  - CLI wrapper maps status codes to typed exits: 2xx -> 0, 401/403 -> 4, 404 -> 3, 429 -> 7 (after one backoff), 4xx-other -> 2, 5xx -> 5. Prints table for human, JSON when piped. Does not truncate; the user can pipe to `head` if they want.
-  - `proxy` skips the tool registry, resolves auth for the named API, and makes the raw request. Mirrors curl: `-X`, `-H` (repeatable), `-d`.
-  - All commands honour `--dry-run`, `--get-schema`, `--skip-auth-check`, and auto-JSON.
-  - Execute does not read any printed CLI's SQLite store and does not run compound commands (KTD-9). Help text states this explicitly and points users to `<api>-pp-cli` for those features.
-
-  Execution note: Characterization-first. Write the wrapper test for megamcp's existing behaviour before extracting, assert parity post-extraction, then layer the CLI wrapper. `internal/megamcp/handler.go:28` is the lifting target.
-
-  Patterns to follow: `internal/megamcp/handler.go`, `internal/megamcp/auth.go`.
-
-  Test scenarios:
-  - Happy path: `run execute espn scores_get --league nfl` returns valid JSON.
-  - Happy path: `run proxy --api dub -X GET /links` returns the raw API response body; no local-store augmentation.
-  - Happy path: megamcp wrapper around `apihttp.Execute` still truncates at 32KB; CLI wrapper does not.
-  - Edge case: missing required path param returns exit 2 and names the missing placeholder.
-  - Edge case: `--skip-auth-check` lets the caller hit an unauthenticated endpoint without a store lookup.
-  - Error path: 401 from the API returns exit 4 with an actionable "run `printing-press auth link <api>`" hint.
-  - Error path: 429 with retry-after returns exit 7 after one backoff attempt.
-  - Error path: host not in the manifest's allowlist returns exit 2 with a "host not permitted by manifest" message; same behaviour on both wrappers.
-  - Integration: `run execute --parallel -d @batch.json` runs three tool calls concurrently and returns responses in input order, not completion order.
-  - Integration: extracting `apihttp` does not regress existing megamcp handler tests; characterization suite passes pre and post extraction.
-
-  Verification: parity on raw HTTP status-and-body between `run execute espn X` and `espn-pp-cli X --json` for five tools (KTD-9 scope: bodies only, not `--select`, `--compact`, or compound outputs). Megamcp handler snapshot tests continue to pass after extraction.
-
-- [ ] Unit 3: Unified auth store + `auth` subcommand group
-
-  Goal: Ship `printing-press auth link`, `auth list`, `auth status`, `auth doctor`, `auth revoke`, and the shared credential store that backs them, with explicit threat model, PKCE+state on OAuth, and divergence detection between env vars and store.
-
-  Requirements: R3, R4, R5
-
-  Dependencies: Unit 0, Unit 1.
-
-  Threat model (accepted in P1, revisited at keychain P2):
-  - Same-UID processes (malicious npm/pip postinstall, VS Code extensions, shell plugins) can read `~/.pp/credentials.json`. 0600 does not defend here. Keychain backend (KTD-4) is the mitigation path; P2 gate is "before trigger daemon general availability OR before $100 user, whichever comes first."
-  - Backup and sync exfiltration via Time Machine, iCloud Desktop, Dropbox, rsync. Mitigation: `~/.pp/` directory ships with a `.nosync` marker file and the README documents how to exclude it from common backup tools.
-  - Shell history / argv leakage during api-key prompts. Mitigation: `auth link` reads secrets only from tty with echo disabled, refuses argv-provided secrets (`--token` flag rejected), and scrubs crash reports of argv containing high-entropy strings.
-  - Crash dumps and core files. Mitigation: plan explicitly does not address; documented as out-of-scope for P1.
-  - No at-rest encryption and no file integrity. Mitigation: accepted in P1 with an HMAC-SHA256 over each entry using a device-local secret derived from the hostname and a random salt stored in `~/.pp/.device-key` (0600). Tampering detected, not prevented.
-  - Concurrent writers. Mitigation: single-writer lock via `~/.pp/.credentials.lock` (flock on Linux/macOS, LockFileEx on Windows). Lock contention returns exit 5 with a "another auth operation in progress" message.
-
-  Files:
-  - Create: `internal/authstore/store.go` (file-based backend at `~/.pp/credentials.json`, `chmod 600`, atomic writes)
-  - Create: `internal/authstore/backend.go` (pluggable `Backend` interface; file backend today; keychain stub)
-  - Create: `internal/authstore/providers/` (registry of OAuth providers by api slug)
-  - Create: `internal/authstore/providers/github.go`, `providers/linear.go`, `providers/slack.go` (initial three)
-  - Create: `internal/cli/auth_cmd.go`
-  - Create: `internal/cliutil/authload/` package emitted into every printed CLI
-  - Modify: generator templates to emit one `authload.Hydrate("<slug>", "<PRIMARY_ENV>")` call in root command `PersistentPreRun`
-  - Test: `internal/authstore/store_test.go`, `internal/authstore/providers/github_test.go`, `internal/cliutil/authload/authload_test.go`
-
-  Approach:
-  - Store schema: `{api: {type, token, refresh_token, expires_at, scopes, linked_at, entry_hmac}}`. One file, one slug per key. `entry_hmac` verifies integrity on read.
-  - Parent dir `~/.pp/` is 0700 with an atomic mkdir. `credentials.json` is 0600 written via `O_EXCL` tempfile + rename under the `.credentials.lock`.
-  - `auth link <api>` OAuth flow hardening:
-    - Bind loopback callback on `127.0.0.1` only (never `0.0.0.0` or `localhost` DNS resolution).
-    - Generate a cryptographically random `state` per flow; reject callbacks with missing or mismatching state.
-    - Use PKCE (RFC 7636) with S256 challenge/verifier on every OAuth flow. Providers that do not support PKCE are documented, not silently downgraded.
-    - Exact redirect URI match; plan a per-provider registration table in `internal/authstore/providers/README.md` that names each provider's accepted redirect URI form (fixed-port, `127.0.0.1` wildcard-port, or out-of-band).
-    - Callback server accepts exactly one request, shuts down in under 120 seconds, and times out with exit 4.
-    - Enumerate callback error codes: `access_denied`, `server_error`, `invalid_scope`, `invalid_request`, missing-`code`, non-2xx token-exchange response. Each maps to exit 4 with a distinct message.
-    - OIDC-bearing providers (Google, some enterprise) verify `iss` and `nonce` when ID tokens are present; others skip.
-  - `auth link <api>` api-key mode: tty-only prompt with echo disabled, refuses argv-sourced secrets.
-  - `auth doctor`:
-    - For every installed printed CLI, probe credential presence, expiry, and env-vs-store divergence (KTD-13).
-    - "Env var and store differ" is a yellow finding with the originating shell file where detectable.
-    - Missing 0600 perms on `credentials.json` or 0700 on `~/.pp/` is a red finding with a `fix-perms` suggestion.
-    - Dir listed in a known sync path (`~/Dropbox`, `~/iCloud`, `~/OneDrive`) is a yellow finding with exclusion guidance.
-  - `auth link <api>` post-success: if the corresponding env var is set in the caller's environment, print a warning with copy-paste `unset` guidance; do not silently override.
-  - `auth list` column model: `api`, `type`, `linked_at`, `expires_at`, `scopes`, `shadowed_by_env` (yes/no), `env_differs` (yes/no).
-  - `auth revoke <api>` removes the entry and posts to the provider's revoke endpoint when available.
-  - Generator change is additive and uses `HydrateForRequest` rather than `os.Setenv` for new printed CLIs. Templates import `cliutil/authload` and call `authload.HydrateForRequest(ctx, "<slug>")` inside HTTP client construction so tokens live in request headers and not in process env. The legacy `Hydrate` that populates env vars remains available for backward compatibility with already-printed CLIs.
-  - Child-process guardrails: `authload` documents a `SafeExec` helper that strips `*_TOKEN`/`*_KEY`/`*_SECRET` env vars before spawning subprocesses. Printed-CLI templates that shell out (e.g. to `curl`, `git`, `gh`) must use `SafeExec`, enforced by the scorer.
-  - Trigger daemon (Unit 5) MUST use `HydrateForRequest`, never `Hydrate`, because env-resident tokens in a long-running process are a material escalation.
-
-  Execution note: Test-first on `authstore`. The file layout, concurrent-write safety, permission bits, PKCE/state round-trips, and env-shadow detection are the kind of thing we want covered before behaviour depends on them. This section touches security surfaces; route through `compound-engineering:review:security-sentinel` before merge.
-
-  Patterns to follow: `internal/megamcp/auth.go` for the placeholder-substitution read path. Keep env-var-as-truth semantics intact.
-
-  Test scenarios:
-  - Happy path: `auth link github` completes OAuth round-trip against a stubbed authorization server with state + PKCE verification and writes the token to the store with 0600 perms.
-  - Happy path: after `auth link`, a printed CLI that does not have its env var set reads its token via `HydrateForRequest` and successfully calls the API; token never appears in process env.
-  - Happy path: `auth list --json` reports `shadowed_by_env: true` when an env var is set and `env_differs: true` when its value does not match the store.
-  - Edge case: store file does not exist; `auth status` reports empty cleanly.
-  - Edge case: env var is set AND store has a different value; env var wins but `auth doctor` yellow-flags the divergence with the originating shell file.
-  - Edge case: store file has wrong perms; `auth doctor` reports a red finding and `auth fix-perms` restores 0600 and 0700 on the parent dir.
-  - Edge case: callback with missing `state` or `state` mismatch returns exit 4 without corrupting the store.
-  - Edge case: callback with `code` but token exchange returns 5xx returns exit 5 with a retry hint.
-  - Edge case: OAuth flow where provider does not support PKCE returns a named warning during `auth link` and refuses to proceed unless `--allow-no-pkce` is passed; records the downgrade in the entry metadata.
-  - Edge case: concurrent `auth link` invocations serialise via the lock file; the second call reports "another auth operation in progress" and exits 5.
-  - Edge case: api-key prompt rejects an argv-supplied `--token` value with an actionable error.
-  - Edge case: tampered `credentials.json` (HMAC mismatch) returns exit 4 on read with a "credentials file integrity failure; re-run auth link" message.
-  - Error path: OAuth callback with `error=access_denied` returns exit 4 and does not corrupt the store.
-  - Error path: token refresh returns 401; `auth doctor` marks the creds expired, does not crash.
-  - Error path: loopback callback binds successfully but no request arrives within 120s; returns exit 4 and shuts down the listener.
-  - Integration: three printed CLIs share one store; each sees only its own slug's token via `HydrateForRequest`; none of them see any slug's token in `os.Environ()`.
-  - Integration: printed CLI using `SafeExec` to invoke `curl` does not leak `*_TOKEN` to the child process's environment.
-
-  Verification: on a clean machine, `printing-press auth link github && github-pp-cli issues list` succeeds without the user ever exporting `GITHUB_TOKEN`, and `ps eww` during the call does not show the token in the printed CLI's environment.
-
-- [ ] Unit 4: `allowed_tools` and `denied_tools` on per-API MCP and megamcp
-
-  Goal: Ship `--allow` and `--deny` flags on printed MCP servers and the aggregate server.
-
-  Requirements: R3
-
-  Dependencies: None. Can ship in parallel with Units 1-3.
-
-  Files:
-  - Modify: megamcp's `internal/megamcp/activation.go` and `metatools.go` to accept allow/deny lists (slug-scoped like `espn:scores_get`).
-  - Modify: generator template for `cmd/<api>-mcp/main.go` to accept `--allow` / `--deny` flags.
-  - Test: `internal/megamcp/activation_test.go` (extend), plus a new template-output snapshot test.
-
-  Approach:
-  - Both lists are comma-separated. Deny wins over allow. Empty allow means "all except denied".
-  - megamcp additionally accepts `--allow-api` / `--deny-api` for whole-API gating.
-  - Scorer recognises the new flag as a ship-ready feature so we do not penalise CLIs that now expose it.
-
-  Execution note: Extend the existing activation tests rather than adding a parallel test file; the behaviour is a filter layered on top of activation.
-
-  Test scenarios:
-  - Happy path: `--allow espn:scores_get` exposes only `scores_get`.
-  - Happy path: `--deny espn:scores_delete` hides one tool; others remain.
-  - Edge case: both `--allow` and `--deny` set; deny wins over allow conflicts.
-  - Edge case: unknown tool name in `--allow` logs a warning, does not crash.
-  - Integration: megamcp with `--allow-api espn,dub` only surfaces those two manifests in `library_info`.
-
-  Verification: `mcp list-tools` on a filtered server returns exactly the expected set across three configurations.
-
-- [ ] Unit 5: Trigger daemon (polling mode)
-
-  Goal: Ship `printing-press trigger add`, `trigger list`, `trigger run`, `trigger logs`, `trigger remove` with polling-based delivery.
-
-  Requirements: R3, R4
-
-  Dependencies: Unit 1 (registry), Unit 3 (auth resolution).
-
-  Files:
-  - Create: `internal/triggers/store.go`, `internal/triggers/runner.go`, `internal/triggers/sinks.go`
-  - Create: `internal/cli/trigger_cmd.go`
-  - Modify: printed-CLI template spec so every sync-capable manifest declares its "trigger-ready" resources in `tools-manifest.json` (additive field, backward compatible).
-  - Test: `internal/triggers/runner_test.go`, `internal/triggers/sinks_test.go`
-
-  Approach:
-  - `trigger add <api> <event> --sink --every` validates the event exists in the API's manifest, writes a trigger record to `~/.pp/triggers.json` (0600, 0700 parent), does not start a daemon.
-  - `trigger run` reads the file and schedules polls using `time.Ticker`. For each trigger it shells out (via `SafeExec` so tokens do not leak into child env) to the printed CLI's incremental sync or list-since command and diffs.
-  - Daemon auth: uses `authstore.HydrateForRequest` at request time only (KTD-13, Unit 3). Tokens never land in the daemon's process env because the daemon is long-lived and `/proc/<pid>/environ` is readable same-UID.
-  - Sinks:
-    - `webhook`: signed POST over HTTPS. Plain `http://` is refused unless `--insecure` is explicitly passed. Signing is HMAC-SHA256 over the canonical raw body bytes (before any JSON re-serialization). Header: `X-PP-Signature: t=<unix-seconds>,v1=<hex-hmac>`. Receivers reject signatures with a timestamp older than 300 seconds (replay window). Each delivery also carries `X-PP-Delivery-Id: <uuidv4>` for receiver-side idempotency.
-    - `stdout`: writes JSONL for an agent to tail.
-    - `file`: appends with rotation at 10MB.
-  - Secret origin and rotation:
-    - On first `trigger add --sink webhook`, auto-generate a 32-byte random secret, print it exactly once with a "save this now" warning, and store it in `~/.pp/triggers.json` under the sink config (0600).
-    - `trigger rotate-secret <id>` generates a new secret and accepts both old and new signatures for an overlap window (default 10 minutes, configurable via `--overlap`). During the window, each outbound request carries both `X-PP-Signature` (v1=new) and `X-PP-Signature-Prev` (v1=old) so receivers can cut over with zero missed deliveries.
-    - A user-supplied `--secret` flag on `trigger add` is supported for receivers that already have a secret from another system.
-    - A missing secret refuses to start the daemon with exit 2 and a named remediation; there is no silent unsigned-send path.
-  - Delivery semantics: at-least-once; receiver-side dedup expected via `X-PP-Delivery-Id`. Failed deliveries retry with exponential backoff up to 6 attempts over ~30 minutes, then queue to disk with bounded depth (default 1000 events per trigger).
-  - `trigger logs` reads the rotated log.
-
-  Execution note: Start with a failing integration test that runs `trigger add` + `trigger run` against a mock HTTP server with seeded data, and asserts the webhook was called with the expected diff.
-
-  Patterns to follow: `sync` cursor handling in printed CLIs. Reuse the cursor format wherever possible so a trigger and an ad-hoc `sync` do not fight each other.
-
-  Test scenarios:
-  - Happy path: new Linear issue appears; trigger fires; webhook receives HTTPS POST with `X-PP-Signature` (HMAC-SHA256 verifies against stored secret), `X-PP-Delivery-Id` (UUIDv4), and body with `event: linear.issue_created`.
-  - Happy path: `trigger list --json` returns every configured trigger with next-poll time.
-  - Happy path: `trigger rotate-secret <id> --overlap 10m` emits both `X-PP-Signature` and `X-PP-Signature-Prev` for 10 minutes, then drops the old secret.
-  - Edge case: API returns the same cursor twice; no duplicate events emitted (daemon-side idempotency).
-  - Edge case: receiver is reachable but returns 5xx; event is queued to disk and retried with exponential backoff up to 6 attempts.
-  - Edge case: `--sink webhook --url http://example` without `--insecure` is refused at `trigger add` with exit 2.
-  - Edge case: request with timestamp older than 300s replay window is correctly rejected by a verifying receiver (test both sides).
-  - Edge case: missing webhook secret refuses to start the daemon with exit 2 and a named remediation.
-  - Error path: the printed CLI is not installed; `trigger add` fails with exit 3 and points to `printing-press run list` to see what is installed.
-  - Error path: polled API returns 429; runner respects retry-after and extends the tick for that trigger only.
-  - Error path: daemon process crash mid-delivery; on restart the undelivered event replays exactly-once per `X-PP-Delivery-Id` (at-least-once delivery, receiver-side dedup expected).
-  - Integration: two triggers against the same API share one HTTP connection pool and do not exceed the API's rate limit budget.
-  - Integration: long-running daemon with 24h uptime shows no authentication token in `/proc/<pid>/environ` (HydrateForRequest confirmed).
-
-  Verification: 24-hour soak with three triggers against Linear, GitHub, and HubSpot printed CLIs shows zero missed events, signed deliveries verify on the receiver, and `ps eww` on the daemon shows no tokens in env for the entire run.
-
-- [ ] Unit 6: `execute --parallel` and megamcp `batch_execute`
-
-  Goal: Concurrent execution across tools.
-
-  Requirements: R3
-
-  Dependencies: Unit 2.
-
-  Files:
-  - Modify: `internal/supercli/execute.go` to accept `--parallel` when input is a batch array.
-  - Modify: `internal/megamcp/metatools.go` to register `batch_execute`.
-  - Test: `internal/supercli/execute_test.go` (extend), `internal/megamcp/metatools_test.go` (extend)
-
-  Approach:
-  - Bounded concurrency via `cliutil.FanoutRun` (already exists). Default fanout of 8. Per-API rate-limit aware: each API gets its own semaphore derived from manifest rate-limit metadata if present, else defaults.
-  - Results returned in input order regardless of completion order.
-  - Any one failure does not abort the batch; the response array contains typed errors per index.
-
-  Patterns to follow: `cliutil.FanoutRun` (see glossary).
-
-  Test scenarios:
-  - Happy path: batch of three tools across three APIs returns three results in input order.
-  - Edge case: one of three fails; result at that index is a typed error, other two succeed.
-  - Edge case: all three target the same API; per-API semaphore prevents flooding.
-  - Integration: batch with 20 tools across 4 APIs completes in under 2x the slowest single call.
-
-  Verification: load test with 100-tool batch against mock servers shows linear-ish concurrency scaling up to the configured fanout cap.
-
-## System-Wide Impact
-
-- Interaction graph: the super-CLI and megamcp converge on one shared `internal/apihttp` package for request building and auth resolution; both depend on `internal/authstore` going forward. Changes to either ripple to both.
-- Error propagation: typed exits (0/2/3/4/5/7) are preserved across the super-CLI, meaning agents do not need new error-handling code to consume `execute`/`proxy` alongside existing printed CLIs.
-- State lifecycle risks: the credential store and the trigger store both live at `~/.pp/`. Concurrent writers (two `printing-press auth link` invocations) need atomic-write discipline. Same applies to `~/.pp/triggers.json` under `trigger add` contention.
-- API surface parity: every new subcommand needs a parallel MCP meta-tool in megamcp so agents that prefer MCP do not lose reach. `list` -> `library_info` (exists), `search` -> `search_tools` (exists), `execute` -> `batch_execute` (new), `auth` ops -> `auth_status`/`auth_link_url` meta-tools (new, link flow is interactive so the MCP version returns the URL and expects the user to complete in-browser).
-- Integration coverage: the store file is accessed by both the super-CLI and every printed CLI via `authload.Hydrate`. Unit tests covering single-process behaviour are necessary but not sufficient; we need one cross-process integration test that exercises "super-CLI writes, printed CLI reads".
-- Unchanged invariants: printed CLIs keep reading env vars as their primary credential source. Absence of the unified store does not break them. Generators keep producing the same two binaries per API. The scorecard bar stays where it is.
-
-## Risks & Dependencies
-
-| Risk | Likelihood | Impact | Mitigation |
-|------|-----------|--------|------------|
-| OAuth providers differ enough per API that `authproviders/` turns into a maintenance tax | Med | Med | Start with three providers (GitHub, Linear, Slack). Require each new provider to fit a 150-line budget; if it does not, capture in retro and reshape the interface before the fourth |
-| Unified auth store becomes a secret-exfiltration target (plaintext JSON) | Med | High | 0600 perms on the file, feature-flag the keychain backend and complete it in a P2 follow-up before promoting the store to default |
-| Trigger daemon drifts from printed CLI sync cursors and double-fires | Med | Med | Share the cursor source of truth rather than duplicating it; test with the same DB that the printed CLI writes to |
-| Super-CLI grows into a monolith that couples to every printed CLI's internals | Med | Med | The boundary is the manifest. Super-CLI reads manifests and uses the generic HTTP handler. If a printed CLI has behaviour the super-CLI cannot reach, that is a signal the manifest needs to be richer, not that the super-CLI needs a shortcut |
-| We ship an `auth` story that feels worse than Composio's because we lack a hosted OAuth broker | High | Med | Set expectations: we will not be cloning Composio's 1,000-toolkit OAuth coverage. We ship the interface and the top five providers, and document how to add the sixth. Positioning emphasises local-first as the feature |
-| megamcp allowed_tools filter collides with activation logic | Low | Med | Extend existing activation tests rather than forking a new code path |
-| Parallel execution breaks fragile APIs without per-API rate budgets | Med | Med | Default fanout of 8; per-API semaphores read from manifest metadata; first-class retry-after handling |
-| Agents get confused by two ways to do the same thing (`<api>-pp-cli X` vs `printing-press run execute api X`), especially since they have different semantics (KTD-9) | Med | Med | Documentation emphasises `run execute` as raw-HTTP-through-manifest, printed CLIs as the full-local-layer path. Help text on both surfaces names the other and states the scope boundary. Scorer adds a dimension for "does the help text point to the sibling surface" |
-| Manifest schema drift between generator and runtime versions causes silent-fail on new fields | Med | High | Unit 0 blocks; `schema_version` field and reader gate enforce the contract before any additive field ships |
-| Secrets exfiltration via child-process env inheritance, core dumps, or `/proc/<pid>/environ` | Med | High | `HydrateForRequest` keeps tokens out of process env; `SafeExec` strips secret env vars from subprocesses; trigger daemon mandated to use `HydrateForRequest`; file-based store documented as a same-UID risk until keychain P2 ships |
-| OAuth loopback flow missing state/PKCE/exact-URI hardening | Med | High | Explicitly specified in Unit 3 Approach; PKCE is default-on; state verification is mandatory; providers without PKCE require an explicit downgrade flag and record the downgrade in entry metadata |
-| Webhook signature scheme is homegrown or ambiguous | Med | Med | Unit 5 commits to HMAC-SHA256 + `X-PP-Signature` + timestamp replay window + `X-PP-Delivery-Id`; rotation primitive with overlap window; refuses plain HTTP without `--insecure` |
-
-## Future Considerations (B and C tier)
-
-Not in this plan. Record decisions here so we do not relitigate.
-
-- B-tier candidate: `printing-press run --file workflow.yaml`. Revisit when we see a concrete user case we cannot serve with `ppl` skills plus `execute --parallel`.
-- B-tier candidate: `printing-press generate ts|py` typed SDK codegen. Revisit when we have a frontend agent customer.
-- B-tier candidate: per-CLI `llms.txt`. Cheap. Bundle it opportunistically when we next touch the README generator.
-- C-tier refused: hosted dashboard, multi-tenant per-user MCP URL hosting, Python sandbox, white-label auth screens. These require a backend or a runtime we do not want to own.
-
-## Sources & References
-
-- CLI reference: https://composio.dev/toolkits/linkedin/framework/cli
-- Platform docs: https://docs.composio.dev
-- MCP overview: https://docs.composio.dev/docs/mcp-overview
-- Triggers: https://docs.composio.dev/docs/triggers
-- Claude Code skills: https://github.com/ComposioHQ/skills
-- Composio monorepo: https://github.com/ComposioHQ/composio
-- PP megamcp plan (shipped): `docs/plans/2026-04-06-002-feat-mega-mcp-aggregate-server-plan.md`
-- PP megamcp implementation: `internal/megamcp/`
-- PP README (feature inventory): `README.md`
-- PP AGENTS.md (machine-vs-printed discipline, glossary): `AGENTS.md`
-- Library registry: `~/printing-press-library/registry.json`
diff --git a/docs/plans/2026-04-19-002-feat-super-cli-run-namespace-plan.md b/docs/plans/2026-04-19-002-feat-super-cli-run-namespace-plan.md
deleted file mode 100644
index 26730781..00000000
--- a/docs/plans/2026-04-19-002-feat-super-cli-run-namespace-plan.md
+++ /dev/null
@@ -1,395 +0,0 @@
----
-title: "feat(cli): super-CLI run namespace for cross-API execute, search, info, and proxy"
-type: feat
-status: active
-date: 2026-04-19
----
-
-# feat(cli): super-CLI run namespace for cross-API execute, search, info, and proxy
-
-## Overview
-
-Add a `printing-press run` subcommand group that gives agents and humans one front door to every printed CLI the user has installed. The surface mirrors the MCP-side capabilities megamcp already provides (library_info, activate_api, search_tools) but as Cobra subcommands on the existing printing-press binary. It does not replace printed CLIs; it sits beside them as a discovery, batching, and raw-HTTP surface for cross-API work.
-
-The plan has one blocking precondition (manifest schema versioning) and five implementation units. It depends on the unified auth plan (`2026-04-19-003-feat-unified-auth-manager-plan.md`) for `run execute` and `run proxy` but not for `run list`, `run info`, or `run search`. Those three can ship independently.
-
-## Problem Frame
-
-The Printing Press generates one printed CLI per API plus a matching per-API MCP server. The library ships 21 printed CLIs today. Agents that need cross-API behaviour must currently:
-
-1. Know every binary name (`espn-pp-cli`, `dub-pp-cli`, `linear-pp-cli`, etc).
-2. Shell out to each one separately, threading auth via per-API env vars.
-3. Re-discover each CLI's command vocabulary from its own `--help`.
-
-The MCP side already solves this via megamcp: one aggregate server with `library_info`, `activate_api`, `deactivate_api`, `search_tools`, `setup_guide`. The CLI side has no equivalent. The same manifest data megamcp uses can drive a CLI-side super-surface. This plan delivers that.
-
-The PP philosophy (README, "Absorb and Transcend") says the GOAT CLI is built by absorbing every good idea and compounding on top. Cross-API single-entry invocation is table-stakes in the broader ecosystem and the absence of a CLI-side version is a visible gap for any agent that composes across APIs.
-
-## Requirements Trace
-
-- R1. Expose one CLI entry point (`printing-press run ...`) that can list installed printed CLIs, show tool schemas, search tools across APIs, execute a tool, and make authenticated raw HTTP calls.
-- R2. Read from the same `tools-manifest.json` source megamcp reads, so MCP and CLI stay in lockstep.
-- R3. Preserve PP's agent-native behaviours: auto-JSON when piped, typed exits (0/2/3/4/5/7), actionable errors, `--dry-run`, `--get-schema`.
-- R4. Do not duplicate the local data-layer behaviour of printed CLIs. Super-CLI is raw-HTTP; printed CLIs remain the opinionated product surface with SQLite, FTS5, compound commands, and domain archetypes.
-- R5. Ship manifest schema versioning before any new additive field so runtime and generator versions can drift safely.
-
-## Scope Boundaries
-
-In scope:
-- `printing-press run list`, `run info`, `run search`, `run execute`, `run proxy`.
-- `internal/apihttp/` shared package extracted from megamcp's handler.
-- `tools-manifest.json` `schema_version` field and reader gate.
-- Parity tests between `run execute` and megamcp's `MakeToolHandler` output for raw HTTP response.
-
-Out of scope:
-- Local data-layer routing. `run execute` does not touch printed-CLI SQLite stores, compound commands, FTS5, or `--select`/`--compact` semantics. Users wanting those keep using `<api>-pp-cli` directly.
-- Authentication flows. Covered by `2026-04-19-003-feat-unified-auth-manager-plan.md`. `run execute` and `run proxy` consume the auth store; they do not create it.
-- Trigger daemon, `--allow`/`--deny` MCP filtering, parallel-batch semantics beyond a thin `--parallel` shim. Those are separate plans.
-- A separate `pp` binary. This is a subcommand group on the existing `printing-press` binary.
-
-## Context & Research
-
-### Relevant code and patterns
-
-- `internal/megamcp/handler.go` - `MakeToolHandler` is the existing request-builder. Path-param substitution, body routing, header handling, host allowlist, auth gate, response classification, and 32KB agent truncation all live here. This is the extraction target for `internal/apihttp/`.
-- `internal/megamcp/manifest.go` - `ToolsManifest` and loader. Schema versioning lands here.
-- `internal/megamcp/registry.go` - multi-manifest loader. `run list` and `run search` read through it.
-- `internal/megamcp/metatools.go` - the shape of `library_info`, `search_tools`, `setup_guide`. `run list`, `run search`, `run info` mirror these shapes on the CLI side.
-- `internal/megamcp/auth.go` - `BuildAuthHeader`, `ApplyAuthFormat`, `hasAuthConfigured`. Used unchanged by `apihttp`.
-- `internal/cli/` - existing Cobra wiring for `generate`, `verify`, `scorecard`, etc. New `run` subcommand group lands beside them.
-- `internal/cliutil/` - generator-reserved helpers (`FanoutRun`, `CleanText`). Do not put super-CLI code here; `internal/supercli/` is its own namespace.
-- `AGENTS.md` glossary - "the printing-press binary" vs "printed CLI" distinction. The super-CLI is part of the printing-press binary (machine role), operating on printed CLIs by reading their manifests.
-
-### Institutional learnings
-
-- `AGENTS.md` machine-vs-printed rule: changes to the super-CLI are machine changes; they affect every installed printed CLI but do not alter any printed CLI's on-disk code.
-- README "Dual interface from one spec" and "Absorb and Transcend" framing: one spec produces CLI + MCP; the super-CLI lets that same manifest drive cross-API execution without duplicating logic.
-- Existing megamcp plan (`docs/plans/2026-04-06-002-feat-mega-mcp-aggregate-server-plan.md`, shipped) established the manifest-as-contract model that this plan extends to the CLI side.
-
-### External references
-
-- `internal/megamcp/` test suite as the ground truth for expected handler behaviour across 21 live manifests in the library.
-- No external library research needed. The super-CLI is a thin reuse of existing in-repo infrastructure.
-
-## Key Technical Decisions
-
-KTD-1. The super-CLI ships as subcommands on the existing `printing-press` binary, not a new binary. Rationale: one install path, one release train, one `/install mvanhorn/cli-printing-press` gets both generator and runtime surfaces. A separate binary doubles distribution concerns without clear upside.
-
-KTD-2. Super-CLI verbs live under a dedicated `printing-press run` namespace. Rationale: `list`, `search`, `info` are high-value English verbs that future generator features (catalog search, spec info) will want. Grouping runtime-role commands under `run` reserves the top-level namespace for generator-role commands.
-
-KTD-3. `run execute` is raw-HTTP-through-manifest. It does NOT invoke local SQLite, compound commands (`stale`, `orphans`, `load`, `reconcile`), domain archetypes, FTS5 search, or `--select`/`--compact` projections. Rationale: without this boundary, `run execute <api> <tool>` silently diverges from `<api>-pp-cli <tool>` for the same tool name, and the user cannot reason about which call gives which result. Help text on both surfaces names the other and states the scope boundary. The scorer adds a dimension for "does help text point to the sibling surface" so drift is caught before ship.
-
-KTD-4. A shared `internal/apihttp/` package returns a transport-neutral `Response` type (`{StatusCode, Body, Headers}`), not `mcp.CallToolResult`. MCP-side wraps it and applies the 32KB `maxAgentResponse` truncation in that wrapper. CLI-side maps status codes to typed exits (0/2/3/4/5/7) in its own wrapper. Rationale: without the split, the shared package grows cli-mode flag branches and the 32KB agent cap silently applies to CLI output. Host validation and credential redaction stay in `apihttp` because both callers need them.
-
-KTD-5. Manifest schema versioning is a blocking precondition. `tools-manifest.json` gets a `schema_version: int` field starting at 1. The reader refuses unknown major versions with an actionable message and accepts unknown minor-version fields with a debug log. Rationale: KTD-4's extraction and future additive fields make every manifest write-then-read a backward-compat contract between two release trains (generator releases vs regenerated library manifests). Without an explicit gate, every new field becomes a silent-fail surface.
-
-KTD-6. `run execute` auth resolution delegates to the `internal/authstore` + `cliutil/authload.HydrateForRequest` pattern defined in the unified auth plan. Env-var-wins precedence is preserved. When the auth plan has not yet shipped, `run execute` still works for APIs whose env vars are set directly, and surfaces a named "credentials not found" exit-4 error with a pointer to `printing-press auth link <api>` (which is planned but not yet present). Rationale: decoupling sequencing so read-only `run list`, `run info`, `run search` can ship in Unit 1 without waiting for auth.
-
-## Open Questions
-
-### Resolved During Planning
-
-- Q: Ship as a new `pp` binary or as subcommands on `printing-press`? A: Subcommands on `printing-press`. See KTD-1.
-- Q: Reserve top-level verbs (`list`, `search`, `info`) or namespace them? A: Namespace under `run`. See KTD-2.
-- Q: Does `run execute` read printed-CLI SQLite when installed? A: No. KTD-3.
-- Q: Does the extracted package return MCP types or neutral types? A: Neutral. KTD-4.
-- Q: Is manifest versioning a precondition or a later addition? A: Precondition. KTD-5.
-
-### Deferred to Implementation
-
-- Exact column order and width heuristics for the `run list` human-friendly table. Resolve once the first implementation hits real terminal widths.
-- Ranking function inside `run search`. Start with the existing `search_tools` ranking in megamcp and adjust only if CLI callers find it inadequate.
-- Whether `run proxy` supports streaming response bodies. Default is buffered; streaming is a P2 follow-up if a printed CLI surfaces a need (e.g., large CSV export endpoints).
-
-## High-Level Technical Design
-
-> This illustrates the intended approach and is directional guidance for review, not implementation specification. The implementing agent should treat it as context, not code to reproduce.
-
-### Command surface
-
-```
-printing-press                       # existing generator entry point, unchanged
-  generate ...
-  verify ...
-  scorecard ...
-  mega-mcp ...
-
-  run list                           # installed printed CLIs + auth status
-  run info <api> [<tool>]            # manifest or tool-schema lookup
-  run search <query> [--api X]       # delegates to manifest registry search
-  run execute <api> <tool> [-d ...]  # raw-HTTP-through-manifest (KTD-3)
-      [--parallel -d @batch.json]
-      [--dry-run] [--get-schema] [--skip-auth-check]
-  run proxy --api X -X METHOD /path  # authenticated raw HTTP
-      [-H ...] [-d ...]
-```
-
-### Package boundary
-
-```
-internal/apihttp/                    # new, shared
-  Execute(ctx, manifest, tool, args) -> (Response, error)
-  Response = { StatusCode, Body, Headers }
-  host allowlist, credential redaction, path-param substitution
-
-internal/megamcp/handler.go          # existing, refactored
-  wraps apihttp.Execute
-  applies 32KB maxAgentResponse truncation here, not in apihttp
-  shapes mcp.CallToolResult
-
-internal/supercli/                   # new, CLI wrappers
-  list.go, info.go, search.go, execute.go, proxy.go, batch.go
-  maps apihttp Response to typed exits (0/2/3/4/5/7)
-  auto-JSON when piped; human table in TTY
-```
-
-### Manifest versioning gate
-
-```
-when reader encounters manifest:
-  missing schema_version  -> treat as 1
-  schema_version = 1 + known fields -> load
-  schema_version = 1 + unknown field -> load, debug-log the skip
-  schema_version > 1      -> refuse with upgrade instruction; do not register
-```
-
-## Implementation Units
-
-- [ ] Unit 0: Manifest schema versioning (blocking precondition)
-
-  Goal: Add `schema_version` to `tools-manifest.json`, enforce it in the manifest reader, and establish the backward-compat contract before any new field ships.
-
-  Requirements: R2, R5
-
-  Dependencies: None. Must land before Units 1-5.
-
-  Files:
-  - Modify: `internal/megamcp/manifest.go` to add `SchemaVersion int` on `ToolsManifest` and refuse unknown major versions.
-  - Modify: `internal/pipeline/toolsmanifest.go` to emit `schema_version: 1` on generation.
-  - Modify: `printing-press-library/registry.json` tolerance for mixed-version manifests.
-  - Test: `internal/megamcp/manifest_test.go` extended with version-missing, version-current, version-future-major, version-future-minor cases.
-
-  Approach:
-  - Current version is 1. Missing field is treated as 1 so all shipped manifests remain valid.
-  - Future-major refusal is actionable: "manifest schema_version=2 is newer than this printing-press build; upgrade to at least vX.Y."
-  - Future-minor unknown-field handling accepts with a debug-level log and never fails. Forward-compat minor bumps must be free.
-
-  Patterns to follow: existing `ToolsManifest` struct and manifest tests.
-
-  Test scenarios:
-  - Happy path: manifest without `schema_version` loads as v1.
-  - Happy path: manifest with `schema_version: 1` and known fields loads.
-  - Edge case: manifest with `schema_version: 1` and an unknown forward-compat field loads with one debug log and no error.
-  - Error path: manifest with `schema_version: 2` returns a typed error naming the minimum required build; no partial registration.
-  - Integration: megamcp started against a library containing one v1 manifest and one v2 manifest registers only the v1 one and surfaces the skip distinctly.
-
-  Verification: `printing-press run list` (Unit 1) shows accepted and skipped manifests so users can diagnose version mismatches without reading logs.
-
-- [ ] Unit 1: Super-CLI read surface - run list, run info, run search
-
-  Goal: Ship the three read-only subcommands. Zero dependency on the auth plan; can ship immediately after Unit 0.
-
-  Requirements: R1, R2, R3
-
-  Dependencies: Unit 0.
-
-  Files:
-  - Create: `internal/supercli/registry.go` wrapping `internal/megamcp/registry.go`.
-  - Create: `internal/supercli/list.go`, `info.go`, `search.go`.
-  - Create: `internal/cli/supercli_cmd.go` for Cobra wiring under `run`.
-  - Modify: `cmd/printing-press/main.go` to register the `run` group.
-  - Test: `internal/supercli/registry_test.go`, `list_test.go`, `info_test.go`, `search_test.go`.
-
-  Approach:
-  - `run list` output: table in TTY, JSON when piped. Columns: api, cli-binary, mcp-binary, tool_count, auth_type, auth_status. `auth_status` is best-effort against env vars only in Unit 1; it is refined in Unit 3 once auth store is live.
-  - `run info <api>` returns the full manifest. `run info <api> <tool>` returns a single tool schema.
-  - `run search <query>` reuses megamcp's `makeSearchToolsHandler` ranking. Result shape matches what the MCP meta-tool returns so agents can move between surfaces without reshaping payloads.
-
-  Patterns to follow: `internal/megamcp/metatools.go` handlers.
-
-  Test scenarios:
-  - Happy path: `run list` returns all 21 library manifests when the library is installed.
-  - Happy path: `run info espn` returns the ESPN manifest.
-  - Happy path: `run info espn scores_get` returns the single tool schema.
-  - Happy path: `run search scores` ranks `espn:scores_get` above unrelated tools.
-  - Edge case: `run info unknown-api` returns exit 3 with a "did you mean" suggestion list.
-  - Edge case: empty library returns `[]` in JSON mode with exit 0, not an error.
-  - Edge case: manifest with `schema_version: 2` is listed as skipped in `run list --all` and omitted by default.
-  - Integration: `run search --api espn scores` filters to ESPN only; same ranking as the unfiltered search when restricted to ESPN's tool set.
-
-  Verification: `run list`, `run info`, `run search` all pass `--json | jq` round-trips. Auto-JSON triggers when piped. Human table output fits 120-column terminals for the 21-library baseline.
-
-- [ ] Unit 2: Shared `internal/apihttp/` extraction
-
-  Goal: Lift the request-building, auth-header, host-allowlist, path-param, and response-classification logic from `internal/megamcp/handler.go` into a transport-neutral `internal/apihttp/` package. Megamcp becomes a thin wrapper; the super-CLI becomes the second caller in Unit 3.
-
-  Requirements: R2, R4
-
-  Dependencies: Unit 0.
-
-  Files:
-  - Create: `internal/apihttp/execute.go` with `Execute(ctx, manifest, tool, args) -> (Response, error)` and the `Response` type.
-  - Create: `internal/apihttp/hosts.go` for host allowlist logic lifted from handler.go.
-  - Create: `internal/apihttp/redact.go` for credential redaction.
-  - Modify: `internal/megamcp/handler.go` to call `apihttp.Execute`, then apply 32KB truncation and shape `mcp.CallToolResult`.
-  - Test: `internal/apihttp/execute_test.go`, `hosts_test.go`, `redact_test.go`.
-  - Modify: `internal/megamcp/handler_test.go` to prove parity pre and post extraction.
-
-  Approach:
-  - Characterization-first. Snapshot the current handler behaviour with a fixture suite covering all 21 manifests, then extract, then re-run the snapshot suite to prove parity.
-  - `Response` is a thin struct: status code, body bytes, response headers. No MCP types. No truncation.
-  - Host allowlist continues to enforce the manifest's declared base URL host; no wildcard scheme changes.
-  - Credential redaction stays keyed on the same auth-format tokens megamcp uses.
-
-  Execution note: characterization-first. Write the pre-extraction snapshot suite first and hold it fixed across the refactor. Add the CLI-side callers in Unit 3 only after snapshots are green.
-
-  Patterns to follow: `internal/megamcp/handler.go`, `internal/megamcp/auth.go`.
-
-  Test scenarios:
-  - Happy path: request construction for a GET with path params produces byte-identical output pre and post extraction across all 21 manifests.
-  - Happy path: POST with body routing produces byte-identical output.
-  - Happy path: auth header is applied identically (same header name, same value, same format token expansion).
-  - Edge case: unsubstituted path placeholder returns the same typed error pre and post.
-  - Edge case: response body larger than 10MB is still rejected at the wire by `apihttp`; the 32KB agent cap lives only in the megamcp wrapper and does not apply via `apihttp` directly.
-  - Error path: host not in allowlist returns the same error on both sides.
-  - Integration: existing megamcp handler_test.go suite passes without modification after the refactor.
-
-  Verification: handler_test.go passes pre and post extraction with no assertion changes. `apihttp` is importable from `supercli` in Unit 3 without circular dependency.
-
-- [ ] Unit 3: run execute + run proxy
-
-  Goal: Ship `printing-press run execute <api> <tool>` and `run proxy --api X -X METHOD /path` on top of `apihttp`, with typed exits, auto-JSON, and auth resolution through the unified auth store.
-
-  Requirements: R1, R3, R4
-
-  Dependencies: Unit 0, Unit 1, Unit 2, and the unified auth plan's Unit 3 (authstore) for credentials.
-
-  Files:
-  - Create: `internal/supercli/execute.go`, `proxy.go`, `batch.go`.
-  - Modify: `internal/cli/supercli_cmd.go` to wire `run execute`, `run proxy`.
-  - Test: `internal/supercli/execute_test.go`, `proxy_test.go`, `batch_test.go`.
-
-  Approach:
-  - `run execute` resolves `<api>` via the registry, resolves `<tool>` against manifest tools, calls `apihttp.Execute`, and maps the status code to a typed exit: 2xx -> 0, 401/403 -> 4, 404 -> 3, 429 -> 7 (after one retry-after backoff), 4xx-other -> 2, 5xx -> 5.
-  - Human output: pretty-printed response body in TTY. JSON when piped. No truncation cap; users can pipe to `head` if they want.
-  - `run proxy` skips the tool registry, resolves auth for the named API, and issues a raw request. Curl-shaped flags: `-X`, `-H` (repeatable), `-d`.
-  - Both honour `--dry-run` (print the request, no send), `--get-schema` (print the tool schema for `execute`; for `proxy` returns an error since there is no schema).
-  - `--skip-auth-check` allows the caller to hit an unauthenticated endpoint without a store lookup.
-  - Help text on both commands names `<api>-pp-cli` as the full-product alternative and explicitly states "this command does not use the local data layer" (KTD-3).
-
-  Patterns to follow: megamcp handler wiring, `internal/cliutil/FanoutRun` for the optional `--parallel` batch shim.
-
-  Test scenarios:
-  - Happy path: `run execute espn scores_get --league nfl` returns valid JSON with exit 0.
-  - Happy path: `run proxy --api dub -X GET /links` returns the raw API response body; no local-store augmentation.
-  - Happy path: megamcp wrapper around `apihttp.Execute` still truncates at 32KB; `run execute` does not.
-  - Edge case: missing required path param returns exit 2 and names the missing placeholder.
-  - Edge case: `--skip-auth-check` lets the caller hit an unauthenticated endpoint without a store lookup.
-  - Edge case: `--dry-run` prints the fully-resolved request (URL, headers with redacted auth, body) without sending.
-  - Error path: 401 returns exit 4 with "run `printing-press auth link <api>`" hint.
-  - Error path: 404 returns exit 3 with the attempted path.
-  - Error path: 429 with retry-after returns exit 7 after exactly one backoff attempt.
-  - Error path: host not in manifest allowlist returns exit 2 with "host not permitted by manifest".
-  - Integration: `run execute --parallel -d @batch.json` runs three tool calls concurrently, returns responses in input order, and a single failure does not abort the batch.
-  - Integration: parity on raw HTTP status and body between `run execute espn X` and `espn-pp-cli X --json` for five tools (raw body only; KTD-3 scope).
-
-  Verification: parity matrix for five tools passes on the raw-HTTP subset defined in KTD-3. Megamcp snapshot suite continues to pass.
-
-- [ ] Unit 4: Scorer dimension for sibling-surface discoverability
-
-  Goal: Add one scorer dimension that checks whether a printed CLI's help text and its `--help` top-of-page reference the sibling surface (`printing-press run execute` for printed CLIs, and a pointer to `<api>-pp-cli` for the super-CLI help text).
-
-  Requirements: R4
-
-  Dependencies: Unit 3.
-
-  Files:
-  - Modify: `internal/generator/scorer.go` (or whichever file holds the Tier 1 dimension logic) to add the new check.
-  - Modify: generator templates for printed CLI root command help text to include the sibling pointer.
-  - Modify: super-CLI help text (`internal/cli/supercli_cmd.go`) to include the printed-CLI pointer.
-  - Test: scorer test fixtures updated; template snapshot tests updated.
-
-  Approach:
-  - Dimension is binary: help text either mentions the sibling surface or does not. Worth 2 points in Tier 1.
-  - Anti-gaming: the scorer checks for a specific sentence shape ("Use `<api>-pp-cli` when you want the local SQLite layer and compound commands") rather than a keyword, so printed CLIs cannot pass by sprinkling the word "run" into unrelated text.
-
-  Patterns to follow: existing Tier 1 dimension entries in the scorer.
-
-  Test scenarios:
-  - Happy path: a printed CLI whose template emits the sibling-pointer sentence scores the dimension.
-  - Edge case: a printed CLI whose help text was manually edited to remove the pointer fails the dimension on rescore.
-  - Integration: a re-scored library after this unit shows 21 CLIs all passing the new dimension (because the template change propagated through a regeneration pass).
-
-  Verification: re-running `printing-press scorecard` across the library shows the new dimension evaluated and the score delta is proportional to the number of CLIs that carry the sibling pointer.
-
-- [ ] Unit 5: Documentation updates
-
-  Goal: Update README, AGENTS.md, and the generator-emitted printed-CLI README template so that the super-CLI is discoverable and the KTD-3 boundary is explicit.
-
-  Requirements: R1, R4
-
-  Dependencies: Units 1-3.
-
-  Files:
-  - Modify: `README.md` to add a "Super-CLI" section between "Dual interface from one spec" and "Domain Archetypes".
-  - Modify: `AGENTS.md` glossary to add `run` namespace, `internal/apihttp/`, and a cross-reference between the super-CLI and printed CLIs.
-  - Modify: generator's README template so every printed CLI's README points to `printing-press run execute` as the cross-API entry point.
-  - Test: `internal/cli/release_test.go` `TestReadmeMentionsSuperCli` ensures future README edits do not drop the pointer.
-
-  Approach: documentation-only unit. Keep the tone consistent with existing README voice. Tables and fenced code blocks, no emphasis markers.
-
-  Test scenarios:
-  - Happy path: README renders with the new section present between the declared anchor headings.
-  - Edge case: generator-emitted README for a newly-printed CLI contains the sibling pointer.
-
-  Verification: a fresh generation pass on one catalog API emits a README mentioning `printing-press run` in the expected position.
-
-## System-Wide Impact
-
-- Interaction graph: megamcp, the super-CLI, and any future surface that needs to execute tools all converge on `internal/apihttp/`. A change to request-building ripples to both MCP and CLI callers uniformly.
-- Error propagation: typed exits (0/2/3/4/5/7) are preserved. Agents that already know PP's exit contract do not need new error-handling code to consume `run execute` alongside `<api>-pp-cli`.
-- State lifecycle risks: no new persistent state. The super-CLI is stateless and reads manifests plus (in Unit 3+) the auth store owned by the auth plan.
-- API surface parity: every `run` subcommand has an MCP meta-tool equivalent on megamcp today (`library_info`, `search_tools`, or the pending `batch_execute`). Keeping that parity is a non-goal per-feature but a guiding principle overall.
-- Integration coverage: the parity tests between `apihttp` pre and post extraction are the backbone. Without them the refactor is unsafe.
-- Unchanged invariants: printed CLIs keep reading env vars as their primary credential source. Printed CLI binaries are not renamed, their command vocabulary does not change, and their local data layers are untouched. The scorer bar does not move except for the one dimension added in Unit 4.
-
-## Risks & Dependencies
-
-| Risk | Likelihood | Impact | Mitigation |
-|------|-----------|--------|------------|
-| `apihttp` extraction regresses megamcp's handler behaviour in a subtle way | Med | High | Characterization-first in Unit 2; snapshot suite covers all 21 live manifests; extraction must keep the suite green |
-| Manifest schema drift between generator and runtime versions causes silent-fail on new fields | Med | High | Unit 0 blocks; `schema_version` field and reader gate enforce the contract |
-| Agents get confused by two ways to do "the same" thing with different semantics (`run execute` vs `<api>-pp-cli`) | Med | Med | Help text on both surfaces names the other; KTD-3 states the boundary; Unit 4 scorer dimension enforces it |
-| `run execute` is misused as a replacement for printed CLIs and users complain that local-layer features are missing | Med | Med | Help text explicit, README section explicit, error messages for missing compound commands point to the sibling surface |
-| Parity matrix between `run execute` and `<api>-pp-cli --json` produces false-positive bug reports every time a printed CLI ships a new compound command | Low | Med | KTD-3 scopes the matrix to raw-HTTP body parity only; compound commands and projection flags are excluded from the matrix |
-| `run proxy` is used to bypass manifest-declared host allowlist | Low | High | `apihttp` enforces the allowlist for `proxy` too; `proxy` accepts only paths against the named API's base URL, not arbitrary URLs |
-| Dependency on auth plan delays Unit 3 ship date | Med | Med | Units 0, 1, 2 ship independently and deliver visible value (list, info, search, `apihttp` refactor); Unit 3 waits on the auth plan's authstore unit |
-
-## Alternative Approaches Considered
-
-- A separate `pp` binary. Rejected in KTD-1: doubles distribution and release plumbing without a new capability.
-- Leaving `apihttp` inside megamcp and having the super-CLI shell out to the `<api>-pp-cli` binary for execute. Rejected: shelling out gives the local-layer path which KTD-3 explicitly rules out; raw-HTTP is what `run execute` must deliver.
-- Reusing megamcp's activation model on the CLI side (activate-before-execute). Rejected: activation was an MCP-specific response-size concession; CLI callers do not need it and it would add latency and state for no gain.
-- Reserving `list`, `search`, `info` at top level on the `printing-press` binary. Rejected in KTD-2: future generator-role commands will want those verbs.
-
-## Success Metrics
-
-- After Unit 1 ships, `printing-press run search <keyword>` returns cross-library results in under 200ms on a 21-manifest library with an FTS-free baseline.
-- After Unit 3 ships, agents can compose a batch across three APIs with a single `run execute --parallel -d @batch.json` call and receive responses in input order with per-entry typed exit codes.
-- Pre/post extraction of `apihttp` (Unit 2) shows zero assertion diffs in megamcp's handler test suite.
-- README and AGENTS.md reference the `run` namespace and the KTD-3 boundary within the release that ships Unit 3.
-
-## Documentation / Operational Notes
-
-- Changelog scope `cli` applies to every unit here. `Unit 4` also carries a `feat(cli):` prefix (scoring change is a user-visible behaviour change).
-- No runtime migration. No state migration. No new env vars introduced by this plan (auth env vars are the auth plan's concern).
-- Breaking changes: none. Every unit is additive.
-- Rollout: single binary release. No feature flag required. Manifest `schema_version: 1` lands in the first regenerated manifest and is retroactively tolerated as-if-1 for manifests already in the library.
-
-## Sources & References
-
-- Related code: `internal/megamcp/`, `internal/pipeline/toolsmanifest.go`, `internal/cli/`, `internal/generator/scorer.go`.
-- Prior shipped plan: `docs/plans/2026-04-06-002-feat-mega-mcp-aggregate-server-plan.md`.
-- Companion plan (dependency for Unit 3): `docs/plans/2026-04-19-003-feat-unified-auth-manager-plan.md`.
-- Repo conventions: `AGENTS.md` (machine-vs-printed rule, glossary, commit style).
-- Product philosophy: `README.md` "Absorb and Transcend", "Dual interface from one spec".
diff --git a/docs/plans/2026-04-19-003-feat-unified-auth-manager-plan.md b/docs/plans/2026-04-19-003-feat-unified-auth-manager-plan.md
index c52adce4..1496d389 100644
--- a/docs/plans/2026-04-19-003-feat-unified-auth-manager-plan.md
+++ b/docs/plans/2026-04-19-003-feat-unified-auth-manager-plan.md
@@ -1,7 +1,7 @@
 ---
 title: "feat(cli): unified auth manager for api-key credentials"
 type: feat
-status: active
+status: superseded
 date: 2026-04-19
 ---
 
diff --git a/docs/plans/2026-04-19-004-feat-auth-doctor-plan.md b/docs/plans/2026-04-19-004-feat-auth-doctor-plan.md
index f5544532..83706ad4 100644
--- a/docs/plans/2026-04-19-004-feat-auth-doctor-plan.md
+++ b/docs/plans/2026-04-19-004-feat-auth-doctor-plan.md
@@ -1,7 +1,7 @@
 ---
 title: "feat(cli): auth doctor - env-var visibility across installed printed CLIs"
 type: feat
-status: active
+status: completed
 date: 2026-04-19
 ---
 
diff --git a/docs/plans/2026-04-22-003-feat-mcp-production-readiness-plan.md b/docs/plans/2026-04-22-003-feat-mcp-production-readiness-plan.md
index 28eda11a..e2bd3ec4 100644
--- a/docs/plans/2026-04-22-003-feat-mcp-production-readiness-plan.md
+++ b/docs/plans/2026-04-22-003-feat-mcp-production-readiness-plan.md
@@ -11,7 +11,7 @@ date: 2026-04-22
 
 Anthropic's April 22 2026 post "Building agents that reach production systems with MCP" argues that as production agents move to the cloud, MCP becomes the critical integration layer. Remote servers, intent-grouped tools, and code-orchestration for large surfaces are the three patterns that separate production-ready MCP from stdio-era toys.
 
-The Printing Press already generates an MCP server alongside every printed CLI, bundles skills + MCP as a plugin, and ships a meta-MCP (`megamcp`) that does library-level tool search. But the per-API MCP servers it emits today are stdio-only, one-tool-per-endpoint, and flat regardless of API size. This plan closes the three highest-leverage gaps. A follow-up plan will cover elicitation, MCP Apps, and OAuth/CIMD.
+The Printing Press already generates an MCP server alongside every printed CLI and bundles skills + MCP as a plugin. But the per-API MCP servers it emits today are stdio-only, one-tool-per-endpoint, and flat regardless of API size. This plan closes the three highest-leverage gaps. A follow-up plan will cover elicitation, MCP Apps, and OAuth/CIMD.
 
 ---
 
@@ -45,7 +45,7 @@ The scorer currently rewards `mcp_token_efficiency` (file size of tool definitio
 - Remote transport is HTTP streamable only. SSE-specific transport is deferred — the article treats HTTP streamable as the modern remote path.
 - No authentication beyond what exists today (env var / bearer / API key). OAuth, CIMD, and token-vault integration are deferred to a separate plan.
 - No elicitation (form/URL mode) or MCP Apps (interactive UI returns). Deferred to a separate plan.
-- No client-side patterns (tool search, programmatic tool calling). PP builds servers, not clients. `megamcp` already implements library-level tool search and is out of scope here.
+- No client-side patterns (tool search, programmatic tool calling). PP builds servers, not clients.
 - Intent-grouped tools are declared in the spec, not inferred automatically. Auto-inference from endpoint semantics is a future consideration.
 
 ### Deferred to Follow-Up Work
@@ -66,8 +66,7 @@ The scorer currently rewards `mcp_token_efficiency` (file size of tool definitio
 - `internal/spec/spec.go` — spec schema; needs a new `mcp:` section for transport + intents + code-orchestration config. The `ExtraCommands` precedent (hand-written cobra commands declared in the spec, documented in SKILL.md) is the right model: spec-declared, generator-respected.
 - `internal/pipeline/scorecard.go` — `SteinerScore` holds the dimension struct; add fields here. `MCPQuality` and `MCPTokenEff` already exist; preserve them and extend.
 - `internal/pipeline/mcp_size.go` — pattern for opt-in scoring with `scored bool` return; follow for new dimensions that only apply when MCP is emitted or the API surface is large.
-- `internal/megamcp/` — existing meta-MCP with library-level tool search, setup guide, activation. Out of scope to modify; in-scope to understand so new transport fields line up with `RegistryMCP.Transport` already declared in `types.go`.
-- `github.com/mark3labs/mcp-go v0.47.0` — pinned MCP SDK. Confirm HTTP streamable server support is available at this version before starting U1.
+- `github.com/mark3labs/mcp-go v0.47.0` — pinned MCP SDK in the per-CLI MCP template's `go.mod`. Confirm HTTP streamable server support is available at this version before starting U1.
 
 ### Institutional Learnings
 
@@ -222,7 +221,7 @@ Scorer adds three fields, all opt-in:
 - Intent syntax declares `steps: [ { endpoint, bind, capture } ]` and a `returns` expression. Binding expression format is decided during U2 prototyping; prefer the simplest dialect that covers the common case. Validator rejects references to undeclared endpoints, undeclared captures, or missing required endpoint params after binding is applied.
 - Intent handler composes existing generated endpoint client calls — no new HTTP code. The handler is a generator-owned Go function that the agent should not hand-modify.
 - `EndpointTools: hidden` hides raw endpoint tools from MCP registration but leaves the underlying Go endpoint handlers intact so intents can call them.
-- Each intent contributes to `tools-manifest.json` so `auth doctor`, scorecard, and megamcp discovery see them.
+- Each intent contributes to `tools-manifest.json` so `auth doctor`, `mcp-audit`, and the scorecard see them.
 
 **Patterns to follow:**
 - `internal/generator/vision_templates.go` — generator-owned higher-level code emission.
@@ -270,7 +269,6 @@ Scorer adds three fields, all opt-in:
 - Auth, error sanitization, and store-read paths continue to work uniformly for both tools since they delegate to the same endpoint handlers.
 
 **Patterns to follow:**
-- `internal/megamcp/metatools.go` for the tool-registration shape of `search_tools` — lift the pattern into the per-API template.
 - `internal/pipeline/reimplementation_check.go` — ensure `execute` handler passes the check (it calls the real client per endpoint).
 
 **Test scenarios:**
@@ -376,12 +374,12 @@ Scorer adds three fields, all opt-in:
 
 ## System-Wide Impact
 
-- **Interaction graph:** `internal/spec` -> `internal/generator` (templates + intent handler) -> published CLI's `internal/mcp` and `cmd/<api>-pp-mcp`. `internal/pipeline/scorecard.go` reads generator output. `internal/megamcp` already consumes `tools-manifest.json`; intent tools must appear there correctly so megamcp discovery works without change.
+- **Interaction graph:** `internal/spec` -> `internal/generator` (templates + intent handler) -> published CLI's `internal/mcp` and `cmd/<api>-pp-mcp`. `internal/pipeline/scorecard.go` reads generator output. `tools-manifest.json` must record intent tools correctly so `auth doctor` and `mcp-audit` continue to enumerate the full surface.
 - **Error propagation:** spec validation errors surface at `generate` time with line numbers. Runtime transport errors (HTTP bind failure) surface at MCP startup with an exit code. `<api>_execute` unknown endpoint returns a structured MCP error; no panics.
 - **State lifecycle risks:** none new — intent tools are stateless compositions over existing endpoint calls. Code-orchestration adds no server-side state.
 - **API surface parity:** CLI side is unchanged. All additions are on the MCP side. The `CLAUDE.md`/`AGENTS.md` principle "agents can do anything users can" is preserved; these changes *expand* what agents can do.
 - **Integration coverage:** U1's transport integration test and U2's intent end-to-end test must build and run the generated binary, not just unit-test the templates. U3's size test must measure actual emitted token count, not theoretical.
-- **Unchanged invariants:** `ExtraCommands`, `tools-manifest.json` schema backward compat, megamcp activation flow, the 7 quality gates, dogfood's reimplementation check, and per-CLI `doctor` behavior. All existing printed CLIs continue to build with zero spec changes.
+- **Unchanged invariants:** `ExtraCommands`, `tools-manifest.json` schema backward compat, the 7 quality gates, dogfood's reimplementation check, and per-CLI `doctor` behavior. All existing printed CLIs continue to build with zero spec changes.
 
 ---
 
@@ -415,5 +413,4 @@ Scorer adds three fields, all opt-in:
 - `internal/generator/templates/mcp_tools.go.tmpl` — current endpoint-mirror registration.
 - `internal/spec/spec.go` — spec schema; `ExtraCommands` is the precedent for additive optional blocks.
 - `internal/pipeline/scorecard.go`, `internal/pipeline/mcp_size.go` — scoring patterns to follow.
-- `internal/megamcp/` — library-level tool search already exists; stays untouched.
 - Cloudflare's MCP server — reference for code-orchestration (~2,500 endpoints in ~1K tokens).
diff --git a/go.mod b/go.mod
index 3faa3192..978b038a 100644
--- a/go.mod
+++ b/go.mod
@@ -6,7 +6,6 @@ require (
 	github.com/dave/dst v0.27.4
 	github.com/enetx/surf v1.0.198
 	github.com/getkin/kin-openapi v0.133.0
-	github.com/mark3labs/mcp-go v0.47.0
 	github.com/spf13/cobra v1.10.2
 	github.com/stretchr/testify v1.11.1
 	golang.org/x/mod v0.33.0
@@ -25,8 +24,7 @@ require (
 	github.com/enetx/iter v0.0.0-20250912135656-f1583323588f // indirect
 	github.com/go-openapi/jsonpointer v0.21.0 // indirect
 	github.com/go-openapi/swag v0.23.0 // indirect
-	github.com/google/jsonschema-go v0.4.2 // indirect
-	github.com/google/uuid v1.6.0 // indirect
+	github.com/google/go-cmp v0.7.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/klauspost/compress v1.18.5 // indirect
@@ -39,11 +37,9 @@ require (
 	github.com/quic-go/qpack v0.6.0 // indirect
 	github.com/quic-go/quic-go v0.59.0 // indirect
 	github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af // indirect
-	github.com/spf13/cast v1.7.1 // indirect
 	github.com/spf13/pflag v1.0.9 // indirect
 	github.com/woodsbury/decimal128 v1.3.0 // indirect
 	github.com/wzshiming/socks5 v0.7.0 // indirect
-	github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
 	golang.org/x/crypto v0.48.0 // indirect
 	golang.org/x/net v0.50.0 // indirect
 	golang.org/x/sys v0.41.0 // indirect
diff --git a/go.sum b/go.sum
index a40557a4..de73e686 100644
--- a/go.sum
+++ b/go.sum
@@ -19,8 +19,6 @@ github.com/enetx/iter v0.0.0-20250912135656-f1583323588f h1:GUW+4AWfECIEJ9oAxgEA
 github.com/enetx/iter v0.0.0-20250912135656-f1583323588f/go.mod h1:oMZN8hGLUpi7QBlMEUqailocNy0NFAO/7Lu+Nwh9HMM=
 github.com/enetx/surf v1.0.198 h1:TJkyEyy5M+GnLZlGKmByeXwG7K2vv7F5L+0SgwlDu7g=
 github.com/enetx/surf v1.0.198/go.mod h1:BtLmZDYAny66azybFr9UdFVnwy8WRV4FTAzElsd7bvE=
-github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
-github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
 github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
 github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE=
 github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
@@ -31,10 +29,6 @@ github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
 github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
-github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
-github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
-github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
-github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
 github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
@@ -47,8 +41,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
-github.com/mark3labs/mcp-go v0.47.0 h1:h44yeM3DduDyQgzImYWu4pt6VRkqP/0p/95AGhWngnA=
-github.com/mark3labs/mcp-go v0.47.0/go.mod h1:JKTC7R2LLVagkEWK7Kwu7DbmA6iIvnNAod6yrHiQMag=
 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
 github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
@@ -70,8 +62,6 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
 github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
-github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
-github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
 github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
 github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
 github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
@@ -86,8 +76,6 @@ github.com/wzshiming/socks5 v0.7.0 h1:euJ+U48WrvVngi+opC8vAnpZ5sK12y1C2hPvb1f48R
 github.com/wzshiming/socks5 v0.7.0/go.mod h1:BvCAqlzocQN5xwLjBZDBbvWlrx8sCYSSbHEOf2wZgT0=
 github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
 github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
-github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4=
-github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4=
 go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
 go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
 go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
diff --git a/internal/authdoctor/scan.go b/internal/authdoctor/scan.go
index 2762f9f0..1959ab64 100644
--- a/internal/authdoctor/scan.go
+++ b/internal/authdoctor/scan.go
@@ -2,9 +2,9 @@
 // the env-var status of every installed printed CLI. It is the data layer
 // for the `printing-press auth doctor` command.
 //
-// The scanner reads each API's tools-manifest.json (the same file megamcp
-// consumes) and classifies each declared env var as ok, suspicious,
-// not-set, or no-auth. No network calls. No writes to disk.
+// The scanner reads each API's tools-manifest.json and classifies each
+// declared env var as ok, suspicious, not-set, or no-auth. No network
+// calls. No writes to disk.
 package authdoctor
 
 import (
diff --git a/internal/cli/bundle.go b/internal/cli/bundle.go
index d347737b..3b9ab2a9 100644
--- a/internal/cli/bundle.go
+++ b/internal/cli/bundle.go
@@ -125,7 +125,7 @@ func autoBundleForHost(cliDir string, w io.Writer) {
 	manifestData, err := os.ReadFile(manifestPath)
 	if err != nil {
 		// "No manifest" means generate decided this CLI doesn't ship as
-		// a bundle (cli-only readiness, no MCP). That's expected — silent.
+		// a bundle (no MCP binary). That's expected — silent.
 		return
 	}
 	var manifest pipeline.MCPBManifest
diff --git a/internal/megamcp/activation.go b/internal/megamcp/activation.go
deleted file mode 100644
index 4cf67b4e..00000000
--- a/internal/megamcp/activation.go
+++ /dev/null
@@ -1,290 +0,0 @@
-package megamcp
-
-import (
-	"context"
-	"fmt"
-	"net/http"
-	"strings"
-	"sync"
-	"time"
-
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
-)
-
-// ActivationManager manages the state of which APIs are activated
-// and handles dynamic tool registration/deregistration.
-type ActivationManager struct {
-	mu        sync.RWMutex
-	server    *server.MCPServer
-	manifests map[string]*APIEntry    // all loaded manifests by slug
-	activated map[string]bool         // which slugs have been activated
-	clients   map[string]*http.Client // per-API HTTP clients
-}
-
-// NewActivationManager creates a new ActivationManager with all loaded API entries.
-// Registers stub tools for ALL APIs immediately so agents can discover tool names
-// via tools/list. Stubs prompt the agent to call activate_api before execution.
-func NewActivationManager(s *server.MCPServer, entries []*APIEntry) *ActivationManager {
-	manifests := make(map[string]*APIEntry, len(entries))
-	for _, entry := range entries {
-		manifests[entry.Slug] = entry
-	}
-	am := &ActivationManager{
-		server:    s,
-		manifests: manifests,
-		activated: make(map[string]bool),
-		clients:   make(map[string]*http.Client),
-	}
-
-	// Register stub tools for all APIs so agents can see tool names in tools/list.
-	am.registerStubs()
-
-	return am
-}
-
-// registerStubs registers lightweight stub handlers for all tools across all APIs.
-// When called, stubs return an activation prompt instead of making HTTP requests.
-func (am *ActivationManager) registerStubs() {
-	var stubs []server.ServerTool
-	for _, entry := range am.manifests {
-		prefix := entry.NormalizedPrefix
-		slug := entry.Slug
-		apiName := entry.Manifest.APIName
-		toolCount := len(entry.Manifest.Tools)
-
-		for _, tool := range entry.Manifest.Tools {
-			toolName := prefix + "__" + tool.Name
-
-			toolOpts := []mcp.ToolOption{
-				mcp.WithDescription(SanitizeText(tool.Description, 500)),
-			}
-			for _, param := range tool.Params {
-				paramOpts := []mcp.PropertyOption{
-					mcp.Description(SanitizeText(param.Description, 200)),
-				}
-				if param.Required {
-					paramOpts = append(paramOpts, mcp.Required())
-				}
-				toolOpts = append(toolOpts, mcp.WithString(param.Name, paramOpts...))
-			}
-
-			mcpTool := mcp.NewTool(toolName, toolOpts...)
-			handler := makeStubHandler(slug, apiName, toolCount)
-			stubs = append(stubs, server.ServerTool{Tool: mcpTool, Handler: handler})
-		}
-	}
-
-	if len(stubs) > 0 {
-		am.server.AddTools(stubs...)
-	}
-}
-
-// makeStubHandler returns a handler that prompts the agent to activate the API first.
-func makeStubHandler(slug, apiName string, toolCount int) server.ToolHandlerFunc {
-	return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-		return mcp.NewToolResultError(
-			fmt.Sprintf("This API is not yet activated. Call activate_api(%q) first to enable %d tools for %s.",
-				slug, toolCount, apiName),
-		), nil
-	}
-}
-
-// Activate registers real HTTP handlers for the given API slug, replacing
-// the stub handlers. Returns the number of tools registered. Idempotent:
-// calling twice for the same slug returns the same count without duplicating tools.
-func (am *ActivationManager) Activate(slug string) (int, error) {
-	am.mu.Lock()
-	defer am.mu.Unlock()
-
-	entry, ok := am.manifests[slug]
-	if !ok {
-		return 0, fmt.Errorf("API not found: %q", slug)
-	}
-
-	if am.activated[slug] {
-		// Already activated — return existing tool count.
-		return len(entry.Manifest.Tools), nil
-	}
-
-	// Create a per-API HTTP client with SSRF-safe dialer.
-	client := SafeHTTPClient(30 * time.Second)
-	am.clients[slug] = client
-
-	// Delete stubs first, then register real handlers.
-	prefix := entry.NormalizedPrefix
-	var stubNames []string
-	var tools []server.ServerTool
-	for _, tool := range entry.Manifest.Tools {
-		toolName := prefix + "__" + tool.Name
-		stubNames = append(stubNames, toolName)
-
-		// Build MCP tool definition with parameter schema.
-		toolOpts := []mcp.ToolOption{
-			mcp.WithDescription(SanitizeText(tool.Description, 500)),
-		}
-		for _, param := range tool.Params {
-			paramOpts := []mcp.PropertyOption{
-				mcp.Description(SanitizeText(param.Description, 200)),
-			}
-			if param.Required {
-				paramOpts = append(paramOpts, mcp.Required())
-			}
-			toolOpts = append(toolOpts, mcp.WithString(param.Name, paramOpts...))
-		}
-
-		mcpTool := mcp.NewTool(toolName, toolOpts...)
-		handler := MakeToolHandler(entry.Manifest, tool, client, slug)
-		tools = append(tools, server.ServerTool{Tool: mcpTool, Handler: handler})
-	}
-
-	// Remove stubs, then add real handlers.
-	if len(stubNames) > 0 {
-		am.server.DeleteTools(stubNames...)
-	}
-	if len(tools) > 0 {
-		am.server.AddTools(tools...)
-	}
-
-	am.activated[slug] = true
-	return len(entry.Manifest.Tools), nil
-}
-
-// Deactivate removes real handlers for the given API slug and re-registers stubs.
-func (am *ActivationManager) Deactivate(slug string) error {
-	am.mu.Lock()
-	defer am.mu.Unlock()
-
-	entry, ok := am.manifests[slug]
-	if !ok {
-		return fmt.Errorf("API not found: %q", slug)
-	}
-
-	if !am.activated[slug] {
-		return fmt.Errorf("API %q is not currently activated", slug)
-	}
-
-	// Collect tool names to delete.
-	prefix := entry.NormalizedPrefix
-	var names []string
-	var stubs []server.ServerTool
-	apiName := entry.Manifest.APIName
-	toolCount := len(entry.Manifest.Tools)
-
-	for _, tool := range entry.Manifest.Tools {
-		toolName := prefix + "__" + tool.Name
-		names = append(names, toolName)
-
-		// Re-register as stub.
-		toolOpts := []mcp.ToolOption{
-			mcp.WithDescription(SanitizeText(tool.Description, 500)),
-		}
-		for _, param := range tool.Params {
-			paramOpts := []mcp.PropertyOption{
-				mcp.Description(SanitizeText(param.Description, 200)),
-			}
-			if param.Required {
-				paramOpts = append(paramOpts, mcp.Required())
-			}
-			toolOpts = append(toolOpts, mcp.WithString(param.Name, paramOpts...))
-		}
-
-		mcpTool := mcp.NewTool(toolName, toolOpts...)
-		handler := makeStubHandler(slug, apiName, toolCount)
-		stubs = append(stubs, server.ServerTool{Tool: mcpTool, Handler: handler})
-	}
-
-	// Delete real handlers, add back stubs.
-	if len(names) > 0 {
-		am.server.DeleteTools(names...)
-	}
-	if len(stubs) > 0 {
-		am.server.AddTools(stubs...)
-	}
-
-	delete(am.activated, slug)
-	delete(am.clients, slug)
-	return nil
-}
-
-// IsActivated returns whether the given API slug is currently activated.
-func (am *ActivationManager) IsActivated(slug string) bool {
-	am.mu.RLock()
-	defer am.mu.RUnlock()
-	return am.activated[slug]
-}
-
-// GetManifest returns the APIEntry for a given slug, or nil if not found.
-func (am *ActivationManager) GetManifest(slug string) *APIEntry {
-	am.mu.RLock()
-	defer am.mu.RUnlock()
-	return am.manifests[slug]
-}
-
-// AllManifests returns all loaded API entries.
-func (am *ActivationManager) AllManifests() []*APIEntry {
-	am.mu.RLock()
-	defer am.mu.RUnlock()
-	result := make([]*APIEntry, 0, len(am.manifests))
-	for _, entry := range am.manifests {
-		result = append(result, entry)
-	}
-	return result
-}
-
-// SearchResult represents a matching tool from a search query.
-type SearchResult struct {
-	APISlug     string `json:"api_slug"`
-	ToolName    string `json:"tool_name"`
-	Description string `json:"description"`
-}
-
-// SearchTools searches across ALL manifests (not just activated) for tools
-// matching the given query. Case-insensitive substring match on tool name
-// and description.
-func (am *ActivationManager) SearchTools(query string) []SearchResult {
-	am.mu.RLock()
-	defer am.mu.RUnlock()
-
-	if query == "" {
-		return nil
-	}
-
-	lowerQuery := strings.ToLower(query)
-	var results []SearchResult
-
-	for _, entry := range am.manifests {
-		prefix := entry.NormalizedPrefix
-		for _, tool := range entry.Manifest.Tools {
-			fullName := prefix + "__" + tool.Name
-			if strings.Contains(strings.ToLower(fullName), lowerQuery) ||
-				strings.Contains(strings.ToLower(tool.Description), lowerQuery) {
-				results = append(results, SearchResult{
-					APISlug:     entry.Slug,
-					ToolName:    fullName,
-					Description: tool.Description,
-				})
-			}
-		}
-	}
-
-	return results
-}
-
-// toolNamesForSlug returns the full prefixed tool names for a given API slug.
-// Used for display purposes.
-func (am *ActivationManager) toolNamesForSlug(slug string) []string {
-	am.mu.RLock()
-	defer am.mu.RUnlock()
-
-	entry, ok := am.manifests[slug]
-	if !ok {
-		return nil
-	}
-
-	names := make([]string, 0, len(entry.Manifest.Tools))
-	for _, tool := range entry.Manifest.Tools {
-		names = append(names, entry.NormalizedPrefix+"__"+tool.Name)
-	}
-	return names
-}
diff --git a/internal/megamcp/activation_test.go b/internal/megamcp/activation_test.go
deleted file mode 100644
index 6256ca54..00000000
--- a/internal/megamcp/activation_test.go
+++ /dev/null
@@ -1,315 +0,0 @@
-package megamcp
-
-import (
-	"testing"
-
-	"github.com/mark3labs/mcp-go/server"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-)
-
-func newTestServer() *server.MCPServer {
-	return server.NewMCPServer("test-server", "0.0.1",
-		server.WithToolCapabilities(true))
-}
-
-func newTestAPIEntries() []*APIEntry {
-	return []*APIEntry{
-		{
-			Slug:             "espn",
-			NormalizedPrefix: "espn",
-			Manifest: &ToolsManifest{
-				APIName: "ESPN",
-				BaseURL: "https://site.api.espn.com",
-				Auth:    ManifestAuth{Type: "api_key", EnvVars: []string{"ESPN_KEY"}, Header: "apikey", In: "query"},
-				Tools: []ManifestTool{
-					{Name: "scores_get", Description: "Get live scores", Method: "GET", Path: "/scores"},
-					{Name: "teams_list", Description: "List all teams", Method: "GET", Path: "/teams"},
-					{Name: "standings_get", Description: "Get league standings", Method: "GET", Path: "/standings"},
-				},
-			},
-		},
-		{
-			Slug:             "dub",
-			NormalizedPrefix: "dub",
-			Manifest: &ToolsManifest{
-				APIName: "Dub",
-				BaseURL: "https://api.dub.co",
-				Auth:    ManifestAuth{Type: "bearer_token", EnvVars: []string{"DUB_TOKEN"}, Header: "Authorization", Format: "Bearer {DUB_TOKEN}"},
-				Tools: []ManifestTool{
-					{Name: "links_list", Description: "List all links", Method: "GET", Path: "/links"},
-					{Name: "links_create", Description: "Create a new short link", Method: "POST", Path: "/links"},
-				},
-			},
-		},
-		{
-			Slug:             "public-api",
-			NormalizedPrefix: "public_api",
-			Manifest: &ToolsManifest{
-				APIName: "Public API",
-				BaseURL: "https://api.publicapis.org",
-				Auth:    ManifestAuth{Type: "none"},
-				Tools: []ManifestTool{
-					{Name: "entries_list", Description: "List all public API entries", Method: "GET", Path: "/entries"},
-				},
-			},
-		},
-	}
-}
-
-func TestActivate_RegistersToolsWithPrefix(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	count, err := am.Activate("espn")
-	require.NoError(t, err)
-	assert.Equal(t, 3, count)
-
-	// Verify tools are registered on the server with prefix.
-	tools := s.ListTools()
-	assert.NotNil(t, tools["espn__scores_get"], "espn__scores_get should be registered")
-	assert.NotNil(t, tools["espn__teams_list"], "espn__teams_list should be registered")
-	assert.NotNil(t, tools["espn__standings_get"], "espn__standings_get should be registered")
-}
-
-func TestActivate_Idempotent(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	count1, err := am.Activate("espn")
-	require.NoError(t, err)
-	assert.Equal(t, 3, count1)
-
-	count2, err := am.Activate("espn")
-	require.NoError(t, err)
-	assert.Equal(t, 3, count2)
-
-	// Verify no duplicates — still only 3 ESPN tools.
-	tools := s.ListTools()
-	espnTools := 0
-	for name := range tools {
-		if len(name) > 6 && name[:6] == "espn__" {
-			espnTools++
-		}
-	}
-	assert.Equal(t, 3, espnTools, "should have exactly 3 ESPN tools, not duplicated")
-}
-
-func TestActivate_UnknownSlug(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	_, err := am.Activate("nonexistent")
-	require.Error(t, err)
-	assert.Contains(t, err.Error(), "API not found")
-}
-
-func TestDeactivate_ReregistersStubTools(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	_, err := am.Activate("espn")
-	require.NoError(t, err)
-
-	err = am.Deactivate("espn")
-	require.NoError(t, err)
-
-	// After deactivation, tools should still be registered (as stubs).
-	tools := s.ListTools()
-	assert.NotNil(t, tools["espn__scores_get"], "espn__scores_get should be a stub")
-	assert.NotNil(t, tools["espn__teams_list"], "espn__teams_list should be a stub")
-
-	// But the API should not be marked as activated.
-	assert.False(t, am.IsActivated("espn"))
-
-	// Calling the stub should return an activation prompt.
-	result, callErr := tools["espn__scores_get"].Handler(t.Context(), makeToolRequest(nil))
-	require.NoError(t, callErr)
-	assert.True(t, result.IsError)
-	text := extractResultText(result)
-	assert.Contains(t, text, "not yet activated")
-}
-
-func TestDeactivate_NotActivated(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	err := am.Deactivate("espn")
-	require.Error(t, err)
-	assert.Contains(t, err.Error(), "not currently activated")
-}
-
-func TestDeactivate_UnknownSlug(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	err := am.Deactivate("nonexistent")
-	require.Error(t, err)
-	assert.Contains(t, err.Error(), "API not found")
-}
-
-func TestIsActivated(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	assert.False(t, am.IsActivated("espn"), "should be false before activation")
-
-	_, err := am.Activate("espn")
-	require.NoError(t, err)
-	assert.True(t, am.IsActivated("espn"), "should be true after activation")
-
-	err = am.Deactivate("espn")
-	require.NoError(t, err)
-	assert.False(t, am.IsActivated("espn"), "should be false after deactivation")
-}
-
-func TestGetManifest(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	entry := am.GetManifest("espn")
-	require.NotNil(t, entry)
-	assert.Equal(t, "ESPN", entry.Manifest.APIName)
-
-	missing := am.GetManifest("nonexistent")
-	assert.Nil(t, missing)
-}
-
-func TestAllManifests(t *testing.T) {
-	s := newTestServer()
-	entries := newTestAPIEntries()
-	am := NewActivationManager(s, entries)
-
-	all := am.AllManifests()
-	assert.Len(t, all, 3)
-}
-
-func TestSearchTools_MatchesName(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	results := am.SearchTools("scores")
-	require.Len(t, results, 1)
-	assert.Equal(t, "espn", results[0].APISlug)
-	assert.Equal(t, "espn__scores_get", results[0].ToolName)
-}
-
-func TestSearchTools_CaseInsensitive(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	results := am.SearchTools("SCORES")
-	require.Len(t, results, 1)
-	assert.Equal(t, "espn__scores_get", results[0].ToolName)
-}
-
-func TestSearchTools_MatchesDescription(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	results := am.SearchTools("short link")
-	require.Len(t, results, 1)
-	assert.Equal(t, "dub", results[0].APISlug)
-	assert.Equal(t, "dub__links_create", results[0].ToolName)
-}
-
-func TestSearchTools_AcrossUnactivatedAPIs(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	// Search across all APIs without activating any.
-	assert.False(t, am.IsActivated("espn"))
-	assert.False(t, am.IsActivated("dub"))
-
-	results := am.SearchTools("list")
-	// Should match: espn__teams_list, dub__links_list, public_api__entries_list
-	assert.GreaterOrEqual(t, len(results), 3)
-}
-
-func TestSearchTools_NoMatches(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	results := am.SearchTools("pizza")
-	assert.Empty(t, results)
-}
-
-func TestSearchTools_EmptyQuery(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	results := am.SearchTools("")
-	assert.Nil(t, results)
-}
-
-func TestActivateMultipleAPIs(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	_, err := am.Activate("espn")
-	require.NoError(t, err)
-
-	_, err = am.Activate("dub")
-	require.NoError(t, err)
-
-	tools := s.ListTools()
-	// Should have 3 ESPN + 2 Dub = 5 tools.
-	assert.NotNil(t, tools["espn__scores_get"])
-	assert.NotNil(t, tools["dub__links_list"])
-	assert.NotNil(t, tools["dub__links_create"])
-}
-
-func TestToolNamesForSlug(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	names := am.toolNamesForSlug("espn")
-	assert.Len(t, names, 3)
-	assert.Contains(t, names, "espn__scores_get")
-	assert.Contains(t, names, "espn__teams_list")
-	assert.Contains(t, names, "espn__standings_get")
-
-	missing := am.toolNamesForSlug("nonexistent")
-	assert.Nil(t, missing)
-}
-
-func TestStubToolsRegisteredAtStartup(t *testing.T) {
-	s := newTestServer()
-	entries := newTestAPIEntries()
-	_ = NewActivationManager(s, entries)
-
-	// Stub tools should be visible in the server's tool list immediately.
-	// We can't directly query the server's tool list, but we can verify
-	// that calling a stub returns the activation prompt.
-	// The stubs are registered via AddTools in registerStubs().
-	// We verify this indirectly by checking that the stub handler works.
-	handler := makeStubHandler("espn", "ESPN", 3)
-	result, err := handler(t.Context(), makeToolRequest(nil))
-	require.NoError(t, err)
-	assert.True(t, result.IsError)
-	text := extractResultText(result)
-	assert.Contains(t, text, "not yet activated")
-	assert.Contains(t, text, "activate_api")
-	assert.Contains(t, text, "espn")
-	assert.Contains(t, text, "3 tools")
-}
-
-func TestDeactivate_ReregistersStubs(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	// Activate then deactivate.
-	_, err := am.Activate("espn")
-	require.NoError(t, err)
-	assert.True(t, am.IsActivated("espn"))
-
-	err = am.Deactivate("espn")
-	require.NoError(t, err)
-	assert.False(t, am.IsActivated("espn"))
-
-	// After deactivation, re-activating should work (stubs were re-registered).
-	count, err := am.Activate("espn")
-	require.NoError(t, err)
-	assert.Equal(t, 3, count)
-	assert.True(t, am.IsActivated("espn"))
-}
diff --git a/internal/megamcp/auth.go b/internal/megamcp/auth.go
deleted file mode 100644
index d0863db2..00000000
--- a/internal/megamcp/auth.go
+++ /dev/null
@@ -1,198 +0,0 @@
-package megamcp
-
-import (
-	"fmt"
-	"os"
-	"strings"
-)
-
-// ApplyAuthFormat expands {PLACEHOLDER} tokens in the format string with values
-// from the envVars map. envVars is keyed by env var name, values are from os.Getenv.
-// Also supports semantic placeholders: {token} and {access_token} map to the first
-// env var value. Rejects format strings with unrecognized placeholders.
-func ApplyAuthFormat(format string, envVars map[string]string) (string, error) {
-	if format == "" {
-		// No format string — return the first env var value directly.
-		for _, v := range envVars {
-			return v, nil
-		}
-		return "", nil
-	}
-
-	result := format
-
-	replacements := make(map[string]string)
-	validPlaceholders := make(map[string]bool)
-	var firstValue string
-	for name, value := range envVars {
-		replacements[name] = value
-		validPlaceholders["{"+name+"}"] = true
-		if firstValue == "" {
-			firstValue = value
-		}
-	}
-	if firstValue != "" {
-		replacements["token"] = firstValue
-		replacements["access_token"] = firstValue
-		validPlaceholders["{token}"] = true
-		validPlaceholders["{access_token}"] = true
-	}
-
-	// Validate all placeholders in the format string before substitution.
-	for i := 0; i < len(result); {
-		idx := strings.Index(result[i:], "{")
-		if idx < 0 {
-			break
-		}
-		idx += i
-		closeIdx := strings.Index(result[idx:], "}")
-		if closeIdx < 0 {
-			break
-		}
-		placeholder := result[idx : idx+closeIdx+1]
-		if !validPlaceholders[placeholder] {
-			return "", fmt.Errorf("unrecognized placeholder %s in auth format %q", placeholder, format)
-		}
-		i = idx + closeIdx + 1
-	}
-
-	// Perform substitutions.
-	for key, value := range replacements {
-		if strings.Contains(value, "}") {
-			return "", fmt.Errorf("env var value for %s contains invalid character }", key)
-		}
-		result = strings.ReplaceAll(result, "{"+key+"}", value)
-	}
-
-	return result, nil
-}
-
-// BuildAuthHeader reads env vars from manifest.Auth.EnvVars via os.Getenv,
-// applies format string expansion, and returns the header name and value.
-// Returns empty strings if no auth configured (type is "none" or empty).
-func BuildAuthHeader(manifest *ToolsManifest) (headerName string, headerValue string, err error) {
-	if manifest == nil || manifest.Auth.Type == "" || manifest.Auth.Type == "none" {
-		return "", "", nil
-	}
-
-	// Build envVars map from os.Getenv.
-	envVars := make(map[string]string)
-	for _, envName := range manifest.Auth.EnvVars {
-		val := os.Getenv(envName)
-		if val != "" {
-			envVars[envName] = val
-		}
-	}
-
-	// If no env vars are set, return empty — caller decides if that's an error.
-	if len(envVars) == 0 {
-		return "", "", nil
-	}
-
-	// Determine the header name.
-	headerName = manifest.Auth.Header
-	if headerName == "" {
-		headerName = "Authorization"
-	}
-
-	// Apply the format string, or construct a default value.
-	if manifest.Auth.Format != "" {
-		headerValue, err = ApplyAuthFormat(manifest.Auth.Format, envVars)
-		if err != nil {
-			return "", "", fmt.Errorf("applying auth format: %w", err)
-		}
-	} else {
-		// No format string — use the first env var value directly.
-		// For bearer_token, default to "Bearer {value}".
-		for _, envName := range manifest.Auth.EnvVars {
-			val := envVars[envName]
-			if val != "" {
-				if manifest.Auth.Type == "bearer_token" {
-					headerValue = "Bearer " + val
-				} else {
-					headerValue = val
-				}
-				break
-			}
-		}
-	}
-
-	return headerName, headerValue, nil
-}
-
-// BuildAuthQueryParam constructs the auth query parameter name and value
-// for APIs that use In:"query" auth. Returns empty strings if not applicable.
-func BuildAuthQueryParam(manifest *ToolsManifest) (paramName string, paramValue string, err error) {
-	if manifest == nil || manifest.Auth.Type == "" || manifest.Auth.Type == "none" {
-		return "", "", nil
-	}
-	if manifest.Auth.In != "query" {
-		return "", "", nil
-	}
-
-	// Build envVars map from os.Getenv.
-	envVars := make(map[string]string)
-	for _, envName := range manifest.Auth.EnvVars {
-		val := os.Getenv(envName)
-		if val != "" {
-			envVars[envName] = val
-		}
-	}
-
-	if len(envVars) == 0 {
-		return "", "", nil
-	}
-
-	// The query param name comes from auth.header (or defaults to "api_key").
-	paramName = manifest.Auth.Header
-	if paramName == "" {
-		paramName = "api_key"
-	}
-
-	// Apply format or use raw value.
-	if manifest.Auth.Format != "" {
-		paramValue, err = ApplyAuthFormat(manifest.Auth.Format, envVars)
-		if err != nil {
-			return "", "", fmt.Errorf("applying auth format: %w", err)
-		}
-	} else {
-		for _, envName := range manifest.Auth.EnvVars {
-			val := envVars[envName]
-			if val != "" {
-				paramValue = val
-				break
-			}
-		}
-	}
-
-	return paramName, paramValue, nil
-}
-
-// RedactCredentials replaces known credential values (from env vars) with
-// [REDACTED] in the given string. Used before returning 4xx error bodies
-// to prevent credential leakage.
-func RedactCredentials(body string, manifest *ToolsManifest) string {
-	if manifest == nil {
-		return body
-	}
-	for _, envName := range manifest.Auth.EnvVars {
-		val := os.Getenv(envName)
-		if val != "" && len(val) >= 4 {
-			body = strings.ReplaceAll(body, val, "[REDACTED]")
-		}
-	}
-	return body
-}
-
-// hasAuthConfigured checks whether the required auth env vars are set.
-func hasAuthConfigured(manifest *ToolsManifest) bool {
-	if manifest == nil || manifest.Auth.Type == "" || manifest.Auth.Type == "none" {
-		return true // No auth needed.
-	}
-	for _, envName := range manifest.Auth.EnvVars {
-		if os.Getenv(envName) != "" {
-			return true
-		}
-	}
-	return false
-}
diff --git a/internal/megamcp/auth_test.go b/internal/megamcp/auth_test.go
deleted file mode 100644
index 3e0f4edc..00000000
--- a/internal/megamcp/auth_test.go
+++ /dev/null
@@ -1,358 +0,0 @@
-package megamcp
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-)
-
-func TestApplyAuthFormat(t *testing.T) {
-	tests := []struct {
-		name    string
-		format  string
-		envVars map[string]string
-		want    string
-		wantErr string
-	}{
-		{
-			name:    "simple substitution",
-			format:  "{API_KEY}",
-			envVars: map[string]string{"API_KEY": "my-secret-key"},
-			want:    "my-secret-key",
-		},
-		{
-			name:    "bearer format",
-			format:  "Bearer {DUB_TOKEN}",
-			envVars: map[string]string{"DUB_TOKEN": "abc123"},
-			want:    "Bearer abc123",
-		},
-		{
-			name:    "semantic placeholder token",
-			format:  "Bearer {token}",
-			envVars: map[string]string{"SOME_TOKEN": "xyz789"},
-			want:    "Bearer xyz789",
-		},
-		{
-			name:    "semantic placeholder access_token",
-			format:  "Bearer {access_token}",
-			envVars: map[string]string{"MY_TOKEN": "tok456"},
-			want:    "Bearer tok456",
-		},
-		{
-			name:    "empty format returns first value",
-			format:  "",
-			envVars: map[string]string{"KEY": "val"},
-			want:    "val",
-		},
-		{
-			name:    "unrecognized placeholder rejected",
-			format:  "Bearer {UNKNOWN_VAR}",
-			envVars: map[string]string{"API_KEY": "secret"},
-			wantErr: "unrecognized placeholder {UNKNOWN_VAR}",
-		},
-		{
-			name:    "empty env vars with format",
-			format:  "Bearer {token}",
-			envVars: map[string]string{},
-			wantErr: "unrecognized placeholder {token}",
-		},
-		{
-			name:    "empty env vars no format",
-			format:  "",
-			envVars: map[string]string{},
-			want:    "",
-		},
-		{
-			name:    "value with closing brace rejected",
-			format:  "Bearer {token}",
-			envVars: map[string]string{"token": "abc}123"},
-			wantErr: "invalid character }",
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got, err := ApplyAuthFormat(tt.format, tt.envVars)
-			if tt.wantErr != "" {
-				require.Error(t, err)
-				assert.Contains(t, err.Error(), tt.wantErr)
-			} else {
-				require.NoError(t, err)
-				assert.Equal(t, tt.want, got)
-			}
-		})
-	}
-}
-
-func TestBuildAuthHeader(t *testing.T) {
-	tests := []struct {
-		name       string
-		manifest   *ToolsManifest
-		envSetup   map[string]string
-		wantHeader string
-		wantValue  string
-		wantErr    bool
-	}{
-		{
-			name:     "nil manifest",
-			manifest: nil,
-		},
-		{
-			name: "auth type none",
-			manifest: &ToolsManifest{
-				Auth: ManifestAuth{Type: "none"},
-			},
-		},
-		{
-			name: "api_key with format",
-			manifest: &ToolsManifest{
-				Auth: ManifestAuth{
-					Type:    "api_key",
-					Header:  "X-Api-Key",
-					Format:  "{MY_API_KEY}",
-					EnvVars: []string{"MY_API_KEY"},
-				},
-			},
-			envSetup:   map[string]string{"MY_API_KEY": "secret123"},
-			wantHeader: "X-Api-Key",
-			wantValue:  "secret123",
-		},
-		{
-			name: "bearer_token no format defaults to Bearer prefix",
-			manifest: &ToolsManifest{
-				Auth: ManifestAuth{
-					Type:    "bearer_token",
-					EnvVars: []string{"BEARER_TOK"},
-				},
-			},
-			envSetup:   map[string]string{"BEARER_TOK": "tok999"},
-			wantHeader: "Authorization",
-			wantValue:  "Bearer tok999",
-		},
-		{
-			name: "bearer_token with custom format",
-			manifest: &ToolsManifest{
-				Auth: ManifestAuth{
-					Type:    "bearer_token",
-					Header:  "Authorization",
-					Format:  "Token {GITHUB_TOKEN}",
-					EnvVars: []string{"GITHUB_TOKEN"},
-				},
-			},
-			envSetup:   map[string]string{"GITHUB_TOKEN": "ghp_abc"},
-			wantHeader: "Authorization",
-			wantValue:  "Token ghp_abc",
-		},
-		{
-			name: "env var not set returns empty",
-			manifest: &ToolsManifest{
-				Auth: ManifestAuth{
-					Type:    "api_key",
-					Header:  "X-Api-Key",
-					EnvVars: []string{"MISSING_KEY"},
-				},
-			},
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			// Set up env vars.
-			for k, v := range tt.envSetup {
-				t.Setenv(k, v)
-			}
-
-			hdr, val, err := BuildAuthHeader(tt.manifest)
-			if tt.wantErr {
-				require.Error(t, err)
-			} else {
-				require.NoError(t, err)
-				assert.Equal(t, tt.wantHeader, hdr)
-				assert.Equal(t, tt.wantValue, val)
-			}
-		})
-	}
-}
-
-func TestBuildAuthQueryParam(t *testing.T) {
-	tests := []struct {
-		name      string
-		manifest  *ToolsManifest
-		envSetup  map[string]string
-		wantName  string
-		wantValue string
-	}{
-		{
-			name:     "nil manifest",
-			manifest: nil,
-		},
-		{
-			name: "in header not query",
-			manifest: &ToolsManifest{
-				Auth: ManifestAuth{
-					Type: "api_key",
-					In:   "header",
-				},
-			},
-		},
-		{
-			name: "api key in query",
-			manifest: &ToolsManifest{
-				Auth: ManifestAuth{
-					Type:    "api_key",
-					Header:  "key",
-					In:      "query",
-					EnvVars: []string{"ESPN_KEY"},
-				},
-			},
-			envSetup:  map[string]string{"ESPN_KEY": "qwerty"},
-			wantName:  "key",
-			wantValue: "qwerty",
-		},
-		{
-			name: "query auth defaults to api_key param name",
-			manifest: &ToolsManifest{
-				Auth: ManifestAuth{
-					Type:    "api_key",
-					In:      "query",
-					EnvVars: []string{"MY_KEY"},
-				},
-			},
-			envSetup:  map[string]string{"MY_KEY": "abc"},
-			wantName:  "api_key",
-			wantValue: "abc",
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			for k, v := range tt.envSetup {
-				t.Setenv(k, v)
-			}
-
-			name, val, err := BuildAuthQueryParam(tt.manifest)
-			require.NoError(t, err)
-			assert.Equal(t, tt.wantName, name)
-			assert.Equal(t, tt.wantValue, val)
-		})
-	}
-}
-
-func TestRedactCredentials(t *testing.T) {
-	tests := []struct {
-		name     string
-		body     string
-		manifest *ToolsManifest
-		envSetup map[string]string
-		want     string
-	}{
-		{
-			name:     "nil manifest returns body unchanged",
-			body:     "error: bad token abc123",
-			manifest: nil,
-			want:     "error: bad token abc123",
-		},
-		{
-			name: "credential value redacted",
-			body: "Invalid API key: sk-secret-key-here is not valid",
-			manifest: &ToolsManifest{
-				Auth: ManifestAuth{
-					Type:    "api_key",
-					EnvVars: []string{"MY_KEY"},
-				},
-			},
-			envSetup: map[string]string{"MY_KEY": "sk-secret-key-here"},
-			want:     "Invalid API key: [REDACTED] is not valid",
-		},
-		{
-			name: "short credential not redacted to avoid false positives",
-			body: "error: bad key ab",
-			manifest: &ToolsManifest{
-				Auth: ManifestAuth{
-					Type:    "api_key",
-					EnvVars: []string{"SHORT_KEY"},
-				},
-			},
-			envSetup: map[string]string{"SHORT_KEY": "ab"},
-			want:     "error: bad key ab",
-		},
-		{
-			name: "multiple env vars redacted",
-			body: "key1=first-secret key2=second-secret",
-			manifest: &ToolsManifest{
-				Auth: ManifestAuth{
-					Type:    "api_key",
-					EnvVars: []string{"KEY1", "KEY2"},
-				},
-			},
-			envSetup: map[string]string{"KEY1": "first-secret", "KEY2": "second-secret"},
-			want:     "key1=[REDACTED] key2=[REDACTED]",
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			for k, v := range tt.envSetup {
-				t.Setenv(k, v)
-			}
-
-			got := RedactCredentials(tt.body, tt.manifest)
-			assert.Equal(t, tt.want, got)
-		})
-	}
-}
-
-func TestHasAuthConfigured(t *testing.T) {
-	tests := []struct {
-		name     string
-		manifest *ToolsManifest
-		envSetup map[string]string
-		want     bool
-	}{
-		{
-			name:     "nil manifest",
-			manifest: nil,
-			want:     true,
-		},
-		{
-			name: "no auth type",
-			manifest: &ToolsManifest{
-				Auth: ManifestAuth{Type: "none"},
-			},
-			want: true,
-		},
-		{
-			name: "env var set",
-			manifest: &ToolsManifest{
-				Auth: ManifestAuth{
-					Type:    "api_key",
-					EnvVars: []string{"MY_KEY"},
-				},
-			},
-			envSetup: map[string]string{"MY_KEY": "secret"},
-			want:     true,
-		},
-		{
-			name: "env var not set",
-			manifest: &ToolsManifest{
-				Auth: ManifestAuth{
-					Type:    "api_key",
-					EnvVars: []string{"MISSING_KEY"},
-				},
-			},
-			want: false,
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			for k, v := range tt.envSetup {
-				t.Setenv(k, v)
-			}
-
-			got := hasAuthConfigured(tt.manifest)
-			assert.Equal(t, tt.want, got)
-		})
-	}
-}
diff --git a/internal/megamcp/handler.go b/internal/megamcp/handler.go
deleted file mode 100644
index ceb7fb40..00000000
--- a/internal/megamcp/handler.go
+++ /dev/null
@@ -1,241 +0,0 @@
-package megamcp
-
-import (
-	"bytes"
-	"context"
-	"encoding/json"
-	"fmt"
-	"io"
-	"net/http"
-	"net/url"
-	"strings"
-
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
-)
-
-// maxResponseBody is the maximum response body size the handler will read from the network (10MB).
-const maxResponseBody = 10 * 1024 * 1024
-
-// maxAgentResponse is the maximum response text returned to the agent (32KB).
-// Larger responses are truncated with a size note to prevent context window flooding.
-const maxAgentResponse = 32 * 1024
-
-// MakeToolHandler returns an MCP tool handler function that makes HTTP requests
-// to APIs using the tools manifest data. It handles auth, parameter routing
-// (path/query/body), required headers, and response classification.
-// The apiSlug is the canonical slug used in setup_guide references (e.g., "dub", not "Dub").
-func MakeToolHandler(manifest *ToolsManifest, tool ManifestTool, httpClient *http.Client, apiSlug string) server.ToolHandlerFunc {
-	return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-		slug := apiSlug
-
-		// 1. Fail-closed auth check.
-		if !tool.NoAuth && manifest.Auth.Type != "" && manifest.Auth.Type != "none" {
-			if !hasAuthConfigured(manifest) {
-				return mcp.NewToolResultError(
-					fmt.Sprintf("Authentication not configured for this API — call setup_guide(%q) for instructions", slug),
-				), nil
-			}
-		}
-
-		// 2. Build URL: substitute path params into the path template.
-		args := req.GetArguments()
-		apiPath := tool.Path
-
-		for _, param := range tool.Params {
-			if param.Location != "path" {
-				continue
-			}
-			val, ok := args[param.Name]
-			if !ok || val == nil {
-				continue
-			}
-			encoded := url.PathEscape(fmt.Sprintf("%v", val))
-			apiPath = strings.Replace(apiPath, "{"+param.Name+"}", encoded, 1)
-		}
-
-		// Check for unsubstituted path placeholders.
-		if idx := strings.Index(apiPath, "{"); idx >= 0 {
-			end := strings.Index(apiPath[idx:], "}")
-			if end > 0 {
-				var missing []string
-				remaining := apiPath
-				for {
-					start := strings.Index(remaining, "{")
-					if start < 0 {
-						break
-					}
-					close := strings.Index(remaining[start:], "}")
-					if close < 0 {
-						break
-					}
-					placeholder := remaining[start+1 : start+close]
-					missing = append(missing, placeholder)
-					remaining = remaining[start+close+1:]
-				}
-				return mcp.NewToolResultError(
-					fmt.Sprintf("Missing required path parameter(s): %s", strings.Join(missing, ", ")),
-				), nil
-			}
-		}
-
-		// 3. Build query string.
-		queryValues := url.Values{}
-		for _, param := range tool.Params {
-			if param.Location != "query" {
-				continue
-			}
-			val, ok := args[param.Name]
-			if !ok || val == nil {
-				continue
-			}
-			queryValues.Set(param.Name, fmt.Sprintf("%v", val))
-		}
-
-		// 4. Build body for POST/PUT/PATCH.
-		var bodyReader io.Reader
-		method := strings.ToUpper(tool.Method)
-		if method == "POST" || method == "PUT" || method == "PATCH" {
-			bodyParams := make(map[string]any)
-			for _, param := range tool.Params {
-				if param.Location != "body" {
-					continue
-				}
-				val, ok := args[param.Name]
-				if !ok || val == nil {
-					continue
-				}
-				bodyParams[param.Name] = val
-			}
-			if len(bodyParams) > 0 {
-				bodyBytes, err := json.Marshal(bodyParams)
-				if err != nil {
-					return mcp.NewToolResultError(fmt.Sprintf("Error serializing request body: %v", err)), nil
-				}
-				bodyReader = bytes.NewReader(bodyBytes)
-			}
-		}
-
-		// Construct the full URL.
-		baseURL := strings.TrimRight(manifest.BaseURL, "/")
-		fullURL := baseURL + apiPath
-
-		if len(queryValues) > 0 {
-			fullURL += "?" + queryValues.Encode()
-		}
-
-		// 5. Post-assembly URL validation: verify host matches base URL.
-		assembledURL, err := url.Parse(fullURL)
-		if err != nil {
-			return mcp.NewToolResultError(fmt.Sprintf("Invalid assembled URL: %v", err)), nil
-		}
-		baseURLParsed, err := url.Parse(baseURL)
-		if err != nil {
-			return mcp.NewToolResultError(fmt.Sprintf("Invalid base URL: %v", err)), nil
-		}
-		if assembledURL.Host != baseURLParsed.Host {
-			return mcp.NewToolResultError(
-				fmt.Sprintf("URL host mismatch: expected %q but got %q — possible injection attempt", baseURLParsed.Host, assembledURL.Host),
-			), nil
-		}
-
-		// Build HTTP request.
-		httpReq, err := http.NewRequestWithContext(ctx, method, fullURL, bodyReader)
-		if err != nil {
-			return mcp.NewToolResultError(fmt.Sprintf("Error creating HTTP request: %v", err)), nil
-		}
-
-		// 6. Set headers.
-		// Auth header or query param.
-		if manifest.Auth.In == "query" {
-			paramName, paramValue, authErr := BuildAuthQueryParam(manifest)
-			if authErr != nil {
-				return mcp.NewToolResultError(fmt.Sprintf("Error building auth query param: %v", authErr)), nil
-			}
-			if paramName != "" && paramValue != "" {
-				q := httpReq.URL.Query()
-				q.Set(paramName, paramValue)
-				httpReq.URL.RawQuery = q.Encode()
-			}
-		} else {
-			headerName, headerValue, authErr := BuildAuthHeader(manifest)
-			if authErr != nil {
-				return mcp.NewToolResultError(fmt.Sprintf("Error building auth header: %v", authErr)), nil
-			}
-			if headerName != "" && headerValue != "" {
-				httpReq.Header.Set(headerName, headerValue)
-			}
-		}
-
-		// Required headers from manifest.
-		for _, h := range manifest.RequiredHeaders {
-			httpReq.Header.Set(h.Name, h.Value)
-		}
-
-		// Per-tool header overrides.
-		for _, h := range tool.HeaderOverrides {
-			httpReq.Header.Set(h.Name, h.Value)
-		}
-
-		// Content-Type for body-bearing methods.
-		if method == "POST" || method == "PUT" || method == "PATCH" {
-			httpReq.Header.Set("Content-Type", "application/json")
-		}
-
-		httpReq.Header.Set("User-Agent", "printing-press-mcp")
-
-		// 7. Make request.
-		resp, err := httpClient.Do(httpReq)
-		if err != nil {
-			return mcp.NewToolResultError(fmt.Sprintf("Network error: %v", err)), nil
-		}
-		defer func() { _ = resp.Body.Close() }()
-
-		respBody, err := io.ReadAll(io.LimitReader(resp.Body, maxResponseBody))
-		if err != nil {
-			return mcp.NewToolResultError(fmt.Sprintf("Error reading response body: %v", err)), nil
-		}
-
-		// 8. Handle response.
-		respText := string(respBody)
-
-		// Success (2xx).
-		if resp.StatusCode >= 200 && resp.StatusCode < 300 {
-			if len(respText) > maxAgentResponse {
-				truncated := respText[:maxAgentResponse]
-				totalKB := len(respText) / 1024
-				return mcp.NewToolResultText(fmt.Sprintf("%s\n\n[Response truncated — showing first 32KB of %dKB. Use more specific query parameters to narrow results, or install the per-API MCP for full responses.]", truncated, totalKB)), nil
-			}
-			return mcp.NewToolResultText(respText), nil
-		}
-
-		// 401/403 — generic auth error, no env var names, redact credentials.
-		if resp.StatusCode == 401 || resp.StatusCode == 403 {
-			redacted := RedactCredentials(respText, manifest)
-			return mcp.NewToolResultError(
-				fmt.Sprintf("Authentication error (HTTP %d): %s\n\nAuthentication not configured — call setup_guide(%q) for instructions",
-					resp.StatusCode, SanitizeText(redacted, 500), slug),
-			), nil
-		}
-
-		// 429 — rate limited, surface response body.
-		if resp.StatusCode == 429 {
-			return mcp.NewToolResultError(
-				fmt.Sprintf("Rate limited (HTTP 429): %s", SanitizeText(respText, 1000)),
-			), nil
-		}
-
-		// Other 4xx — return API error with credential redaction.
-		if resp.StatusCode >= 400 && resp.StatusCode < 500 {
-			redacted := RedactCredentials(respText, manifest)
-			return mcp.NewToolResultError(
-				fmt.Sprintf("API error (HTTP %d): %s", resp.StatusCode, SanitizeText(redacted, 1000)),
-			), nil
-		}
-
-		// 5xx — server error.
-		return mcp.NewToolResultError(
-			fmt.Sprintf("Server error (HTTP %d): %s", resp.StatusCode, SanitizeText(respText, 500)),
-		), nil
-	}
-}
diff --git a/internal/megamcp/handler_test.go b/internal/megamcp/handler_test.go
deleted file mode 100644
index 2e4b7425..00000000
--- a/internal/megamcp/handler_test.go
+++ /dev/null
@@ -1,835 +0,0 @@
-package megamcp
-
-import (
-	"encoding/json"
-	"io"
-	"net/http"
-	"net/http/httptest"
-	"strings"
-	"testing"
-	"time"
-
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-)
-
-// newTestManifest returns a basic manifest for testing.
-func newTestManifest(baseURL string) *ToolsManifest {
-	return &ToolsManifest{
-		APIName: "testapi",
-		BaseURL: baseURL,
-		Auth: ManifestAuth{
-			Type: "none",
-		},
-	}
-}
-
-// newTestManifestWithAuth returns a manifest with API key auth.
-func newTestManifestWithAuth(baseURL, authType string, envVars []string) *ToolsManifest {
-	m := newTestManifest(baseURL)
-	m.Auth = ManifestAuth{
-		Type:    authType,
-		Header:  "X-Api-Key",
-		EnvVars: envVars,
-	}
-	return m
-}
-
-// makeToolRequest creates an MCP CallToolRequest with the given arguments.
-func makeToolRequest(args map[string]any) mcp.CallToolRequest {
-	// Arguments is typed as `any` in CallToolParams, but GetArguments()
-	// casts it to map[string]any. We pass args as map[string]any.
-	var arguments any
-	if args != nil {
-		arguments = map[string]any(args)
-	}
-	return mcp.CallToolRequest{
-		Params: mcp.CallToolParams{
-			Arguments: arguments,
-		},
-	}
-}
-
-func TestMakeToolHandler_GetWithPathParam(t *testing.T) {
-	var capturedPath string
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		capturedPath = r.URL.Path
-		w.WriteHeader(200)
-		w.Write([]byte(`{"id": 42}`))
-	}))
-	defer srv.Close()
-
-	manifest := newTestManifest(srv.URL)
-	tool := ManifestTool{
-		Name:   "users_get",
-		Method: "GET",
-		Path:   "/users/{user_id}",
-		Params: []ManifestParam{
-			{Name: "user_id", Type: "string", Location: "path", Required: true},
-		},
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(map[string]any{
-		"user_id": "123",
-	}))
-
-	require.NoError(t, err)
-	assert.False(t, result.IsError)
-	assert.Equal(t, "/users/123", capturedPath)
-}
-
-func TestMakeToolHandler_GetWithQueryParams(t *testing.T) {
-	var capturedQuery string
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		capturedQuery = r.URL.RawQuery
-		w.WriteHeader(200)
-		w.Write([]byte(`[]`))
-	}))
-	defer srv.Close()
-
-	manifest := newTestManifest(srv.URL)
-	tool := ManifestTool{
-		Name:   "users_list",
-		Method: "GET",
-		Path:   "/users",
-		Params: []ManifestParam{
-			{Name: "page", Type: "integer", Location: "query"},
-			{Name: "limit", Type: "integer", Location: "query"},
-		},
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(map[string]any{
-		"page":  float64(2),
-		"limit": float64(10),
-	}))
-
-	require.NoError(t, err)
-	assert.False(t, result.IsError)
-	assert.Contains(t, capturedQuery, "page=2")
-	assert.Contains(t, capturedQuery, "limit=10")
-}
-
-func TestMakeToolHandler_PostWithBodyParams(t *testing.T) {
-	var capturedBody string
-	var capturedPath string
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		capturedPath = r.URL.Path
-		body, _ := io.ReadAll(r.Body)
-		capturedBody = string(body)
-		w.WriteHeader(201)
-		w.Write([]byte(`{"id": 1}`))
-	}))
-	defer srv.Close()
-
-	manifest := newTestManifest(srv.URL)
-	tool := ManifestTool{
-		Name:   "users_create",
-		Method: "POST",
-		Path:   "/orgs/{org_id}/users",
-		Params: []ManifestParam{
-			{Name: "org_id", Type: "string", Location: "path", Required: true},
-			{Name: "name", Type: "string", Location: "body", Required: true},
-			{Name: "email", Type: "string", Location: "body", Required: true},
-		},
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(map[string]any{
-		"org_id": "acme",
-		"name":   "Alice",
-		"email":  "alice@example.com",
-	}))
-
-	require.NoError(t, err)
-	assert.False(t, result.IsError)
-	assert.Equal(t, "/orgs/acme/users", capturedPath)
-
-	// Verify body contains name and email but NOT org_id (path param).
-	var bodyMap map[string]any
-	require.NoError(t, json.Unmarshal([]byte(capturedBody), &bodyMap))
-	assert.Equal(t, "Alice", bodyMap["name"])
-	assert.Equal(t, "alice@example.com", bodyMap["email"])
-	_, hasOrgID := bodyMap["org_id"]
-	assert.False(t, hasOrgID, "path param org_id should not appear in body")
-}
-
-func TestMakeToolHandler_ApiKeyAuthInHeader(t *testing.T) {
-	var capturedAuthHeader string
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		capturedAuthHeader = r.Header.Get("X-Api-Key")
-		w.WriteHeader(200)
-		w.Write([]byte(`{"ok": true}`))
-	}))
-	defer srv.Close()
-
-	t.Setenv("TEST_API_KEY", "sk-test-12345")
-	manifest := newTestManifestWithAuth(srv.URL, "api_key", []string{"TEST_API_KEY"})
-
-	tool := ManifestTool{
-		Name:   "status_get",
-		Method: "GET",
-		Path:   "/status",
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(nil))
-
-	require.NoError(t, err)
-	assert.False(t, result.IsError)
-	assert.Equal(t, "sk-test-12345", capturedAuthHeader)
-}
-
-func TestMakeToolHandler_ApiKeyAuthInQuery(t *testing.T) {
-	var capturedQuery string
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		capturedQuery = r.URL.RawQuery
-		w.WriteHeader(200)
-		w.Write([]byte(`{"ok": true}`))
-	}))
-	defer srv.Close()
-
-	t.Setenv("ESPN_KEY", "espn-abc")
-	manifest := &ToolsManifest{
-		APIName: "espn",
-		BaseURL: srv.URL,
-		Auth: ManifestAuth{
-			Type:    "api_key",
-			Header:  "apikey",
-			In:      "query",
-			EnvVars: []string{"ESPN_KEY"},
-		},
-	}
-
-	tool := ManifestTool{
-		Name:   "scores_get",
-		Method: "GET",
-		Path:   "/scores",
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(nil))
-
-	require.NoError(t, err)
-	assert.False(t, result.IsError)
-	assert.Contains(t, capturedQuery, "apikey=espn-abc")
-}
-
-func TestMakeToolHandler_BearerTokenAuth(t *testing.T) {
-	var capturedAuth string
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		capturedAuth = r.Header.Get("Authorization")
-		w.WriteHeader(200)
-		w.Write([]byte(`{}`))
-	}))
-	defer srv.Close()
-
-	t.Setenv("DUB_TOKEN", "dub-tok-xyz")
-	manifest := &ToolsManifest{
-		APIName: "dub",
-		BaseURL: srv.URL,
-		Auth: ManifestAuth{
-			Type:    "bearer_token",
-			Header:  "Authorization",
-			Format:  "Bearer {DUB_TOKEN}",
-			EnvVars: []string{"DUB_TOKEN"},
-		},
-	}
-
-	tool := ManifestTool{
-		Name:   "links_list",
-		Method: "GET",
-		Path:   "/links",
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(nil))
-
-	require.NoError(t, err)
-	assert.False(t, result.IsError)
-	assert.Equal(t, "Bearer dub-tok-xyz", capturedAuth)
-}
-
-func TestMakeToolHandler_RequiredHeaders(t *testing.T) {
-	var capturedHeaders http.Header
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		capturedHeaders = r.Header
-		w.WriteHeader(200)
-		w.Write([]byte(`{}`))
-	}))
-	defer srv.Close()
-
-	manifest := newTestManifest(srv.URL)
-	manifest.RequiredHeaders = []ManifestHeader{
-		{Name: "Accept", Value: "application/json"},
-		{Name: "X-Custom", Value: "custom-value"},
-	}
-
-	tool := ManifestTool{
-		Name:   "data_get",
-		Method: "GET",
-		Path:   "/data",
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(nil))
-
-	require.NoError(t, err)
-	assert.False(t, result.IsError)
-	assert.Equal(t, "application/json", capturedHeaders.Get("Accept"))
-	assert.Equal(t, "custom-value", capturedHeaders.Get("X-Custom"))
-}
-
-func TestMakeToolHandler_PerToolHeaderOverrides(t *testing.T) {
-	var capturedVersion string
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		capturedVersion = r.Header.Get("X-API-Version")
-		w.WriteHeader(200)
-		w.Write([]byte(`{}`))
-	}))
-	defer srv.Close()
-
-	manifest := newTestManifest(srv.URL)
-	manifest.RequiredHeaders = []ManifestHeader{
-		{Name: "X-API-Version", Value: "2023-01-01"},
-	}
-
-	tool := ManifestTool{
-		Name:   "v2_data_get",
-		Method: "GET",
-		Path:   "/v2/data",
-		HeaderOverrides: []ManifestHeader{
-			{Name: "X-API-Version", Value: "2024-06-01"},
-		},
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(nil))
-
-	require.NoError(t, err)
-	assert.False(t, result.IsError)
-	// Per-tool override should win over required headers.
-	assert.Equal(t, "2024-06-01", capturedVersion)
-}
-
-func TestMakeToolHandler_NoAuth(t *testing.T) {
-	var capturedAuthHeader string
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		capturedAuthHeader = r.Header.Get("Authorization")
-		w.WriteHeader(200)
-		w.Write([]byte(`{}`))
-	}))
-	defer srv.Close()
-
-	manifest := newTestManifest(srv.URL)
-
-	tool := ManifestTool{
-		Name:   "public_get",
-		Method: "GET",
-		Path:   "/public",
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(nil))
-
-	require.NoError(t, err)
-	assert.False(t, result.IsError)
-	assert.Empty(t, capturedAuthHeader)
-}
-
-func TestMakeToolHandler_MissingEnvVarNonNoAuth(t *testing.T) {
-	// Don't start a server — request should never be made.
-	manifest := &ToolsManifest{
-		APIName: "myapi",
-		BaseURL: "https://api.example.com",
-		Auth: ManifestAuth{
-			Type:    "api_key",
-			Header:  "X-Api-Key",
-			EnvVars: []string{"MISSING_ENV_VAR"},
-		},
-	}
-
-	tool := ManifestTool{
-		Name:   "data_get",
-		Method: "GET",
-		Path:   "/data",
-	}
-
-	handler := MakeToolHandler(manifest, tool, &http.Client{Timeout: time.Second}, "test-api")
-	result, err := handler(t.Context(), makeToolRequest(nil))
-
-	require.NoError(t, err)
-	assert.True(t, result.IsError)
-	assertResultContains(t, result, "Authentication not configured")
-	assertResultContains(t, result, "setup_guide")
-}
-
-func TestMakeToolHandler_MissingEnvVarNoAuthEndpoint(t *testing.T) {
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.WriteHeader(200)
-		w.Write([]byte(`{"public": true}`))
-	}))
-	defer srv.Close()
-
-	manifest := &ToolsManifest{
-		APIName: "myapi",
-		BaseURL: srv.URL,
-		Auth: ManifestAuth{
-			Type:    "api_key",
-			Header:  "X-Api-Key",
-			EnvVars: []string{"MISSING_ENV_VAR"},
-		},
-	}
-
-	tool := ManifestTool{
-		Name:   "public_get",
-		Method: "GET",
-		Path:   "/public",
-		NoAuth: true, // This endpoint doesn't need auth.
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(nil))
-
-	require.NoError(t, err)
-	assert.False(t, result.IsError, "NoAuth endpoint should succeed without env var")
-}
-
-func TestMakeToolHandler_401Response(t *testing.T) {
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.WriteHeader(401)
-		w.Write([]byte(`{"error": "Invalid token: sk-real-key-123"}`))
-	}))
-	defer srv.Close()
-
-	t.Setenv("MY_TOKEN", "sk-real-key-123")
-	manifest := &ToolsManifest{
-		APIName: "myapi",
-		BaseURL: srv.URL,
-		Auth: ManifestAuth{
-			Type:    "api_key",
-			Header:  "X-Api-Key",
-			EnvVars: []string{"MY_TOKEN"},
-		},
-	}
-
-	tool := ManifestTool{
-		Name:   "data_get",
-		Method: "GET",
-		Path:   "/data",
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(nil))
-
-	require.NoError(t, err)
-	assert.True(t, result.IsError)
-	assertResultContains(t, result, "Authentication error")
-	assertResultContains(t, result, "setup_guide")
-	// Credential should be redacted.
-	assertResultNotContains(t, result, "sk-real-key-123")
-	assertResultContains(t, result, "[REDACTED]")
-}
-
-func TestMakeToolHandler_429Response(t *testing.T) {
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.WriteHeader(429)
-		w.Write([]byte(`{"error": "rate limit exceeded", "retry_after": 30}`))
-	}))
-	defer srv.Close()
-
-	manifest := newTestManifest(srv.URL)
-	tool := ManifestTool{
-		Name:   "data_get",
-		Method: "GET",
-		Path:   "/data",
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(nil))
-
-	require.NoError(t, err)
-	assert.True(t, result.IsError)
-	assertResultContains(t, result, "Rate limited")
-	assertResultContains(t, result, "rate limit exceeded")
-}
-
-func TestMakeToolHandler_LargeResponseTruncated(t *testing.T) {
-	// Create a response larger than 10MB.
-	largeBody := strings.Repeat("x", maxResponseBody+1000)
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.WriteHeader(200)
-		w.Write([]byte(largeBody))
-	}))
-	defer srv.Close()
-
-	manifest := newTestManifest(srv.URL)
-	tool := ManifestTool{
-		Name:   "data_get",
-		Method: "GET",
-		Path:   "/data",
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(nil))
-
-	require.NoError(t, err)
-	assert.False(t, result.IsError)
-	// The response should be truncated at maxResponseBody.
-	text := extractResultText(result)
-	assert.LessOrEqual(t, len(text), maxResponseBody)
-}
-
-func TestMakeToolHandler_AgentResponseTruncation(t *testing.T) {
-	// Returns a 50KB response — should be truncated to 32KB for the agent.
-	bigBody := strings.Repeat("x", 50*1024)
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.WriteHeader(200)
-		w.Write([]byte(bigBody))
-	}))
-	defer srv.Close()
-
-	manifest := &ToolsManifest{
-		APIName: "bigapi",
-		BaseURL: srv.URL,
-		Auth:    ManifestAuth{Type: "none"},
-	}
-	tool := ManifestTool{Name: "big_get", Method: "GET", Path: "/big"}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(nil))
-
-	require.NoError(t, err)
-	assert.False(t, result.IsError)
-	text := extractResultText(result)
-	assert.Contains(t, text, "[Response truncated")
-	assert.Contains(t, text, "32KB of 50KB")
-	// The truncated output should be around 32KB + the truncation message.
-	assert.Less(t, len(text), 34*1024)
-}
-
-func TestMakeToolHandler_SmallResponseNotTruncated(t *testing.T) {
-	body := `{"status": "ok"}`
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.WriteHeader(200)
-		w.Write([]byte(body))
-	}))
-	defer srv.Close()
-
-	manifest := &ToolsManifest{
-		APIName: "smallapi",
-		BaseURL: srv.URL,
-		Auth:    ManifestAuth{Type: "none"},
-	}
-	tool := ManifestTool{Name: "small_get", Method: "GET", Path: "/small"}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(nil))
-
-	require.NoError(t, err)
-	assert.False(t, result.IsError)
-	text := extractResultText(result)
-	assert.Equal(t, body, text)
-	assert.NotContains(t, text, "[Response truncated")
-}
-
-func TestMakeToolHandler_MissingRequiredPathParam(t *testing.T) {
-	manifest := &ToolsManifest{
-		APIName: "myapi",
-		BaseURL: "https://api.example.com",
-		Auth:    ManifestAuth{Type: "none"},
-	}
-
-	tool := ManifestTool{
-		Name:   "users_get",
-		Method: "GET",
-		Path:   "/users/{user_id}/posts/{post_id}",
-		Params: []ManifestParam{
-			{Name: "user_id", Type: "string", Location: "path", Required: true},
-			{Name: "post_id", Type: "string", Location: "path", Required: true},
-		},
-	}
-
-	handler := MakeToolHandler(manifest, tool, &http.Client{Timeout: time.Second}, "test-api")
-	// Only provide user_id, not post_id.
-	result, err := handler(t.Context(), makeToolRequest(map[string]any{
-		"user_id": "123",
-	}))
-
-	require.NoError(t, err)
-	assert.True(t, result.IsError)
-	assertResultContains(t, result, "Missing required path parameter")
-	assertResultContains(t, result, "post_id")
-}
-
-func TestMakeToolHandler_PathParamWithTraversal(t *testing.T) {
-	var capturedRawPath string
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		capturedRawPath = r.URL.RawPath
-		w.WriteHeader(200)
-		w.Write([]byte(`{}`))
-	}))
-	defer srv.Close()
-
-	manifest := newTestManifest(srv.URL)
-	tool := ManifestTool{
-		Name:   "files_get",
-		Method: "GET",
-		Path:   "/files/{file_id}",
-		Params: []ManifestParam{
-			{Name: "file_id", Type: "string", Location: "path", Required: true},
-		},
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(map[string]any{
-		"file_id": "../../../etc/passwd",
-	}))
-
-	require.NoError(t, err)
-	assert.False(t, result.IsError, "URL-encoded path traversal should not cause host mismatch")
-	// The ../ should be URL-encoded in the raw path (Go's HTTP server decodes
-	// %2F back to / in r.URL.Path, but RawPath preserves the encoding).
-	assert.Contains(t, capturedRawPath, "%2F")
-}
-
-func TestMakeToolHandler_500Response(t *testing.T) {
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.WriteHeader(500)
-		w.Write([]byte(`internal server error`))
-	}))
-	defer srv.Close()
-
-	manifest := newTestManifest(srv.URL)
-	tool := ManifestTool{
-		Name:   "data_get",
-		Method: "GET",
-		Path:   "/data",
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(nil))
-
-	require.NoError(t, err)
-	assert.True(t, result.IsError)
-	assertResultContains(t, result, "Server error (HTTP 500)")
-}
-
-func TestMakeToolHandler_IntegrationRoundTrip(t *testing.T) {
-	// Full round-trip test: GET with path+query params, auth header, required headers.
-	var capturedReq struct {
-		Method  string
-		Path    string
-		Query   string
-		Headers http.Header
-		Body    string
-	}
-
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		capturedReq.Method = r.Method
-		capturedReq.Path = r.URL.Path
-		capturedReq.Query = r.URL.RawQuery
-		capturedReq.Headers = r.Header
-		body, _ := io.ReadAll(r.Body)
-		capturedReq.Body = string(body)
-		w.WriteHeader(200)
-		w.Write([]byte(`{"result": "success"}`))
-	}))
-	defer srv.Close()
-
-	t.Setenv("DUB_TOKEN", "tok-abc")
-	manifest := &ToolsManifest{
-		APIName: "dub",
-		BaseURL: srv.URL,
-		Auth: ManifestAuth{
-			Type:    "bearer_token",
-			Header:  "Authorization",
-			Format:  "Bearer {DUB_TOKEN}",
-			EnvVars: []string{"DUB_TOKEN"},
-		},
-		RequiredHeaders: []ManifestHeader{
-			{Name: "Accept", Value: "application/json"},
-		},
-	}
-
-	tool := ManifestTool{
-		Name:   "links_update",
-		Method: "PATCH",
-		Path:   "/links/{link_id}",
-		Params: []ManifestParam{
-			{Name: "link_id", Type: "string", Location: "path", Required: true},
-			{Name: "url", Type: "string", Location: "body"},
-			{Name: "title", Type: "string", Location: "body"},
-		},
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(map[string]any{
-		"link_id": "lnk_42",
-		"url":     "https://example.com",
-		"title":   "Example",
-	}))
-
-	require.NoError(t, err)
-	assert.False(t, result.IsError)
-
-	// Verify request details.
-	assert.Equal(t, "PATCH", capturedReq.Method)
-	assert.Equal(t, "/links/lnk_42", capturedReq.Path)
-	assert.Equal(t, "Bearer tok-abc", capturedReq.Headers.Get("Authorization"))
-	assert.Equal(t, "application/json", capturedReq.Headers.Get("Accept"))
-	assert.Equal(t, "application/json", capturedReq.Headers.Get("Content-Type"))
-
-	// Verify body: url and title present, link_id absent.
-	var bodyMap map[string]any
-	require.NoError(t, json.Unmarshal([]byte(capturedReq.Body), &bodyMap))
-	assert.Equal(t, "https://example.com", bodyMap["url"])
-	assert.Equal(t, "Example", bodyMap["title"])
-	_, hasLinkID := bodyMap["link_id"]
-	assert.False(t, hasLinkID, "path param link_id should not be in body")
-}
-
-func TestMakeToolHandler_403Response(t *testing.T) {
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.WriteHeader(403)
-		w.Write([]byte(`{"error": "forbidden"}`))
-	}))
-	defer srv.Close()
-
-	t.Setenv("MY_KEY", "secret-key-value")
-	manifest := &ToolsManifest{
-		APIName: "myapi",
-		BaseURL: srv.URL,
-		Auth: ManifestAuth{
-			Type:    "api_key",
-			Header:  "X-Api-Key",
-			EnvVars: []string{"MY_KEY"},
-		},
-	}
-
-	tool := ManifestTool{
-		Name:   "admin_get",
-		Method: "GET",
-		Path:   "/admin",
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(nil))
-
-	require.NoError(t, err)
-	assert.True(t, result.IsError)
-	assertResultContains(t, result, "Authentication error")
-	assertResultContains(t, result, "setup_guide")
-	// Env var name should not be in the error.
-	assertResultNotContains(t, result, "MY_KEY")
-}
-
-func TestMakeToolHandler_4xxResponse(t *testing.T) {
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		w.WriteHeader(422)
-		w.Write([]byte(`{"error": "validation failed"}`))
-	}))
-	defer srv.Close()
-
-	manifest := newTestManifest(srv.URL)
-	tool := ManifestTool{
-		Name:   "data_create",
-		Method: "POST",
-		Path:   "/data",
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(nil))
-
-	require.NoError(t, err)
-	assert.True(t, result.IsError)
-	assertResultContains(t, result, "API error (HTTP 422)")
-	assertResultContains(t, result, "validation failed")
-}
-
-func TestMakeToolHandler_ContentTypeForPOST(t *testing.T) {
-	var capturedContentType string
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		capturedContentType = r.Header.Get("Content-Type")
-		w.WriteHeader(200)
-		w.Write([]byte(`{}`))
-	}))
-	defer srv.Close()
-
-	manifest := newTestManifest(srv.URL)
-	tool := ManifestTool{
-		Name:   "data_create",
-		Method: "POST",
-		Path:   "/data",
-		Params: []ManifestParam{
-			{Name: "name", Type: "string", Location: "body"},
-		},
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	_, err := handler(t.Context(), makeToolRequest(map[string]any{
-		"name": "test",
-	}))
-
-	require.NoError(t, err)
-	assert.Equal(t, "application/json", capturedContentType)
-}
-
-func TestMakeToolHandler_DeleteMethod(t *testing.T) {
-	var capturedMethod string
-	srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		capturedMethod = r.Method
-		w.WriteHeader(204)
-	}))
-	defer srv.Close()
-
-	manifest := newTestManifest(srv.URL)
-	tool := ManifestTool{
-		Name:   "data_delete",
-		Method: "DELETE",
-		Path:   "/data/{id}",
-		Params: []ManifestParam{
-			{Name: "id", Type: "string", Location: "path", Required: true},
-		},
-	}
-
-	handler := MakeToolHandler(manifest, tool, srv.Client(), "test-api")
-	result, err := handler(t.Context(), makeToolRequest(map[string]any{
-		"id": "42",
-	}))
-
-	require.NoError(t, err)
-	assert.False(t, result.IsError)
-	assert.Equal(t, "DELETE", capturedMethod)
-}
-
-// --- Helper functions ---
-
-// extractResultText extracts the text content from an MCP CallToolResult.
-func extractResultText(result *mcp.CallToolResult) string {
-	if result == nil || len(result.Content) == 0 {
-		return ""
-	}
-	for _, c := range result.Content {
-		if tc, ok := c.(mcp.TextContent); ok {
-			return tc.Text
-		}
-	}
-	return ""
-}
-
-// assertResultContains checks that the result text contains the expected substring.
-func assertResultContains(t *testing.T, result *mcp.CallToolResult, expected string) {
-	t.Helper()
-	text := extractResultText(result)
-	assert.Contains(t, text, expected, "expected result to contain %q, got %q", expected, text)
-}
-
-// assertResultNotContains checks that the result text does NOT contain the given substring.
-func assertResultNotContains(t *testing.T, result *mcp.CallToolResult, unexpected string) {
-	t.Helper()
-	text := extractResultText(result)
-	assert.NotContains(t, text, unexpected, "expected result NOT to contain %q, got %q", unexpected, text)
-}
diff --git a/internal/megamcp/manifest.go b/internal/megamcp/manifest.go
deleted file mode 100644
index b7955db9..00000000
--- a/internal/megamcp/manifest.go
+++ /dev/null
@@ -1,358 +0,0 @@
-package megamcp
-
-import (
-	"encoding/json"
-	"fmt"
-	"io"
-	"net/http"
-	"os"
-	"path/filepath"
-	"strings"
-	"sync"
-
-	"golang.org/x/sync/errgroup"
-)
-
-// LoadManifests fetches and caches tools manifests for all eligible registry entries.
-// It applies the PRINTING_PRESS_APIS env var filter, loads manifests in parallel,
-// and returns successfully loaded API entries plus any warnings.
-// Failed fetches produce warnings but do not block other APIs.
-// If entries is nil (registry fetch failed), falls back to cached manifests.
-func LoadManifests(entries []RegistryEntry, cacheDir, baseURL string) ([]*APIEntry, []string) {
-	// If no registry was provided (fetch failed), try loading from cache.
-	if entries == nil {
-		return loadFromCacheOnly(cacheDir)
-	}
-
-	// Apply PRINTING_PRESS_APIS filter if set.
-	filtered := filterEntries(entries)
-
-	var (
-		mu       sync.Mutex
-		results  []*APIEntry
-		warnings []string
-	)
-
-	var g errgroup.Group
-
-	for _, entry := range filtered {
-		g.Go(func() error {
-			apiEntry, warn := loadSingleManifest(entry, cacheDir, baseURL)
-			mu.Lock()
-			defer mu.Unlock()
-			if warn != "" {
-				warnings = append(warnings, warn)
-			}
-			if apiEntry != nil {
-				results = append(results, apiEntry)
-			}
-			return nil // never fail the group; warnings are collected
-		})
-	}
-
-	// errgroup.Group (not WithContext) — errors don't cancel siblings.
-	_ = g.Wait()
-
-	return results, warnings
-}
-
-// loadFromCacheOnly scans the cache directory for previously cached manifests.
-// Used as a fallback when the registry fetch fails.
-func loadFromCacheOnly(cacheDir string) ([]*APIEntry, []string) {
-	manifestsDir := filepath.Join(cacheDir, "manifests")
-	dirEntries, err := os.ReadDir(manifestsDir)
-	if err != nil {
-		return nil, []string{"no cached manifests available (registry fetch failed)"}
-	}
-
-	apiFilter := parseAPIFilter()
-	var results []*APIEntry
-	var warnings []string
-
-	for _, de := range dirEntries {
-		if !de.IsDir() {
-			continue
-		}
-		slug := de.Name()
-		if err := ValidateSlug(slug); err != nil {
-			continue
-		}
-		if apiFilter != nil && !apiFilter[slug] {
-			continue
-		}
-
-		cachePath := filepath.Join(manifestsDir, slug, "tools-manifest.json")
-		data, err := os.ReadFile(cachePath)
-		if err != nil {
-			continue
-		}
-
-		manifest, err := parseManifest(data)
-		if err != nil {
-			warnings = append(warnings, fmt.Sprintf("cached manifest for %q is corrupt: %v", slug, err))
-			continue
-		}
-
-		// Validate base URL even for cached manifests.
-		if err := ValidateBaseURL(manifest.BaseURL); err != nil {
-			warnings = append(warnings, fmt.Sprintf("skipping cached %q: unsafe base URL: %v", slug, err))
-			continue
-		}
-
-		prefix, err := slugToToolPrefix(slug)
-		if err != nil {
-			continue
-		}
-
-		results = append(results, &APIEntry{
-			Slug:             slug,
-			Dir:              filepath.Join(manifestsDir, slug),
-			Manifest:         manifest,
-			NormalizedPrefix: prefix,
-		})
-	}
-
-	if len(results) > 0 {
-		warnings = append(warnings, fmt.Sprintf("registry fetch failed; loaded %d APIs from cache", len(results)))
-	}
-
-	return results, warnings
-}
-
-// filterEntries applies the PRINTING_PRESS_APIS env var filter and excludes
-// entries that are cli-only or have no ManifestURL.
-func filterEntries(entries []RegistryEntry) []RegistryEntry {
-	apiFilter := parseAPIFilter()
-
-	var filtered []RegistryEntry
-	for _, entry := range entries {
-		// Skip entries without manifest URL.
-		if entry.MCP.ManifestURL == "" {
-			continue
-		}
-		// Skip cli-only entries.
-		if entry.MCP.MCPReady == "cli-only" {
-			continue
-		}
-		// Apply PRINTING_PRESS_APIS filter if set.
-		if apiFilter != nil {
-			slug := extractSlug(entry)
-			if !apiFilter[slug] {
-				continue
-			}
-		}
-		filtered = append(filtered, entry)
-	}
-	return filtered
-}
-
-// parseAPIFilter returns a set of allowed API slugs from PRINTING_PRESS_APIS,
-// or nil if the env var is not set (meaning all APIs are allowed).
-func parseAPIFilter() map[string]bool {
-	val := os.Getenv("PRINTING_PRESS_APIS")
-	if val == "" {
-		return nil
-	}
-	parts := strings.Split(val, ",")
-	filter := make(map[string]bool, len(parts))
-	for _, p := range parts {
-		p = strings.TrimSpace(p)
-		if p != "" {
-			filter[p] = true
-		}
-	}
-	return filter
-}
-
-// extractSlug derives the API slug from a registry entry's API field.
-func extractSlug(entry RegistryEntry) string {
-	return entry.API
-}
-
-// loadSingleManifest loads and caches a single API's tools manifest.
-// Returns the APIEntry on success, or a warning string on failure.
-func loadSingleManifest(entry RegistryEntry, cacheDir, baseURL string) (*APIEntry, string) {
-	slug := extractSlug(entry)
-
-	// Validate slug before constructing any paths.
-	if err := ValidateSlug(slug); err != nil {
-		return nil, fmt.Sprintf("skipping %q: %v", slug, err)
-	}
-
-	prefix, err := slugToToolPrefix(slug)
-	if err != nil {
-		return nil, fmt.Sprintf("skipping %q: invalid tool prefix: %v", slug, err)
-	}
-
-	// Construct cache path and validate it.
-	cacheSubDir := filepath.Join(cacheDir, "manifests", slug)
-	cachePath := filepath.Join(cacheSubDir, "tools-manifest.json")
-
-	if err := ValidateCachePath(cachePath, cacheDir); err != nil {
-		return nil, fmt.Sprintf("skipping %q: %v", slug, err)
-	}
-
-	// Try cached manifest first.
-	manifest, err := tryCache(cachePath, entry.MCP.ManifestChecksum)
-	if err == nil && manifest != nil {
-		// Validate base URL even for cached manifests.
-		if urlErr := ValidateBaseURL(manifest.BaseURL); urlErr != nil {
-			return nil, fmt.Sprintf("skipping %q: unsafe base URL %q: %v", slug, manifest.BaseURL, urlErr)
-		}
-		return &APIEntry{
-			Slug:             slug,
-			Dir:              cacheSubDir,
-			Manifest:         manifest,
-			NormalizedPrefix: prefix,
-		}, ""
-	}
-
-	// Cache miss or checksum mismatch — fetch from remote.
-	manifestURL := baseURL + "/" + entry.MCP.ManifestURL
-	data, err := fetchManifestData(manifestURL)
-	if err != nil {
-		return nil, fmt.Sprintf("skipping %q: fetch failed: %v", slug, err)
-	}
-
-	// Verify checksum if provided.
-	if entry.MCP.ManifestChecksum != "" {
-		if err := VerifyChecksum(data, entry.MCP.ManifestChecksum); err != nil {
-			return nil, fmt.Sprintf("skipping %q: %v", slug, err)
-		}
-	}
-
-	// Parse manifest.
-	manifest, err = parseManifest(data)
-	if err != nil {
-		return nil, fmt.Sprintf("skipping %q: %v", slug, err)
-	}
-
-	// Validate the manifest's base URL against SSRF protections.
-	if err := ValidateBaseURL(manifest.BaseURL); err != nil {
-		return nil, fmt.Sprintf("skipping %q: unsafe base URL %q: %v", slug, manifest.BaseURL, err)
-	}
-
-	// Write to cache via temp-then-rename.
-	if err := writeCache(cacheSubDir, cachePath, data); err != nil {
-		// Cache write failure is a warning, not a blocker.
-		return &APIEntry{
-			Slug:             slug,
-			Dir:              cacheSubDir,
-			Manifest:         manifest,
-			NormalizedPrefix: prefix,
-		}, fmt.Sprintf("warning: %q: cache write failed: %v", slug, err)
-	}
-
-	return &APIEntry{
-		Slug:             slug,
-		Dir:              cacheSubDir,
-		Manifest:         manifest,
-		NormalizedPrefix: prefix,
-	}, ""
-}
-
-// tryCache attempts to read and verify a cached manifest.
-// Returns nil, nil if cache doesn't exist.
-// Returns nil, error if cache exists but checksum mismatches or is unreadable.
-func tryCache(cachePath, expectedChecksum string) (*ToolsManifest, error) {
-	data, err := os.ReadFile(cachePath)
-	if err != nil {
-		if os.IsNotExist(err) {
-			return nil, nil
-		}
-		return nil, fmt.Errorf("reading cache: %w", err)
-	}
-
-	// Verify checksum if expected is provided.
-	if expectedChecksum != "" {
-		if err := VerifyChecksum(data, expectedChecksum); err != nil {
-			return nil, fmt.Errorf("cache checksum mismatch: %w", err)
-		}
-	}
-
-	manifest, err := parseManifest(data)
-	if err != nil {
-		return nil, fmt.Errorf("parsing cached manifest: %w", err)
-	}
-
-	return manifest, nil
-}
-
-// fetchManifestData downloads manifest data from a URL.
-// When GITHUB_TOKEN is set, an Authorization header is attached so private-repo
-// reads via raw.githubusercontent.com succeed.
-func fetchManifestData(manifestURL string) ([]byte, error) {
-	req, err := http.NewRequest(http.MethodGet, manifestURL, nil)
-	if err != nil {
-		return nil, fmt.Errorf("building request: %w", err)
-	}
-	if token := os.Getenv("GITHUB_TOKEN"); token != "" {
-		req.Header.Set("Authorization", "token "+token)
-	}
-
-	resp, err := http.DefaultClient.Do(req)
-	if err != nil {
-		return nil, fmt.Errorf("HTTP request: %w", err)
-	}
-	defer func() { _ = resp.Body.Close() }()
-
-	if resp.StatusCode != http.StatusOK {
-		return nil, fmt.Errorf("HTTP %d", resp.StatusCode)
-	}
-
-	data, err := io.ReadAll(io.LimitReader(resp.Body, 10*1024*1024)) // 10MB limit
-	if err != nil {
-		return nil, fmt.Errorf("reading response: %w", err)
-	}
-
-	return data, nil
-}
-
-// parseManifest unmarshals JSON data into a ToolsManifest.
-func parseManifest(data []byte) (*ToolsManifest, error) {
-	var manifest ToolsManifest
-	if err := json.Unmarshal(data, &manifest); err != nil {
-		return nil, fmt.Errorf("parsing manifest JSON: %w", err)
-	}
-	return &manifest, nil
-}
-
-// writeCache writes manifest data to disk via temp-then-rename for atomicity.
-// Creates the directory structure with 0700 permissions and writes files with 0600.
-func writeCache(dir, cachePath string, data []byte) error {
-	if err := os.MkdirAll(dir, 0o700); err != nil {
-		return fmt.Errorf("creating cache directory: %w", err)
-	}
-
-	tmpPath := cachePath + ".tmp"
-	if err := os.WriteFile(tmpPath, data, 0o600); err != nil {
-		return fmt.Errorf("writing temp file: %w", err)
-	}
-
-	if err := os.Rename(tmpPath, cachePath); err != nil {
-		_ = os.Remove(tmpPath) // clean up on failure
-		return fmt.Errorf("renaming temp file: %w", err)
-	}
-
-	return nil
-}
-
-// slugToToolPrefix converts an API slug to a tool name prefix.
-// Hyphens become underscores, consecutive underscores are collapsed,
-// and the result is rejected if it still contains "__".
-func slugToToolPrefix(slug string) (string, error) {
-	// Replace hyphens with underscores.
-	result := strings.ReplaceAll(slug, "-", "_")
-
-	// Collapse consecutive underscores.
-	for strings.Contains(result, "__") {
-		result = strings.ReplaceAll(result, "__", "_")
-	}
-
-	// Reject if empty after normalization.
-	if result == "" || result == "_" {
-		return "", fmt.Errorf("slug %q normalizes to empty prefix", slug)
-	}
-
-	return result, nil
-}
diff --git a/internal/megamcp/manifest_test.go b/internal/megamcp/manifest_test.go
deleted file mode 100644
index 10edd344..00000000
--- a/internal/megamcp/manifest_test.go
+++ /dev/null
@@ -1,445 +0,0 @@
-package megamcp
-
-import (
-	"encoding/json"
-	"net/http"
-	"net/http/httptest"
-	"os"
-	"path/filepath"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-)
-
-// makeTestManifest creates a ToolsManifest for testing.
-func makeTestManifest(apiName string) *ToolsManifest {
-	return &ToolsManifest{
-		APIName:     apiName,
-		BaseURL:     "https://api.example.com",
-		Description: apiName + " API",
-		MCPReady:    "full",
-		Auth: ManifestAuth{
-			Type:    "api_key",
-			Header:  "Authorization",
-			Format:  "Bearer {TOKEN}",
-			In:      "header",
-			EnvVars: []string{"TOKEN"},
-		},
-		RequiredHeaders: []ManifestHeader{},
-		Tools: []ManifestTool{
-			{
-				Name:        "items_list",
-				Description: "List items",
-				Method:      "GET",
-				Path:        "/items",
-				Params: []ManifestParam{
-					{Name: "limit", Type: "integer", Location: "query"},
-				},
-			},
-		},
-	}
-}
-
-// marshalManifest marshals a manifest to JSON bytes.
-func marshalManifest(t *testing.T, m *ToolsManifest) []byte {
-	t.Helper()
-	data, err := json.MarshalIndent(m, "", "  ")
-	require.NoError(t, err)
-	return data
-}
-
-// makeManifestServer creates an httptest server that serves manifests by slug.
-func makeManifestServer(t *testing.T, manifests map[string][]byte) *httptest.Server {
-	t.Helper()
-	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		for slug, data := range manifests {
-			if r.URL.Path == "/library/"+slug+"/tools-manifest.json" {
-				w.Header().Set("Content-Type", "application/json")
-				w.Write(data)
-				return
-			}
-		}
-		w.WriteHeader(http.StatusNotFound)
-	}))
-}
-
-func TestLoadManifestsHappyPath(t *testing.T) {
-	espnManifest := makeTestManifest("ESPN")
-	dubManifest := makeTestManifest("Dub")
-	espnData := marshalManifest(t, espnManifest)
-	dubData := marshalManifest(t, dubManifest)
-	espnChecksum := ComputeChecksum(espnData)
-	dubChecksum := ComputeChecksum(dubData)
-
-	server := makeManifestServer(t, map[string][]byte{
-		"espn": espnData,
-		"dub":  dubData,
-	})
-	defer server.Close()
-
-	entries := []RegistryEntry{
-		{
-			API: "espn",
-			MCP: RegistryMCP{
-				MCPReady:         "full",
-				ManifestChecksum: espnChecksum,
-				ManifestURL:      "library/espn/tools-manifest.json",
-			},
-		},
-		{
-			API: "dub",
-			MCP: RegistryMCP{
-				MCPReady:         "full",
-				ManifestChecksum: dubChecksum,
-				ManifestURL:      "library/dub/tools-manifest.json",
-			},
-		},
-	}
-
-	cacheDir := t.TempDir()
-	results, warnings := LoadManifests(entries, cacheDir, server.URL)
-
-	assert.Empty(t, warnings)
-	assert.Len(t, results, 2)
-
-	// Both manifests should be cached.
-	for _, entry := range results {
-		cachePath := filepath.Join(cacheDir, "manifests", entry.Slug, "tools-manifest.json")
-		assert.FileExists(t, cachePath)
-	}
-}
-
-func TestLoadManifestsCachedWithMatchingChecksum(t *testing.T) {
-	manifest := makeTestManifest("ESPN")
-	data := marshalManifest(t, manifest)
-	checksum := ComputeChecksum(data)
-
-	// Pre-populate cache.
-	cacheDir := t.TempDir()
-	cacheSubDir := filepath.Join(cacheDir, "manifests", "espn")
-	require.NoError(t, os.MkdirAll(cacheSubDir, 0o700))
-	require.NoError(t, os.WriteFile(filepath.Join(cacheSubDir, "tools-manifest.json"), data, 0o600))
-
-	// Server that should NOT be called.
-	fetchCalled := false
-	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
-		fetchCalled = true
-		w.WriteHeader(http.StatusInternalServerError)
-	}))
-	defer server.Close()
-
-	entries := []RegistryEntry{
-		{
-			API: "espn",
-			MCP: RegistryMCP{
-				MCPReady:         "full",
-				ManifestChecksum: checksum,
-				ManifestURL:      "library/espn/tools-manifest.json",
-			},
-		},
-	}
-
-	results, warnings := LoadManifests(entries, cacheDir, server.URL)
-
-	assert.Empty(t, warnings)
-	assert.Len(t, results, 1)
-	assert.Equal(t, "espn", results[0].Slug)
-	assert.Equal(t, "ESPN", results[0].Manifest.APIName)
-	assert.False(t, fetchCalled, "server should not have been called when cache is valid")
-}
-
-func TestLoadManifestsCachedWithMismatchedChecksum(t *testing.T) {
-	oldManifest := makeTestManifest("ESPN Old")
-	oldData := marshalManifest(t, oldManifest)
-
-	newManifest := makeTestManifest("ESPN New")
-	newData := marshalManifest(t, newManifest)
-	newChecksum := ComputeChecksum(newData)
-
-	// Pre-populate cache with old data.
-	cacheDir := t.TempDir()
-	cacheSubDir := filepath.Join(cacheDir, "manifests", "espn")
-	require.NoError(t, os.MkdirAll(cacheSubDir, 0o700))
-	require.NoError(t, os.WriteFile(filepath.Join(cacheSubDir, "tools-manifest.json"), oldData, 0o600))
-
-	// Server returns new manifest.
-	server := makeManifestServer(t, map[string][]byte{
-		"espn": newData,
-	})
-	defer server.Close()
-
-	entries := []RegistryEntry{
-		{
-			API: "espn",
-			MCP: RegistryMCP{
-				MCPReady:         "full",
-				ManifestChecksum: newChecksum,
-				ManifestURL:      "library/espn/tools-manifest.json",
-			},
-		},
-	}
-
-	results, warnings := LoadManifests(entries, cacheDir, server.URL)
-
-	assert.Empty(t, warnings)
-	require.Len(t, results, 1)
-	assert.Equal(t, "ESPN New", results[0].Manifest.APIName, "should have fetched new manifest")
-
-	// Verify cache was updated.
-	cachedData, err := os.ReadFile(filepath.Join(cacheSubDir, "tools-manifest.json"))
-	require.NoError(t, err)
-	assert.NoError(t, VerifyChecksum(cachedData, newChecksum), "cache should contain new data")
-}
-
-func TestLoadManifestsPrintingPressAPIsFilter(t *testing.T) {
-	espnData := marshalManifest(t, makeTestManifest("ESPN"))
-	dubData := marshalManifest(t, makeTestManifest("Dub"))
-	espnChecksum := ComputeChecksum(espnData)
-	dubChecksum := ComputeChecksum(dubData)
-
-	server := makeManifestServer(t, map[string][]byte{
-		"espn": espnData,
-		"dub":  dubData,
-	})
-	defer server.Close()
-
-	entries := []RegistryEntry{
-		{
-			API: "espn",
-			MCP: RegistryMCP{
-				MCPReady:         "full",
-				ManifestChecksum: espnChecksum,
-				ManifestURL:      "library/espn/tools-manifest.json",
-			},
-		},
-		{
-			API: "dub",
-			MCP: RegistryMCP{
-				MCPReady:         "full",
-				ManifestChecksum: dubChecksum,
-				ManifestURL:      "library/dub/tools-manifest.json",
-			},
-		},
-	}
-
-	t.Setenv("PRINTING_PRESS_APIS", "espn")
-	cacheDir := t.TempDir()
-	results, warnings := LoadManifests(entries, cacheDir, server.URL)
-
-	assert.Empty(t, warnings)
-	require.Len(t, results, 1)
-	assert.Equal(t, "espn", results[0].Slug)
-}
-
-func TestLoadManifestsOneFetchFailsOthersSucceed(t *testing.T) {
-	dubData := marshalManifest(t, makeTestManifest("Dub"))
-	dubChecksum := ComputeChecksum(dubData)
-
-	// Server returns 404 for espn, 200 for dub.
-	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		if r.URL.Path == "/library/dub/tools-manifest.json" {
-			w.Header().Set("Content-Type", "application/json")
-			w.Write(dubData)
-			return
-		}
-		w.WriteHeader(http.StatusNotFound)
-	}))
-	defer server.Close()
-
-	entries := []RegistryEntry{
-		{
-			API: "espn",
-			MCP: RegistryMCP{
-				MCPReady:         "full",
-				ManifestChecksum: "sha256:abc",
-				ManifestURL:      "library/espn/tools-manifest.json",
-			},
-		},
-		{
-			API: "dub",
-			MCP: RegistryMCP{
-				MCPReady:         "full",
-				ManifestChecksum: dubChecksum,
-				ManifestURL:      "library/dub/tools-manifest.json",
-			},
-		},
-	}
-
-	cacheDir := t.TempDir()
-	results, warnings := LoadManifests(entries, cacheDir, server.URL)
-
-	require.Len(t, results, 1)
-	assert.Equal(t, "dub", results[0].Slug)
-	require.Len(t, warnings, 1)
-	assert.Contains(t, warnings[0], "espn")
-}
-
-func TestLoadManifestsCLIOnlyFiltered(t *testing.T) {
-	entries := []RegistryEntry{
-		{
-			API: "pagliacci",
-			MCP: RegistryMCP{
-				MCPReady:    "cli-only",
-				ManifestURL: "library/pagliacci/tools-manifest.json",
-			},
-		},
-	}
-
-	cacheDir := t.TempDir()
-	// No server needed since cli-only entries are filtered out before fetch.
-	results, warnings := LoadManifests(entries, cacheDir, "http://unused")
-
-	assert.Empty(t, results)
-	assert.Empty(t, warnings)
-}
-
-func TestLoadManifestsSlugTraversalRejected(t *testing.T) {
-	entries := []RegistryEntry{
-		{
-			API: "../etc",
-			MCP: RegistryMCP{
-				MCPReady:    "full",
-				ManifestURL: "library/etc/tools-manifest.json",
-			},
-		},
-	}
-
-	cacheDir := t.TempDir()
-	results, warnings := LoadManifests(entries, cacheDir, "http://unused")
-
-	assert.Empty(t, results)
-	require.Len(t, warnings, 1)
-	assert.Contains(t, warnings[0], "path traversal")
-}
-
-func TestLoadManifestsCacheDirCreated(t *testing.T) {
-	manifest := makeTestManifest("ESPN")
-	data := marshalManifest(t, manifest)
-	checksum := ComputeChecksum(data)
-
-	server := makeManifestServer(t, map[string][]byte{
-		"espn": data,
-	})
-	defer server.Close()
-
-	entries := []RegistryEntry{
-		{
-			API: "espn",
-			MCP: RegistryMCP{
-				MCPReady:         "full",
-				ManifestChecksum: checksum,
-				ManifestURL:      "library/espn/tools-manifest.json",
-			},
-		},
-	}
-
-	// Use a nested cache dir that doesn't exist yet.
-	cacheDir := filepath.Join(t.TempDir(), "nested", "cache")
-	results, warnings := LoadManifests(entries, cacheDir, server.URL)
-
-	assert.Empty(t, warnings)
-	require.Len(t, results, 1)
-
-	// Verify the cache directory was created.
-	cachePath := filepath.Join(cacheDir, "manifests", "espn", "tools-manifest.json")
-	assert.FileExists(t, cachePath)
-}
-
-func TestLoadManifestsEmptyManifestURLFiltered(t *testing.T) {
-	entries := []RegistryEntry{
-		{
-			API: "nourl",
-			MCP: RegistryMCP{
-				MCPReady:    "full",
-				ManifestURL: "", // empty
-			},
-		},
-	}
-
-	cacheDir := t.TempDir()
-	results, warnings := LoadManifests(entries, cacheDir, "http://unused")
-
-	assert.Empty(t, results)
-	assert.Empty(t, warnings)
-}
-
-func TestSlugToToolPrefix(t *testing.T) {
-	tests := []struct {
-		name    string
-		slug    string
-		want    string
-		wantErr bool
-	}{
-		{
-			name: "simple slug",
-			slug: "dub",
-			want: "dub",
-		},
-		{
-			name: "hyphenated slug",
-			slug: "steam-web",
-			want: "steam_web",
-		},
-		{
-			name: "consecutive hyphens collapsed",
-			slug: "steam--web",
-			want: "steam_web",
-		},
-		{
-			name: "multiple hyphens",
-			slug: "my-cool-api",
-			want: "my_cool_api",
-		},
-		{
-			name:    "empty slug errors",
-			slug:    "",
-			wantErr: true,
-		},
-		{
-			name:    "single hyphen errors",
-			slug:    "-",
-			wantErr: true,
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got, err := slugToToolPrefix(tt.slug)
-			if tt.wantErr {
-				assert.Error(t, err)
-			} else {
-				require.NoError(t, err)
-				assert.Equal(t, tt.want, got)
-			}
-		})
-	}
-}
-
-func TestLoadManifestsNormalizedPrefix(t *testing.T) {
-	data := marshalManifest(t, makeTestManifest("Steam Web"))
-	checksum := ComputeChecksum(data)
-
-	server := makeManifestServer(t, map[string][]byte{
-		"steam-web": data,
-	})
-	defer server.Close()
-
-	entries := []RegistryEntry{
-		{
-			API: "steam-web",
-			MCP: RegistryMCP{
-				MCPReady:         "full",
-				ManifestChecksum: checksum,
-				ManifestURL:      "library/steam-web/tools-manifest.json",
-			},
-		},
-	}
-
-	cacheDir := t.TempDir()
-	results, warnings := LoadManifests(entries, cacheDir, server.URL)
-
-	assert.Empty(t, warnings)
-	require.Len(t, results, 1)
-	assert.Equal(t, "steam_web", results[0].NormalizedPrefix)
-}
diff --git a/internal/megamcp/metatools.go b/internal/megamcp/metatools.go
deleted file mode 100644
index 5038b182..00000000
--- a/internal/megamcp/metatools.go
+++ /dev/null
@@ -1,463 +0,0 @@
-package megamcp
-
-import (
-	"context"
-	"encoding/json"
-	"fmt"
-	"net/http"
-	"os"
-	"path/filepath"
-	"strings"
-	"time"
-
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
-	"github.com/mvanhorn/cli-printing-press/v2/internal/naming"
-	"github.com/mvanhorn/cli-printing-press/v2/internal/pipeline"
-	"github.com/mvanhorn/cli-printing-press/v2/internal/version"
-)
-
-// RegisterMetaTools registers the 6 agent-facing discovery and activation tools.
-func RegisterMetaTools(s *server.MCPServer, am *ActivationManager) {
-	s.AddTool(
-		mcp.NewTool("library_info",
-			mcp.WithDescription("List all available APIs in the printing press library with metadata, auth status, and tool counts"),
-		),
-		makeLibraryInfoHandler(am),
-	)
-
-	s.AddTool(
-		mcp.NewTool("setup_guide",
-			mcp.WithDescription("Get auth setup instructions for a specific API: env var names, key URL, and example claude mcp add command"),
-			mcp.WithString("api_slug", mcp.Required(), mcp.Description("API slug (e.g., 'dub', 'espn')")),
-		),
-		makeSetupGuideHandler(am),
-	)
-
-	s.AddTool(
-		mcp.NewTool("activate_api",
-			mcp.WithDescription("Activate an API to register its tools for use. Call library_info first to see available APIs."),
-			mcp.WithString("api_slug", mcp.Required(), mcp.Description("API slug to activate (e.g., 'dub', 'espn')")),
-		),
-		makeActivateAPIHandler(am),
-	)
-
-	s.AddTool(
-		mcp.NewTool("deactivate_api",
-			mcp.WithDescription("Deactivate an API and remove its tools"),
-			mcp.WithString("api_slug", mcp.Required(), mcp.Description("API slug to deactivate")),
-		),
-		makeDeactivateAPIHandler(am),
-	)
-
-	s.AddTool(
-		mcp.NewTool("search_tools",
-			mcp.WithDescription("Search for tools across all APIs by keyword. Works on unactivated APIs too."),
-			mcp.WithString("query", mcp.Required(), mcp.Description("Search query to match against tool names and descriptions")),
-		),
-		makeSearchToolsHandler(am),
-	)
-
-	s.AddTool(
-		mcp.NewTool("about",
-			mcp.WithDescription("Describe this mega MCP server's version, API count, and total tool count"),
-		),
-		makeAboutHandler(am),
-	)
-
-	s.AddTool(
-		mcp.NewTool("debug_api",
-			mcp.WithDescription("Health check an API: verifies base URL, auth configuration, and connectivity. Use when API calls are failing."),
-			mcp.WithString("api_slug", mcp.Required(), mcp.Description("API slug to debug (e.g., 'dub', 'espn')")),
-		),
-		makeDebugAPIHandler(am),
-	)
-}
-
-// --- library_info ---
-
-// libraryInfoResponse is the JSON structure returned by library_info.
-type libraryInfoResponse struct {
-	APIs       []libraryInfoAPI `json:"apis"`
-	TotalAPIs  int              `json:"total_apis"`
-	TotalTools int              `json:"total_tools"`
-	Version    string           `json:"version"`
-}
-
-type libraryInfoAPI struct {
-	Slug             string   `json:"slug"`
-	Name             string   `json:"name"`
-	Description      string   `json:"description"`
-	ToolCount        int      `json:"tool_count"`
-	PublicToolCount  int      `json:"public_tool_count"`
-	AuthType         string   `json:"auth_type"`
-	MCPReady         string   `json:"mcp_ready"`
-	AuthConfigured   bool     `json:"auth_configured"`
-	Activated        bool     `json:"activated"`
-	UpgradeAvailable bool     `json:"upgrade_available"`
-	NovelFeatures    []string `json:"novel_features,omitempty"`
-}
-
-func makeLibraryInfoHandler(am *ActivationManager) server.ToolHandlerFunc {
-	return func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-		entries := am.AllManifests()
-
-		resp := libraryInfoResponse{
-			APIs:    make([]libraryInfoAPI, 0, len(entries)),
-			Version: version.Get(),
-		}
-
-		totalTools := 0
-		for _, entry := range entries {
-			m := entry.Manifest
-			toolCount := len(m.Tools)
-			publicToolCount := countPublicTools(m)
-			totalTools += toolCount
-
-			api := libraryInfoAPI{
-				Slug:             entry.Slug,
-				Name:             m.APIName,
-				Description:      SanitizeText(m.Description, 200),
-				ToolCount:        toolCount,
-				PublicToolCount:  publicToolCount,
-				AuthType:         m.Auth.Type,
-				MCPReady:         m.MCPReady,
-				AuthConfigured:   hasAuthConfigured(m),
-				Activated:        am.IsActivated(entry.Slug),
-				UpgradeAvailable: checkUpgradeAvailable(entry.Slug),
-			}
-
-			// Include novel features if present (from manifest description, not a separate field).
-			// The ToolsManifest doesn't carry novel features directly, but we can note
-			// their existence through the registry. For now, this field is omitted.
-
-			resp.APIs = append(resp.APIs, api)
-		}
-
-		resp.TotalAPIs = len(entries)
-		resp.TotalTools = totalTools
-
-		data, err := json.MarshalIndent(resp, "", "  ")
-		if err != nil {
-			return mcp.NewToolResultError(fmt.Sprintf("Error serializing library info: %v", err)), nil
-		}
-
-		return mcp.NewToolResultText(string(data)), nil
-	}
-}
-
-// countPublicTools counts tools that have NoAuth=true.
-func countPublicTools(m *ToolsManifest) int {
-	count := 0
-	for _, t := range m.Tools {
-		if t.NoAuth {
-			count++
-		}
-	}
-	// If auth type is "none", all tools are public.
-	if m.Auth.Type == "" || m.Auth.Type == "none" {
-		return len(m.Tools)
-	}
-	return count
-}
-
-// checkUpgradeAvailable checks if a local per-API MCP binary exists,
-// indicating the user could use a dedicated MCP server with full features.
-func checkUpgradeAvailable(slug string) bool {
-	libraryRoot := pipeline.PublishedLibraryRoot()
-	mcpBinary := naming.MCP(slug)
-
-	p := filepath.Join(libraryRoot, slug, "cmd", mcpBinary)
-	if info, err := os.Stat(p); err == nil && info.IsDir() {
-		return true
-	}
-	return false
-}
-
-// --- setup_guide ---
-
-func makeSetupGuideHandler(am *ActivationManager) server.ToolHandlerFunc {
-	return func(_ context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-		slug, err := getStringArg(req, "api_slug")
-		if err != nil {
-			return mcp.NewToolResultError(err.Error()), nil
-		}
-
-		entry := am.GetManifest(slug)
-		if entry == nil {
-			return mcp.NewToolResultError(fmt.Sprintf("API not found: %q", slug)), nil
-		}
-
-		m := entry.Manifest
-
-		// No auth API.
-		if m.Auth.Type == "" || m.Auth.Type == "none" {
-			return mcp.NewToolResultText(fmt.Sprintf(
-				"No authentication required for %s — all endpoints are publicly accessible.\n\n"+
-					"Activate with: activate_api(%q)", m.APIName, slug)), nil
-		}
-
-		var sb strings.Builder
-		fmt.Fprintf(&sb, "# Setup Guide for %s\n\n", m.APIName)
-		fmt.Fprintf(&sb, "**Auth type:** %s\n\n", m.Auth.Type)
-
-		// Env vars.
-		if len(m.Auth.EnvVars) > 0 {
-			sb.WriteString("**Required environment variables:**\n")
-			for _, envVar := range m.Auth.EnvVars {
-				fmt.Fprintf(&sb, "- `%s`\n", envVar)
-			}
-			sb.WriteString("\n")
-		}
-
-		// Key URL.
-		if m.Auth.KeyURL != "" {
-			fmt.Fprintf(&sb, "**Get your API key:** %s\n\n", m.Auth.KeyURL)
-		}
-
-		// Example claude mcp add command.
-		if len(m.Auth.EnvVars) > 0 {
-			sb.WriteString("**Example setup command:**\n```\nclaude mcp add printing-press")
-			for _, envVar := range m.Auth.EnvVars {
-				fmt.Fprintf(&sb, " --env %s=<your-key>", envVar)
-			}
-			sb.WriteString(" -- printing-press-mcp\n```\n\n")
-		}
-
-		fmt.Fprintf(&sb, "Once configured, activate with: activate_api(%q)", slug)
-
-		return mcp.NewToolResultText(sb.String()), nil
-	}
-}
-
-// --- activate_api ---
-
-func makeActivateAPIHandler(am *ActivationManager) server.ToolHandlerFunc {
-	return func(_ context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-		slug, err := getStringArg(req, "api_slug")
-		if err != nil {
-			return mcp.NewToolResultError(err.Error()), nil
-		}
-
-		count, err := am.Activate(slug)
-		if err != nil {
-			return mcp.NewToolResultError(fmt.Sprintf("Failed to activate API: %v", err)), nil
-		}
-
-		// Get example tool names for confirmation message.
-		toolNames := am.toolNamesForSlug(slug)
-		examples := ""
-		if len(toolNames) > 0 {
-			limit := min(len(toolNames), 3)
-			examples = "\n\nExample tools:\n"
-			for _, name := range toolNames[:limit] {
-				examples += fmt.Sprintf("- %s\n", name)
-			}
-			if len(toolNames) > 3 {
-				examples += fmt.Sprintf("- ... and %d more\n", len(toolNames)-3)
-			}
-		}
-
-		return mcp.NewToolResultText(fmt.Sprintf(
-			"Activated %q — %d tools registered.%s", slug, count, examples)), nil
-	}
-}
-
-// --- deactivate_api ---
-
-func makeDeactivateAPIHandler(am *ActivationManager) server.ToolHandlerFunc {
-	return func(_ context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-		slug, err := getStringArg(req, "api_slug")
-		if err != nil {
-			return mcp.NewToolResultError(err.Error()), nil
-		}
-
-		if err := am.Deactivate(slug); err != nil {
-			return mcp.NewToolResultError(fmt.Sprintf("Failed to deactivate API: %v", err)), nil
-		}
-
-		return mcp.NewToolResultText(fmt.Sprintf("Deactivated %q — tools removed.", slug)), nil
-	}
-}
-
-// --- search_tools ---
-
-type searchToolsResponse struct {
-	Results []SearchResult `json:"results"`
-	Count   int            `json:"count"`
-}
-
-func makeSearchToolsHandler(am *ActivationManager) server.ToolHandlerFunc {
-	return func(_ context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-		query, err := getStringArg(req, "query")
-		if err != nil {
-			return mcp.NewToolResultError(err.Error()), nil
-		}
-
-		results := am.SearchTools(query)
-
-		resp := searchToolsResponse{
-			Results: results,
-			Count:   len(results),
-		}
-
-		if resp.Results == nil {
-			resp.Results = make([]SearchResult, 0)
-		}
-
-		data, err := json.MarshalIndent(resp, "", "  ")
-		if err != nil {
-			return mcp.NewToolResultError(fmt.Sprintf("Error serializing search results: %v", err)), nil
-		}
-
-		return mcp.NewToolResultText(string(data)), nil
-	}
-}
-
-// --- about ---
-
-type aboutResponse struct {
-	Name       string `json:"name"`
-	Version    string `json:"version"`
-	TotalAPIs  int    `json:"total_apis"`
-	TotalTools int    `json:"total_tools"`
-}
-
-func makeAboutHandler(am *ActivationManager) server.ToolHandlerFunc {
-	return func(_ context.Context, _ mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-		entries := am.AllManifests()
-
-		totalTools := 0
-		for _, entry := range entries {
-			totalTools += len(entry.Manifest.Tools)
-		}
-
-		resp := aboutResponse{
-			Name:       "printing-press-mcp",
-			Version:    version.Get(),
-			TotalAPIs:  len(entries),
-			TotalTools: totalTools,
-		}
-
-		data, err := json.MarshalIndent(resp, "", "  ")
-		if err != nil {
-			return mcp.NewToolResultError(fmt.Sprintf("Error serializing about info: %v", err)), nil
-		}
-
-		return mcp.NewToolResultText(string(data)), nil
-	}
-}
-
-// --- debug_api ---
-
-type debugAPIResponse struct {
-	Slug           string             `json:"slug"`
-	APIName        string             `json:"api_name"`
-	BaseURL        string             `json:"base_url"`
-	AuthType       string             `json:"auth_type"`
-	AuthConfigured bool               `json:"auth_configured"`
-	Activated      bool               `json:"activated"`
-	ToolCount      int                `json:"tool_count"`
-	HealthCheck    *healthCheckResult `json:"health_check,omitempty"`
-}
-
-type healthCheckResult struct {
-	StatusCode int               `json:"status_code"`
-	Status     string            `json:"status"`
-	Headers    map[string]string `json:"headers,omitempty"`
-	Error      string            `json:"error,omitempty"`
-}
-
-func makeDebugAPIHandler(am *ActivationManager) server.ToolHandlerFunc {
-	return func(ctx context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) {
-		slug, err := getStringArg(req, "api_slug")
-		if err != nil {
-			return mcp.NewToolResultError(err.Error()), nil
-		}
-
-		entry := am.GetManifest(slug)
-		if entry == nil {
-			return mcp.NewToolResultError(fmt.Sprintf("API not found: %q. Call library_info to see available APIs.", slug)), nil
-		}
-
-		m := entry.Manifest
-
-		// Check auth status.
-		authConfigured := false
-		if m.Auth.Type == "" || m.Auth.Type == "none" {
-			authConfigured = true
-		} else {
-			for _, envVar := range m.Auth.EnvVars {
-				if os.Getenv(envVar) != "" {
-					authConfigured = true
-					break
-				}
-			}
-		}
-
-		resp := debugAPIResponse{
-			Slug:           slug,
-			APIName:        m.APIName,
-			BaseURL:        m.BaseURL,
-			AuthType:       m.Auth.Type,
-			AuthConfigured: authConfigured,
-			Activated:      am.IsActivated(slug),
-			ToolCount:      len(m.Tools),
-		}
-
-		// Perform a lightweight health check (GET to base URL).
-		client := SafeHTTPClient(10 * time.Second)
-		httpReq, reqErr := http.NewRequestWithContext(ctx, "GET", m.BaseURL, nil)
-		if reqErr != nil {
-			resp.HealthCheck = &healthCheckResult{
-				Error: fmt.Sprintf("Could not create request: %v", reqErr),
-			}
-		} else {
-			httpReq.Header.Set("User-Agent", "printing-press-mcp/debug")
-			httpResp, doErr := client.Do(httpReq)
-			if doErr != nil {
-				resp.HealthCheck = &healthCheckResult{
-					Error: fmt.Sprintf("Connection failed: %v", doErr),
-				}
-			} else {
-				_ = httpResp.Body.Close()
-				headers := make(map[string]string)
-				for _, key := range []string{"Content-Type", "Server", "X-RateLimit-Limit", "X-RateLimit-Remaining"} {
-					if v := httpResp.Header.Get(key); v != "" {
-						headers[key] = v
-					}
-				}
-				resp.HealthCheck = &healthCheckResult{
-					StatusCode: httpResp.StatusCode,
-					Status:     httpResp.Status,
-					Headers:    headers,
-				}
-			}
-		}
-
-		data, jsonErr := json.MarshalIndent(resp, "", "  ")
-		if jsonErr != nil {
-			return mcp.NewToolResultError(fmt.Sprintf("Error serializing debug info: %v", jsonErr)), nil
-		}
-		return mcp.NewToolResultText(string(data)), nil
-	}
-}
-
-// --- helpers ---
-
-// getStringArg extracts a required string argument from an MCP request.
-func getStringArg(req mcp.CallToolRequest, name string) (string, error) {
-	args := req.GetArguments()
-	val, ok := args[name]
-	if !ok || val == nil {
-		return "", fmt.Errorf("missing required argument: %q", name)
-	}
-	s, ok := val.(string)
-	if !ok {
-		return "", fmt.Errorf("argument %q must be a string", name)
-	}
-	if s == "" {
-		return "", fmt.Errorf("argument %q must not be empty", name)
-	}
-	return s, nil
-}
diff --git a/internal/megamcp/metatools_test.go b/internal/megamcp/metatools_test.go
deleted file mode 100644
index 76209dbf..00000000
--- a/internal/megamcp/metatools_test.go
+++ /dev/null
@@ -1,524 +0,0 @@
-package megamcp
-
-import (
-	"encoding/json"
-	"testing"
-
-	"github.com/mark3labs/mcp-go/mcp"
-	"github.com/mark3labs/mcp-go/server"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-)
-
-// newMetaToolsServer creates a server with meta-tools registered for testing.
-func newMetaToolsServer() (*server.MCPServer, *ActivationManager) {
-	s := newTestServer()
-	entries := newTestAPIEntries()
-	am := NewActivationManager(s, entries)
-	RegisterMetaTools(s, am)
-	return s, am
-}
-
-// callMetaTool invokes a registered meta-tool by name with given arguments.
-func callMetaTool(t *testing.T, s *server.MCPServer, toolName string, args map[string]any) *mcp.CallToolResult {
-	t.Helper()
-	tool := s.GetTool(toolName)
-	require.NotNil(t, tool, "tool %q should be registered", toolName)
-	result, err := tool.Handler(t.Context(), makeToolRequest(args))
-	require.NoError(t, err)
-	return result
-}
-
-func TestRegisterMetaTools_AllRegistered(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	tools := s.ListTools()
-	expected := []string{"library_info", "setup_guide", "activate_api", "deactivate_api", "search_tools", "about", "debug_api"}
-	for _, name := range expected {
-		assert.NotNil(t, tools[name], "meta-tool %q should be registered", name)
-	}
-}
-
-// --- library_info ---
-
-func TestLibraryInfo_ReturnsAllAPIs(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	result := callMetaTool(t, s, "library_info", nil)
-	assert.False(t, result.IsError)
-
-	var resp libraryInfoResponse
-	text := extractResultText(result)
-	require.NoError(t, json.Unmarshal([]byte(text), &resp))
-
-	assert.Equal(t, 3, resp.TotalAPIs)
-	assert.Equal(t, 6, resp.TotalTools) // 3 ESPN + 2 Dub + 1 Public
-	assert.NotEmpty(t, resp.Version)
-	assert.Len(t, resp.APIs, 3)
-}
-
-func TestLibraryInfo_AuthConfiguredReflectsEnvVars(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	// Without env var set.
-	result := callMetaTool(t, s, "library_info", nil)
-	var resp libraryInfoResponse
-	require.NoError(t, json.Unmarshal([]byte(extractResultText(result)), &resp))
-
-	// Find ESPN entry — auth should not be configured.
-	for _, api := range resp.APIs {
-		if api.Slug == "espn" {
-			assert.False(t, api.AuthConfigured, "ESPN auth should not be configured without env var")
-		}
-		if api.Slug == "public-api" {
-			assert.True(t, api.AuthConfigured, "no-auth API should report auth as configured")
-		}
-	}
-
-	// Now set the env var.
-	t.Setenv("ESPN_KEY", "test-key")
-	result2 := callMetaTool(t, s, "library_info", nil)
-	var resp2 libraryInfoResponse
-	require.NoError(t, json.Unmarshal([]byte(extractResultText(result2)), &resp2))
-
-	for _, api := range resp2.APIs {
-		if api.Slug == "espn" {
-			assert.True(t, api.AuthConfigured, "ESPN auth should be configured with env var set")
-		}
-	}
-}
-
-func TestLibraryInfo_ActivatedStatus(t *testing.T) {
-	s, am := newMetaToolsServer()
-
-	// Before activation.
-	result := callMetaTool(t, s, "library_info", nil)
-	var resp libraryInfoResponse
-	require.NoError(t, json.Unmarshal([]byte(extractResultText(result)), &resp))
-
-	for _, api := range resp.APIs {
-		assert.False(t, api.Activated, "no API should be activated initially")
-	}
-
-	// Activate ESPN.
-	_, err := am.Activate("espn")
-	require.NoError(t, err)
-
-	result2 := callMetaTool(t, s, "library_info", nil)
-	var resp2 libraryInfoResponse
-	require.NoError(t, json.Unmarshal([]byte(extractResultText(result2)), &resp2))
-
-	for _, api := range resp2.APIs {
-		if api.Slug == "espn" {
-			assert.True(t, api.Activated, "ESPN should be activated")
-		} else {
-			assert.False(t, api.Activated)
-		}
-	}
-}
-
-func TestLibraryInfo_NoAPIsLoaded(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, nil)
-	RegisterMetaTools(s, am)
-
-	result := callMetaTool(t, s, "library_info", nil)
-	assert.False(t, result.IsError)
-
-	var resp libraryInfoResponse
-	require.NoError(t, json.Unmarshal([]byte(extractResultText(result)), &resp))
-
-	assert.Equal(t, 0, resp.TotalAPIs)
-	assert.Equal(t, 0, resp.TotalTools)
-	assert.Empty(t, resp.APIs)
-}
-
-func TestLibraryInfo_PublicToolCount(t *testing.T) {
-	s := newTestServer()
-	entries := []*APIEntry{
-		{
-			Slug:             "mixed",
-			NormalizedPrefix: "mixed",
-			Manifest: &ToolsManifest{
-				APIName: "Mixed",
-				Auth:    ManifestAuth{Type: "api_key", EnvVars: []string{"MIX_KEY"}},
-				Tools: []ManifestTool{
-					{Name: "public_get", NoAuth: true},
-					{Name: "private_get", NoAuth: false},
-					{Name: "also_private", NoAuth: false},
-				},
-			},
-		},
-	}
-	am := NewActivationManager(s, entries)
-	RegisterMetaTools(s, am)
-
-	result := callMetaTool(t, s, "library_info", nil)
-	var resp libraryInfoResponse
-	require.NoError(t, json.Unmarshal([]byte(extractResultText(result)), &resp))
-
-	require.Len(t, resp.APIs, 1)
-	assert.Equal(t, 3, resp.APIs[0].ToolCount)
-	assert.Equal(t, 1, resp.APIs[0].PublicToolCount)
-}
-
-// --- setup_guide ---
-
-func TestSetupGuide_APIKeyAuth(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	result := callMetaTool(t, s, "setup_guide", map[string]any{"api_slug": "espn"})
-	assert.False(t, result.IsError)
-
-	text := extractResultText(result)
-	assert.Contains(t, text, "ESPN")
-	assert.Contains(t, text, "ESPN_KEY")
-	assert.Contains(t, text, "api_key")
-	assert.Contains(t, text, "claude mcp add")
-}
-
-func TestSetupGuide_BearerTokenAuth(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	result := callMetaTool(t, s, "setup_guide", map[string]any{"api_slug": "dub"})
-	assert.False(t, result.IsError)
-
-	text := extractResultText(result)
-	assert.Contains(t, text, "DUB_TOKEN")
-	assert.Contains(t, text, "bearer_token")
-}
-
-func TestSetupGuide_NoAuthAPI(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	result := callMetaTool(t, s, "setup_guide", map[string]any{"api_slug": "public-api"})
-	assert.False(t, result.IsError)
-
-	text := extractResultText(result)
-	assert.Contains(t, text, "No authentication required")
-}
-
-func TestSetupGuide_UnknownSlug(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	result := callMetaTool(t, s, "setup_guide", map[string]any{"api_slug": "nonexistent"})
-	assert.True(t, result.IsError)
-	assertResultContains(t, result, "API not found")
-}
-
-func TestSetupGuide_MissingArg(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	result := callMetaTool(t, s, "setup_guide", nil)
-	assert.True(t, result.IsError)
-	assertResultContains(t, result, "missing required argument")
-}
-
-func TestSetupGuide_WithKeyURL(t *testing.T) {
-	s := newTestServer()
-	entries := []*APIEntry{
-		{
-			Slug:             "withurl",
-			NormalizedPrefix: "withurl",
-			Manifest: &ToolsManifest{
-				APIName: "WithURL",
-				Auth: ManifestAuth{
-					Type:    "api_key",
-					EnvVars: []string{"MY_KEY"},
-					KeyURL:  "https://example.com/settings/keys",
-				},
-				Tools: []ManifestTool{
-					{Name: "data_get", Method: "GET", Path: "/data"},
-				},
-			},
-		},
-	}
-	am := NewActivationManager(s, entries)
-	RegisterMetaTools(s, am)
-
-	result := callMetaTool(t, s, "setup_guide", map[string]any{"api_slug": "withurl"})
-	assert.False(t, result.IsError)
-
-	text := extractResultText(result)
-	assert.Contains(t, text, "https://example.com/settings/keys")
-}
-
-// --- activate_api ---
-
-func TestActivateAPI_RegistersTools(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	result := callMetaTool(t, s, "activate_api", map[string]any{"api_slug": "espn"})
-	assert.False(t, result.IsError)
-
-	text := extractResultText(result)
-	assert.Contains(t, text, "Activated")
-	assert.Contains(t, text, "3 tools registered")
-
-	// Verify tools registered on server.
-	tools := s.ListTools()
-	assert.NotNil(t, tools["espn__scores_get"])
-}
-
-func TestActivateAPI_Idempotent(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	result1 := callMetaTool(t, s, "activate_api", map[string]any{"api_slug": "espn"})
-	assert.False(t, result1.IsError)
-
-	result2 := callMetaTool(t, s, "activate_api", map[string]any{"api_slug": "espn"})
-	assert.False(t, result2.IsError)
-
-	// Should still have the same tool count.
-	assertResultContains(t, result2, "3 tools registered")
-}
-
-func TestActivateAPI_UnknownSlug(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	result := callMetaTool(t, s, "activate_api", map[string]any{"api_slug": "nonexistent"})
-	assert.True(t, result.IsError)
-	assertResultContains(t, result, "API not found")
-}
-
-func TestActivateAPI_MissingArg(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	result := callMetaTool(t, s, "activate_api", nil)
-	assert.True(t, result.IsError)
-	assertResultContains(t, result, "missing required argument")
-}
-
-func TestActivateAPI_ShowsExampleTools(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	result := callMetaTool(t, s, "activate_api", map[string]any{"api_slug": "espn"})
-	assert.False(t, result.IsError)
-
-	text := extractResultText(result)
-	assert.Contains(t, text, "Example tools")
-	assert.Contains(t, text, "espn__")
-}
-
-// --- deactivate_api ---
-
-func TestDeactivateAPI_ReplacesWithStubs(t *testing.T) {
-	s, am := newMetaToolsServer()
-
-	_, err := am.Activate("espn")
-	require.NoError(t, err)
-
-	result := callMetaTool(t, s, "deactivate_api", map[string]any{"api_slug": "espn"})
-	assert.False(t, result.IsError)
-	assertResultContains(t, result, "Deactivated")
-
-	// After deactivation, tools should still be registered (as stubs).
-	tools := s.ListTools()
-	assert.NotNil(t, tools["espn__scores_get"], "tool should exist as a stub after deactivation")
-	assert.False(t, am.IsActivated("espn"))
-}
-
-func TestDeactivateAPI_NotActivated(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	result := callMetaTool(t, s, "deactivate_api", map[string]any{"api_slug": "espn"})
-	assert.True(t, result.IsError)
-	assertResultContains(t, result, "not currently activated")
-}
-
-func TestDeactivateAPI_UnknownSlug(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	result := callMetaTool(t, s, "deactivate_api", map[string]any{"api_slug": "nonexistent"})
-	assert.True(t, result.IsError)
-	assertResultContains(t, result, "API not found")
-}
-
-// --- search_tools ---
-
-func TestSearchTools_FindsMatches(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	result := callMetaTool(t, s, "search_tools", map[string]any{"query": "scores"})
-	assert.False(t, result.IsError)
-
-	var resp searchToolsResponse
-	require.NoError(t, json.Unmarshal([]byte(extractResultText(result)), &resp))
-
-	assert.Equal(t, 1, resp.Count)
-	assert.Equal(t, "espn", resp.Results[0].APISlug)
-	assert.Equal(t, "espn__scores_get", resp.Results[0].ToolName)
-}
-
-func TestSearchTools_EmptyResults(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	result := callMetaTool(t, s, "search_tools", map[string]any{"query": "pizza"})
-	assert.False(t, result.IsError)
-
-	var resp searchToolsResponse
-	require.NoError(t, json.Unmarshal([]byte(extractResultText(result)), &resp))
-
-	assert.Equal(t, 0, resp.Count)
-	assert.Empty(t, resp.Results)
-}
-
-func TestSearchTools_SearchesAcrossUnactivatedAPIs(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	// Neither ESPN nor Dub are activated, but search should still find tools.
-	result := callMetaTool(t, s, "search_tools", map[string]any{"query": "list"})
-	assert.False(t, result.IsError)
-
-	var resp searchToolsResponse
-	require.NoError(t, json.Unmarshal([]byte(extractResultText(result)), &resp))
-
-	// Should match: espn__teams_list, dub__links_list, public_api__entries_list
-	assert.GreaterOrEqual(t, resp.Count, 3)
-}
-
-func TestSearchTools_MissingArg(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	result := callMetaTool(t, s, "search_tools", nil)
-	assert.True(t, result.IsError)
-	assertResultContains(t, result, "missing required argument")
-}
-
-// --- about ---
-
-func TestAbout_ReturnsVersionAndCounts(t *testing.T) {
-	s, _ := newMetaToolsServer()
-
-	result := callMetaTool(t, s, "about", nil)
-	assert.False(t, result.IsError)
-
-	var resp aboutResponse
-	require.NoError(t, json.Unmarshal([]byte(extractResultText(result)), &resp))
-
-	assert.Equal(t, "printing-press-mcp", resp.Name)
-	assert.NotEmpty(t, resp.Version)
-	assert.Equal(t, 3, resp.TotalAPIs)
-	assert.Equal(t, 6, resp.TotalTools) // 3 + 2 + 1
-}
-
-func TestAbout_NoAPIs(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, nil)
-	RegisterMetaTools(s, am)
-
-	result := callMetaTool(t, s, "about", nil)
-	assert.False(t, result.IsError)
-
-	var resp aboutResponse
-	require.NoError(t, json.Unmarshal([]byte(extractResultText(result)), &resp))
-
-	assert.Equal(t, 0, resp.TotalAPIs)
-	assert.Equal(t, 0, resp.TotalTools)
-}
-
-// --- getStringArg ---
-
-func TestGetStringArg_Valid(t *testing.T) {
-	req := makeToolRequest(map[string]any{"api_slug": "espn"})
-	val, err := getStringArg(req, "api_slug")
-	require.NoError(t, err)
-	assert.Equal(t, "espn", val)
-}
-
-func TestGetStringArg_Missing(t *testing.T) {
-	req := makeToolRequest(nil)
-	_, err := getStringArg(req, "api_slug")
-	require.Error(t, err)
-	assert.Contains(t, err.Error(), "missing required argument")
-}
-
-func TestGetStringArg_Empty(t *testing.T) {
-	req := makeToolRequest(map[string]any{"api_slug": ""})
-	_, err := getStringArg(req, "api_slug")
-	require.Error(t, err)
-	assert.Contains(t, err.Error(), "must not be empty")
-}
-
-func TestGetStringArg_WrongType(t *testing.T) {
-	req := makeToolRequest(map[string]any{"api_slug": 42})
-	_, err := getStringArg(req, "api_slug")
-	require.Error(t, err)
-	assert.Contains(t, err.Error(), "must be a string")
-}
-
-// --- countPublicTools ---
-
-func TestCountPublicTools_MixedAuth(t *testing.T) {
-	m := &ToolsManifest{
-		Auth: ManifestAuth{Type: "api_key"},
-		Tools: []ManifestTool{
-			{Name: "public", NoAuth: true},
-			{Name: "private1", NoAuth: false},
-			{Name: "private2", NoAuth: false},
-		},
-	}
-	assert.Equal(t, 1, countPublicTools(m))
-}
-
-func TestCountPublicTools_NoAuth(t *testing.T) {
-	m := &ToolsManifest{
-		Auth: ManifestAuth{Type: "none"},
-		Tools: []ManifestTool{
-			{Name: "a"},
-			{Name: "b"},
-		},
-	}
-	// All tools are public when auth type is "none".
-	assert.Equal(t, 2, countPublicTools(m))
-}
-
-func TestCountPublicTools_EmptyAuthType(t *testing.T) {
-	m := &ToolsManifest{
-		Auth: ManifestAuth{Type: ""},
-		Tools: []ManifestTool{
-			{Name: "a"},
-		},
-	}
-	assert.Equal(t, 1, countPublicTools(m))
-}
-
-func TestDebugAPI_ValidAPI(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	handler := makeDebugAPIHandler(am)
-	result, err := handler(t.Context(), makeToolRequest(map[string]any{"api_slug": "espn"}))
-	require.NoError(t, err)
-	assert.False(t, result.IsError)
-
-	text := extractResultText(result)
-	assert.Contains(t, text, "espn")
-	assert.Contains(t, text, "base_url")
-	assert.Contains(t, text, "auth_configured")
-	assert.Contains(t, text, "health_check")
-}
-
-func TestDebugAPI_UnknownSlug(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	handler := makeDebugAPIHandler(am)
-	result, err := handler(t.Context(), makeToolRequest(map[string]any{"api_slug": "nonexistent"}))
-	require.NoError(t, err)
-	assert.True(t, result.IsError)
-	text := extractResultText(result)
-	assert.Contains(t, text, "API not found")
-}
-
-func TestDebugAPI_MissingArg(t *testing.T) {
-	s := newTestServer()
-	am := NewActivationManager(s, newTestAPIEntries())
-
-	handler := makeDebugAPIHandler(am)
-	result, err := handler(t.Context(), makeToolRequest(nil))
-	require.NoError(t, err)
-	assert.True(t, result.IsError)
-	text := extractResultText(result)
-	assert.Contains(t, text, "missing required")
-}
diff --git a/internal/megamcp/registry.go b/internal/megamcp/registry.go
deleted file mode 100644
index 33014675..00000000
--- a/internal/megamcp/registry.go
+++ /dev/null
@@ -1,50 +0,0 @@
-package megamcp
-
-import (
-	"encoding/json"
-	"fmt"
-	"io"
-	"net/http"
-	"os"
-)
-
-// DefaultBaseURL is the default GitHub raw content URL for the library repo.
-const DefaultBaseURL = "https://raw.githubusercontent.com/mvanhorn/printing-press-library/main"
-
-// FetchRegistry fetches registry.json from baseURL and parses it into a Registry.
-// baseURL is injectable for testing (use httptest.NewServer).
-// When GITHUB_TOKEN is set, an Authorization header is attached so private-repo
-// reads via raw.githubusercontent.com succeed.
-func FetchRegistry(baseURL string) (*Registry, error) {
-	registryURL := baseURL + "/registry.json"
-
-	req, err := http.NewRequest(http.MethodGet, registryURL, nil)
-	if err != nil {
-		return nil, fmt.Errorf("building registry request: %w", err)
-	}
-	if token := os.Getenv("GITHUB_TOKEN"); token != "" {
-		req.Header.Set("Authorization", "token "+token)
-	}
-
-	resp, err := http.DefaultClient.Do(req)
-	if err != nil {
-		return nil, fmt.Errorf("fetching registry: %w", err)
-	}
-	defer func() { _ = resp.Body.Close() }()
-
-	if resp.StatusCode != http.StatusOK {
-		return nil, fmt.Errorf("fetching registry: HTTP %d", resp.StatusCode)
-	}
-
-	body, err := io.ReadAll(io.LimitReader(resp.Body, 10*1024*1024)) // 10MB limit
-	if err != nil {
-		return nil, fmt.Errorf("reading registry body: %w", err)
-	}
-
-	var registry Registry
-	if err := json.Unmarshal(body, &registry); err != nil {
-		return nil, fmt.Errorf("parsing registry JSON: %w", err)
-	}
-
-	return &registry, nil
-}
diff --git a/internal/megamcp/registry_test.go b/internal/megamcp/registry_test.go
deleted file mode 100644
index 34f668d3..00000000
--- a/internal/megamcp/registry_test.go
+++ /dev/null
@@ -1,124 +0,0 @@
-package megamcp
-
-import (
-	"encoding/json"
-	"net/http"
-	"net/http/httptest"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-)
-
-func TestFetchRegistryHappyPath(t *testing.T) {
-	registry := Registry{
-		SchemaVersion: 1,
-		Entries: []RegistryEntry{
-			{
-				Name:        "espn-pp-cli",
-				Category:    "media-and-entertainment",
-				API:         "espn",
-				Description: "ESPN sports data",
-				Path:        "library/media-and-entertainment/espn-pp-cli",
-				MCP: RegistryMCP{
-					Binary:           "espn-pp-mcp",
-					Transport:        "stdio",
-					ToolCount:        3,
-					PublicToolCount:  3,
-					AuthType:         "none",
-					MCPReady:         "full",
-					ManifestChecksum: "sha256:abc123",
-					SpecFormat:       "openapi3",
-					ManifestURL:      "library/media-and-entertainment/espn-pp-cli/tools-manifest.json",
-				},
-			},
-			{
-				Name:        "dub-pp-cli",
-				Category:    "developer-tools",
-				API:         "dub",
-				Description: "Dub link management",
-				Path:        "library/developer-tools/dub-pp-cli",
-				MCP: RegistryMCP{
-					Binary:           "dub-pp-mcp",
-					Transport:        "stdio",
-					ToolCount:        10,
-					PublicToolCount:  10,
-					AuthType:         "api_key",
-					EnvVars:          []string{"DUB_TOKEN"},
-					MCPReady:         "full",
-					ManifestChecksum: "sha256:def456",
-					SpecFormat:       "openapi3",
-					ManifestURL:      "library/developer-tools/dub-pp-cli/tools-manifest.json",
-				},
-			},
-		},
-	}
-
-	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		assert.Equal(t, "/registry.json", r.URL.Path)
-		w.Header().Set("Content-Type", "application/json")
-		json.NewEncoder(w).Encode(registry)
-	}))
-	defer server.Close()
-
-	result, err := FetchRegistry(server.URL)
-	require.NoError(t, err)
-	require.NotNil(t, result)
-	assert.Equal(t, 1, result.SchemaVersion)
-	assert.Len(t, result.Entries, 2)
-	assert.Equal(t, "espn", result.Entries[0].API)
-	assert.Equal(t, "dub", result.Entries[1].API)
-	assert.Equal(t, "sha256:abc123", result.Entries[0].MCP.ManifestChecksum)
-	assert.Equal(t, []string{"DUB_TOKEN"}, result.Entries[1].MCP.EnvVars)
-}
-
-func TestFetchRegistry404(t *testing.T) {
-	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
-		w.WriteHeader(http.StatusNotFound)
-	}))
-	defer server.Close()
-
-	result, err := FetchRegistry(server.URL)
-	require.Error(t, err)
-	assert.Nil(t, result)
-	assert.Contains(t, err.Error(), "HTTP 404")
-}
-
-func TestFetchRegistryInvalidJSON(t *testing.T) {
-	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
-		w.Header().Set("Content-Type", "application/json")
-		w.Write([]byte("{invalid json"))
-	}))
-	defer server.Close()
-
-	result, err := FetchRegistry(server.URL)
-	require.Error(t, err)
-	assert.Nil(t, result)
-	assert.Contains(t, err.Error(), "parsing registry JSON")
-}
-
-func TestFetchRegistryAttachesGitHubToken(t *testing.T) {
-	var gotAuth string
-	server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		gotAuth = r.Header.Get("Authorization")
-		w.Header().Set("Content-Type", "application/json")
-		json.NewEncoder(w).Encode(Registry{SchemaVersion: 1})
-	}))
-	defer server.Close()
-
-	t.Run("with token set", func(t *testing.T) {
-		gotAuth = ""
-		t.Setenv("GITHUB_TOKEN", "ghp_testtoken123")
-		_, err := FetchRegistry(server.URL)
-		require.NoError(t, err)
-		assert.Equal(t, "token ghp_testtoken123", gotAuth)
-	})
-
-	t.Run("with token unset", func(t *testing.T) {
-		gotAuth = ""
-		t.Setenv("GITHUB_TOKEN", "")
-		_, err := FetchRegistry(server.URL)
-		require.NoError(t, err)
-		assert.Empty(t, gotAuth)
-	})
-}
diff --git a/internal/megamcp/security.go b/internal/megamcp/security.go
deleted file mode 100644
index c77c147a..00000000
--- a/internal/megamcp/security.go
+++ /dev/null
@@ -1,194 +0,0 @@
-package megamcp
-
-import (
-	"context"
-	"crypto/sha256"
-	"encoding/hex"
-	"fmt"
-	"net"
-	"net/http"
-	"net/url"
-	"path/filepath"
-	"strings"
-	"time"
-	"unicode"
-)
-
-// ValidateBaseURL rejects non-HTTPS URLs and literal private/loopback IPs.
-// Does NOT perform DNS resolution — that check happens at request time via
-// SafeDialer to prevent DNS rebinding attacks. This function validates the
-// URL format and catches obviously unsafe literal IP addresses.
-func ValidateBaseURL(rawURL string) error {
-	parsed, err := url.Parse(rawURL)
-	if err != nil {
-		return fmt.Errorf("invalid URL: %w", err)
-	}
-
-	if parsed.Scheme != "https" {
-		return fmt.Errorf("URL must use HTTPS, got %q", parsed.Scheme)
-	}
-
-	host := parsed.Hostname()
-	if host == "" {
-		return fmt.Errorf("URL has no hostname")
-	}
-
-	// If the host is a literal IP address, check it directly.
-	// Hostname-based URLs are checked at request time via SafeDialer.
-	if ip := net.ParseIP(host); ip != nil {
-		if err := checkIP(ip); err != nil {
-			return fmt.Errorf("IP address %s: %w", host, err)
-		}
-	}
-
-	return nil
-}
-
-// checkIP rejects private, loopback, link-local, and metadata IPs.
-func checkIP(ip net.IP) error {
-	if ip.IsLoopback() {
-		return fmt.Errorf("loopback address rejected")
-	}
-	if ip.IsPrivate() {
-		return fmt.Errorf("private IP address rejected")
-	}
-	if ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
-		return fmt.Errorf("link-local address rejected")
-	}
-	// Cloud metadata endpoint (169.254.169.254).
-	if ip.Equal(net.ParseIP("169.254.169.254")) {
-		return fmt.Errorf("cloud metadata address rejected")
-	}
-	return nil
-}
-
-// SafeHTTPClient returns an http.Client with a custom dialer that rejects
-// connections to private, loopback, link-local, and cloud metadata IPs.
-// This is the runtime SSRF guard that ValidateBaseURL defers DNS checks to.
-func SafeHTTPClient(timeout time.Duration) *http.Client {
-	dialer := &safeDialer{
-		inner: &net.Dialer{Timeout: 10 * time.Second},
-	}
-	transport := &http.Transport{
-		Proxy:       http.ProxyFromEnvironment,
-		DialContext: dialer.DialContext,
-	}
-	return &http.Client{
-		Timeout:   timeout,
-		Transport: transport,
-	}
-}
-
-// safeDialer wraps a net.Dialer and checks resolved IPs before connecting.
-type safeDialer struct {
-	inner *net.Dialer
-}
-
-// DialContext resolves the hostname, validates all IPs, then connects.
-func (d *safeDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
-	host, port, err := net.SplitHostPort(addr)
-	if err != nil {
-		return nil, fmt.Errorf("invalid address %q: %w", addr, err)
-	}
-
-	// Resolve hostname to IPs.
-	ips, err := net.DefaultResolver.LookupHost(ctx, host)
-	if err != nil {
-		return nil, fmt.Errorf("cannot resolve %q: %w", host, err)
-	}
-
-	// Check every resolved IP.
-	for _, ipStr := range ips {
-		ip := net.ParseIP(ipStr)
-		if ip == nil {
-			continue
-		}
-		if err := checkIP(ip); err != nil {
-			return nil, fmt.Errorf("hostname %q resolves to %s: %w", host, ipStr, err)
-		}
-	}
-
-	// All IPs are safe — connect to the first one.
-	safeAddr := net.JoinHostPort(ips[0], port)
-	return d.inner.DialContext(ctx, network, safeAddr)
-}
-
-// SanitizeText strips control characters (except newline and tab),
-// and truncates to maxLen.
-func SanitizeText(s string, maxLen int) string {
-	var b strings.Builder
-	for _, r := range s {
-		if r == '\n' || r == '\t' {
-			b.WriteRune(r)
-			continue
-		}
-		if unicode.IsControl(r) {
-			continue
-		}
-		b.WriteRune(r)
-	}
-	result := b.String()
-	if maxLen > 0 && len(result) > maxLen {
-		result = result[:maxLen]
-	}
-	return result
-}
-
-// VerifyChecksum compares the SHA-256 hash of data against an expected
-// checksum string in the format "sha256:<hex>".
-func VerifyChecksum(data []byte, expected string) error {
-	if expected == "" {
-		return fmt.Errorf("expected checksum is empty")
-	}
-	if !strings.HasPrefix(expected, "sha256:") {
-		return fmt.Errorf("unsupported checksum format: %q (expected sha256:<hex>)", expected)
-	}
-	expectedHex := strings.TrimPrefix(expected, "sha256:")
-	actual := sha256.Sum256(data)
-	actualHex := hex.EncodeToString(actual[:])
-	if actualHex != expectedHex {
-		return fmt.Errorf("checksum mismatch: expected sha256:%s, got sha256:%s", expectedHex, actualHex)
-	}
-	return nil
-}
-
-// ComputeChecksum returns the SHA-256 checksum of data in "sha256:<hex>" format.
-func ComputeChecksum(data []byte) string {
-	h := sha256.Sum256(data)
-	return "sha256:" + hex.EncodeToString(h[:])
-}
-
-// ValidateSlug rejects slugs that contain path traversal characters or are empty.
-func ValidateSlug(slug string) error {
-	if slug == "" {
-		return fmt.Errorf("slug is empty")
-	}
-	if strings.Contains(slug, "..") {
-		return fmt.Errorf("slug contains path traversal: %q", slug)
-	}
-	if strings.Contains(slug, "/") {
-		return fmt.Errorf("slug contains forward slash: %q", slug)
-	}
-	if strings.Contains(slug, "\\") {
-		return fmt.Errorf("slug contains backslash: %q", slug)
-	}
-	return nil
-}
-
-// ValidateCachePath verifies that the resolved path is under cacheRoot.
-// Belt-and-suspenders defense against path traversal.
-func ValidateCachePath(path, cacheRoot string) error {
-	absPath, err := filepath.Abs(path)
-	if err != nil {
-		return fmt.Errorf("cannot resolve path %q: %w", path, err)
-	}
-	absRoot, err := filepath.Abs(cacheRoot)
-	if err != nil {
-		return fmt.Errorf("cannot resolve cache root %q: %w", cacheRoot, err)
-	}
-	// Ensure the path is under the root with a trailing separator.
-	if !strings.HasPrefix(absPath, absRoot+string(filepath.Separator)) && absPath != absRoot {
-		return fmt.Errorf("path %q escapes cache root %q", absPath, absRoot)
-	}
-	return nil
-}
diff --git a/internal/megamcp/security_test.go b/internal/megamcp/security_test.go
deleted file mode 100644
index 83cb3c45..00000000
--- a/internal/megamcp/security_test.go
+++ /dev/null
@@ -1,330 +0,0 @@
-package megamcp
-
-import (
-	"context"
-	"crypto/sha256"
-	"encoding/hex"
-	"net"
-	"path/filepath"
-	"testing"
-	"time"
-
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-)
-
-func TestValidateBaseURL(t *testing.T) {
-	tests := []struct {
-		name    string
-		url     string
-		wantErr bool
-		errMsg  string
-	}{
-		{
-			name:    "HTTPS passes",
-			url:     "https://raw.githubusercontent.com/mvanhorn/printing-press-library/main",
-			wantErr: false,
-		},
-		{
-			name:    "HTTP rejected",
-			url:     "http://example.com",
-			wantErr: true,
-			errMsg:  "must use HTTPS",
-		},
-		{
-			name:    "empty scheme rejected",
-			url:     "://example.com",
-			wantErr: true,
-			errMsg:  "invalid URL",
-		},
-		{
-			name:    "loopback rejected",
-			url:     "https://127.0.0.1/test",
-			wantErr: true,
-			errMsg:  "loopback",
-		},
-		{
-			name:    "localhost hostname passes format check (DNS check at request time)",
-			url:     "https://localhost/test",
-			wantErr: false,
-		},
-		{
-			name:    "private IP 10.x rejected",
-			url:     "https://10.0.0.1/test",
-			wantErr: true,
-			errMsg:  "private",
-		},
-		{
-			name:    "private IP 192.168.x rejected",
-			url:     "https://192.168.1.1/test",
-			wantErr: true,
-			errMsg:  "private",
-		},
-		{
-			name:    "no hostname rejected",
-			url:     "https:///path",
-			wantErr: true,
-			errMsg:  "no hostname",
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			err := ValidateBaseURL(tt.url)
-			if tt.wantErr {
-				require.Error(t, err)
-				if tt.errMsg != "" {
-					assert.Contains(t, err.Error(), tt.errMsg)
-				}
-			} else {
-				assert.NoError(t, err)
-			}
-		})
-	}
-}
-
-func TestSafeHTTPClient(t *testing.T) {
-	// SafeHTTPClient should return a non-nil client with a custom transport.
-	client := SafeHTTPClient(30 * time.Second)
-	require.NotNil(t, client)
-	assert.Equal(t, 30*time.Second, client.Timeout)
-	assert.NotNil(t, client.Transport, "should have custom transport with safe dialer")
-}
-
-func TestSafeDialer_RejectsLoopback(t *testing.T) {
-	d := &safeDialer{inner: &net.Dialer{Timeout: 5 * time.Second}}
-	ctx := context.Background()
-
-	// Connecting to 127.0.0.1 should be rejected.
-	_, err := d.DialContext(ctx, "tcp", "127.0.0.1:443")
-	require.Error(t, err)
-	assert.Contains(t, err.Error(), "loopback")
-}
-
-func TestSanitizeText(t *testing.T) {
-	tests := []struct {
-		name   string
-		input  string
-		maxLen int
-		want   string
-	}{
-		{
-			name:   "normal text preserved",
-			input:  "Hello, world!",
-			maxLen: 100,
-			want:   "Hello, world!",
-		},
-		{
-			name:   "control chars stripped",
-			input:  "Hello\x00World\x01Test\x1F",
-			maxLen: 100,
-			want:   "HelloWorldTest",
-		},
-		{
-			name:   "newline preserved",
-			input:  "line1\nline2",
-			maxLen: 100,
-			want:   "line1\nline2",
-		},
-		{
-			name:   "tab preserved",
-			input:  "col1\tcol2",
-			maxLen: 100,
-			want:   "col1\tcol2",
-		},
-		{
-			name:   "length limited",
-			input:  "abcdefghij",
-			maxLen: 5,
-			want:   "abcde",
-		},
-		{
-			name:   "zero maxLen means no limit",
-			input:  "abcdefghij",
-			maxLen: 0,
-			want:   "abcdefghij",
-		},
-		{
-			name:   "empty string",
-			input:  "",
-			maxLen: 100,
-			want:   "",
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			got := SanitizeText(tt.input, tt.maxLen)
-			assert.Equal(t, tt.want, got)
-		})
-	}
-}
-
-func TestVerifyChecksum(t *testing.T) {
-	data := []byte("hello world")
-	h := sha256.Sum256(data)
-	validChecksum := "sha256:" + hex.EncodeToString(h[:])
-
-	tests := []struct {
-		name     string
-		data     []byte
-		expected string
-		wantErr  bool
-		errMsg   string
-	}{
-		{
-			name:     "matching hash passes",
-			data:     data,
-			expected: validChecksum,
-			wantErr:  false,
-		},
-		{
-			name:     "mismatched hash fails",
-			data:     []byte("different data"),
-			expected: validChecksum,
-			wantErr:  true,
-			errMsg:   "checksum mismatch",
-		},
-		{
-			name:     "empty expected fails",
-			data:     data,
-			expected: "",
-			wantErr:  true,
-			errMsg:   "empty",
-		},
-		{
-			name:     "wrong format fails",
-			data:     data,
-			expected: "md5:abc123",
-			wantErr:  true,
-			errMsg:   "unsupported checksum format",
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			err := VerifyChecksum(tt.data, tt.expected)
-			if tt.wantErr {
-				require.Error(t, err)
-				if tt.errMsg != "" {
-					assert.Contains(t, err.Error(), tt.errMsg)
-				}
-			} else {
-				assert.NoError(t, err)
-			}
-		})
-	}
-}
-
-func TestComputeChecksum(t *testing.T) {
-	data := []byte("hello world")
-	checksum := ComputeChecksum(data)
-	assert.True(t, len(checksum) > 7, "checksum should have sha256: prefix plus hex")
-	assert.NoError(t, VerifyChecksum(data, checksum), "ComputeChecksum output should verify")
-}
-
-func TestValidateSlug(t *testing.T) {
-	tests := []struct {
-		name    string
-		slug    string
-		wantErr bool
-		errMsg  string
-	}{
-		{
-			name:    "valid simple slug",
-			slug:    "espn",
-			wantErr: false,
-		},
-		{
-			name:    "valid hyphenated slug",
-			slug:    "steam-web",
-			wantErr: false,
-		},
-		{
-			name:    "path traversal rejected",
-			slug:    "../etc",
-			wantErr: true,
-			errMsg:  "path traversal",
-		},
-		{
-			name:    "double dot in middle rejected",
-			slug:    "foo..bar",
-			wantErr: true,
-			errMsg:  "path traversal",
-		},
-		{
-			name:    "forward slash rejected",
-			slug:    "foo/bar",
-			wantErr: true,
-			errMsg:  "forward slash",
-		},
-		{
-			name:    "backslash rejected",
-			slug:    "foo\\bar",
-			wantErr: true,
-			errMsg:  "backslash",
-		},
-		{
-			name:    "empty slug rejected",
-			slug:    "",
-			wantErr: true,
-			errMsg:  "empty",
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			err := ValidateSlug(tt.slug)
-			if tt.wantErr {
-				require.Error(t, err)
-				if tt.errMsg != "" {
-					assert.Contains(t, err.Error(), tt.errMsg)
-				}
-			} else {
-				assert.NoError(t, err)
-			}
-		})
-	}
-}
-
-func TestValidateCachePath(t *testing.T) {
-	root := t.TempDir()
-
-	tests := []struct {
-		name    string
-		path    string
-		root    string
-		wantErr bool
-	}{
-		{
-			name:    "path under root passes",
-			path:    filepath.Join(root, "manifests", "espn", "tools-manifest.json"),
-			root:    root,
-			wantErr: false,
-		},
-		{
-			name:    "escape attempt rejected",
-			path:    filepath.Join(root, "..", "outside"),
-			root:    root,
-			wantErr: true,
-		},
-		{
-			name:    "exact root passes",
-			path:    root,
-			root:    root,
-			wantErr: false,
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			err := ValidateCachePath(tt.path, tt.root)
-			if tt.wantErr {
-				require.Error(t, err)
-				assert.Contains(t, err.Error(), "escapes cache root")
-			} else {
-				assert.NoError(t, err)
-			}
-		})
-	}
-}
diff --git a/internal/megamcp/types.go b/internal/megamcp/types.go
deleted file mode 100644
index 2e854b6f..00000000
--- a/internal/megamcp/types.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package megamcp
-
-import (
-	"github.com/mvanhorn/cli-printing-press/v2/internal/pipeline"
-)
-
-// Re-export manifest types from pipeline to avoid duplication.
-// These were defined in internal/pipeline/toolsmanifest.go (Unit 1).
-type ToolsManifest = pipeline.ToolsManifest
-type ManifestTool = pipeline.ManifestTool
-type ManifestParam = pipeline.ManifestParam
-type ManifestAuth = pipeline.ManifestAuth
-type ManifestHeader = pipeline.ManifestHeader
-
-// RegistryEntry matches the schema of a single entry in registry.json
-// from the public library repo.
-type RegistryEntry struct {
-	Name        string      `json:"name"`
-	Category    string      `json:"category"`
-	API         string      `json:"api"`
-	Description string      `json:"description"`
-	Path        string      `json:"path"`
-	MCP         RegistryMCP `json:"mcp"`
-}
-
-// RegistryMCP holds MCP-specific metadata within a registry entry.
-type RegistryMCP struct {
-	Binary           string   `json:"binary"`
-	Transport        string   `json:"transport"`
-	ToolCount        int      `json:"tool_count"`
-	PublicToolCount  int      `json:"public_tool_count"`
-	AuthType         string   `json:"auth_type"`
-	EnvVars          []string `json:"env_vars"`
-	MCPReady         string   `json:"mcp_ready"`
-	ManifestChecksum string   `json:"manifest_checksum"`
-	SpecFormat       string   `json:"spec_format"`
-	ManifestURL      string   `json:"manifest_url"`
-}
-
-// Registry represents the top-level registry.json structure.
-type Registry struct {
-	SchemaVersion int             `json:"schema_version"`
-	Entries       []RegistryEntry `json:"entries"`
-}
-
-// APIEntry holds aggregated runtime state for a loaded API.
-type APIEntry struct {
-	Slug             string
-	Dir              string
-	Manifest         *ToolsManifest
-	NormalizedPrefix string
-}
diff --git a/internal/pipeline/climanifest.go b/internal/pipeline/climanifest.go
index 893cef9f..073ea84a 100644
--- a/internal/pipeline/climanifest.go
+++ b/internal/pipeline/climanifest.go
@@ -164,20 +164,9 @@ func specChecksum(path string) (string, error) {
 // to ship?". The label exists to set user expectations: full = every
 // tool works without per-tool auth setup; partial = some tools work
 // without credentials, others need auth provided through the companion
-// CLI's flow (composed, cookie). cli-only is reserved for the
-// degenerate case of no MCP tools at all and is rarely set in practice.
-//
-// Why no `publicTools > 0` gate for composed/cookie any more: the count
-// relies on spec authors tagging endpoints `no_auth: true`, which most
-// generated specs don't audit carefully. A composed-auth CLI with zero
-// no_auth tags was previously labeled cli-only even when many endpoints
-// (registration, login, public discovery) actually work without auth.
-// Defaulting to "partial" matches the typical reality and avoids
-// suppressing manifest emission downstream.
+// CLI's flow (composed, cookie).
 func computeMCPReady(authType string) string {
 	switch authType {
-	case "none", "api_key", "bearer_token":
-		return "full"
 	case "cookie", "composed":
 		return "partial"
 	default:
diff --git a/internal/pipeline/mcpb_bundle.go b/internal/pipeline/mcpb_bundle.go
index 089b1d2c..975d780d 100644
--- a/internal/pipeline/mcpb_bundle.go
+++ b/internal/pipeline/mcpb_bundle.go
@@ -36,8 +36,8 @@ type BundleParams struct {
 //	bin/<binary>            (the path declared by manifest's server.entry_point)
 //
 // Returns nil and creates no file when manifest.json is missing — the
-// caller's CLI dir is presumably one we don't want to bundle (cli-only
-// readiness, no MCP, etc., the same gates WriteMCPBManifest uses).
+// caller's CLI dir is presumably one we don't want to bundle (no MCP
+// binary, the same gate WriteMCPBManifest uses).
 func BuildMCPBBundle(params BundleParams) error {
 	manifestPath := filepath.Join(params.CLIDir, MCPBManifestFilename)
 	manifestData, err := os.ReadFile(manifestPath)
diff --git a/internal/pipeline/mcpb_manifest.go b/internal/pipeline/mcpb_manifest.go
index a2c9ce79..2a913f85 100644
--- a/internal/pipeline/mcpb_manifest.go
+++ b/internal/pipeline/mcpb_manifest.go
@@ -129,11 +129,9 @@ type MCPBCompat struct {
 // WriteMCPBManifest emits manifest.json for a published CLI directory by
 // reading .printing-press.json. Skips silently only when the CLI dir has
 // no .printing-press.json or no MCP binary — every other CLI ships a
-// manifest, including composed/cookie-auth ones whose MCPReady label
-// says "partial" or "cli-only". The user_config block already conveys
-// auth-required-or-optional (per authRequiresCredential), so withholding
-// the manifest from cli-only readiness CLIs only hurt users who could
-// otherwise install the bundle and use any unauthenticated tools.
+// manifest, including composed/cookie-auth ones with a "partial" MCPReady
+// label. The user_config block conveys auth-required-or-optional via
+// authRequiresCredential, which is enough for the host to prompt or skip.
 //
 // Callers that already have the CLIManifest in memory should use
 // WriteMCPBManifestFromStruct to avoid the re-read.
diff --git a/internal/pipeline/publish.go b/internal/pipeline/publish.go
index 3ffc001d..01cf7a1f 100644
--- a/internal/pipeline/publish.go
+++ b/internal/pipeline/publish.go
@@ -250,8 +250,9 @@ func writeCLIManifestForPublish(state *PipelineState, dir string) error {
 			populateMCPMetadata(&m, parsed)
 		}
 
-		// Generate tools-manifest.json for the mega MCP server.
-		// Non-blocking: log warning on error but don't fail the publish.
+		// Generate tools-manifest.json for diagnostic commands
+		// (auth-doctor, mcp-audit). Non-blocking: log warning on error
+		// but don't fail the publish.
 		if parsed != nil {
 			if tmErr := WriteToolsManifest(dir, parsed); tmErr != nil {
 				fmt.Fprintf(os.Stderr, "warning: could not write tools manifest: %v\n", tmErr)
diff --git a/internal/pipeline/toolsmanifest.go b/internal/pipeline/toolsmanifest.go
index 9b6cb037..767b67cd 100644
--- a/internal/pipeline/toolsmanifest.go
+++ b/internal/pipeline/toolsmanifest.go
@@ -15,13 +15,13 @@ import (
 )
 
 // ToolsManifestFilename is the name of the tools manifest file written to each
-// published CLI directory for use by the mega MCP server.
+// published CLI directory. Consumed by `printing-press auth doctor` and
+// `printing-press mcp-audit` to inspect the published library without parsing
+// the original spec.
 const ToolsManifestFilename = "tools-manifest.json"
 
 // ToolsManifest describes every MCP tool for an API, along with API-level
-// metadata needed by the mega MCP to register and execute tools without
-// runtime spec parsing. These types will move to internal/megamcp/types.go
-// in Unit 3.
+// metadata, in a form the diagnostic commands can read directly.
 type ToolsManifest struct {
 	APIName         string           `json:"api_name"`
 	BaseURL         string           `json:"base_url"`
diff --git a/smithery.yaml b/smithery.yaml
deleted file mode 100644
index 0a4e3c44..00000000
--- a/smithery.yaml
+++ /dev/null
@@ -1,8 +0,0 @@
-name: printing-press-mcp
-description: >
-  Access 270+ API tools across 6 APIs through one MCP server.
-  Sports scores (ESPN), link management (Dub), pizza ordering (Pagliacci),
-  game stats (Steam), and more. Zero config for public APIs — set env vars
-  for authenticated APIs. Discover APIs with library_info, activate on demand.
-startCommand:
-  command: printing-press-mcp

← 95cf6c88 fix(ci): shard test lanes (#362)  ·  back to Cli Printing Press  ·  chore(cli): drop dead manifest_url + manifest_checksum from 5777d267 →