better fuzzing
This commit is contained in:
3661
Cargo.lock
generated
3661
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -5,10 +5,13 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.100"
|
anyhow = "1.0.100"
|
||||||
gpui = "0.1.0"
|
iced = "0.13.1"
|
||||||
imara-diff = "0.2.0"
|
imara-diff = "0.2.0"
|
||||||
nucleo-matcher = "0.3.1"
|
nucleo-matcher = "0.3.1"
|
||||||
rmcp = { version = "0.8.0", features = ["server", "macros", "transport-sse-server", "transport-io", "transport-streamable-http-server", "elicitation", "schemars"] }
|
rmcp = { version = "0.8.0", features = ["server", "macros", "transport-sse-server", "transport-io", "transport-streamable-http-server", "elicitation", "schemars"] }
|
||||||
|
seal = "0.1.6"
|
||||||
serde = "1.0.228"
|
serde = "1.0.228"
|
||||||
serde_json = "1.0.145"
|
serde_json = "1.0.145"
|
||||||
tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.47.1", features = ["rt-multi-thread", "macros"] }
|
||||||
|
tracing = "0.1.41"
|
||||||
|
tracing-subscriber = { version = "0.3.20", features = ["fmt", "env-filter", "std"] }
|
||||||
|
|||||||
0
src/gui.rs
Normal file
0
src/gui.rs
Normal file
19
src/main.rs
19
src/main.rs
@@ -1,5 +1,6 @@
|
|||||||
use crate::mcp::FossilEditor;
|
use crate::mcp::FossilEditor;
|
||||||
use rmcp::{ServiceExt, transport::stdio};
|
use rmcp::{ServiceExt, transport::stdio};
|
||||||
|
use tracing_subscriber::{self, EnvFilter};
|
||||||
|
|
||||||
mod fossil;
|
mod fossil;
|
||||||
mod matcher;
|
mod matcher;
|
||||||
@@ -7,6 +8,22 @@ mod mcp;
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::DEBUG.into()))
|
||||||
|
.with_writer(std::io::stderr)
|
||||||
|
.with_ansi(false)
|
||||||
|
.init();
|
||||||
|
|
||||||
|
tracing::info!("Starting MCP server");
|
||||||
|
|
||||||
let editor = FossilEditor::new();
|
let editor = FossilEditor::new();
|
||||||
let _ = editor.serve(stdio()).await;
|
let service = editor
|
||||||
|
.serve(stdio())
|
||||||
|
.await
|
||||||
|
.inspect_err(|e| {
|
||||||
|
tracing::error!("serving error: {:?}", e);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
service.waiting().await.unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
166
src/matcher.rs
166
src/matcher.rs
@@ -1,105 +1,89 @@
|
|||||||
use nucleo_matcher::{
|
use seal::pair::{AlignmentSet, InMemoryAlignmentMatrix, SmithWaterman, Step};
|
||||||
Config, Matcher,
|
|
||||||
pattern::{AtomKind, CaseMatching, Normalization, Pattern},
|
|
||||||
};
|
|
||||||
use std::sync::{Mutex, OnceLock};
|
|
||||||
|
|
||||||
static MATCHER: OnceLock<Mutex<Matcher>> = OnceLock::new();
|
|
||||||
|
|
||||||
pub fn match_lines(code: &str, start: &str, end: &str) -> Option<(usize, usize)> {
|
pub fn match_lines(code: &str, start: &str, end: &str) -> Option<(usize, usize)> {
|
||||||
let mut matcher = MATCHER
|
let code_chars: Vec<char> = code.chars().collect();
|
||||||
.get_or_init(|| Mutex::new(Matcher::new(Config::DEFAULT)))
|
|
||||||
.lock()
|
if code_chars.is_empty() || start.is_empty() || end.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let strategy = SmithWaterman::new(3, -1, -1, -1);
|
||||||
|
|
||||||
|
if start.ends_with(end) {
|
||||||
|
let pattern_chars: Vec<char> = start.chars().collect();
|
||||||
|
let set: AlignmentSet<InMemoryAlignmentMatrix> =
|
||||||
|
AlignmentSet::new(code_chars.len(), pattern_chars.len(), strategy, |x, y| {
|
||||||
|
code_chars[x] == pattern_chars[y]
|
||||||
|
})
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
|
||||||
let lines: Vec<&str> = code.lines().collect();
|
let alignment = set.local_alignment();
|
||||||
let n = lines.len();
|
let mut start_char_pos = None;
|
||||||
|
let mut end_char_pos = None;
|
||||||
|
|
||||||
let start_parts: Vec<&str> = start.split('\n').collect();
|
for step in alignment.steps() {
|
||||||
let end_parts: Vec<&str> = end.split('\n').collect();
|
if let Step::Align { x, .. } = step {
|
||||||
|
if start_char_pos.is_none() {
|
||||||
let start_len = start_parts.len();
|
start_char_pos = Some(x);
|
||||||
let end_len = end_parts.len();
|
|
||||||
|
|
||||||
if start_len == 0 || end_len == 0 || n == 0 {
|
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
|
end_char_pos = Some(x);
|
||||||
let non_empty: Vec<bool> = lines.iter().map(|line| !line.trim().is_empty()).collect();
|
|
||||||
let nei: Vec<usize> = (0..n).filter(|&i| non_empty[i]).collect();
|
|
||||||
let m = nei.len();
|
|
||||||
|
|
||||||
if m < start_len || m < end_len {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let start_patterns: Vec<Pattern> = start_parts
|
|
||||||
.iter()
|
|
||||||
.map(|&part| {
|
|
||||||
Pattern::new(
|
|
||||||
part,
|
|
||||||
CaseMatching::Ignore,
|
|
||||||
Normalization::Smart,
|
|
||||||
AtomKind::Fuzzy,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let end_patterns: Vec<Pattern> = end_parts
|
|
||||||
.iter()
|
|
||||||
.map(|&part| {
|
|
||||||
Pattern::new(
|
|
||||||
part,
|
|
||||||
CaseMatching::Ignore,
|
|
||||||
Normalization::Smart,
|
|
||||||
AtomKind::Fuzzy,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut start_candidates: Vec<(usize, usize)> = vec![];
|
|
||||||
|
|
||||||
for p in 0..=m - start_len {
|
|
||||||
let matches_all = (0..start_len).all(|k| {
|
|
||||||
let line = lines[nei[p + k]];
|
|
||||||
!start_patterns[k]
|
|
||||||
.match_list(std::iter::once(line), &mut matcher)
|
|
||||||
.is_empty()
|
|
||||||
});
|
|
||||||
if matches_all {
|
|
||||||
start_candidates.push((nei[p], nei[p + start_len - 1]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut end_candidates: Vec<(usize, usize)> = vec![];
|
let start_char = start_char_pos?;
|
||||||
|
let end_char = end_char_pos?;
|
||||||
|
|
||||||
for p in 0..=m - end_len {
|
let start_line = code[..start_char].lines().count().saturating_sub(1);
|
||||||
let matches_all = (0..end_len).all(|k| {
|
let end_line = code[..=end_char].lines().count().saturating_sub(1);
|
||||||
let line = lines[nei[p + k]];
|
|
||||||
!end_patterns[k]
|
|
||||||
.match_list(std::iter::once(line), &mut matcher)
|
|
||||||
.is_empty()
|
|
||||||
});
|
|
||||||
if matches_all {
|
|
||||||
end_candidates.push((nei[p], nei[p + end_len - 1]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut result = None;
|
Some((start_line, end_line))
|
||||||
|
|
||||||
for &(s_start, s_end) in &start_candidates {
|
|
||||||
let pos = end_candidates.partition_point(|&(e_start, _)| e_start <= s_end);
|
|
||||||
|
|
||||||
if end_candidates[pos..].len() == 1 {
|
|
||||||
let (_, e_end) = end_candidates[pos];
|
|
||||||
if result.is_some() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
result = Some((s_start, e_end));
|
|
||||||
} else {
|
} else {
|
||||||
return None;
|
let start_chars: Vec<char> = start.chars().collect();
|
||||||
}
|
let start_set: AlignmentSet<InMemoryAlignmentMatrix> = AlignmentSet::new(
|
||||||
}
|
code_chars.len(),
|
||||||
|
start_chars.len(),
|
||||||
|
strategy.clone(),
|
||||||
|
|x, y| code_chars[x] == start_chars[y],
|
||||||
|
)
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
result
|
let start_alignment = start_set.local_alignment();
|
||||||
|
let start_char_pos = start_alignment
|
||||||
|
.steps()
|
||||||
|
.filter_map(|step| {
|
||||||
|
if let Step::Align { x, .. } = step {
|
||||||
|
Some(x)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.next()?;
|
||||||
|
|
||||||
|
let end_chars: Vec<char> = end.chars().collect();
|
||||||
|
let remaining_code: Vec<char> = code_chars[start_char_pos..].to_vec();
|
||||||
|
let end_set: AlignmentSet<InMemoryAlignmentMatrix> =
|
||||||
|
AlignmentSet::new(remaining_code.len(), end_chars.len(), strategy, |x, y| {
|
||||||
|
remaining_code[x] == end_chars[y]
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let end_alignment = end_set.local_alignments().next().unwrap();
|
||||||
|
let end_char_pos_relative = end_alignment
|
||||||
|
.steps()
|
||||||
|
.filter_map(|step| {
|
||||||
|
if let Step::Align { x, .. } = step {
|
||||||
|
Some(x)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.last()?;
|
||||||
|
|
||||||
|
let end_char_pos = start_char_pos + end_char_pos_relative;
|
||||||
|
|
||||||
|
let start_line = code[..start_char_pos].lines().count().saturating_sub(1);
|
||||||
|
let end_line = code[..=end_char_pos].lines().count().saturating_sub(1);
|
||||||
|
|
||||||
|
Some((start_line, end_line))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
505
src/mcp.rs
505
src/mcp.rs
@@ -1,7 +1,7 @@
|
|||||||
use crate::fossil::{Fossil, FossilManager};
|
use crate::fossil::{Fossil, FossilManager};
|
||||||
use crate::matcher::match_lines;
|
use crate::matcher::match_lines;
|
||||||
use rmcp::model::{
|
use rmcp::model::{
|
||||||
AnnotateAble, ListResourceTemplatesRequest, ListResourceTemplatesResult, ListResourcesResult,
|
AnnotateAble, Implementation, ListResourceTemplatesResult, ListResourcesResult,
|
||||||
PaginatedRequestParam, RawResource, ReadResourceRequestParam, ReadResourceResult,
|
PaginatedRequestParam, RawResource, ReadResourceRequestParam, ReadResourceResult,
|
||||||
ResourceContents,
|
ResourceContents,
|
||||||
};
|
};
|
||||||
@@ -9,10 +9,7 @@ use rmcp::service::RequestContext;
|
|||||||
use rmcp::{ErrorData, RoleServer};
|
use rmcp::{ErrorData, RoleServer};
|
||||||
use rmcp::{
|
use rmcp::{
|
||||||
ServerHandler,
|
ServerHandler,
|
||||||
handler::server::{
|
handler::server::{router::tool::ToolRouter, wrapper::Parameters},
|
||||||
router::tool::ToolRouter,
|
|
||||||
wrapper::{Json, Parameters},
|
|
||||||
},
|
|
||||||
model::{ServerCapabilities, ServerInfo},
|
model::{ServerCapabilities, ServerInfo},
|
||||||
schemars, tool, tool_handler, tool_router,
|
schemars, tool, tool_handler, tool_router,
|
||||||
};
|
};
|
||||||
@@ -36,9 +33,13 @@ pub struct FossilIdRequest {
|
|||||||
pub struct FuzzyEditRequest {
|
pub struct FuzzyEditRequest {
|
||||||
#[schemars(description = "The unique identifier for the fossil to edit.")]
|
#[schemars(description = "The unique identifier for the fossil to edit.")]
|
||||||
pub id: String,
|
pub id: String,
|
||||||
#[schemars(description = "The start of the code to replace.")]
|
#[schemars(
|
||||||
|
description = "The start of the text to replace. Please try your best to provide the whole line. Do not attempt to mess with it since it will make it worse."
|
||||||
|
)]
|
||||||
pub start_pattern: String,
|
pub start_pattern: String,
|
||||||
#[schemars(description = "The end of the code to replace.")]
|
#[schemars(
|
||||||
|
description = "The end of the text to replace. Please try your best to provide the whole line. Do not attempt to mess with it since it will make it worse."
|
||||||
|
)]
|
||||||
pub end_pattern: String,
|
pub end_pattern: String,
|
||||||
#[schemars(
|
#[schemars(
|
||||||
description = "The new code that will replace the matched section. An empty string performs a deletion."
|
description = "The new code that will replace the matched section. An empty string performs a deletion."
|
||||||
@@ -80,19 +81,29 @@ impl FossilEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tool(description = "Creates a new code fossil in memory with initial content.")]
|
#[tool(description = "Creates a new code fossil in memory with initial content.")]
|
||||||
fn create_fossil(&self, Parameters(req): Parameters<CreateFossilRequest>) -> String {
|
fn create_fossil(
|
||||||
|
&self,
|
||||||
|
Parameters(req): Parameters<CreateFossilRequest>,
|
||||||
|
) -> Result<String, ErrorData> {
|
||||||
let mut fossils = self.state.fossils.lock().unwrap();
|
let mut fossils = self.state.fossils.lock().unwrap();
|
||||||
if fossils.contains_key(&req.id) {
|
if fossils.contains_key(&req.id) {
|
||||||
return format!("Error: Fossil with id '{}' already exists.", req.id);
|
return Err(ErrorData::invalid_params(
|
||||||
|
"fossil_exists",
|
||||||
|
Some(json!({
|
||||||
|
"id": req.id,
|
||||||
|
"message": format!("Fossil with id '{}' already exists.", req.id)
|
||||||
|
})),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
fossils.insert(req.id.clone(), Fossil::new(req.content));
|
fossils.insert(req.id.clone(), Fossil::new(req.content));
|
||||||
format!("Successfully created fossil '{}'.", req.id)
|
Ok(format!("Successfully created fossil '{}'.", req.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tool(description = "Lists the IDs of all currently managed fossils.")]
|
#[tool(description = "Lists the IDs of all currently managed fossils.")]
|
||||||
fn list_fossils(&self) -> Json<Vec<String>> {
|
fn list_fossils(&self) -> Result<String, ErrorData> {
|
||||||
let fossils = self.state.fossils.lock().unwrap();
|
let fossils = self.state.fossils.lock().unwrap();
|
||||||
Json(fossils.keys().cloned().collect())
|
let ids: Vec<String> = fossils.keys().cloned().collect();
|
||||||
|
Ok(format!("Current fossils: {}", ids.join(", ")))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tool(
|
#[tool(
|
||||||
@@ -101,51 +112,97 @@ impl FossilEditor {
|
|||||||
fn get_version(
|
fn get_version(
|
||||||
&self,
|
&self,
|
||||||
Parameters(req): Parameters<GetVersionRequest>,
|
Parameters(req): Parameters<GetVersionRequest>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, ErrorData> {
|
||||||
let fossils = self.state.fossils.lock().unwrap();
|
let fossils = self.state.fossils.lock().unwrap();
|
||||||
let fossil = fossils
|
let fossil = fossils.get(&req.id).ok_or_else(|| {
|
||||||
.get(&req.id)
|
ErrorData::resource_not_found(
|
||||||
.ok_or(format!("Error: Fossil '{}' not found.", req.id))?;
|
"fossil_not_found",
|
||||||
|
Some(json!({
|
||||||
|
"id": req.id,
|
||||||
|
"message": format!("Fossil '{}' not found.", req.id)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
let history = fossil.history();
|
let history = fossil.history();
|
||||||
let len = history.len() as i32;
|
let len = history.len() as i32;
|
||||||
if len == 0 {
|
if len == 0 {
|
||||||
return Err("Error: Fossil has no history.".to_string());
|
return Err(ErrorData::invalid_params(
|
||||||
|
"no_history",
|
||||||
|
Some(json!({
|
||||||
|
"id": req.id,
|
||||||
|
"message": "Fossil has no history."
|
||||||
|
})),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
let mut idx = req.version.unwrap_or(-1);
|
let mut idx = req.version.unwrap_or(-1);
|
||||||
if idx < 0 {
|
if idx < 0 {
|
||||||
idx += len;
|
idx += len;
|
||||||
}
|
}
|
||||||
if idx < 0 || idx >= len {
|
if idx < 0 || idx >= len {
|
||||||
return Err(format!(
|
return Err(ErrorData::invalid_params(
|
||||||
"Error: Invalid version index {} for history of length {}.",
|
"invalid_version",
|
||||||
|
Some(json!({
|
||||||
|
"id": req.id,
|
||||||
|
"version": req.version.unwrap_or(-1),
|
||||||
|
"history_length": len,
|
||||||
|
"message": format!(
|
||||||
|
"Invalid version index {} for history of length {}.",
|
||||||
req.version.unwrap_or(-1),
|
req.version.unwrap_or(-1),
|
||||||
len
|
len
|
||||||
|
)
|
||||||
|
})),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Ok(history[idx as usize].clone())
|
Ok(history[idx as usize].clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tool(
|
#[tool(
|
||||||
description = "Replaces a section of code in an fossil using fuzzy matching. Providing an empty string for `replacement` deletes the section. Optional flags for more flexible matching."
|
description = "Replaces a section of code in an fossil using fuzzy matching. Providing an empty string for `replacement` deletes the section."
|
||||||
)]
|
)]
|
||||||
fn fuzzy_edit(&self, Parameters(req): Parameters<FuzzyEditRequest>) -> Result<String, String> {
|
fn fuzzy_edit(
|
||||||
|
&self,
|
||||||
|
Parameters(req): Parameters<FuzzyEditRequest>,
|
||||||
|
) -> Result<String, ErrorData> {
|
||||||
let mut fossils = self.state.fossils.lock().unwrap();
|
let mut fossils = self.state.fossils.lock().unwrap();
|
||||||
let fossil = fossils
|
let fossil = fossils.get_mut(&req.id).ok_or_else(|| {
|
||||||
.get_mut(&req.id)
|
ErrorData::resource_not_found(
|
||||||
.ok_or_else(|| format!("Error: Fossil '{}' not found.", req.id))?;
|
"fossil_not_found",
|
||||||
|
Some(json!({
|
||||||
|
"id": req.id,
|
||||||
|
"message": format!("Fossil '{}' not found.", req.id)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
let current_code = fossil.latest().ok_or("Error: Fossil is empty.")?.clone();
|
let current_code = fossil
|
||||||
|
.latest()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
ErrorData::invalid_params(
|
||||||
|
"fossil_empty",
|
||||||
|
Some(json!({
|
||||||
|
"id": req.id,
|
||||||
|
"message": "Fossil is empty."
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.clone();
|
||||||
|
|
||||||
let (start_line, end_line) =
|
let (start_line, end_line) =
|
||||||
match_lines(¤t_code, &req.start_pattern, &req.end_pattern)
|
match_lines(¤t_code, &req.start_pattern, &req.end_pattern)
|
||||||
.ok_or("Error: Fuzzy match failed. More than one pattern match detected. Please make start/end a bit longer and more specific?")?;
|
.ok_or_else(|| ErrorData::invalid_params(
|
||||||
|
"fuzzy_match_failed",
|
||||||
|
Some(json!({
|
||||||
|
"id": req.id,
|
||||||
|
"message": "Fuzzy match failed. More than one pattern match detected. Please make start/end a bit longer and more specific."
|
||||||
|
})),
|
||||||
|
))?;
|
||||||
|
|
||||||
let lines: Vec<&str> = current_code.lines().collect();
|
let lines: Vec<&str> = current_code.lines().collect();
|
||||||
let mut new_code_lines = Vec::new();
|
let mut new_code_lines = Vec::new();
|
||||||
|
|
||||||
new_code_lines.extend_from_slice(&lines[..start_line - 1]);
|
new_code_lines.extend_from_slice(&lines[..start_line + 1]);
|
||||||
new_code_lines.push(&req.replacement);
|
new_code_lines.push(&req.replacement);
|
||||||
new_code_lines.extend_from_slice(&lines[end_line..]);
|
new_code_lines.extend_from_slice(&lines[end_line + 1..]);
|
||||||
|
|
||||||
let new_code = new_code_lines.join("\n");
|
let new_code = new_code_lines.join("\n");
|
||||||
let diff = fossil.commit(new_code.clone());
|
let diff = fossil.commit(new_code.clone());
|
||||||
@@ -157,11 +214,17 @@ impl FossilEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tool(description = "Reverts an fossil to a previous version (undo).")]
|
#[tool(description = "Reverts an fossil to a previous version (undo).")]
|
||||||
fn revert(&self, Parameters(req): Parameters<RevertRequest>) -> Result<String, String> {
|
fn revert(&self, Parameters(req): Parameters<RevertRequest>) -> Result<String, ErrorData> {
|
||||||
let mut fossils = self.state.fossils.lock().unwrap();
|
let mut fossils = self.state.fossils.lock().unwrap();
|
||||||
let fossil = fossils
|
let fossil = fossils.get_mut(&req.id).ok_or_else(|| {
|
||||||
.get_mut(&req.id)
|
ErrorData::resource_not_found(
|
||||||
.ok_or_else(|| format!("Error: Fossil '{}' not found.", req.id))?;
|
"fossil_not_found",
|
||||||
|
Some(json!({
|
||||||
|
"id": req.id,
|
||||||
|
"message": format!("Fossil '{}' not found.", req.id)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
if let Ok(diff) = fossil.revert(req.steps.unwrap_or(1)) {
|
if let Ok(diff) = fossil.revert(req.steps.unwrap_or(1)) {
|
||||||
Ok(format!(
|
Ok(format!(
|
||||||
@@ -169,20 +232,35 @@ impl FossilEditor {
|
|||||||
req.id, diff
|
req.id, diff
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
Err(format!(
|
Err(ErrorData::invalid_params(
|
||||||
"Error: Could not revert fossil '{}'. Not enough history.",
|
"revert_failed",
|
||||||
|
Some(json!({
|
||||||
|
"id": req.id,
|
||||||
|
"message": format!(
|
||||||
|
"Could not revert fossil '{}'. Not enough history.",
|
||||||
req.id
|
req.id
|
||||||
|
)
|
||||||
|
})),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tool(description = "Deletes a fossil by its ID.")]
|
#[tool(description = "Deletes a fossil by its ID.")]
|
||||||
fn delete_fossil(&self, Parameters(req): Parameters<FossilIdRequest>) -> String {
|
fn delete_fossil(
|
||||||
|
&self,
|
||||||
|
Parameters(req): Parameters<FossilIdRequest>,
|
||||||
|
) -> Result<String, ErrorData> {
|
||||||
let mut fossils = self.state.fossils.lock().unwrap();
|
let mut fossils = self.state.fossils.lock().unwrap();
|
||||||
if fossils.remove(&req.id).is_some() {
|
if fossils.remove(&req.id).is_some() {
|
||||||
format!("Successfully deleted fossil '{}'.", req.id)
|
Ok(format!("Successfully deleted fossil '{}'.", req.id))
|
||||||
} else {
|
} else {
|
||||||
format!("Error: Fossil '{}' not found.", req.id)
|
Err(ErrorData::resource_not_found(
|
||||||
|
"fossil_not_found",
|
||||||
|
Some(json!({
|
||||||
|
"id": req.id,
|
||||||
|
"message": format!("Fossil '{}' not found.", req.id)
|
||||||
|
})),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -263,21 +341,350 @@ impl ServerHandler for FossilEditor {
|
|||||||
|
|
||||||
fn get_info(&self) -> ServerInfo {
|
fn get_info(&self) -> ServerInfo {
|
||||||
ServerInfo {
|
ServerInfo {
|
||||||
|
server_info: Implementation::from_build_env(),
|
||||||
instructions: Some(
|
instructions: Some(
|
||||||
r#"Artifact but you don't have to rewrite the entire code/poem/letter to edit things iteratively. This toolset allows you to manage versioned text artifacts (called "fossils") in memory, with flexible editing capabilities designed for iterative refinement. Think of each fossil as a living document you can create, read, modify, and undo changes on without starting over. Handles edge cases like empty fossils gracefully (e.g., errors on edit/get if empty), and content can be large enough.
|
r#"# Fossil MCP Server - Complete User Guide
|
||||||
|
|
||||||
- Use `create_fossil` to start a new fossil with initial content. Provide a unique ID (like a filename) and the starting text.
|
## Overview
|
||||||
- Use `list_fossils` to see a list of all current fossil IDs.
|
Fossil is an MCP server that provides git-like version control for code artifacts with fuzzy pattern matching. It enables precise, incremental code editing without rewriting entire files.
|
||||||
- Use `get_version` to retrieve a specific version of the fossil by its ID and version index (0 for initial, -1 for latest, etc.). Use this to preview history or past states without reverting.
|
|
||||||
- Use `fuzzy_edit` as your main tool for making changes. This lets you target and replace (or delete) sections of the content using flexible pattern matching:
|
## Core Concepts
|
||||||
- `start_pattern`: A string that identifies the beginning of the section you want to edit. It can be a partial line, a substring, or even a few words— the matching is fuzzy and tolerant of minor variations like extra whitespace, comments, or small typos, as long as it's unique in the document.
|
|
||||||
- `end_pattern`: A string that identifies the end of the section (exclusive). Similar to start_pattern, it can be partial and fuzzy.
|
### What is a Fossil?
|
||||||
- `replacement`: The new text to insert in place of the matched section. Use an empty string ("") to delete the section entirely.
|
A \"fossil\" is a versioned code artifact stored in memory. Each fossil has:
|
||||||
- Flexibility: The fuzzy matching is designed to be robust and LLM-friendly—it searches for the best unique match without requiring exact full-line copies. If the document has only one reasonable match for your patterns, it will find it even if the patterns are short or approximate. If there's ambiguity (e.g., multiple possible matches), it will error out and suggest making patterns longer/more specific. This means you can confidently use descriptive snippets like "def my_function(" for start and ")" for end in code, or "Dear [Name]," for start and "Best regards," for end in a letter. For insertions, set start_pattern and end_pattern to point to the same spot (e.g., both matching the line before/after where you want to insert). For appends, use an end_pattern that matches beyond the current end or leave it minimal if the tool supports it. Experiment iteratively— if a match fails, just refine the patterns based on the error.
|
- A unique ID (e.g., `\"main.py\"`, `\"config-v2.json\"`)
|
||||||
- Use `revert` to undo recent edits on a fossil. Specify `steps` to undo multiple (defaults to 1). This rolls back to previous versions without losing history. Preview with `get_version` first if needed.
|
- Full version history (like git commits)
|
||||||
- Use `delete_fossil` to clean up and remove unused fossils by ID."#.into(),
|
- Support for fuzzy pattern matching during edits
|
||||||
|
|
||||||
|
### Why Fuzzy Matching?
|
||||||
|
Traditional string replacement requires exact character-by-character matches. Fuzzy matching allows you to:
|
||||||
|
- Ignore whitespace differences (spaces, tabs, blank lines)
|
||||||
|
- Match patterns even if formatting changed slightly
|
||||||
|
- Target code sections without knowing exact formatting
|
||||||
|
|
||||||
|
## Available Functions
|
||||||
|
|
||||||
|
### 1. `create_fossil`
|
||||||
|
Creates a new fossil with initial content.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `id` (string, required): Unique identifier for the fossil
|
||||||
|
- Use descriptive names like `\"api-handler.py\"` or `\"utils-v1.js\"`
|
||||||
|
- This ID is used to reference the fossil in all other operations
|
||||||
|
- `content` (string, required): Initial code/text content
|
||||||
|
- Can be any text content (code, JSON, markdown, etc.)
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```
|
||||||
|
create_fossil(
|
||||||
|
id: \"calculator.py\",
|
||||||
|
content: \"def add(a, b):\
|
||||||
|
return a + b\"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** Success message with the fossil ID
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. `list_fossils`
|
||||||
|
Lists all currently managed fossils.
|
||||||
|
|
||||||
|
**Parameters:** None
|
||||||
|
|
||||||
|
**Returns:** Array of fossil IDs currently in memory
|
||||||
|
|
||||||
|
**Example output:**
|
||||||
|
```
|
||||||
|
Current fossils: main.py, config.json, utils.js
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. `fuzzy_edit`
|
||||||
|
Replaces a section of code using fuzzy pattern matching. This is the primary editing function.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `id` (string, required): The fossil ID to edit
|
||||||
|
- `start_pattern` (string, required): Pattern marking the START of the section to replace
|
||||||
|
- Should be distinctive enough to find the right location
|
||||||
|
- Doesn't need exact whitespace - fuzzy matching handles variations
|
||||||
|
- **Best practice:** Use complete lines when possible
|
||||||
|
- `end_pattern` (string, required): Pattern marking the END of the section to replace
|
||||||
|
- The matched section includes both start and end patterns
|
||||||
|
- **Best practice:** Use complete lines when possible
|
||||||
|
- `replacement` (string, required): New code that replaces the matched section
|
||||||
|
- Empty string `\"\"` performs deletion (equivalent to `delete_fossil` functionality)
|
||||||
|
|
||||||
|
**How it works:**
|
||||||
|
1. Finds the first occurrence of `start_pattern` in the file
|
||||||
|
2. Finds the first occurrence of `end_pattern` after the start
|
||||||
|
3. Replaces everything from start to end (inclusive) with `replacement`
|
||||||
|
|
||||||
|
**Important Notes:**
|
||||||
|
- Patterns are fuzzy matched - whitespace differences are tolerated
|
||||||
|
- Both start and end patterns are INCLUDED in the replacement
|
||||||
|
- If you want to keep the start/end patterns, include them in the replacement
|
||||||
|
- Empty replacement = deletion
|
||||||
|
|
||||||
|
**Example 1 - Simple replacement:**
|
||||||
|
```
|
||||||
|
# Original code:
|
||||||
|
def greet(name):
|
||||||
|
return \"Hello\"
|
||||||
|
|
||||||
|
# Edit:
|
||||||
|
fuzzy_edit(
|
||||||
|
id: \"main.py\",
|
||||||
|
start_pattern: \"def greet(name):\",
|
||||||
|
end_pattern: \"return \\\"Hello\\\"\",
|
||||||
|
replacement: \"def greet(name):\
|
||||||
|
return f\\\"Hello, {name}!\\\"\"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Result:
|
||||||
|
def greet(name):
|
||||||
|
return f\"Hello, {name}!\"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example 2 - Adding a parameter:**
|
||||||
|
```
|
||||||
|
# Original:
|
||||||
|
def calculate(x, y):
|
||||||
|
|
||||||
|
# Edit:
|
||||||
|
fuzzy_edit(
|
||||||
|
id: \"calc.py\",
|
||||||
|
start_pattern: \"def calculate(x, y):\",
|
||||||
|
end_pattern: \"def calculate(x, y):\",
|
||||||
|
replacement: \"def calculate(x, y, operation='add'):\"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Result:
|
||||||
|
def calculate(x, y, operation='add'):
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example 3 - Deletion (empty replacement):**
|
||||||
|
```
|
||||||
|
fuzzy_edit(
|
||||||
|
id: \"main.py\",
|
||||||
|
start_pattern: \"\# TODO: remove this\",
|
||||||
|
end_pattern: \"\# End of temporary code\",
|
||||||
|
replacement: \"\"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Example 4 - Append to end of file:**
|
||||||
|
```
|
||||||
|
# Target the last line and rewrite from there
|
||||||
|
fuzzy_edit(
|
||||||
|
id: \"main.py\",
|
||||||
|
start_pattern: \"if __name__ == '__main__':\",
|
||||||
|
end_pattern: \" main()\",
|
||||||
|
replacement: \"if __name__ == '__main__':\
|
||||||
|
main()\
|
||||||
|
\
|
||||||
|
def new_function():\
|
||||||
|
pass\"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- Success message with a clean diff showing what changed
|
||||||
|
- The diff uses `@@ line numbers @@` format
|
||||||
|
- `+` indicates added lines, `-` indicates removed lines
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. `get_version`
|
||||||
|
Retrieves a specific version of a fossil without modifying it. Use this to preview history.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `id` (string, required): The fossil ID
|
||||||
|
- `version` (integer, optional): Version index to retrieve
|
||||||
|
- `0` = initial version (first creation)
|
||||||
|
- `1, 2, 3...` = subsequent versions (counting from start)
|
||||||
|
- `-1` = latest version (default)
|
||||||
|
- `-2` = previous version
|
||||||
|
- `-3` = two versions ago, etc.
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```
|
||||||
|
get_version(id: \"main.py\", version: 0) # See original
|
||||||
|
get_version(id: \"main.py\", version: -1) # See latest
|
||||||
|
get_version(id: \"main.py\", version: -2) # See previous
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:** The complete content of the fossil at that version
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. `revert`
|
||||||
|
Undoes recent edits by rolling back to a previous version. This actually modifies the fossil.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `id` (string, required): The fossil ID to revert
|
||||||
|
- `steps` (integer, optional): Number of edits to undo (default: 1)
|
||||||
|
- `steps: 1` = undo last edit
|
||||||
|
- `steps: 2` = undo last 2 edits
|
||||||
|
- `steps: 5` = undo last 5 edits
|
||||||
|
|
||||||
|
**Example:**
|
||||||
|
```
|
||||||
|
revert(id: \"main.py\", steps: 1) # Undo last edit
|
||||||
|
revert(id: \"main.py\", steps: 3) # Undo last 3 edits
|
||||||
|
```
|
||||||
|
|
||||||
|
**Returns:**
|
||||||
|
- Success message with diff showing what was reverted
|
||||||
|
- The fossil is now at the earlier version
|
||||||
|
|
||||||
|
**Use cases:**
|
||||||
|
- \"Oops, that edit broke something\" → `revert(steps: 1)`
|
||||||
|
- \"Let's try a different approach\" → `revert()` then make new edit
|
||||||
|
- \"Go back to working version\" → `revert(steps: 5)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. `delete_fossil`
|
||||||
|
Permanently deletes a fossil and all its history.
|
||||||
|
|
||||||
|
**Parameters:**
|
||||||
|
- `id` (string, required): The fossil ID to delete
|
||||||
|
|
||||||
|
**Returns:** Success confirmation
|
||||||
|
|
||||||
|
**Warning:** This cannot be undone! The entire fossil and its version history are removed from memory.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Workflow Patterns
|
||||||
|
|
||||||
|
### Pattern 1: Create and Iterate
|
||||||
|
```
|
||||||
|
1. create_fossil(id: \"app.py\", content: \"initial code\")
|
||||||
|
2. fuzzy_edit(id: \"app.py\", ...) # Add feature
|
||||||
|
3. fuzzy_edit(id: \"app.py\", ...) # Fix bug
|
||||||
|
4. fuzzy_edit(id: \"app.py\", ...) # Refactor
|
||||||
|
5. If something breaks: revert(id: \"app.py\", steps: 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 2: Surgical Edits
|
||||||
|
```
|
||||||
|
# Only change what needs changing
|
||||||
|
fuzzy_edit(
|
||||||
|
start_pattern: \"def problematic_function\",
|
||||||
|
end_pattern: \"return result\",
|
||||||
|
replacement: \"def fixed_function...\
|
||||||
|
return new_result\"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 3: Exploration with Safety Net
|
||||||
|
```
|
||||||
|
1. get_version(id: \"main.py\", version: -1) # Check current state
|
||||||
|
2. fuzzy_edit(...) # Try experimental change
|
||||||
|
3. Test it mentally/logically
|
||||||
|
4. If it works: continue
|
||||||
|
5. If it breaks: revert(steps: 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pattern 4: Append Pattern
|
||||||
|
```
|
||||||
|
# To add to end of file, target the last line(s)
|
||||||
|
fuzzy_edit(
|
||||||
|
start_pattern: \"last_line_or_function\",
|
||||||
|
end_pattern: \"last_line_or_function\",
|
||||||
|
replacement: \"last_line_or_function\
|
||||||
|
\
|
||||||
|
new_content_here\"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### ✅ DO:
|
||||||
|
- Use distinctive patterns that uniquely identify the location
|
||||||
|
- Include complete lines in patterns when possible
|
||||||
|
- Use `get_version` to check current state before editing
|
||||||
|
- Use `revert` freely - it's there to help you experiment
|
||||||
|
- Keep fossil IDs descriptive and clear
|
||||||
|
- Remember that both start and end patterns are INCLUDED in the match
|
||||||
|
|
||||||
|
### ❌ DON'T:
|
||||||
|
- Use overly generic patterns that might match multiple locations
|
||||||
|
- Worry about exact whitespace in patterns - fuzzy matching handles it
|
||||||
|
- Rewrite entire files - use targeted edits instead
|
||||||
|
- Forget that the start/end patterns get replaced too
|
||||||
|
- Use patterns that are too short (might match wrong location)
|
||||||
|
|
||||||
|
## Advanced Tips
|
||||||
|
|
||||||
|
### Tip 1: Multi-line Patterns
|
||||||
|
Patterns can span multiple lines. Use `\
|
||||||
|
` for newlines:
|
||||||
|
```
|
||||||
|
start_pattern: \"class MyClass:\
|
||||||
|
def __init__\"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tip 2: Partial Line Matching
|
||||||
|
You don't need the entire line, just enough to be distinctive:
|
||||||
|
```
|
||||||
|
start_pattern: \"def calculate(\" # Matches any line starting with this
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tip 3: Pattern Selection Strategy
|
||||||
|
- **Too specific:** Might not match due to whitespace differences
|
||||||
|
- **Too generic:** Might match wrong location
|
||||||
|
- **Just right:** Distinctive enough to be unique, fuzzy enough to be flexible
|
||||||
|
|
||||||
|
### Tip 4: When Edits Fail
|
||||||
|
If fuzzy matching can't find your pattern:
|
||||||
|
1. Use `get_version` to see the current actual content
|
||||||
|
2. Check if your pattern actually exists in the file
|
||||||
|
3. Try a more distinctive or simpler pattern
|
||||||
|
4. Remember: patterns are case-sensitive for content (but whitespace-flexible)
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
If a fuzzy match fails, you'll get an error message. Common causes:
|
||||||
|
- Pattern doesn't exist in the file
|
||||||
|
- Pattern exists multiple times (uses first match)
|
||||||
|
- Pattern text doesn't match actual content
|
||||||
|
|
||||||
|
**Solution:** Use `get_version` to inspect the current content and adjust your pattern.
|
||||||
|
|
||||||
|
## Comparison to Traditional Artifacts
|
||||||
|
|
||||||
|
### Traditional Artifacts:
|
||||||
|
- ❌ Full file rewrites every edit
|
||||||
|
- ❌ Exact string matching (brittle)
|
||||||
|
- ❌ No version history
|
||||||
|
- ❌ No undo capability
|
||||||
|
- ❌ Token-heavy
|
||||||
|
|
||||||
|
### Fossil System:
|
||||||
|
- ✅ Surgical edits to specific sections
|
||||||
|
- ✅ Fuzzy pattern matching (robust)
|
||||||
|
- ✅ Full version history
|
||||||
|
- ✅ Git-like revert functionality
|
||||||
|
- ✅ Token-efficient
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
**Create:** `create_fossil(id, content)`
|
||||||
|
**Edit:** `fuzzy_edit(id, start_pattern, end_pattern, replacement)`
|
||||||
|
**Undo:** `revert(id, steps)`
|
||||||
|
**View:** `get_version(id, version)`
|
||||||
|
**List:** `list_fossils()`
|
||||||
|
**Delete:** `delete_fossil(id)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Remember: Fossil enables **iterative, precise code editing** with full version control. Use fuzzy patterns to target sections, make incremental changes, and revert freely when needed. It's designed to make LLM-assisted coding efficient and safe.`,"#.into(),
|
||||||
),
|
),
|
||||||
capabilities: ServerCapabilities::builder().enable_tools().enable_resources().build(),
|
capabilities: ServerCapabilities::builder().enable_tools().enable_resources().enable_tool_list_changed().build(),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user