---
phase: 18-publishing-pipeline
plan: 02
type: execute
wave: 2
depends_on: [18-01]
files_modified:
  - cargo-pmcp/src/publishing/landing.rs
  - cargo-pmcp/src/publishing/mod.rs
  - cargo-pmcp/src/commands/app.rs
  - cargo-pmcp/src/templates/mcp_app.rs
autonomous: true
requirements: [PUBL-02]

must_haves:
  truths:
    - "Running `cargo pmcp app landing` in an MCP Apps project with a mock-data/ directory produces dist/landing.html"
    - "Generated landing.html is a single self-contained file with widget HTML, mock bridge JS, and CSS all inlined"
    - "Opening landing.html in a browser renders the widget in an iframe with a product-showcase frame"
    - "Widget JavaScript calling callTool() in the landing page receives hardcoded mock responses from mock-data/*.json"
    - "Running `cargo pmcp app build --url <URL>` generates both manifest.json and landing.html in dist/"
    - "Missing mock-data/ directory produces a clear error message"
  artifacts:
    - path: "cargo-pmcp/src/publishing/landing.rs"
      provides: "Landing page HTML generation with mock bridge"
      contains: "generate_landing"
    - path: "cargo-pmcp/src/commands/app.rs"
      provides: "Landing and Build variants on AppCommand enum"
      contains: "Landing"
    - path: "cargo-pmcp/src/templates/mcp_app.rs"
      provides: "Updated scaffold template with mock-data/ directory"
      contains: "mock-data"
  key_links:
    - from: "cargo-pmcp/src/commands/app.rs"
      to: "cargo-pmcp/src/publishing/landing.rs"
      via: "AppCommand::Landing calls publishing::landing::generate_landing"
      pattern: "publishing::landing"
    - from: "cargo-pmcp/src/publishing/landing.rs"
      to: "cargo-pmcp/src/publishing/detect.rs"
      via: "generate_landing calls detect_project for project info and widget HTML"
      pattern: "detect::detect_project"
    - from: "cargo-pmcp/src/commands/app.rs"
      to: "cargo-pmcp/src/publishing/manifest.rs"
      via: "AppCommand::Build delegates to both manifest and landing generation"
      pattern: "publishing::manifest"
---

<objective>
Add `cargo pmcp app landing` and `cargo pmcp app build` commands that generate a standalone demo HTML page and a combined build output.

Purpose: Developers need a shareable landing page that showcases their widget with mock data (no running server required), and a single "ship it" command that produces both the manifest and the landing page.

Output: `dist/landing.html` -- a single self-contained HTML file with the widget embedded in an iframe using srcdoc, a mock bridge returning hardcoded responses from `mock-data/*.json`, and product-showcase styling. `cargo pmcp app build --url <URL>` generates both `dist/manifest.json` and `dist/landing.html`.
</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/18-publishing-pipeline/18-CONTEXT.md
@.planning/phases/18-publishing-pipeline/18-RESEARCH.md
@.planning/phases/18-publishing-pipeline/18-01-SUMMARY.md

<interfaces>
<!-- From Plan 18-01 (must exist before this plan runs) -->

From cargo-pmcp/src/publishing/detect.rs:
```rust
pub struct ProjectInfo {
    pub name: String,
    pub description: String,
    pub logo: Option<String>,
    pub widgets: Vec<WidgetInfo>,
}

pub struct WidgetInfo {
    pub name: String,   // file stem, e.g., "board"
    pub uri: String,    // "ui://app/board"
    pub html: String,   // full HTML content
}

pub fn detect_project() -> Result<ProjectInfo>;
```

From cargo-pmcp/src/publishing/manifest.rs:
```rust
pub fn generate_manifest(project: &ProjectInfo, server_url: &str, logo_override: Option<&str>) -> Result<String>;
pub fn write_manifest(output_dir: &str, content: &str) -> Result<()>;
```

From cargo-pmcp/src/commands/app.rs (after Plan 18-01):
```rust
#[derive(Subcommand)]
pub enum AppCommand {
    New { name: String, #[arg(long)] path: Option<String> },
    Manifest { #[arg(long)] url: String, #[arg(long)] logo: Option<String>, #[arg(long, default_value = "dist")] output: String },
    // Landing and Build to be added by this plan
}
```

From cargo-pmcp/src/templates/mcp_app.rs (existing scaffold to update):
```rust
pub fn generate(project_dir: &Path, name: &str) -> Result<()> {
    // Currently creates: Cargo.toml, src/main.rs, widgets/hello.html, README.md
    // This plan adds: mock-data/hello.json
}
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Implement landing page generation with mock bridge</name>
  <files>
    cargo-pmcp/src/publishing/landing.rs
    cargo-pmcp/src/publishing/mod.rs
  </files>
  <action>
**landing.rs** -- Landing page HTML generation:

1. Implement `pub fn load_mock_data() -> Result<HashMap<String, serde_json::Value>>` that:
   - Reads `./mock-data/` directory. Bail with "No mock data found. Create mock-data/tool-name.json for each tool." if directory missing.
   - For each `.json` file: parse content with `serde_json::from_str`, store in HashMap keyed by file stem (e.g., `mock-data/hello.json` -> key "hello").
   - Bail if directory exists but contains no `.json` files (same error message).
   - Return the HashMap.

2. Implement `fn escape_for_srcdoc(html: &str) -> String` that:
   - Replaces `&` with `&amp;` FIRST (order matters).
   - Replaces `"` with `&quot;`.
   - This is the minimum escaping needed for HTML attribute value context. `<` and `>` are fine inside srcdoc -- the browser parses srcdoc content as HTML.

3. Implement `fn inject_mock_bridge(widget_html: &str, mock_data_json: &str) -> String` that:
   - Builds a `<script>` tag containing the mock bridge:
     ```javascript
     window.mcpBridge = {
         _mockData: {mock_data_json},
         _state: {},
         callTool: async function(name, args) {
             const data = this._mockData[name];
             return data || { error: 'No mock data for: ' + name };
         },
         getState: function() { return this._state; },
         setState: function(s) { Object.assign(this._state, s); },
         get theme() { return 'light'; },
         get locale() { return 'en-US'; },
         get displayMode() { return 'inline'; }
     };
     window.dispatchEvent(new Event('mcpBridgeReady'));
     ```
   - Injects the script tag into the widget HTML using the same strategy as WidgetDir::inject_bridge_script: before `</head>` if present, else after `<body>`, else prepend.
   - Returns the modified HTML string.

4. Define a `const LANDING_CSS: &str` with product-showcase styling:
   - Clean, modern design: centered layout, subtle background (#f5f5f7), max-width 960px.
   - Header: app name in large font, description in muted text, centered.
   - Widget frame: centered, max-width 100%, border-radius 12px, subtle border (#e0e0e0), box-shadow.
   - iframe: width 100%, height 600px, no border within the frame.
   - Footer: "Built with pmcp" link, muted text.
   - Responsive: widget frame scales down on narrow viewports.

5. Implement `pub fn generate_landing(project: &ProjectInfo, mock_data: &HashMap<String, serde_json::Value>, widget_name: Option<&str>) -> Result<String>` that:
   - Selects widget: if `widget_name` is Some, find that widget in project.widgets (bail if not found). Otherwise use the first widget (bail if project.widgets is empty).
   - Serializes mock_data HashMap to JSON string for embedding in the mock bridge.
   - Calls `inject_mock_bridge(&widget.html, &mock_data_json)` to produce widget HTML with mock bridge.
   - Calls `escape_for_srcdoc(&widget_with_bridge)` to escape for HTML attribute.
   - Assembles the landing page HTML using `format!()`:
     ```html
     <!DOCTYPE html>
     <html lang="en">
     <head>
         <meta charset="UTF-8">
         <meta name="viewport" content="width=device-width, initial-scale=1.0">
         <title>{name}</title>
         <style>{LANDING_CSS}</style>
     </head>
     <body>
         <header class="showcase-header">
             <h1>{name}</h1>
             <p class="description">{description}</p>
         </header>
         <main class="showcase-main">
             <div class="widget-frame">
                 <iframe srcdoc="{escaped}" sandbox="allow-scripts" loading="lazy"></iframe>
             </div>
         </main>
         <footer class="showcase-footer">
             <p>Built with <a href="https://crates.io/crates/pmcp">pmcp</a></p>
         </footer>
     </body>
     </html>
     ```
   - Returns the assembled HTML string.

6. Implement `pub fn write_landing(output_dir: &str, content: &str) -> Result<()>` that:
   - Creates output directory if it doesn't exist.
   - Writes content to `{output_dir}/landing.html`.
   - Prints: "ok Generated dist/landing.html" using colored.

**mod.rs** -- Add module declaration:
- Add `pub mod landing;` alongside existing `pub mod detect;` and `pub mod manifest;`.

Add unit tests in landing.rs:
- `test_escape_for_srcdoc`: verify `"` becomes `&quot;`, `&` becomes `&amp;`, `<` stays `<`.
- `test_inject_mock_bridge_before_head`: verify script inserted before `</head>`.
- `test_inject_mock_bridge_after_body`: verify script inserted after `<body>` when no `</head>`.
- `test_inject_mock_bridge_prepend`: verify script prepended when no head or body.
- `test_generate_landing_basic`: create a ProjectInfo with one widget, mock data HashMap with one entry, verify output contains the widget content (escaped) and mock data.
- `test_generate_landing_widget_selection`: verify --widget flag selects correct widget.
- `test_load_mock_data`: use tempfile to create mock-data/ with a JSON file, verify HashMap populated.
  </action>
  <verify>
    <automated>cd /Users/guy/Development/mcp/sdk/rust-mcp-sdk && cargo test -p cargo-pmcp -- publishing::landing --no-fail-fast 2>&1 | tail -20</automated>
  </verify>
  <done>landing.rs generates a self-contained HTML page with escaped widget HTML in iframe srcdoc, mock bridge with embedded JSON data, and product-showcase styling; all unit tests pass</done>
</task>

<task type="auto">
  <name>Task 2: Wire landing and build commands into AppCommand CLI and update scaffold template</name>
  <files>
    cargo-pmcp/src/commands/app.rs
    cargo-pmcp/src/templates/mcp_app.rs
  </files>
  <action>
**cargo-pmcp/src/commands/app.rs:**

1. Add two new variants to `AppCommand` enum:
   ```rust
   /// Generate standalone landing page HTML
   Landing {
       /// Widget to showcase (defaults to first alphabetically)
       #[arg(long)]
       widget: Option<String>,
       /// Output directory
       #[arg(long, default_value = "dist")]
       output: String,
   },

   /// Generate both manifest and landing page
   Build {
       /// Server URL (required for manifest)
       #[arg(long)]
       url: String,
       /// Logo URL
       #[arg(long)]
       logo: Option<String>,
       /// Widget to showcase in landing page (defaults to first)
       #[arg(long)]
       widget: Option<String>,
       /// Output directory
       #[arg(long, default_value = "dist")]
       output: String,
   },
   ```

2. Add match arms in `execute()`:
   ```rust
   AppCommand::Landing { widget, output } => create_landing(widget, output),
   AppCommand::Build { url, logo, widget, output } => build_all(url, logo, widget, output),
   ```

3. Implement `fn create_landing(widget: Option<String>, output: String) -> Result<()>`:
   - Print header: "Generating landing page" (bright_cyan, bold).
   - Call `publishing::detect::detect_project()?`.
   - Call `publishing::landing::load_mock_data()?`.
   - Call `publishing::landing::generate_landing(&project, &mock_data, widget.as_deref())?`.
   - Call `publishing::landing::write_landing(&output, &html)?`.
   - Print completion message.

4. Implement `fn build_all(url: String, logo: Option<String>, widget: Option<String>, output: String) -> Result<()>`:
   - Print header: "Building MCP App" (bright_cyan, bold).
   - Call `publishing::detect::detect_project()?` ONCE (shared).
   - Generate manifest: call `publishing::manifest::generate_manifest(&project, &url, logo.as_deref())?`, then `publishing::manifest::write_manifest(&output, &manifest_json)?`.
   - Generate landing: call `publishing::landing::load_mock_data()?`, then `publishing::landing::generate_landing(&project, &mock_data, widget.as_deref())?`, then `publishing::landing::write_landing(&output, &landing_html)?`.
   - Print summary: "ok Built MCP App artifacts in {output}/".
   - Print file list: "  - manifest.json", "  - landing.html".

**cargo-pmcp/src/templates/mcp_app.rs:**

1. Update the `generate()` function to also create `mock-data/hello.json`:
   - Add `fs::create_dir_all(project_dir.join("mock-data"))` after the existing widgets/ directory creation.
   - Generate a `mock-data/hello.json` file containing a sample response matching the hello tool:
     ```json
     {
       "greeting": "Hello, World!",
       "name": "World"
     }
     ```
   - Print: "ok Generated mock-data/hello.json".

2. Update `test_generate_creates_files` to verify `mock-data/hello.json` exists.

3. Add test for the mock-data content:
   ```rust
   #[test]
   fn test_generate_creates_mock_data() {
       // Use tempfile, generate, verify mock-data/hello.json exists and contains valid JSON
   }
   ```
  </action>
  <verify>
    <automated>cd /Users/guy/Development/mcp/sdk/rust-mcp-sdk && cargo build -p cargo-pmcp 2>&1 | tail -5 && cargo test -p cargo-pmcp -- app --no-fail-fast 2>&1 | tail -20 && cargo test -p cargo-pmcp -- mcp_app --no-fail-fast 2>&1 | tail -20</automated>
  </verify>
  <done>`cargo pmcp app landing` produces dist/landing.html; `cargo pmcp app build --url <URL>` produces both dist/manifest.json and dist/landing.html; scaffold template creates mock-data/hello.json; all tests pass</done>
</task>

</tasks>

<verification>
1. `cargo build -p cargo-pmcp` compiles without errors or warnings
2. `cargo clippy -p cargo-pmcp -- -D warnings` passes with zero warnings
3. `cargo test -p cargo-pmcp` runs all tests (existing + new publishing + updated template tests)
4. `cargo fmt -p cargo-pmcp --check` passes
5. Manual spot-check: generated landing.html opens in a browser and renders the widget iframe with mock data
</verification>

<success_criteria>
- `cargo pmcp app landing` produces a single self-contained `dist/landing.html` file that renders a widget in an iframe with mock bridge data, viewable without a running server
- `cargo pmcp app build --url https://example.com` produces both `dist/manifest.json` and `dist/landing.html` in a single command
- Mock bridge in landing page returns data from `mock-data/*.json` files when widget JavaScript calls `callTool()`
- Missing `mock-data/` directory produces a clear error: "No mock data found. Create mock-data/tool-name.json for each tool."
- Scaffold template (`cargo pmcp app new`) now creates `mock-data/hello.json` alongside existing files
- All new code has unit tests; clippy and fmt pass
</success_criteria>

<output>
After completion, create `.planning/phases/18-publishing-pipeline/18-02-SUMMARY.md`
</output>
