Skip to content

Commit ede44f2

Browse files
authored
Merge pull request #21 from devwhodevs/feat/v1.5.5-housekeeping
v1.5.5: rmcp 1.4, auto_link parameter, reindex_file tool
2 parents 31246cf + 00b864f commit ede44f2

File tree

10 files changed

+186
-16
lines changed

10 files changed

+186
-16
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
# Changelog
22

3+
## v1.5.5 — Housekeeping (2026-04-10)
4+
5+
### Added
6+
- **`auto_link` parameter** on `create` — set to `false` to skip automatic wikilink resolution. Applies to MCP, HTTP, and CLI. Discovered links still appear as suggestions in the response.
7+
- **`reindex_file` MCP tool + HTTP endpoint** — re-indexes a single file after external edits. Reads from disk, re-embeds chunks, rebuilds edges. Available as MCP tool, `POST /api/reindex-file`, and OpenAPI operation.
8+
9+
### Changed
10+
- **rmcp** bumped from 1.2.0 to 1.4.0 — host validation, non-Send handler support, transport fixes. Does not yet fix [#20](https://github.com/devwhodevs/engraph/issues/20) (protocol `2025-11-25` needed for Claude Desktop Cowork/Code modes — blocked upstream on [modelcontextprotocol/rust-sdk#800](https://github.com/modelcontextprotocol/rust-sdk/issues/800)).
11+
- MCP tools: 22 → 23
12+
- HTTP endpoints: 23 → 24
13+
- OpenAPI version: 1.5.0 → 1.5.5
14+
315
## v1.5.0 — ChatGPT Actions (2026-03-26)
416

517
### Added

Cargo.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "engraph"
3-
version = "1.5.4"
3+
version = "1.5.5"
44
edition = "2024"
55
description = "Local knowledge graph for AI agents. Hybrid search + MCP server for Obsidian vaults."
66
license = "MIT"
@@ -31,7 +31,7 @@ rayon = "1"
3131
time = { version = "0.3", features = ["parsing", "formatting", "macros"] }
3232
strsim = "0.11"
3333
ignore = "0.4"
34-
rmcp = { version = "1.2", features = ["transport-io"] }
34+
rmcp = { version = "1.4", features = ["transport-io"] }
3535
tokio = { version = "1", features = ["macros", "rt-multi-thread", "process", "time", "net"] }
3636
notify = "7.0"
3737
notify-debouncer-full = "0.4"

README.md

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ Returns orphan notes (no links in or out), broken wikilinks, stale notes, and ta
268268

269269
`engraph serve --http` adds a full REST API alongside the MCP server, exposing the same capabilities over HTTP for web agents, scripts, and integrations.
270270

271-
**23 endpoints:**
271+
**24 endpoints:**
272272

273273
| Method | Endpoint | Permission | Description |
274274
|--------|----------|------------|-------------|
@@ -292,6 +292,7 @@ Returns orphan notes (no links in or out), broken wikilinks, stale notes, and ta
292292
| POST | `/api/unarchive` | write | Restore archived note |
293293
| POST | `/api/update-metadata` | write | Update note metadata |
294294
| POST | `/api/delete` | write | Delete note (soft or hard) |
295+
| POST | `/api/reindex-file` | write | Re-index a single file after external edits |
295296
| POST | `/api/migrate/preview` | write | Preview PARA migration (classify + suggest moves) |
296297
| POST | `/api/migrate/apply` | write | Apply PARA migration (move files) |
297298
| POST | `/api/migrate/undo` | write | Undo last PARA migration |
@@ -542,8 +543,8 @@ engraph is not a replacement for Obsidian — it's the intelligence layer that s
542543
- LLM research orchestrator: query intent classification + query expansion + adaptive lane weights
543544
- llama.cpp inference via Rust bindings (GGUF models, Metal GPU on macOS, CUDA on Linux)
544545
- Intelligence opt-in: heuristic fallback when disabled, LLM-powered when enabled
545-
- MCP server with 22 tools (8 read, 10 write, 1 diagnostic, 3 migrate) via stdio
546-
- HTTP REST API with 23 endpoints, API key auth (`eg_` prefix), rate limiting, CORS — enabled via `engraph serve --http`
546+
- MCP server with 23 tools (8 read, 10 write, 1 index, 1 diagnostic, 3 migrate) via stdio
547+
- HTTP REST API with 24 endpoints, API key auth (`eg_` prefix), rate limiting, CORS — enabled via `engraph serve --http`
547548
- Section-level reading and editing: target specific headings with replace/prepend/append modes
548549
- Full note rewriting with automatic frontmatter preservation
549550
- Granular frontmatter mutations: set/remove fields, add/remove tags and aliases
@@ -572,7 +573,9 @@ engraph is not a replacement for Obsidian — it's the intelligence layer that s
572573
- [x] ~~HTTP/REST API — complement MCP with a standard web API~~ (v1.3)
573574
- [x] ~~PARA migration — AI-assisted vault restructuring with preview/apply/undo~~ (v1.4)
574575
- [x] ~~ChatGPT Actions — OpenAPI 3.1.0 spec + plugin manifest + `--setup-chatgpt` helper~~ (v1.5)
575-
- [ ] Multi-vault — search across multiple vaults (v1.6)
576+
- [ ] Identity — user context at session start, enhanced onboarding (v1.6)
577+
- [ ] Timeline — temporal knowledge graph with point-in-time queries (v1.7)
578+
- [ ] Mining — automatic fact extraction from vault notes (v1.8)
576579

577580
## Configuration
578581

src/http.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ struct CreateBody {
268268
#[serde(default)]
269269
tags: Vec<String>,
270270
folder: Option<String>,
271+
auto_link: Option<bool>,
271272
}
272273

273274
#[derive(Debug, Deserialize)]
@@ -326,6 +327,11 @@ struct DeleteBody {
326327
mode: Option<String>,
327328
}
328329

330+
#[derive(Debug, Deserialize)]
331+
struct ReindexFileBody {
332+
file: String,
333+
}
334+
329335
// ---------------------------------------------------------------------------
330336
// CORS
331337
// ---------------------------------------------------------------------------
@@ -380,6 +386,8 @@ pub fn build_router(state: ApiState) -> Router {
380386
.route("/api/unarchive", post(handle_unarchive))
381387
.route("/api/update-metadata", post(handle_update_metadata))
382388
.route("/api/delete", post(handle_delete))
389+
// Index maintenance
390+
.route("/api/reindex-file", post(handle_reindex_file))
383391
// Migration endpoints
384392
.route("/api/migrate/preview", post(handle_migrate_preview))
385393
.route("/api/migrate/apply", post(handle_migrate_apply))
@@ -712,6 +720,7 @@ async fn handle_create(
712720
tags: body.tags,
713721
folder: body.folder,
714722
created_by: "http-api".into(),
723+
auto_link: body.auto_link,
715724
};
716725
let result = writer::create_note(
717726
input,
@@ -1011,6 +1020,52 @@ async fn handle_delete(
10111020
})))
10121021
}
10131022

1023+
async fn handle_reindex_file(
1024+
State(state): State<ApiState>,
1025+
headers: HeaderMap,
1026+
Json(body): Json<ReindexFileBody>,
1027+
) -> Result<impl IntoResponse, ApiError> {
1028+
authorize(&headers, &state, true)?;
1029+
let store = state.store.lock().await;
1030+
let mut embedder = state.embedder.lock().await;
1031+
let full_path = state.vault_path.join(&body.file);
1032+
1033+
let content = std::fs::read_to_string(&full_path)
1034+
.map_err(|e| ApiError::internal(&format!("Cannot read file {}: {e}", body.file)))?;
1035+
1036+
let content_hash = {
1037+
use sha2::{Digest, Sha256};
1038+
let mut hasher = Sha256::new();
1039+
hasher.update(content.as_bytes());
1040+
format!("{:x}", hasher.finalize())
1041+
};
1042+
1043+
let config = crate::config::Config::load().unwrap_or_default();
1044+
1045+
let result = crate::indexer::index_file(
1046+
&body.file,
1047+
&content,
1048+
&content_hash,
1049+
&store,
1050+
&mut *embedder,
1051+
&state.vault_path,
1052+
&config,
1053+
)
1054+
.map_err(|e| ApiError::internal(&format!("{e:#}")))?;
1055+
1056+
store
1057+
.delete_edges_for_file(result.file_id)
1058+
.map_err(|e| ApiError::internal(&format!("{e:#}")))?;
1059+
crate::indexer::build_edges_for_file(&store, result.file_id, &content)
1060+
.map_err(|e| ApiError::internal(&format!("{e:#}")))?;
1061+
1062+
Ok(Json(serde_json::json!({
1063+
"file": body.file,
1064+
"chunks": result.total_chunks,
1065+
"docid": result.docid,
1066+
})))
1067+
}
1068+
10141069
// ---------------------------------------------------------------------------
10151070
// Tests
10161071
// ---------------------------------------------------------------------------

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1280,6 +1280,7 @@ async fn main() -> Result<()> {
12801280
tags,
12811281
folder,
12821282
created_by: "cli".into(),
1283+
auto_link: None,
12831284
};
12841285
let result = engraph::writer::create_note(
12851286
input,

src/openapi.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub fn build_openapi_spec(server_url: &str) -> serde_json::Value {
2727
paths.insert("/api/unarchive".into(), build_unarchive());
2828
paths.insert("/api/update-metadata".into(), build_update_metadata());
2929
paths.insert("/api/delete".into(), build_delete());
30+
paths.insert("/api/reindex-file".into(), build_reindex_file());
3031

3132
// Migration endpoints
3233
paths.insert("/api/migrate/preview".into(), build_migrate_preview());
@@ -37,7 +38,7 @@ pub fn build_openapi_spec(server_url: &str) -> serde_json::Value {
3738
"openapi": "3.1.0",
3839
"info": {
3940
"title": "engraph",
40-
"version": "1.5.0",
41+
"version": "1.5.5",
4142
"description": "AI-powered semantic search and management API for Obsidian vaults."
4243
},
4344
"servers": [{ "url": server_url }],
@@ -220,7 +221,8 @@ fn build_create() -> serde_json::Value {
220221
"filename": { "type": "string", "description": "Filename without .md" },
221222
"type_hint": { "type": "string", "description": "Type hint for placement" },
222223
"tags": { "type": "array", "items": { "type": "string" }, "description": "Tags to apply" },
223-
"folder": { "type": "string", "description": "Explicit folder (skips auto-placement)" }
224+
"folder": { "type": "string", "description": "Explicit folder (skips auto-placement)" },
225+
"auto_link": { "type": "boolean", "description": "Set to false to skip automatic wikilink resolution. Defaults to true." }
224226
}
225227
}}}
226228
},
@@ -428,6 +430,26 @@ fn build_delete() -> serde_json::Value {
428430
})
429431
}
430432

433+
fn build_reindex_file() -> serde_json::Value {
434+
serde_json::json!({
435+
"post": {
436+
"operationId": "reindexFile",
437+
"summary": "Re-index a single file after external edits. Re-reads, re-embeds, and updates search index.",
438+
"requestBody": {
439+
"required": true,
440+
"content": { "application/json": { "schema": {
441+
"type": "object",
442+
"required": ["file"],
443+
"properties": {
444+
"file": { "type": "string", "description": "File path relative to vault root" }
445+
}
446+
}}}
447+
},
448+
"responses": { "200": { "description": "Re-indexed file info (chunks, docid)" } }
449+
}
450+
})
451+
}
452+
431453
fn build_migrate_preview() -> serde_json::Value {
432454
serde_json::json!({
433455
"post": {

src/serve.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,8 @@ pub struct CreateParams {
8383
pub tags: Option<Vec<String>>,
8484
/// Explicit folder path (skips placement engine).
8585
pub folder: Option<String>,
86+
/// Set to false to skip automatic wikilink resolution. Defaults to true.
87+
pub auto_link: Option<bool>,
8688
}
8789

8890
#[derive(Debug, Deserialize, JsonSchema)]
@@ -184,6 +186,12 @@ pub struct DeleteParams {
184186
pub mode: Option<String>,
185187
}
186188

189+
#[derive(Debug, Deserialize, JsonSchema)]
190+
pub struct ReindexFileParams {
191+
/// File path relative to vault root (e.g. "07-Daily/2026-04-10.md").
192+
pub file: String,
193+
}
194+
187195
// ---------------------------------------------------------------------------
188196
// Server
189197
// ---------------------------------------------------------------------------
@@ -198,6 +206,7 @@ pub struct EngraphServer {
198206
embedder: Arc<Mutex<Box<dyn EmbedModel + Send>>>,
199207
vault_path: Arc<PathBuf>,
200208
profile: Arc<Option<VaultProfile>>,
209+
#[allow(dead_code)] // Required by rmcp #[tool_router] macro infrastructure
201210
tool_router: ToolRouter<Self>,
202211
/// Query expansion orchestrator (None when intelligence is disabled or failed to load).
203212
orchestrator: Option<Arc<Mutex<Box<dyn OrchestratorModel + Send>>>>,
@@ -512,6 +521,7 @@ impl EngraphServer {
512521
tags: params.0.tags.unwrap_or_default(),
513522
folder: params.0.folder,
514523
created_by: "claude-code".into(),
524+
auto_link: params.0.auto_link,
515525
};
516526
let result = crate::writer::create_note(
517527
input,
@@ -818,6 +828,64 @@ impl EngraphServer {
818828
});
819829
to_json_result(&result)
820830
}
831+
832+
#[tool(
833+
name = "reindex_file",
834+
description = "Re-index a single file after external edits. Reads the file from disk, re-embeds its chunks, and updates the search index. Use when a file was modified outside engraph and you need the index to reflect current content."
835+
)]
836+
async fn reindex_file(
837+
&self,
838+
params: Parameters<ReindexFileParams>,
839+
) -> Result<CallToolResult, McpError> {
840+
let store = self.store.lock().await;
841+
let mut embedder = self.embedder.lock().await;
842+
let rel_path = params.0.file;
843+
let full_path = self.vault_path.join(&rel_path);
844+
845+
// Read file content from disk
846+
let content = std::fs::read_to_string(&full_path).map_err(|e| {
847+
McpError::new(
848+
rmcp::model::ErrorCode::INVALID_PARAMS,
849+
format!("Cannot read file {rel_path}: {e}"),
850+
None::<serde_json::Value>,
851+
)
852+
})?;
853+
854+
let content_hash = {
855+
use sha2::{Digest, Sha256};
856+
let mut hasher = Sha256::new();
857+
hasher.update(content.as_bytes());
858+
format!("{:x}", hasher.finalize())
859+
};
860+
861+
let config = crate::config::Config::load().unwrap_or_default();
862+
863+
// Re-index the file (handles cleanup of old entries automatically)
864+
let result = crate::indexer::index_file(
865+
&rel_path,
866+
&content,
867+
&content_hash,
868+
&store,
869+
&mut *embedder,
870+
&self.vault_path,
871+
&config,
872+
)
873+
.map_err(|e| mcp_err(&e))?;
874+
875+
// Rebuild edges for the re-indexed file
876+
store
877+
.delete_edges_for_file(result.file_id)
878+
.map_err(|e| mcp_err(&e))?;
879+
crate::indexer::build_edges_for_file(&store, result.file_id, &content)
880+
.map_err(|e| mcp_err(&e))?;
881+
882+
let output = serde_json::json!({
883+
"file": rel_path,
884+
"chunks": result.total_chunks,
885+
"docid": result.docid,
886+
});
887+
to_json_result(&output)
888+
}
821889
}
822890

823891
#[tool_handler]
@@ -829,6 +897,7 @@ impl rmcp::handler::server::ServerHandler for EngraphServer {
829897
Write: create for new notes, append to add content, edit to modify a section, rewrite to replace body, \
830898
edit_frontmatter for tags/properties, update_metadata for bulk tag/alias replacement. \
831899
Lifecycle: move_note to relocate, archive to soft-delete, unarchive to restore, delete for permanent removal. \
900+
Index: reindex_file to refresh a single file's index after external edits. \
832901
Migration: migrate_preview to classify notes into PARA folders, migrate_apply to execute the migration, migrate_undo to revert.",
833902
)
834903
}

0 commit comments

Comments
 (0)