依据 F-260619-02 设计,对外暴露数据层接入 Claude Code/Cursor: - 新建 df-mcp crate:JSON-RPC 2.0 over stdio(initialize/tools/list/tools/call) - 暴露 19 数据层工具(项目/任务/灵感/工作流/回收站),复用 df-storage Repo 零重复 CRUD - 三层安全降级:High 风险(delete/purge/run_workflow)tools/list 不暴露+dispatch 拒+handler 二次拒;Medium 允许+审计日志;--read-only 仅 Low - 不暴露文件系统工具(防绕过应用内路径校验) - Tauri CLI 子命令 devflow mcp-server [--db/--read-only/--print-config] - SQLite WAL 已启用(复用 Database::open),应用与 MCP server 并发写安全 自验: df-mcp 8 passed + workspace EXIT 0
202 lines
6.1 KiB
Rust
202 lines
6.1 KiB
Rust
//! JSON-RPC 2.0 over stdio 协议层
|
|
//!
|
|
//! 职责:
|
|
//! - 按行读 stdin,解析 JSON-RPC Request/Notification
|
|
//! - 构造 Response / Error 对象并序列化为单行 JSON 写 stdout
|
|
//! - MCP 三类方法路由(initialize / tools/list / tools/call)由上层 dispatch
|
|
//!
|
|
//! 不解析 batch 请求(MCP 客户端实测逐条发,单条足够)。
|
|
//! 错误码遵循 JSON-RPC 2.0 + MCP 自定义段(-32xxx 协议层 / 0xffff 工具层)。
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
|
|
// ============================================================
|
|
// JSON-RPC 2.0 错误码
|
|
// ============================================================
|
|
|
|
/// 协议层错误(JSON-RPC 2.0 预留段 -32000..-32099)
|
|
pub const PARSE_ERROR: i32 = -32700;
|
|
pub const INVALID_REQUEST: i32 = -32600;
|
|
pub const METHOD_NOT_FOUND: i32 = -32601;
|
|
pub const INVALID_PARAMS: i32 = -32602;
|
|
pub const INTERNAL_ERROR: i32 = -32603;
|
|
|
|
// ============================================================
|
|
// 请求 / 响应数据结构
|
|
// ============================================================
|
|
|
|
/// JSON-RPC 2.0 请求(请求与通知共用,Notification 时 id=None)
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct Request {
|
|
pub jsonrpc: Option<String>,
|
|
pub id: Option<Value>,
|
|
pub method: String,
|
|
#[serde(default)]
|
|
pub params: Value,
|
|
}
|
|
|
|
/// JSON-RPC 2.0 错误对象
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct ErrorObject {
|
|
pub code: i32,
|
|
pub message: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub data: Option<Value>,
|
|
}
|
|
|
|
/// JSON-RPC 2.0 响应
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct Response {
|
|
pub jsonrpc: &'static str,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub id: Option<Value>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub result: Option<Value>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub error: Option<ErrorObject>,
|
|
}
|
|
|
|
impl Response {
|
|
/// 成功响应
|
|
pub fn ok(id: Option<Value>, result: Value) -> Self {
|
|
Self {
|
|
jsonrpc: "2.0",
|
|
id,
|
|
result: Some(result),
|
|
error: None,
|
|
}
|
|
}
|
|
|
|
/// 错误响应
|
|
pub fn err(id: Option<Value>, code: i32, message: impl Into<String>, data: Option<Value>) -> Self {
|
|
Self {
|
|
jsonrpc: "2.0",
|
|
id,
|
|
result: None,
|
|
error: Some(ErrorObject {
|
|
code,
|
|
message: message.into(),
|
|
data,
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// MCP 方法路由 — 暴露给上层 dispatch 的 enum
|
|
// ============================================================
|
|
|
|
/// MCP 协议层方法(已剥离 JSON-RPC 包装)。未知方法返回 `Unknown`,由 dispatch 回 METHOD_NOT_FOUND。
|
|
#[derive(Debug, Clone)]
|
|
pub enum McpMethod {
|
|
/// initialize 握手:返 server info + capabilities
|
|
Initialize { params: Value },
|
|
/// initialized 通知:客户端确认握手完成,无需响应
|
|
Initialized,
|
|
/// tools/list:列出所有工具(MCP Tool schema)
|
|
ToolsList,
|
|
/// tools/call:调用某工具
|
|
ToolsCall { name: String, arguments: Value },
|
|
/// ping 心跳(MCP 2025 规范),回空 result
|
|
Ping,
|
|
/// 未识别的方法(回 METHOD_NOT_FOUND)
|
|
Unknown(String),
|
|
}
|
|
|
|
impl McpMethod {
|
|
/// 从 Request 路由出 MCP 语义方法。
|
|
pub fn from_request(req: &Request) -> Self {
|
|
match req.method.as_str() {
|
|
"initialize" => McpMethod::Initialize { params: req.params.clone() },
|
|
"notifications/initialized" => McpMethod::Initialized,
|
|
"tools/list" => McpMethod::ToolsList,
|
|
"tools/call" => {
|
|
let name = req
|
|
.params
|
|
.get("name")
|
|
.and_then(|v| v.as_str())
|
|
.unwrap_or("")
|
|
.to_owned();
|
|
let arguments = req.params.get("arguments").cloned().unwrap_or(Value::Null);
|
|
McpMethod::ToolsCall { name, arguments }
|
|
}
|
|
"ping" => McpMethod::Ping,
|
|
other => McpMethod::Unknown(other.to_owned()),
|
|
}
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// MCP 协议数据结构(initialize 握手 / Tool schema / CallToolResult)
|
|
// ============================================================
|
|
|
|
/// MCP initialize 握手响应结构(server info + capabilities)
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct InitializeResult {
|
|
#[serde(rename = "protocolVersion")]
|
|
pub protocol_version: &'static str,
|
|
pub capabilities: Capabilities,
|
|
#[serde(rename = "serverInfo")]
|
|
pub server_info: ServerInfo,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct Capabilities {
|
|
pub tools: ToolsCapability,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct ToolsCapability {
|
|
#[serde(rename = "listChanged")]
|
|
pub list_changed: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct ServerInfo {
|
|
pub name: &'static str,
|
|
pub version: &'static str,
|
|
}
|
|
|
|
/// MCP Tool 描述符(tools/list 返回数组元素)
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct Tool {
|
|
pub name: &'static str,
|
|
pub description: &'static str,
|
|
#[serde(rename = "inputSchema")]
|
|
pub input_schema: Value,
|
|
}
|
|
|
|
/// MCP tools/call 成功响应(CallToolResult)
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct CallToolResult {
|
|
pub content: Vec<ContentBlock>,
|
|
#[serde(rename = "isError", skip_serializing_if = "Option::is_none")]
|
|
pub is_error: Option<bool>,
|
|
}
|
|
|
|
/// MCP 内容块(本 server 仅用 text)
|
|
#[derive(Debug, Clone, Serialize)]
|
|
#[serde(tag = "type", rename_all = "camelCase")]
|
|
pub enum ContentBlock {
|
|
Text { text: String },
|
|
}
|
|
|
|
impl CallToolResult {
|
|
/// 文本成功响应
|
|
pub fn text(text: impl Into<String>) -> Self {
|
|
Self {
|
|
content: vec![ContentBlock::Text { text: text.into() }],
|
|
is_error: None,
|
|
}
|
|
}
|
|
|
|
/// 文本错误响应(is_error=true 让客户端区分「工具业务错」与「协议错」)
|
|
pub fn error(text: impl Into<String>) -> Self {
|
|
Self {
|
|
content: vec![ContentBlock::Text { text: text.into() }],
|
|
is_error: Some(true),
|
|
}
|
|
}
|
|
}
|