---
phase: 18-publishing-pipeline
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - cargo-pmcp/src/commands/app.rs
  - cargo-pmcp/src/publishing/mod.rs
  - cargo-pmcp/src/publishing/detect.rs
  - cargo-pmcp/src/publishing/manifest.rs
  - cargo-pmcp/src/main.rs
autonomous: true
requirements: [PUBL-01]

must_haves:
  truths:
    - "Running `cargo pmcp app manifest --url https://example.com` in an MCP Apps project produces dist/manifest.json"
    - "Generated manifest.json contains schema_version, name, description, auth, api, logo_url, and mcp_apps extension fields"
    - "Tool-to-widget mappings in manifest are auto-discovered from widgets/ directory files"
    - "Running the command outside an MCP Apps project produces a clear error message"
  artifacts:
    - path: "cargo-pmcp/src/publishing/mod.rs"
      provides: "Publishing module declarations"
    - path: "cargo-pmcp/src/publishing/detect.rs"
      provides: "MCP Apps project detection from Cargo.toml"
      contains: "detect_project"
    - path: "cargo-pmcp/src/publishing/manifest.rs"
      provides: "ChatGPT-compatible manifest JSON generation"
      contains: "generate_manifest"
    - path: "cargo-pmcp/src/commands/app.rs"
      provides: "Manifest variant on AppCommand enum"
      contains: "Manifest"
  key_links:
    - from: "cargo-pmcp/src/commands/app.rs"
      to: "cargo-pmcp/src/publishing/manifest.rs"
      via: "AppCommand::Manifest calls publishing::manifest::generate_manifest"
      pattern: "publishing::manifest"
    - from: "cargo-pmcp/src/publishing/manifest.rs"
      to: "cargo-pmcp/src/publishing/detect.rs"
      via: "generate_manifest calls detect_project for project info and widget list"
      pattern: "detect::detect_project"
---

<objective>
Add `cargo pmcp app manifest --url <URL>` command that generates a ChatGPT-compatible ai-plugin.json manifest with auto-discovered widget-to-tool mappings.

Purpose: Developers need a single command to produce the manifest JSON required for ChatGPT App Directory submission, with tool-to-widget mappings auto-discovered from the project's widgets/ directory.

Output: `dist/manifest.json` containing schema_version "v1", package name/description from Cargo.toml, server URL from --url flag, and widget mappings from widgets/*.html files.
</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/17-widget-authoring-dx-and-scaffolding/17-02-SUMMARY.md

<interfaces>
<!-- Key types and contracts the executor needs. Extracted from codebase. -->

From cargo-pmcp/src/commands/app.rs (existing pattern to extend):
```rust
use clap::Subcommand;

#[derive(Subcommand)]
pub enum AppCommand {
    /// Create a new MCP Apps project
    New {
        name: String,
        #[arg(long)]
        path: Option<String>,
    },
}

impl AppCommand {
    pub fn execute(self) -> Result<()> {
        match self {
            AppCommand::New { name, path } => create_app(name, path),
        }
    }
}
```

From cargo-pmcp/src/main.rs (App command wiring):
```rust
Commands::App { command } => {
    command.execute()?;
},
```

From cargo-pmcp/Cargo.toml (available dependencies):
- toml = "0.9"
- serde = { version = "1", features = ["derive"] }
- serde_json = "1"
- colored = "3"
- anyhow = "1"
- clap = { version = "4", features = ["derive", "env"] }

From src/server/mcp_apps/widget_fs.rs (WidgetDir API -- DO NOT import, reimplement inline):
```rust
// WidgetDir is in pmcp crate; cargo-pmcp cannot depend on it.
// Reimplement the ~30-line widget directory scan inline.
// Convention: widgets/*.html -> stem becomes tool name, uri = "ui://app/{stem}"
```

From cargo-pmcp/src/templates/mcp_app.rs (Cargo.toml template for feature detection):
```rust
// Generated Cargo.toml contains:
// pmcp = { version = "1.10", features = ["mcp-apps"] }
// This is the pattern detect_project must look for.
// Also check for features = ["full"] which is a superset.
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create publishing module with project detection and manifest generation</name>
  <files>
    cargo-pmcp/src/publishing/mod.rs
    cargo-pmcp/src/publishing/detect.rs
    cargo-pmcp/src/publishing/manifest.rs
  </files>
  <action>
Create a new `cargo-pmcp/src/publishing/` module directory with three files.

**detect.rs** -- Shared project detection logic:

1. Define `ProjectInfo` struct with fields: `name: String`, `description: String`, `logo: Option<String>`, `widgets: Vec<WidgetInfo>`.
2. Define `WidgetInfo` struct with fields: `name: String` (file stem), `uri: String` (e.g., "ui://app/board"), `html: String` (full HTML content read from disk).
3. Implement `pub fn detect_project() -> Result<ProjectInfo>` that:
   - Reads `Cargo.toml` from current directory (bail with "No Cargo.toml found. Are you in a Rust project directory?" if missing).
   - Parses with `toml::from_str::<toml::Value>()`.
   - Checks `dependencies.pmcp.features` array contains "mcp-apps" OR "full". Bail with "Not an MCP Apps project. Run `cargo pmcp app new` first." if neither found.
   - Also handle the case where pmcp dependency might be a simple string version (no features table) -- treat as not an MCP Apps project.
   - Scans `./widgets/` directory for `.html` files. Bail with "No widgets found in widgets/. Add .html files or run `cargo pmcp app new` to scaffold a project." if directory missing or empty.
   - For each `.html` file: read content, create WidgetInfo with stem as name, `ui://app/{stem}` as uri, file content as html.
   - Sort widgets by name for deterministic output.
   - Extract `[package].name` (default "unnamed"), `[package].description` (default ""), and `[package.metadata.pmcp].logo` (optional) using `toml::Value` navigation.
   - Return `ProjectInfo`.

**manifest.rs** -- Manifest JSON generation:

1. Implement `pub fn generate_manifest(project: &ProjectInfo, server_url: &str, logo_override: Option<&str>) -> Result<String>` that:
   - Determines logo_url: `logo_override` > `project.logo` > empty string.
   - Creates `name_for_model` by replacing `-` and spaces with `_` in project name.
   - Builds JSON using `serde_json::json!()` macro with structure:
     ```json
     {
       "schema_version": "v1",
       "name_for_human": project.name,
       "name_for_model": name_for_model,
       "description_for_human": project.description,
       "description_for_model": project.description,
       "auth": { "type": "none" },
       "api": { "type": "openapi", "url": "{server_url}/openapi.json" },
       "logo_url": logo_url,
       "contact_email": "",
       "legal_info_url": "",
       "mcp_apps": {
         "server_url": server_url,
         "widgets": [ { "tool": w.name, "resource_uri": w.uri } for each widget ]
       }
     }
     ```
   - Trim trailing `/` from server_url before appending `/openapi.json`.
   - Return `serde_json::to_string_pretty(&manifest)?`.

2. Implement `pub fn write_manifest(output_dir: &str, content: &str) -> Result<()>` that:
   - Creates output directory if it doesn't exist (`fs::create_dir_all`).
   - Writes content to `{output_dir}/manifest.json`.
   - Prints success message with `colored`: "ok Generated dist/manifest.json".

**mod.rs** -- Module declarations:
```rust
pub mod detect;
pub mod manifest;
```

Add unit tests in each file:
- `detect.rs`: test feature detection for "mcp-apps", "full", missing features, missing Cargo.toml scenarios (use tempfile).
- `manifest.rs`: test JSON output structure, server URL trimming, logo override precedence, name_for_model sanitization.
  </action>
  <verify>
    <automated>cd /Users/guy/Development/mcp/sdk/rust-mcp-sdk && cargo test -p cargo-pmcp -- publishing --no-fail-fast 2>&1 | tail -20</automated>
  </verify>
  <done>publishing module exists with detect.rs and manifest.rs; unit tests pass for project detection and manifest JSON generation</done>
</task>

<task type="auto">
  <name>Task 2: Wire manifest command into AppCommand CLI</name>
  <files>
    cargo-pmcp/src/commands/app.rs
    cargo-pmcp/src/main.rs
  </files>
  <action>
**cargo-pmcp/src/commands/app.rs:**

1. Add `use crate::publishing;` import at top.
2. Extend `AppCommand` enum with a `Manifest` variant:
   ```rust
   /// Generate ChatGPT-compatible manifest JSON
   Manifest {
       /// Server URL (required)
       #[arg(long)]
       url: String,
       /// Logo URL (overrides [package.metadata.pmcp].logo)
       #[arg(long)]
       logo: Option<String>,
       /// Output directory
       #[arg(long, default_value = "dist")]
       output: String,
   },
   ```
3. Add match arm in `execute()`:
   ```rust
   AppCommand::Manifest { url, logo, output } => generate_manifest(url, logo, output),
   ```
4. Implement `fn generate_manifest(url: String, logo: Option<String>, output: String) -> Result<()>`:
   - Print header: "Generating manifest" (bright_cyan, bold).
   - Call `publishing::detect::detect_project()?` to get ProjectInfo.
   - Call `publishing::manifest::generate_manifest(&project, &url, logo.as_deref())?` to get JSON string.
   - Call `publishing::manifest::write_manifest(&output, &json)?` to write file.
   - Print widget count: "  ok Found {N} widget(s)".
   - Print completion message with output path.

**cargo-pmcp/src/main.rs:**

1. Add `mod publishing;` to the module declarations at the top (alongside existing `mod commands; mod deployment; mod landing; mod secrets; mod templates; mod utils;`).

No changes needed to the Commands enum or execute_command -- the App command already delegates to `command.execute()` which handles the new variant.
  </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</automated>
  </verify>
  <done>Running `cargo pmcp app manifest --url https://example.com` from an MCP Apps project directory produces dist/manifest.json with correct structure; building cargo-pmcp succeeds with zero warnings</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 -- publishing` runs all publishing module tests
4. `cargo test -p cargo-pmcp -- app` runs app command tests (existing + new)
5. `cargo fmt -p cargo-pmcp --check` passes
</verification>

<success_criteria>
- `cargo pmcp app manifest --url https://example.com` produces a valid `dist/manifest.json` when run in a project with `pmcp = { features = ["mcp-apps"] }` in Cargo.toml and a `widgets/` directory containing at least one `.html` file
- The generated JSON follows the ChatGPT ai-plugin.json schema with schema_version "v1" and includes an mcp_apps extension block with server_url and widget mappings
- Running the command outside an MCP Apps project produces a clear error message (not a panic or stack trace)
- All new code has unit tests; clippy and fmt pass
</success_criteria>

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