Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 19 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -336,15 +336,23 @@ no editing logic in the adapter.
### embedded MCP server (`crates/kerf-app/src/mcp.rs`)

The app **is** the MCP server — there is no separate binary. `mcp::serve` hosts the
tools over `rmcp` 1.7's **streamable-HTTP** transport (`StreamableHttpService` +
tools over `rmcp` 3.1's **streamable-HTTP** transport (`StreamableHttpService` +
`LocalSessionManager`, nested into an `axum` router) on `127.0.0.1:7777/mcp`
(`KERF_MCP_ADDR` overrides). It is spawned from `lib.rs`'s Tauri `.setup` hook on
(`KERF_MCP_ADDR` overrides). rmcp validates the inbound **`Host`** header against
an allow-list that defaults to loopback (a DNS-rebinding guard), which would make
every `KERF_MCP_ADDR` override reject its own clients — so `allowed_hosts` (pure +
unit-tested) derives the list from the bind address: a concrete address is added to
the loopback defaults, and a wildcard bind (`0.0.0.0` / `[::]`) can't be enumerated
at all, so it yields an empty list, rmcp's "allow any".
It is spawned from `lib.rs`'s Tauri `.setup` hook on
`tauri::async_runtime` and shares the **same** `Arc<Mutex<Project>>` the Tauri commands
hold, so the agent edits the project the user has open. Patterns that matter if you edit
it: `#[tool_router]` on the impl + `#[tool_handler]` on `impl ServerHandler` — **no
`tool_router` field on the struct** (the macro calls `Self::tool_router()`).
`ServerInfo` is `#[non_exhaustive]`, so `get_info` builds it via `Default::default()`
then mutates fields. Most tools return `Result<String, McpError>` (pretty JSON), but the
then mutates fields — including `server_info` (`server_identity`), because that
default is filled from **rmcp's own** crate identity and left alone the server
introduces itself to every client as "rmcp". Most tools return `Result<String, McpError>` (pretty JSON), but the
three **visual** tools — `get_frame` (a single drill-in frame), `skim_asset` (a
contact-sheet montage of an asset + a text index of cell→timestamp, for finding good
parts) and `preview_timeline` (the composited cut at a timeline time) — return
Expand Down Expand Up @@ -414,6 +422,14 @@ snake_case (`{ assetId }` → `asset_id`). Config: `tauri.conf.json` points
which for this `crates/kerf-app` layout resolves to `crates/`, not the config dir or repo
root — so they anchor to the repo via `cd "$(git rev-parse --show-toplevel)/frontend" && bun run dev`
instead of a fragile relative path.
`build.rs` takes the **Windows app manifest** away from Tauri
(`new_without_app_manifest`) and embeds `windows-app-manifest.xml` through the
linker instead: Tauri's copy rides in the `.res`, which cargo links into *bins*
only, so the lib's test binary ran with no activation context, bound comctl32
**v5**, and died with `STATUS_ENTRYPOINT_NOT_FOUND` on the `TaskDialogIndirect`
import rfd (via `tauri-plugin-dialog`) contributes — before a single test ran.
Whether the linker pulls that object in at all shifts with unrelated dependency
bumps, which is how an rmcp upgrade broke `cargo test -p kerf-app` on Windows.
`capabilities/default.json` grants `core:default` + `dialog:default` +
`updater:default` + `process:allow-restart` + `opener:allow-open-url`. That last
one enables the command **with no scope of its own** (`allow-default-urls` is a
Expand Down
87 changes: 61 additions & 26 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ resolver = "2"
members = ["crates/kerf-core", "crates/kerf-app"]

[workspace.package]
version = "0.19.1"
version = "0.19.2"
edition = "2021"
rust-version = "1.95"
license = "PolyForm-Noncommercial-1.0.0"
Expand Down Expand Up @@ -45,7 +45,7 @@ fontdb = "0.24"
ffmpeg-next = "9.0"

# MCP — official Rust SDK (streamable-HTTP server transport, hosted by kerf-app)
rmcp = { version = "1.7", features = ["server", "macros", "transport-streamable-http-server"] }
rmcp = { version = "3.1", features = ["server", "macros", "transport-streamable-http-server"] }
schemars = "1.2"

# HTTP server for the embedded MCP endpoint (matches rmcp's tower/axum 0.8)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ kerf/
| Styling | Tailwind CSS **4** (CSS config) + design tokens |
| Media | FFmpeg binaries (always) · `ffmpeg-next` **8.1** (optional libav) |
| Persistence | `rusqlite` (bundled SQLite) — one `.kerf` file |
| MCP | `rmcp` **1.7** (streamable-HTTP transport) |
| MCP | `rmcp` **3.1** (streamable-HTTP transport) |

The engine's export and timeline-still paths are **pure and unit-tested** — clip
positions, gaps, track layering, effects, keyframes, overlays, ducking and loudnorm all
Expand Down
20 changes: 19 additions & 1 deletion crates/kerf-app/build.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
fn main() {
tauri_build::build();
// Tauri's default app manifest — the one asking for Common-Controls v6 —
// rides in the Windows resource file, which cargo links into **bins only**.
// The lib's own test binary therefore starts with no activation context, so
// the loader binds comctl32 v5; and rfd (via tauri-plugin-dialog) statically
// imports `TaskDialogIndirect`, which only v6 exports. The test exe then dies
// with STATUS_ENTRYPOINT_NOT_FOUND before a single test runs. Whether the
// linker pulls that object in at all shifts with unrelated dependency
// changes, so pass the same manifest through the linker instead: that covers
// every binary this crate links, tests included.
let attributes =
tauri_build::Attributes::new().windows_attributes(tauri_build::WindowsAttributes::new_without_app_manifest());
tauri_build::try_build(attributes).expect("tauri-build failed");

if std::env::var("CARGO_CFG_TARGET_ENV").as_deref() == Ok("msvc") {
let manifest = std::path::Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()).join("windows-app-manifest.xml");
println!("cargo:rerun-if-changed=windows-app-manifest.xml");
println!("cargo:rustc-link-arg=/MANIFEST:EMBED");
println!("cargo:rustc-link-arg=/MANIFESTINPUT:{}", manifest.display());
}
}
Loading
Loading