---
phase: 14-preview-bridge-infrastructure
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - crates/mcp-preview/src/proxy.rs
  - crates/mcp-preview/src/server.rs
  - crates/mcp-preview/src/handlers/api.rs
  - crates/mcp-preview/src/handlers/mod.rs
autonomous: true
requirements:
  - PREV-02
  - PREV-03
  - PREV-04

must_haves:
  truths:
    - "MCP session is initialized exactly once on first proxy call and reused for all subsequent calls"
    - "Proxy can list resources from MCP server via resources/list JSON-RPC"
    - "Proxy can read resource content from MCP server via resources/read JSON-RPC"
    - "API endpoints /api/resources and /api/resources/read return resource data as JSON"
    - "API endpoint /api/reconnect resets session and re-initializes"
    - "notifications/initialized is sent after successful session initialization"
  artifacts:
    - path: "crates/mcp-preview/src/proxy.rs"
      provides: "McpProxy with session persistence, list_resources(), read_resource(), reset_session()"
      contains: "RwLock<Option<SessionInfo>>"
    - path: "crates/mcp-preview/src/handlers/api.rs"
      provides: "list_resources, read_resource, reconnect handlers"
      contains: "list_resources"
    - path: "crates/mcp-preview/src/server.rs"
      provides: "New routes for /api/resources, /api/resources/read, /api/reconnect"
      contains: "/api/resources"
  key_links:
    - from: "crates/mcp-preview/src/handlers/api.rs"
      to: "crates/mcp-preview/src/proxy.rs"
      via: "state.proxy.list_resources() and state.proxy.read_resource()"
      pattern: "proxy\\.(list_resources|read_resource|reset_session)"
    - from: "crates/mcp-preview/src/server.rs"
      to: "crates/mcp-preview/src/handlers/api.rs"
      via: "route registration for resource endpoints"
      pattern: "/api/resources"
---

<objective>
Add session-persistent MCP proxy with resource listing and reading capabilities, plus new API routes for the preview server backend.

Purpose: The preview server currently re-initializes the MCP session on every request and has no resource proxy methods. This plan adds session-once initialization (PREV-03), resources/list and resources/read proxy methods (PREV-04), and the API routes the frontend will call to load widgets and reconnect.

Output: Working backend that initializes MCP session once, proxies resource list/read calls, and exposes /api/resources, /api/resources/read, and /api/reconnect endpoints.
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/14-preview-bridge-infrastructure/14-CONTEXT.md
@.planning/phases/14-preview-bridge-infrastructure/14-RESEARCH.md
@crates/mcp-preview/src/proxy.rs
@crates/mcp-preview/src/server.rs
@crates/mcp-preview/src/handlers/api.rs
@crates/mcp-preview/src/handlers/mod.rs
@crates/mcp-preview/src/lib.rs
@crates/mcp-preview/Cargo.toml
</context>

<tasks>

<task type="auto">
  <name>Task 1: Refactor McpProxy for session persistence and add resource methods</name>
  <files>crates/mcp-preview/src/proxy.rs</files>
  <action>
Refactor McpProxy to use session-once initialization and add resource proxy methods. Specific changes:

1. **Replace per-request initialization with session persistence:**
   - Add a `SessionInfo` struct with `session_id: Option<String>` and `server_info: serde_json::Value` fields.
   - Replace the existing session state (currently `initialize()` is called in `list_tools()` every time) with `session: tokio::sync::RwLock<Option<SessionInfo>>`.
   - Add `ensure_initialized(&self) -> Result<()>` method using double-checked locking pattern: read lock first (fast path if already initialized), then write lock with re-check (slow path for first call). See 14-RESEARCH.md Pattern 1 for the exact pattern.
   - In `ensure_initialized()`, after receiving the `initialize` response, immediately send a `notifications/initialized` notification (fire-and-forget). See Pitfall 2 in research.
   - Capture `Mcp-Session-Id` response header from the initialize response if present. Store in `SessionInfo.session_id`. Forward this header in all subsequent `send_request` calls.

2. **Add `send_notification(&self, method: &str) -> Result<()>` method:**
   - Sends a JSON-RPC notification (no `id` field) to the MCP server. Fire-and-forget (ignore response).

3. **Add `reset_session(&self)` method:**
   - Acquires write lock on `self.session` and sets it to `None`. Next call to `ensure_initialized()` will re-initialize.

4. **Add `list_resources(&self) -> Result<Vec<ResourceInfo>>` method:**
   - Calls `ensure_initialized()` first.
   - Sends `resources/list` JSON-RPC request with no params.
   - Parses the `resources` array from the response into `Vec<ResourceInfo>`.
   - Add a `ResourceInfo` struct with fields: `uri: String`, `name: String`, `description: Option<String>`, `mime_type: Option<String>`. Derive `Serialize, Deserialize, Clone`. Use `#[serde(rename_all = "camelCase")]` and `#[serde(default)]` on optional fields.

5. **Add `read_resource(&self, uri: &str) -> Result<ResourceReadResult>` method:**
   - Calls `ensure_initialized()` first.
   - Sends `resources/read` JSON-RPC request with `{ "uri": uri }` params.
   - Parses the `contents` array from the response. Add `ContentItem` struct with `uri: Option<String>`, `text: Option<String>`, `mime_type: Option<String>` (with `#[serde(rename_all = "camelCase")]`).
   - Returns `ResourceReadResult { contents: Vec<ContentItem> }`.

6. **Update existing `list_tools()` and `call_tool()` methods:**
   - Replace the inline `initialize()` call at the start of `list_tools()` with `self.ensure_initialized().await?`.
   - Ensure `call_tool()` also calls `ensure_initialized()` first (check if it already does; if not, add it).

7. **Add `is_connected(&self) -> bool` method:**
   - Returns `true` if session is initialized (read lock, check `is_some()`).

Keep all existing public API signatures stable (no breaking changes to `McpProxy::new()`, `list_tools()`, `call_tool()`). The `session` field replaces any existing session tracking.
  </action>
  <verify>
Run `cargo check -p mcp-preview` -- must compile with zero errors. Run `cargo clippy -p mcp-preview -- -D warnings` -- must pass with zero warnings.
  </verify>
  <done>
McpProxy initializes session exactly once via RwLock-based double-checked locking. Has list_resources(), read_resource(), reset_session(), and is_connected() methods. All existing methods (list_tools, call_tool) call ensure_initialized() instead of per-request initialize(). notifications/initialized sent after init. Session ID header captured and forwarded.
  </done>
</task>

<task type="auto">
  <name>Task 2: Add resource and reconnect API routes</name>
  <files>crates/mcp-preview/src/handlers/api.rs, crates/mcp-preview/src/server.rs, crates/mcp-preview/src/handlers/mod.rs</files>
  <action>
Add new API endpoint handlers and register them in the router.

**In `handlers/api.rs`:**

1. **Add `list_resources` handler:**
   ```
   pub async fn list_resources(State(state): State<Arc<AppState>>) -> Json<Value>
   ```
   - Calls `state.proxy.list_resources().await`.
   - On success: returns `{ "resources": [...] }`.
   - On error: returns `{ "resources": [], "error": "message" }`.
   - Filter to UI resources only: keep resources whose `mime_type` contains "html" (case-insensitive check). This implements the CONTEXT.md decision "Only shows UI resources (HTML/widget MIME types) -- non-UI resources are filtered out". Do the filtering in the handler, not in the proxy (the proxy returns all resources; the handler filters for the UI).

2. **Add `read_resource` handler:**
   ```
   pub async fn read_resource(
       State(state): State<Arc<AppState>>,
       axum::extract::Query(params): axum::extract::Query<ReadResourceParams>,
   ) -> Json<Value>
   ```
   - Add `ReadResourceParams` struct with `uri: String` (derive `Deserialize`).
   - Calls `state.proxy.read_resource(&params.uri).await`.
   - On success: returns `{ "contents": [...] }`.
   - On error: returns `{ "contents": null, "error": "message" }`.

3. **Add `reconnect` handler:**
   ```
   pub async fn reconnect(State(state): State<Arc<AppState>>) -> Json<Value>
   ```
   - Calls `state.proxy.reset_session().await` to clear the session.
   - Then calls `state.proxy.list_tools().await` to re-initialize and verify connection.
   - On success: returns `{ "success": true, "toolCount": N }`.
   - On error: returns `{ "success": false, "error": "message" }`.

4. **Add `status` handler:**
   ```
   pub async fn status(State(state): State<Arc<AppState>>) -> Json<Value>
   ```
   - Calls `state.proxy.is_connected()`.
   - Returns `{ "connected": true/false }`.

**In `server.rs`:**

5. **Register new routes** in the Router chain:
   - `.route("/api/resources", get(handlers::api::list_resources))`
   - `.route("/api/resources/read", get(handlers::api::read_resource))`
   - `.route("/api/reconnect", post(handlers::api::reconnect))`
   - `.route("/api/status", get(handlers::api::status))`

**In `handlers/mod.rs`:**

6. Ensure `api` module is publicly exported (should already be, verify).

All handlers use `Json<Value>` return type for simplicity (matching the existing pattern in the codebase). Use `serde_json::json!()` macro for response construction.
  </action>
  <verify>
Run `cargo check -p mcp-preview` -- must compile with zero errors. Run `cargo clippy -p mcp-preview -- -D warnings` -- must pass with zero warnings. Verify routes are registered by searching for "/api/resources" in server.rs.
  </verify>
  <done>
Four new API endpoints registered: GET /api/resources (returns UI-filtered resource list), GET /api/resources/read?uri=X (returns resource content), POST /api/reconnect (resets and re-initializes session), GET /api/status (returns connection state). All handlers delegate to McpProxy methods and return JSON responses with error handling.
  </done>
</task>

</tasks>

<verification>
1. `cargo check -p mcp-preview` compiles successfully
2. `cargo clippy -p mcp-preview -- -D warnings` passes with zero warnings
3. `cargo fmt --check -p mcp-preview` passes
4. Verify session persistence: grep for `RwLock<Option<SessionInfo>>` in proxy.rs
5. Verify resource methods: grep for `list_resources` and `read_resource` in proxy.rs
6. Verify API routes: grep for `/api/resources` in server.rs
7. Verify notifications/initialized: grep for `notifications/initialized` in proxy.rs
</verification>

<success_criteria>
- McpProxy compiles with session-once initialization pattern
- All 4 new API endpoints registered and compiling
- No clippy warnings
- Resource list handler filters to UI resources only
- Reconnect handler resets session and re-initializes
</success_criteria>

<output>
After completion, create `.planning/phases/14-preview-bridge-infrastructure/14-01-SUMMARY.md`
</output>
