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
17 changes: 12 additions & 5 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 crates/dowse/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ zip = { version = "8.6.0", default-features = false, features = ["deflate"] }
clap = { version = "4.6.1", features = ["derive"], optional = true }
ctrlc = { version = "3.5.2", optional = true }
directories = { version = "6.0.0", optional = true }
rmcp = { version = "2.2.0", features = ["server", "macros", "transport-io", "schemars"], optional = true }
rmcp = { version = "3.0.1", features = ["server", "macros", "transport-io", "schemars"], optional = true }
schemars = { version = "1.2.1", optional = true }
tokio = { version = "1.52.3", features = ["rt-multi-thread", "macros", "io-std"], optional = true }

[dev-dependencies]
rmcp = { version = "2.2.0", features = ["client", "transport-child-process"] }
rmcp = { version = "3.0.1", features = ["client", "transport-child-process"] }
tempfile = "3.27.0"

# MCP 集成测试要起编译好的 dowse 二进制走 stdio 握手,依赖 `cli` feature 下才存在的
Expand Down
29 changes: 23 additions & 6 deletions crates/dowse/src/bin/dowse/mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use dowse::{
};
use rmcp::handler::server::tool::IntoCallToolResult;
use rmcp::handler::server::wrapper::{Json, Parameters};
use rmcp::model::{CallToolResult, ServerCapabilities, ServerInfo};
use rmcp::model::{CallToolResponse, CallToolResult, ServerCapabilities, ServerInfo};
use rmcp::{ErrorData as McpError, ServerHandler, tool, tool_handler, tool_router};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -215,7 +215,8 @@ fn index_unavailable_result(err: &anyhow::Error) -> CallToolResult {
/// 常驻一个 Searcher 会读到过期数据(见 docs/DESIGN-M5-MCP.md 第二节的并发约束)。
///
/// 返回 `Err(CallToolResult)` 而不是 `Err(McpError)`:索引不可用是工具级错误,
/// 调用方直接 `return Ok(e)` 短路成功分支即可,见上面 index_unavailable_result 的说明。
/// 调用方直接 `return Ok(e)` 短路成功分支即可,
/// 见上面 index_unavailable_result 的说明。
fn open_and_reload(index_dir: &std::path::Path) -> Result<Searcher, CallToolResult> {
let searcher = Searcher::open(index_dir).map_err(|e| index_unavailable_result(&e))?;
searcher
Expand All @@ -224,6 +225,23 @@ fn open_and_reload(index_dir: &std::path::Path) -> Result<Searcher, CallToolResu
Ok(searcher)
}

/// rmcp 3 为 MRTR 引入了 `CallToolResponse`,`Json<T>` 的转换结果也因此
/// 从 `CallToolResult` 变成了 `CallToolResponse`。这里的三个工具都是同步完成的
/// 只读工具,因此只接受 `Complete` 分支,并保持工具路由的返回类型为
/// `CallToolResult`。
fn json_tool_result<T>(value: T) -> Result<CallToolResult, McpError>
where
T: Serialize + JsonSchema + 'static,
{
match Json(value).into_call_tool_result()? {
CallToolResponse::Complete(result) => Ok(result),
_ => Err(McpError::internal_error(
"JSON 工具结果不应产生 MRTR 中间响应",
None,
)),
}
}

#[derive(Clone)]
pub struct DowseMcpServer {
index_dir: PathBuf,
Expand Down Expand Up @@ -289,7 +307,7 @@ impl DowseMcpServer {

// 只有相关性排序下 BM25 分数才有意义;mtime/size 排序时不带 score。
let include_score = sort_mode == SortMode::Relevance;
Json(SearchOutput {
json_tool_result(SearchOutput {
hits: page
.hits
.into_iter()
Expand All @@ -298,7 +316,6 @@ impl DowseMcpServer {
total_hits: page.total,
total_docs: searcher.num_docs(),
})
.into_call_tool_result()
}

#[tool(
Expand All @@ -325,7 +342,7 @@ impl DowseMcpServer {
.map_err(|e| McpError::invalid_params(format!("查询语法有问题:{e}"), None))?;

match hit {
Some(hit) => Json(to_preview_output(hit)).into_call_tool_result(),
Some(hit) => json_tool_result(to_preview_output(hit)),
// 路径本身没问题(不是参数错误),是索引里当下就是找不到这篇——
// 文件可能已被删除、或改名后索引还没重新收录。跟"索引不可用"一样,
// 是运行时状态问题,agent 应该看得到,所以也是工具级错误。
Expand All @@ -343,7 +360,7 @@ impl DowseMcpServer {
)]
async fn index_status(&self) -> Result<CallToolResult, McpError> {
match core_index_status(&self.index_dir) {
Ok(status) => Json(to_index_status_output(status)).into_call_tool_result(),
Ok(status) => json_tool_result(to_index_status_output(status)),
Err(e) => Ok(index_unavailable_result(&e)),
}
}
Expand Down