---
phase: 06-agentic-integration-security
plan: 02
type: execute
wave: 1
depends_on: []
files_modified:
  - src/ztlctl/mcp/resources.py
  - src/ztlctl/mcp/generator.py
  - src/ztlctl/mcp/server.py
  - src/ztlctl/actions/_register_core.py
  - src/ztlctl/controllers/discovery.py
  - tests/mcp/test_resources.py
  - tests/mcp/test_generator.py
autonomous: true
requirements:
  - AGNT-03
  - AGNT-04

must_haves:
  truths:
    - "MCP resources expose multi-step orchestration recipes (research-capture, review-triage, knowledge-synthesis) that agents can follow step-by-step"
    - "MCP tool surface supports category-based discovery -- agents can discover tool categories and their active/core status"
    - "Agents can activate/deactivate non-core categories to manage tool surface"
  artifacts:
    - path: "src/ztlctl/mcp/resources.py"
      provides: "Three recipe resource _impl functions + ztlctl://recipes index"
      contains: "recipe_research_capture_impl"
    - path: "src/ztlctl/mcp/generator.py"
      provides: "Category activation state + get_active_categories/activate_category/deactivate_category"
      contains: "_DEFAULT_ACTIVE_CATEGORIES"
    - path: "src/ztlctl/controllers/discovery.py"
      provides: "discover_categories handler"
      contains: "discover_categories"
  key_links:
    - from: "src/ztlctl/mcp/resources.py"
      to: "src/ztlctl/mcp/server.py"
      via: "register_resources() called in create_server()"
      pattern: "register_resources"
    - from: "src/ztlctl/mcp/generator.py"
      to: "src/ztlctl/actions/registry.py"
      via: "get_action_registry().list_actions()"
      pattern: "list_actions"
---

<objective>
Add agent orchestration recipe resources and progressive tool disclosure to the MCP server.

Purpose: Agents need structured, step-by-step workflow guidance (recipes) to orchestrate ztlctl effectively, and a way to understand/manage the growing MCP tool surface by category. Recipes provide machine-readable multi-step workflows. Progressive disclosure lets agents discover and selectively engage with tool categories.

Output: Three recipe MCP resources + recipes index; discover_categories/activate_category/deactivate_category tools; category state management in generator.py.
</objective>

<execution_context>
@/Users/shparki/.claude/get-shit-done/workflows/execute-plan.md
@/Users/shparki/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/06-agentic-integration-security/06-RESEARCH.md

<interfaces>
<!-- Key types and contracts the executor needs -->

From src/ztlctl/mcp/resources.py:
```python
_RESOURCE_CATALOG: tuple[dict[str, str], ...]  # URI + description pairs
def resource_catalog(vault: Any | None = None) -> tuple[dict[str, str], ...]:
def register_resources(server: Any, vault: Any) -> None:
    # Pattern: @server.resource("ztlctl://uri")  # type: ignore[untyped-decorator]
    # Each has a _impl function for testability
```

From src/ztlctl/mcp/generator.py:
```python
_vault_ref: Any = None  # Module-level vault reference pattern
def set_vault(vault: Any) -> None: ...
def generate_tools(server: Any, vault: Any) -> None:
    set_vault(vault)
    registry = get_action_registry()
    for action in registry.list_actions():
        fn = _make_tool_fn(action, vault)
        server.tool()(fn)
    _register_plugin_tools(server, vault)
```

From src/ztlctl/actions/registry.py:
```python
class ActionRegistry:
    def list_actions(self, *, category: str | None = None, ...) -> tuple[ActionDefinition, ...]:
    def get(self, name: str) -> ActionDefinition | None:
def get_action_registry() -> ActionRegistry:
```

From src/ztlctl/actions/definitions.py:
```python
@dataclass(frozen=True)
class ActionDefinition:
    name: str
    description: str
    category: str  # e.g. "creation", "query", "graph", etc.
    params: tuple[ActionParam, ...]
    handler: Callable[..., Any]
    side_effect: Literal["read", "write"]
    # ... more fields
```

From src/ztlctl/mcp/server.py:
```python
def create_server(*, vault_root=None, host="127.0.0.1", port=8000) -> Any:
    # Creates vault, then calls:
    generate_tools(server, vault)
    register_resources(server, vault)
    register_prompts(server, vault)
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Add orchestration recipe resources (AGNT-03)</name>
  <files>src/ztlctl/mcp/resources.py, tests/mcp/test_resources.py</files>
  <read_first>
    - src/ztlctl/mcp/resources.py
    - tests/mcp/test_resources.py
  </read_first>
  <behavior>
    - recipe_research_capture_impl(vault) returns dict with name="research-capture", description str, and steps list with 3 step objects
    - recipe_review_triage_impl(vault) returns dict with name="review-triage" and 4 steps
    - recipe_knowledge_synthesis_impl(vault) returns dict with name="knowledge-synthesis" and 4 steps
    - recipe_index_impl(vault) returns dict with recipes list containing 3 entries (name + uri + description per entry)
    - Each step object has keys: step (int), action (str), params (dict), description (str), conditions (list)
    - All four recipe URIs appear in _RESOURCE_CATALOG
    - register_resources() registers all four recipe resources
  </behavior>
  <action>
**Step 1 — Write tests first (RED):**

In `tests/mcp/test_resources.py`, add:

- `test_recipe_research_capture_impl`: Call `recipe_research_capture_impl(None)` (vault not needed — pure data). Assert result has keys `name`, `description`, `steps`. Assert `result["name"] == "research-capture"`. Assert `len(result["steps"]) == 3`. Assert each step has `step`, `action`, `params`, `description`, `conditions` keys. Assert step 1 action is `"search"`.
- `test_recipe_review_triage_impl`: Similar for review-triage, 4 steps, step 1 action is `"work_queue"`.
- `test_recipe_knowledge_synthesis_impl`: Similar for knowledge-synthesis, 4 steps, step 1 action is `"search"`.
- `test_recipe_index_impl`: Call `recipe_index_impl(None)`. Assert result has `recipes` key with 3 entries. Each entry has `name`, `uri`, `description`.
- `test_recipe_catalog_in_resource_catalog`: Call `resource_catalog()`. Assert `"ztlctl://recipes"`, `"ztlctl://recipes/research-capture"`, `"ztlctl://recipes/review-triage"`, `"ztlctl://recipes/knowledge-synthesis"` all appear in the URI list.
- `test_recipe_resources_registered`: Use the DummyServer fixture pattern from existing tests. Call `register_resources(dummy_server, vault)`. Assert the four recipe URIs were registered.

Run tests — all new tests FAIL.

**Step 2 — Implement recipe _impl functions:**

In `src/ztlctl/mcp/resources.py`, add four functions BEFORE `register_resources()`:

`recipe_research_capture_impl(_vault: Any) -> dict[str, Any]`: Returns:
```python
{
    "name": "research-capture",
    "description": "Capture research findings: search existing content, create synthesis notes, link evidence.",
    "steps": [
        {"step": 1, "action": "search", "params": {"query": "{topic}", "limit": 10}, "description": "Search for existing content on the topic to avoid duplication.", "conditions": []},
        {"step": 2, "action": "create_note", "params": {"title": "{synthesis_title}", "maturity": "seed"}, "description": "Create a seed note to capture the synthesis.", "conditions": ["skip if step 1 returns a note with identical title"]},
        {"step": 3, "action": "reweave", "params": {"content_id": "{step_2.content_id}"}, "description": "Connect the new note to related content found in step 1.", "conditions": []},
    ],
}
```

`recipe_review_triage_impl(_vault: Any) -> dict[str, Any]`: Returns review-triage recipe with 4 steps: work_queue -> get_document -> update -> archive.

`recipe_knowledge_synthesis_impl(_vault: Any) -> dict[str, Any]`: Returns knowledge-synthesis recipe with 4 steps: search -> gaps -> draft_from_topic -> reweave.

`recipe_index_impl(_vault: Any) -> dict[str, Any]`: Returns:
```python
{
    "recipes": [
        {"name": "research-capture", "uri": "ztlctl://recipes/research-capture", "description": "..."},
        {"name": "review-triage", "uri": "ztlctl://recipes/review-triage", "description": "..."},
        {"name": "knowledge-synthesis", "uri": "ztlctl://recipes/knowledge-synthesis", "description": "..."},
    ]
}
```

**Step 3 — Add recipe URIs to _RESOURCE_CATALOG:**

Add 4 entries to `_RESOURCE_CATALOG`:
```python
{"uri": "ztlctl://recipes", "description": "Index of available agent orchestration recipes."},
{"uri": "ztlctl://recipes/research-capture", "description": "Research-capture workflow: search, create notes, link evidence."},
{"uri": "ztlctl://recipes/review-triage", "description": "Review-triage workflow: work queue, inspect, update, archive stale."},
{"uri": "ztlctl://recipes/knowledge-synthesis", "description": "Knowledge-synthesis workflow: search, find gaps, draft, reweave."},
```

**Step 4 — Register recipe resources in register_resources():**

Add 4 resource registrations following the existing pattern:
```python
@server.resource("ztlctl://recipes")  # type: ignore[untyped-decorator]
def recipes_index_resource() -> str:
    import json
    return json.dumps(recipe_index_impl(vault), indent=2)

@server.resource("ztlctl://recipes/research-capture")  # type: ignore[untyped-decorator]
def recipe_research_capture_resource() -> str:
    import json
    return json.dumps(recipe_research_capture_impl(vault), indent=2)
# ... same for review-triage, knowledge-synthesis
```

**Step 5 — Run tests (GREEN):**
`uv run pytest tests/mcp/test_resources.py -x`
  </action>
  <verify>
    <automated>uv run pytest tests/mcp/test_resources.py -x -q</automated>
  </verify>
  <acceptance_criteria>
    - grep -q "recipe_research_capture_impl" src/ztlctl/mcp/resources.py
    - grep -q "recipe_review_triage_impl" src/ztlctl/mcp/resources.py
    - grep -q "recipe_knowledge_synthesis_impl" src/ztlctl/mcp/resources.py
    - grep -q "recipe_index_impl" src/ztlctl/mcp/resources.py
    - grep -q "ztlctl://recipes/research-capture" src/ztlctl/mcp/resources.py
    - grep -q "test_recipe_research_capture_impl" tests/mcp/test_resources.py
    - grep -q "test_recipe_catalog_in_resource_catalog" tests/mcp/test_resources.py
    - uv run pytest tests/mcp/test_resources.py -x -q passes
  </acceptance_criteria>
  <done>Three recipe _impl functions return structured JSON step lists; recipe index lists all three; all four URIs in resource catalog; resources registered on server; all tests pass.</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Add progressive tool disclosure -- category discovery and activation (AGNT-04)</name>
  <files>src/ztlctl/mcp/generator.py, src/ztlctl/controllers/discovery.py, src/ztlctl/actions/_register_core.py, tests/mcp/test_generator.py</files>
  <read_first>
    - src/ztlctl/mcp/generator.py
    - src/ztlctl/controllers/discovery.py
    - src/ztlctl/actions/_register_core.py
    - tests/mcp/test_generator.py
  </read_first>
  <behavior>
    - _DEFAULT_ACTIVE_CATEGORIES is a frozenset containing: creation, mutation, query, graph, lifecycle, session
    - get_active_categories() returns a copy of the current active set
    - activate_category("export") adds "export" to active set, returns True
    - activate_category("nonexistent_category") returns False (category not known)
    - deactivate_category("export") removes export from active set if present, returns True
    - deactivate_category("creation") returns False (cannot deactivate core/default category)
    - reset_active_categories() resets to defaults
    - discover_categories handler returns ServiceResult with categories list, each with category name, active bool, core bool, and tools list
    - activate_category handler returns ServiceResult with category name and tool names
    - deactivate_category handler returns ServiceResult with category name
    - All three new ActionDefinitions registered in "discovery" category
  </behavior>
  <action>
**Step 1 — Write tests first (RED):**

In `tests/mcp/test_generator.py`, add:

- `test_default_active_categories`: Import `_DEFAULT_ACTIVE_CATEGORIES` from generator. Assert it contains `"creation"`, `"mutation"`, `"query"`, `"graph"`, `"lifecycle"`, `"session"`. Assert it is a frozenset.
- `test_get_active_categories_returns_copy`: Call `get_active_categories()` twice, assert they are equal but not the same object (copy, not reference).
- `test_activate_category`: Call `reset_active_categories()`. Call `activate_category("export")`. Assert `"export" in get_active_categories()`. Assert return value is True.
- `test_activate_nonexistent_category`: Assert `activate_category("nonexistent_xyz")` returns False.
- `test_deactivate_category`: Call `reset_active_categories()`. Call `activate_category("export")`. Assert `deactivate_category("export")` returns True. Assert `"export" not in get_active_categories()`.
- `test_deactivate_default_category_blocked`: Assert `deactivate_category("creation")` returns False. Assert `"creation" in get_active_categories()`.
- `test_reset_active_categories`: Call `activate_category("export")`. Call `reset_active_categories()`. Assert `get_active_categories() == set(_DEFAULT_ACTIVE_CATEGORIES)`.

Run tests — all FAIL.

**Step 2 — Implement category state in generator.py:**

In `src/ztlctl/mcp/generator.py`, add after the `_vault_ref` section:

```python
# ---------------------------------------------------------------------------
# Progressive tool disclosure — category activation state (AGNT-04)
# ---------------------------------------------------------------------------

#: Default categories active at session start (core CRUD operations).
_DEFAULT_ACTIVE_CATEGORIES: frozenset[str] = frozenset(
    {"creation", "mutation", "query", "graph", "lifecycle", "session"}
)

#: Session-scoped active category set (one server process = one MCP session).
_active_categories: set[str] = set(_DEFAULT_ACTIVE_CATEGORIES)


def get_active_categories() -> set[str]:
    """Return a copy of the currently active categories."""
    return set(_active_categories)


def activate_category(category: str) -> bool:
    """Add category to active set. Returns True if category exists and was added."""
    all_cats = _get_all_categories()
    if category not in all_cats:
        return False
    _active_categories.add(category)
    return True


def deactivate_category(category: str) -> bool:
    """Remove category from active set. Returns False if it is a default category."""
    if category in _DEFAULT_ACTIVE_CATEGORIES:
        return False
    if category not in _active_categories:
        return False
    _active_categories.discard(category)
    return True


def reset_active_categories() -> None:
    """Reset active categories to defaults."""
    _active_categories.clear()
    _active_categories.update(_DEFAULT_ACTIVE_CATEGORIES)


def _get_all_categories() -> set[str]:
    """Return all known category strings from the ActionRegistry."""
    registry = get_action_registry()
    return {a.category for a in registry.list_actions()}
```

**Step 3 — Add discover_categories, activate_category, deactivate_category handlers:**

In `src/ztlctl/controllers/discovery.py`, read the existing file first. Add three new handler methods or standalone functions following the established controller pattern. These handlers should:

`_discover_categories_handler(vault, **kwargs)`:
- Import `get_action_registry` and category functions from generator
- Group actions by category
- For each category, include: name, active status, core status, tool names list, tool count
- Return `ServiceResult(ok=True, op="discover_categories", data={"categories": [...]})`

`_activate_category_handler(vault, *, category, **kwargs)`:
- Call `activate_category(category)` from generator
- If False, return error ServiceResult with code "VALIDATION_FAILED"
- If True, return ServiceResult with data containing category name and list of tool names in that category

`_deactivate_category_handler(vault, *, category, **kwargs)`:
- Call `deactivate_category(category)` from generator
- If False, return error ServiceResult (either default category or not active)
- If True, return ServiceResult with data confirming deactivation

**Step 4 — Register ActionDefinitions:**

In `src/ztlctl/actions/_register_core.py`, add three new ActionDefinitions in the "discovery" category:

```python
ActionDefinition(
    name="discover_categories",
    description="List all tool categories with their active/core status and tool names.",
    category="discovery",
    params=(),
    handler=lambda vault, **kw: _discover_categories_handler(vault, **kw),
    side_effect="read",
    mcp_when_to_use="Use to understand the available tool surface and which categories are active.",
    mcp_avoid_when="Unnecessary if you already know which tools you need.",
),
ActionDefinition(
    name="activate_category",
    description="Activate a tool category to include its tools in the active surface.",
    category="discovery",
    params=(ActionParam(name="category", type=str, required=True, description="Category name to activate"),),
    handler=lambda vault, **kw: _activate_category_handler(vault, **kw),
    side_effect="write",
    mcp_when_to_use="Use when you need tools from an inactive category (e.g., export, workflow, admin).",
    mcp_avoid_when="Core categories are already active by default.",
),
ActionDefinition(
    name="deactivate_category",
    description="Deactivate a non-core tool category to reduce the active tool surface.",
    category="discovery",
    params=(ActionParam(name="category", type=str, required=True, description="Category name to deactivate"),),
    handler=lambda vault, **kw: _deactivate_category_handler(vault, **kw),
    side_effect="write",
    mcp_when_to_use="Use to reduce tool noise after finishing work in a specific category.",
    mcp_avoid_when="Cannot deactivate core categories (creation, mutation, query, graph, lifecycle, session).",
),
```

The handlers should use lazy local imports following the established controller pattern:
```python
def _discover_categories_handler(vault: Any, **kwargs: Any) -> Any:
    from ztlctl.controllers.discovery import DiscoveryController
    # ... or implement inline following the lambda pattern
```

Alternatively, if DiscoveryController doesn't exist or the pattern doesn't fit, use the lambda-to-module-function pattern like other discovery actions (discover_tools, describe_tool). Read `_register_core.py` to see the existing pattern and follow it exactly.

**Step 5 — Run tests (GREEN):**
`uv run pytest tests/mcp/test_generator.py -x`

**Step 6 — Full validation:**
`uv run ruff check . && uv run ruff format --check . && uv run mypy src/ && uv run pytest`
  </action>
  <verify>
    <automated>uv run pytest tests/mcp/test_generator.py tests/mcp/test_resources.py -x -q</automated>
  </verify>
  <acceptance_criteria>
    - grep -q "_DEFAULT_ACTIVE_CATEGORIES" src/ztlctl/mcp/generator.py
    - grep -q "def get_active_categories" src/ztlctl/mcp/generator.py
    - grep -q "def activate_category" src/ztlctl/mcp/generator.py
    - grep -q "def deactivate_category" src/ztlctl/mcp/generator.py
    - grep -q "def reset_active_categories" src/ztlctl/mcp/generator.py
    - grep -q "discover_categories" src/ztlctl/actions/_register_core.py
    - grep -q "activate_category" src/ztlctl/actions/_register_core.py
    - grep -q "deactivate_category" src/ztlctl/actions/_register_core.py
    - grep -q "test_default_active_categories" tests/mcp/test_generator.py
    - grep -q "test_activate_category" tests/mcp/test_generator.py
    - uv run pytest tests/mcp/test_generator.py -x -q passes
  </acceptance_criteria>
  <done>Category activation state (default set, activate, deactivate, reset) in generator.py; three new ActionDefinitions registered; discover_categories returns all categories with status and tool lists; activate/deactivate manage the active set; all tests pass.</done>
</task>

</tasks>

<verification>
- `uv run pytest tests/mcp/test_resources.py tests/mcp/test_generator.py -x` passes
- `uv run ruff check . && uv run mypy src/` passes
- `uv run pytest` full suite passes (no regressions)
- Recipe resources accessible at ztlctl://recipes/* URIs
- discover_categories returns all categories with active/core annotations
</verification>

<success_criteria>
1. Three recipe MCP resources return structured JSON step lists with correct action names and parameters
2. Recipe index resource lists all three recipes
3. discover_categories tool returns all categories with active/core/tools metadata
4. activate_category adds non-default categories to active set; deactivate_category removes them
5. Default categories cannot be deactivated
6. All tools remain registered on the server (progressive disclosure is informational, not registration-gating)
7. All tests pass
</success_criteria>

<output>
After completion, create `.planning/phases/06-agentic-integration-security/06-02-SUMMARY.md`
</output>
