Skip to content

Commit 5d3bc64

Browse files
committed
feat(serve): add migrate_preview/apply/undo MCP tools and HTTP endpoints
1 parent cad9ff4 commit 5d3bc64

2 files changed

Lines changed: 100 additions & 1 deletion

File tree

src/http.rs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,10 @@ pub fn build_router(state: ApiState) -> Router {
376376
.route("/api/unarchive", post(handle_unarchive))
377377
.route("/api/update-metadata", post(handle_update_metadata))
378378
.route("/api/delete", post(handle_delete))
379+
// Migration endpoints
380+
.route("/api/migrate/preview", post(handle_migrate_preview))
381+
.route("/api/migrate/apply", post(handle_migrate_apply))
382+
.route("/api/migrate/undo", post(handle_migrate_undo))
379383
.layer(cors)
380384
.with_state(state)
381385
}
@@ -834,6 +838,52 @@ async fn handle_update_metadata(
834838
Ok(Json(serde_json::json!(result)))
835839
}
836840

841+
// ---------------------------------------------------------------------------
842+
// Migration endpoint handlers
843+
// ---------------------------------------------------------------------------
844+
845+
async fn handle_migrate_preview(
846+
State(state): State<ApiState>,
847+
headers: HeaderMap,
848+
) -> Result<impl IntoResponse, ApiError> {
849+
authorize(&headers, &state, true)?;
850+
let store = state.store.lock().await;
851+
let profile_ref = state.profile.as_ref().as_ref();
852+
let preview = crate::migrate::generate_preview(&store, &state.vault_path, profile_ref)
853+
.map_err(|e| ApiError::internal(&format!("{e:#}")))?;
854+
Ok(Json(serde_json::to_value(&preview).unwrap()))
855+
}
856+
857+
#[derive(Deserialize)]
858+
struct MigrateApplyBody {
859+
preview: serde_json::Value,
860+
}
861+
862+
async fn handle_migrate_apply(
863+
State(state): State<ApiState>,
864+
headers: HeaderMap,
865+
Json(body): Json<MigrateApplyBody>,
866+
) -> Result<impl IntoResponse, ApiError> {
867+
authorize(&headers, &state, true)?;
868+
let store = state.store.lock().await;
869+
let preview: crate::migrate::MigrationPreview = serde_json::from_value(body.preview)
870+
.map_err(|e| ApiError::bad_request(&format!("Invalid preview: {e}")))?;
871+
let result = crate::migrate::apply_preview(&preview, &store, &state.vault_path)
872+
.map_err(|e| ApiError::internal(&format!("{e:#}")))?;
873+
Ok(Json(serde_json::to_value(&result).unwrap()))
874+
}
875+
876+
async fn handle_migrate_undo(
877+
State(state): State<ApiState>,
878+
headers: HeaderMap,
879+
) -> Result<impl IntoResponse, ApiError> {
880+
authorize(&headers, &state, true)?;
881+
let store = state.store.lock().await;
882+
let result = crate::migrate::undo_last(&store, &state.vault_path)
883+
.map_err(|e| ApiError::internal(&format!("{e:#}")))?;
884+
Ok(Json(serde_json::to_value(&result).unwrap()))
885+
}
886+
837887
async fn handle_delete(
838888
State(state): State<ApiState>,
839889
headers: HeaderMap,

src/serve.rs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,18 @@ pub struct ReadSectionParams {
134134
#[derive(Debug, Deserialize, JsonSchema)]
135135
pub struct HealthParams {}
136136

137+
#[derive(Debug, Deserialize, JsonSchema)]
138+
pub struct MigratePreviewParams {}
139+
140+
#[derive(Debug, Deserialize, JsonSchema)]
141+
pub struct MigrateApplyParams {
142+
/// Migration preview JSON (from migrate_preview).
143+
pub preview: serde_json::Value,
144+
}
145+
146+
#[derive(Debug, Deserialize, JsonSchema)]
147+
pub struct MigrateUndoParams {}
148+
137149
#[derive(Debug, Deserialize, JsonSchema)]
138150
pub struct EditParams {
139151
/// Target note: file path, basename, or #docid.
@@ -685,6 +697,42 @@ impl EngraphServer {
685697
to_json_result(&result)
686698
}
687699

700+
#[tool(
701+
name = "migrate_preview",
702+
description = "Generate PARA migration preview. Classifies all notes into Projects/Areas/Resources/Archive and returns proposed moves with confidence scores."
703+
)]
704+
async fn migrate_preview(&self, _params: Parameters<MigratePreviewParams>) -> Result<CallToolResult, McpError> {
705+
let store = self.store.lock().await;
706+
let profile_ref = self.profile.as_ref().as_ref();
707+
let preview = crate::migrate::generate_preview(&store, &self.vault_path, profile_ref)
708+
.map_err(|e| mcp_err(&e))?;
709+
to_json_result(&preview)
710+
}
711+
712+
#[tool(
713+
name = "migrate_apply",
714+
description = "Apply a PARA migration preview. Moves files to their classified PARA locations. Reversible via migrate_undo."
715+
)]
716+
async fn migrate_apply(&self, params: Parameters<MigrateApplyParams>) -> Result<CallToolResult, McpError> {
717+
let store = self.store.lock().await;
718+
let preview: crate::migrate::MigrationPreview = serde_json::from_value(params.0.preview)
719+
.map_err(|e| mcp_err(&anyhow::anyhow!("Invalid preview JSON: {e}")))?;
720+
let result = crate::migrate::apply_preview(&preview, &store, &self.vault_path)
721+
.map_err(|e| mcp_err(&e))?;
722+
to_json_result(&result)
723+
}
724+
725+
#[tool(
726+
name = "migrate_undo",
727+
description = "Undo the most recent PARA migration, restoring all moved files to their original locations."
728+
)]
729+
async fn migrate_undo(&self, _params: Parameters<MigrateUndoParams>) -> Result<CallToolResult, McpError> {
730+
let store = self.store.lock().await;
731+
let result = crate::migrate::undo_last(&store, &self.vault_path)
732+
.map_err(|e| mcp_err(&e))?;
733+
to_json_result(&result)
734+
}
735+
688736
#[tool(
689737
name = "delete",
690738
description = "Delete a note. Soft mode (default) moves it to the archive folder. Hard mode permanently removes it from disk and index."
@@ -725,7 +773,8 @@ impl rmcp::handler::server::ServerHandler for EngraphServer {
725773
Read: vault_map to orient, search to find, read/read_section for content, who/project for context bundles, health for vault diagnostics. \
726774
Write: create for new notes, append to add content, edit to modify a section, rewrite to replace body, \
727775
edit_frontmatter for tags/properties, update_metadata for bulk tag/alias replacement. \
728-
Lifecycle: move_note to relocate, archive to soft-delete, unarchive to restore, delete for permanent removal.",
776+
Lifecycle: move_note to relocate, archive to soft-delete, unarchive to restore, delete for permanent removal. \
777+
Migration: migrate_preview to classify notes into PARA folders, migrate_apply to execute the migration, migrate_undo to revert.",
729778
)
730779
}
731780
}

0 commit comments

Comments
 (0)