| Crates.io | turbovault-tools |
| lib.rs | turbovault-tools |
| version | 1.2.6 |
| created_at | 2025-10-24 16:30:33.267878+00 |
| updated_at | 2025-12-16 18:24:14.568354+00 |
| description | MCP tools implementation using turbomcp |
| homepage | |
| repository | |
| max_upload_size | |
| id | 1898760 |
| size | 346,711 |
MCP Tools Layer - The bridge between AI agents and Obsidian vault operations.
This crate implements the Model Context Protocol (MCP) tools that enable AI agents to discover, query, analyze, and manage Obsidian vaults through a structured, type-safe API. It orchestrates all vault operations by integrating the parser, graph, vault, batch, and export crates into a cohesive agent-friendly interface.
AI Agent (Claude, GPT, etc.)
↓
MCP Protocol
↓
┌────────────────────────────────────────┐
│ turbovault-tools (THIS CRATE) │
│ │
│ ┌──────────────────────────────────┐ │
│ │ Search Engine (Tantivy) │ │
│ │ - Full-text search │ │
│ │ - TF-IDF ranking │ │
│ │ - Tag & metadata filtering │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────┐ │
│ │ Template Engine │ │
│ │ - Pre-built templates │ │
│ │ - Field validation │ │
│ │ - Note generation │ │
│ └──────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────┐ │
│ │ 11 Tool Categories │ │
│ │ - File ops │ │
│ │ - Search & discovery │ │
│ │ - Graph analysis │ │
│ │ - Batch operations │ │
│ │ - Export & reporting │ │
│ │ - Templates │ │
│ │ - Validation │ │
│ │ - Metadata queries │ │
│ │ - Relationships │ │
│ │ - Vault lifecycle │ │
│ │ - Analysis tools │ │
│ └──────────────────────────────────┘ │
└────────────────────────────────────────┘
↓
┌────────────────────────────────────────┐
│ Integration with Other Crates │
│ │
│ turbovault-vault ─ File I/O │
│ turbovault-parser ─ OFM parsing │
│ turbovault-graph ─ Link analysis │
│ turbovault-batch ─ Transactions │
│ turbovault-export ─ Data export │
│ turbovault-core ─ Types & errors │
└────────────────────────────────────────┘
Every tool is designed with AI agents in mind:
Built on Tantivy (Apache Lucene-inspired):
Pre-built templates for common patterns:
Field types with validation:
Health monitoring and metrics:
Basic file lifecycle management:
use TurboVault_tools::FileTools;
let tools = FileTools::new(vault_manager);
// Read note content
let content = tools.read_file("notes/readme.md").await?;
// Write/create note (atomic, creates directories)
tools.write_file("notes/new-idea.md", "# My Idea\n\nContent...").await?;
// Move/rename (updates all backlinks)
tools.move_file("old/path.md", "new/path.md").await?;
// Copy with metadata preservation
tools.copy_file("template.md", "new-note.md").await?;
// Safe deletion (validates path traversal)
tools.delete_file("archive/old.md").await?;
Key Features:
Relationship discovery and navigation:
use TurboVault_tools::SearchTools;
let tools = SearchTools::new(vault_manager);
// Find all notes linking to this note
let backlinks = tools.find_backlinks("concepts/rust.md").await?;
// Find all notes this note links to
let forward_links = tools.find_forward_links("concepts/rust.md").await?;
// Find notes within N hops in link graph
let related = tools.find_related_notes("concepts/rust.md", 2).await?;
// Simple filename search
let matching = tools.search_files("todo").await?;
Key Features:
Full-text search powered by Tantivy:
use TurboVault_tools::{SearchEngine, SearchQuery};
let engine = SearchEngine::new(vault_manager).await?;
// Simple keyword search
let results = engine.search("async rust patterns").await?;
// Returns: Vec<SearchResultInfo> with scores, snippets, metadata
// Advanced search with filters
let query = SearchQuery::new("database design")
.with_tags(vec!["architecture".to_string()])
.with_frontmatter("status".to_string(), "complete".to_string())
.exclude(vec!["archive/".to_string()])
.limit(20);
let results = engine.advanced_search(query).await?;
// Search by tags only
let tagged = engine.search_by_tags(vec!["urgent", "bug"]).await?;
// Find similar notes (content-based)
let similar = engine.find_related("notes/current.md", 5).await?;
// Get recommendations for an agent
let recommendations = engine.recommend_related("notes/current.md").await?;
SearchResultInfo Structure:
pub struct SearchResultInfo {
pub path: String, // Relative to vault root
pub title: String, // From frontmatter or first heading
pub preview: String, // First 200 chars
pub score: f64, // Relevance (0.0-1.0)
pub snippet: String, // Match context with highlighting
pub tags: Vec<String>, // Frontmatter tags
pub outgoing_links: Vec<String>, // Files this note links to
pub backlink_count: usize, // How many notes link here
}
Performance: Indexes on creation (~1000 files/sec), searches in <100ms for 10k+ note vaults.
Structured note creation for agents:
use TurboVault_tools::{TemplateEngine, TemplateFieldType};
use std::collections::HashMap;
let engine = TemplateEngine::new(vault_manager);
// List all available templates
let templates = engine.list_templates();
// Returns: ["doc", "task", "research"]
// Get template details
let task_template = engine.get_template("task").unwrap();
println!("Fields: {:?}", task_template.fields);
// Create note from template
let mut fields = HashMap::new();
fields.insert("title".to_string(), "Fix database connection".to_string());
fields.insert("priority".to_string(), "high".to_string());
fields.insert("due_date".to_string(), "2025-12-31".to_string());
let created = engine.create_from_template(
"task",
"tasks/fix-db-connection.md",
fields
).await?;
// Result: CreatedNoteInfo with path, preview, template_id
// Find all notes created from a template
let task_notes = engine.find_notes_from_template("task").await?;
Built-in Templates:
doc (Documentation):
task (Action Item):
research (Research Note):
Field Validation Examples:
// Date validation
template.validate_field("due_date", "2025-12-31")?; // OK
template.validate_field("due_date", "invalid")?; // Error: Invalid date format
// Select validation
template.validate_field("priority", "high")?; // OK
template.validate_field("priority", "urgent")?; // Error: Invalid option
// Required field check
template.validate_field("title", "")?; // Error: Field is required
Vault-wide statistics and metrics:
use TurboVault_tools::AnalysisTools;
let tools = AnalysisTools::new(vault_manager);
// Get overall vault statistics
let stats = tools.get_vault_stats().await?;
// Returns: VaultStats {
// total_files, total_links, orphaned_files, average_links_per_file
// }
// Find orphaned notes (no incoming or outgoing links)
let orphans = tools.list_orphaned_notes().await?;
// Detect cycles (mutual reference chains)
let cycles = tools.detect_cycles().await?;
// Returns: Vec<Vec<String>> - each cycle is a loop of file paths
// Calculate link density (actual links / possible links)
let density = tools.get_link_density().await?;
// Returns: f64 (0.0 = no links, 1.0 = fully connected)
// Get comprehensive connectivity metrics
let metrics = tools.get_connectivity_metrics().await?;
// Returns: JSON with all metrics combined
Link graph analysis and health monitoring:
use TurboVault_tools::GraphTools;
let tools = GraphTools::new(vault_manager);
// Quick health check (fast, essential metrics only)
let health = tools.quick_health_check().await?;
// Returns: HealthInfo {
// health_score: 85,
// total_notes: 1250,
// broken_links_count: 3,
// is_healthy: true
// }
// Full health analysis (comprehensive, slower)
let full = tools.full_health_analysis().await?;
// Includes: hub_notes, dead_end_notes, isolated_clusters
// Get broken links with suggestions
let broken = tools.get_broken_links().await?;
// Returns: Vec<BrokenLinkInfo> with suggestions for fixes
// Find hub notes (highly connected)
let hubs = tools.get_hub_notes(10).await?;
// Returns: Top 10 notes by link count
// Find dead-end notes (no outgoing links)
let dead_ends = tools.get_dead_end_notes().await?;
// Detect cycles in link graph
let cycles = tools.detect_cycles().await?;
// Get connected components (isolated groups)
let components = tools.get_connected_components().await?;
// Find isolated clusters (small disconnected groups)
let clusters = tools.get_isolated_clusters().await?;
Health Score Calculation:
Atomic multi-file operations:
use TurboVault_batch::BatchOperation;
use TurboVault_tools::BatchTools;
let tools = BatchTools::new(vault_manager);
let operations = vec![
BatchOperation::CreateFile {
path: "new/note.md".to_string(),
content: "# New Note".to_string(),
},
BatchOperation::MoveFile {
from: "old/path.md".to_string(),
to: "new/path.md".to_string(),
},
BatchOperation::UpdateLinks {
file: "index.md".to_string(),
old_target: "old/path".to_string(),
new_target: "new/path".to_string(),
},
];
// Execute atomically: all succeed or all fail
let result = tools.batch_execute(operations).await?;
// Returns: BatchResult {
// success: true,
// executed: 3,
// total: 3,
// transaction_id: "uuid",
// duration_ms: 45,
// changes: ["Created: new/note.md", "Moved: old/path.md → new/path.md", ...]
// }
Batch Operation Types:
CreateFile: Create new file with contentWriteFile: Write/overwrite existing fileDeleteFile: Remove fileMoveFile: Rename/move fileUpdateLinks: Find and replace link targetsTransaction Guarantees:
Data export for downstream processing:
use TurboVault_tools::ExportTools;
let tools = ExportTools::new(vault_manager);
// Export health report
let json = tools.export_health_report("json").await?;
let csv = tools.export_health_report("csv").await?;
// Export broken links
let broken_json = tools.export_broken_links("json").await?;
// Export vault statistics
let stats_csv = tools.export_vault_stats("csv").await?;
// Export comprehensive analysis report
let analysis = tools.export_analysis_report("json").await?;
Export Formats:
Use Cases:
Content validation and quality checks:
use TurboVault_tools::ValidationTools;
let tools = ValidationTools::new(vault_manager);
// Validate single note with default rules
let report = tools.validate_note("notes/readme.md").await?;
// Returns: ValidationReportInfo {
// passed: true,
// total_issues: 2,
// warning_count: 2,
// issues: [...]
// }
// Validate with custom rules
let report = tools.validate_note_with_rules(
"notes/readme.md",
true, // require_frontmatter
vec!["title".to_string(), "tags".to_string()], // required_fields
true, // check_links
Some(100) // min_length
).await?;
// Validate entire vault
let vault_report = tools.validate_vault().await?;
// Quick validation with issue limit (for large vaults)
let quick = tools.validate_vault_quick(50).await?;
Validation Rules:
Severity Levels:
Frontmatter querying and extraction:
use TurboVault_tools::MetadataTools;
let tools = MetadataTools::new(vault_manager);
// Query files by metadata pattern
let results = tools.query_metadata(r#"status: "draft""#).await?;
let results = tools.query_metadata("priority > 3").await?;
let results = tools.query_metadata("priority < 5").await?;
let results = tools.query_metadata(r#"tags: contains("important")"#).await?;
// Get specific metadata value (supports dot notation)
let value = tools.get_metadata_value("notes/task.md", "priority").await?;
let nested = tools.get_metadata_value("notes/task.md", "config.timeout").await?;
Query Syntax:
key: "value" - Exact matchkey > number - Greater thankey < number - Less thankey: contains("substring") - String containsReturns: JSON with matched files and their metadata
Link strength analysis and suggestions:
use TurboVault_tools::RelationshipTools;
let tools = RelationshipTools::new(vault_manager);
// Calculate link strength between two files
let strength = tools.get_link_strength("notes/a.md", "notes/b.md").await?;
// Returns: {
// strength: 0.75,
// components: {
// direct_links: 2,
// backlinks: 1,
// shared_references: 3
// },
// interpretation: "Strong - frequently connected"
// }
// Get link suggestions for a file
let suggestions = tools.suggest_links("notes/current.md", 5).await?;
// Returns: Top 5 suggested links with reasons
// Get centrality ranking (importance scores)
let rankings = tools.get_centrality_ranking().await?;
// Returns: All files ranked by betweenness, closeness, eigenvector centrality
Link Strength Calculation:
strength = (direct_links * 1.0) + (backlinks * 0.7) + (shared_references * 0.3)
normalized to 0.0-1.0
Centrality Metrics:
Multi-vault management and lifecycle:
use TurboVault_tools::VaultLifecycleTools;
use std::path::Path;
let tools = VaultLifecycleTools::new(multi_vault_manager);
// Create new vault with template
let vault_info = tools.create_vault(
"research",
Path::new("/vaults/research"),
Some("research") // template: default, research, or team
).await?;
// Add existing vault
let vault_info = tools.add_vault_from_path(
"personal",
Path::new("/vaults/personal")
).await?;
// List all registered vaults
let vaults = tools.list_vaults().await?;
// Get active vault
let active = tools.get_active_vault().await?;
// Switch to different vault
tools.set_active_vault("research").await?;
// Remove vault from registry (doesn't delete files)
tools.remove_vault("old-vault").await?;
// Validate vault structure
let validation = tools.validate_vault("research").await?;
Vault Templates:
Agent Goal: "Find all high-priority tasks that are overdue"
// Step 1: Search by metadata
let results = metadata_tools.query_metadata("priority > 3").await?;
// Step 2: Filter by date
let mut overdue = Vec::new();
for file in results["files"].as_array().unwrap() {
let path = file["path"].as_str().unwrap();
let due_date = metadata_tools.get_metadata_value(path, "due_date").await?;
// Compare with current date
if is_overdue(&due_date) {
overdue.push(path);
}
}
// Step 3: Get task details
for path in overdue {
let content = file_tools.read_file(path).await?;
// Process task...
}
Agent Goal: "Analyze vault health and generate improvement report"
// Step 1: Quick health check
let health = graph_tools.quick_health_check().await?;
if health.health_score < 70 {
// Step 2: Detailed analysis
let full = graph_tools.full_health_analysis().await?;
// Step 3: Get broken links
let broken = graph_tools.get_broken_links().await?;
// Step 4: Find orphans
let orphans = analysis_tools.list_orphaned_notes().await?;
// Step 5: Get hub notes (potential index pages)
let hubs = graph_tools.get_hub_notes(10).await?;
// Step 6: Export comprehensive report
let report = export_tools.export_analysis_report("json").await?;
// Agent generates: "Your vault needs attention:
// - 15 broken links found (see suggestions)
// - 23 orphaned notes that should be linked
// - Consider creating index pages for hub topics: [hubs]"
}
Agent Goal: "Reorganize project notes into archive"
// Step 1: Find completed project notes
let completed = metadata_tools.query_metadata(r#"status: "completed""#).await?;
// Step 2: Build batch operations
let mut operations = Vec::new();
for file in completed {
let old_path = file["path"].as_str().unwrap();
let new_path = format!("archive/{}", old_path);
operations.push(BatchOperation::MoveFile {
from: old_path.to_string(),
to: new_path.clone(),
});
// Update any references
let backlinks = search_tools.find_backlinks(old_path).await?;
for backlink in backlinks {
operations.push(BatchOperation::UpdateLinks {
file: backlink.clone(),
old_target: old_path.to_string(),
new_target: new_path.clone(),
});
}
}
// Step 3: Execute atomically
let result = batch_tools.batch_execute(operations).await?;
if result.success {
// Agent reports: "Archived 12 completed projects and updated 45 references"
}
Agent Goal: "Find notes related to current topic for context"
let current_note = "concepts/async-programming.md";
// Step 1: Get direct relationships
let backlinks = search_tools.find_backlinks(current_note).await?;
let forward_links = search_tools.find_forward_links(current_note).await?;
// Step 2: Get semantically similar notes
let similar = search_engine.find_related(current_note, 10).await?;
// Step 3: Get notes within 2 hops in graph
let nearby = search_tools.find_related_notes(current_note, 2).await?;
// Step 4: Search for related tags
let vault_file = vault_manager.parse_file(Path::new(current_note)).await?;
if let Some(fm) = vault_file.frontmatter {
let tags = fm.tags();
let tagged = search_engine.search_by_tags(tags).await?;
}
// Agent synthesizes: "I found 25 related notes:
// - 5 direct references
// - 10 semantically similar (by content)
// - 8 nearby in your knowledge graph
// - 12 with shared tags"
Agent Goal: "Create a new research note about Rust concurrency"
// Step 1: List available templates
let templates = template_engine.list_templates();
// Step 2: Select appropriate template
let research = template_engine.get_template("research").unwrap();
// Step 3: Fill in fields
let mut fields = std::collections::HashMap::new();
fields.insert("topic".to_string(), "Rust Concurrency Patterns".to_string());
fields.insert("date_researched".to_string(), "2025-10-16".to_string());
// Step 4: Create note from template
let created = template_engine.create_from_template(
"research",
"research/rust-concurrency.md",
fields
).await?;
// Step 5: Find related notes to link
let related = search_engine.search("rust async concurrency").await?;
// Step 6: Update note with links
let mut content = file_tools.read_file(&created.path).await?;
content.push_str("\n## Related Notes\n");
for result in related.iter().take(5) {
content.push_str(&format!("- [[{}]]\n", result.path));
}
file_tools.write_file(&created.path, &content).await?;
// Agent reports: "Created research note and linked to 5 related topics"
The tools are registered with the MCP server via turbomcp macros:
// From crates/turbovault-server/src/tools.rs
use TurboVault_tools::*;
use turbomcp::prelude::*;
#[turbomcp::server(name = "obsidian-mcp", version = "1.0.0")]
impl ObsidianServer {
/// File operations
#[tool("read_note")]
async fn read_note(&self, path: String) -> McpResult<String> {
self.file_tools().read_file(&path).await.into_mcp()
}
/// Search operations
#[tool("search")]
async fn search(&self, query: String) -> McpResult<Vec<SearchResultInfo>> {
let engine = SearchEngine::new(self.vault_manager.clone()).await?;
engine.search(&query).await.into_mcp()
}
// ... 36 more tools ...
}
Total for 10,000 note vault: ~80MB
| Operation | Typical Latency | Notes |
|---|---|---|
| File Read | <10ms | Direct filesystem access |
| File Write | <20ms | Atomic write via temp file |
| Simple Search | <50ms | In-memory index lookup |
| Advanced Search | <100ms | With filters and ranking |
| Graph Analysis | <200ms | Full vault traversal |
| Health Check | <300ms | Comprehensive metrics |
| Batch Operation | 50ms * ops | Sequential with validation |
For Large Vaults (10k+ notes):
validate_vault_quick() instead of validate_vault().limit()For Multiple Vaults:
VaultLifecycleTools to manage multiple vaultsset_active_vault)All tools return Result<T, Error> which maps to MCP errors:
// File not found
Err(Error::NotFound("File not found: notes/missing.md"))
// → Agent receives: "NotFound" error, can suggest creating file
// Path traversal attempt
Err(Error::InvalidPath("Path outside vault: ../../etc/passwd"))
// → Agent receives: "InvalidPath" error, understands security boundary
// Validation failure
Err(Error::ValidationError("Missing required field: title"))
// → Agent receives: error with suggestion to add field
// Batch conflict
Err(Error::ConfigError("Conflicting operations on same file"))
// → Agent receives: error explaining operations need to be sequential
For Agents:
validate() before batch_execute()Example Agent Error Handling:
match file_tools.read_file("notes/task.md").await {
Ok(content) => { /* Process content */ },
Err(e) if e.is_not_found() => {
// Suggest creating file
"File doesn't exist. Should I create it from a template?"
},
Err(e) => {
// Other errors
format!("Error: {}", e)
}
}
Simple String Arguments:
read_file(path: String)
search(query: String)
validate_note(path: String)
Structured Options:
validate_note_with_rules(
path: String,
require_frontmatter: bool,
required_fields: Vec<String>,
check_links: bool,
min_length: Option<usize>
)
Builder Pattern for Complex Queries:
let query = SearchQuery::new("database")
.with_tags(vec!["architecture"])
.with_frontmatter("status", "published")
.limit(10);
Batch Operations (Enum-based):
vec![
BatchOperation::CreateFile { path, content },
BatchOperation::MoveFile { from, to },
]
Simple Values:
String // File content
Vec<String> // List of paths
bool // Success/failure
f64 // Metrics
Structured Results:
VaultStats { // Metrics
total_files: usize,
total_links: usize,
orphaned_files: usize,
average_links_per_file: f64,
}
SearchResultInfo { // Search results
path: String,
title: String,
score: f64,
snippet: String,
tags: Vec<String>,
// ...
}
BatchResult { // Operation results
success: bool,
executed: usize,
total: usize,
changes: Vec<String>,
errors: Vec<String>,
transaction_id: String,
duration_ms: u64,
}
JSON Values (for flexibility):
serde_json::Value // Metadata queries, metrics
# Run all tests in this crate
cargo test -p turbovault-tools
# Run with output
cargo test -p turbovault-tools -- --nocapture
# Run specific test
cargo test -p turbovault-tools test_search_engine
# Run with test coverage
cargo tarpaulin --packages turbovault-tools
src/my_tools.rs):use TurboVault_vault::VaultManager;
use std::sync::Arc;
pub struct MyTools {
pub manager: Arc<VaultManager>,
}
impl MyTools {
pub fn new(manager: Arc<VaultManager>) -> Self {
Self { manager }
}
pub async fn my_operation(&self, param: String) -> Result<String> {
// Implementation
Ok("result".to_string())
}
}
pub mod my_tools;
pub use my_tools::MyTools;
#[tool("my_operation")]
async fn my_operation(&self, param: String) -> McpResult<String> {
let tools = MyTools::new(self.vault_manager.clone());
tools.my_operation(param).await.into_mcp()
}
Unit Tests (in tool modules):
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_helper_function() {
assert_eq!(extract_keywords("hello world"), vec!["hello", "world"]);
}
}
Integration Tests (require vault setup):
#[tokio::test]
async fn test_search_integration() {
let temp_dir = tempfile::tempdir().unwrap();
let vault_path = temp_dir.path();
// Create test vault
let config = ServerConfig::new()
.with_vault("test", vault_path);
let manager = VaultManager::new(config).unwrap();
// Create test files
manager.write_file(
Path::new("test.md"),
"# Test\nContent here"
).await.unwrap();
// Test search
let engine = SearchEngine::new(Arc::new(manager)).await.unwrap();
let results = engine.search("content").await.unwrap();
assert!(!results.is_empty());
}
This crate integrates all other TurboVault crates:
[dependencies]
# Internal crates (ordered by dependency)
turbovault-core = { workspace = true }
turbovault-parser = { workspace = true }
turbovault-graph = { workspace = true }
turbovault-vault = { workspace = true }
turbovault-batch = { workspace = true }
turbovault-export = { workspace = true }
# MCP integration (turbomcp)
turbomcp = { version = "2.0.2", features = ["full"] }
turbomcp-protocol = "2.0.2"
turbomcp-server = "2.0.2"
# Search engine (Apache Lucene-inspired)
tantivy = "0.22"
# Core async/serde/etc
tokio = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
chrono = { workspace = true, features = ["serde"] }
Alternatives Considered:
Single Responsibility:
Composability:
Performance:
Shared State:
Thread Safety:
Lifetime Simplicity:
For deeper dives into specific areas:
crates/turbovault-core/README.mdcrates/turbovault-parser/README.mdcrates/turbovault-graph/README.mdcrates/turbovault-vault/README.mdcrates/turbovault-batch/README.mdcrates/turbovault-export/README.mdcrates/turbovault-server/README.md/docs/deployment/index.md (project root)/DILIGENCE_PASS_COMPLETE.md (project root)Potential additions (not yet implemented):
Part of the TurboVault project. See project root for license information.