初始化提交
Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled
Some checks failed
CI / Check / macos-latest (push) Has been cancelled
CI / Check / ubuntu-latest (push) Has been cancelled
CI / Check / windows-latest (push) Has been cancelled
CI / Test / macos-latest (push) Has been cancelled
CI / Test / ubuntu-latest (push) Has been cancelled
CI / Test / windows-latest (push) Has been cancelled
CI / Clippy (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Security Audit (push) Has been cancelled
CI / Secrets Scan (push) Has been cancelled
CI / Install Script Smoke Test (push) Has been cancelled
This commit is contained in:
186
crates/openfang-runtime/src/mcp_server.rs
Normal file
186
crates/openfang-runtime/src/mcp_server.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
//! MCP Server — expose OpenFang tools via the Model Context Protocol.
|
||||
//!
|
||||
//! Implements the server-side MCP protocol so external MCP clients
|
||||
//! (Claude Desktop, VS Code, etc.) can use OpenFang's built-in tools.
|
||||
//!
|
||||
//! This module provides a reusable handler function — the CLI team
|
||||
//! wires it into a stdio transport.
|
||||
|
||||
use openfang_types::tool::ToolDefinition;
|
||||
use serde_json::json;
|
||||
|
||||
/// MCP protocol version supported by this server.
|
||||
const PROTOCOL_VERSION: &str = "2024-11-05";
|
||||
|
||||
/// Handle an incoming MCP JSON-RPC request and return a response.
|
||||
///
|
||||
/// This is a stateless handler that can be called from any transport
|
||||
/// (stdio, HTTP, etc.). The caller provides the available tool definitions.
|
||||
pub async fn handle_mcp_request(
|
||||
request: &serde_json::Value,
|
||||
tools: &[ToolDefinition],
|
||||
) -> serde_json::Value {
|
||||
let method = request["method"].as_str().unwrap_or("");
|
||||
let id = request.get("id").cloned();
|
||||
|
||||
match method {
|
||||
"initialize" => make_response(
|
||||
id,
|
||||
json!({
|
||||
"protocolVersion": PROTOCOL_VERSION,
|
||||
"capabilities": {
|
||||
"tools": {}
|
||||
},
|
||||
"serverInfo": {
|
||||
"name": "openfang",
|
||||
"version": env!("CARGO_PKG_VERSION")
|
||||
}
|
||||
}),
|
||||
),
|
||||
"notifications/initialized" => {
|
||||
// Notification — no response needed
|
||||
json!(null)
|
||||
}
|
||||
"tools/list" => {
|
||||
let tool_list: Vec<serde_json::Value> = tools
|
||||
.iter()
|
||||
.map(|t| {
|
||||
json!({
|
||||
"name": t.name,
|
||||
"description": t.description,
|
||||
"inputSchema": t.input_schema,
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
make_response(id, json!({ "tools": tool_list }))
|
||||
}
|
||||
"tools/call" => {
|
||||
let tool_name = request["params"]["name"].as_str().unwrap_or("");
|
||||
let _arguments = request["params"]
|
||||
.get("arguments")
|
||||
.cloned()
|
||||
.unwrap_or(json!({}));
|
||||
|
||||
// Verify the tool exists
|
||||
if !tools.iter().any(|t| t.name == tool_name) {
|
||||
return make_error(id, -32602, &format!("Unknown tool: {tool_name}"));
|
||||
}
|
||||
|
||||
// Tool execution is delegated to the caller (kernel/CLI).
|
||||
// This handler just validates the request format.
|
||||
// In a full implementation, the caller would wire this to execute_tool().
|
||||
make_response(
|
||||
id,
|
||||
json!({
|
||||
"content": [{
|
||||
"type": "text",
|
||||
"text": format!("Tool '{tool_name}' is available. Execution must be wired by the host.")
|
||||
}]
|
||||
}),
|
||||
)
|
||||
}
|
||||
_ => make_error(id, -32601, &format!("Method not found: {method}")),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a JSON-RPC 2.0 success response.
|
||||
fn make_response(id: Option<serde_json::Value>, result: serde_json::Value) -> serde_json::Value {
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": id,
|
||||
"result": result,
|
||||
})
|
||||
}
|
||||
|
||||
/// Build a JSON-RPC 2.0 error response.
|
||||
fn make_error(id: Option<serde_json::Value>, code: i64, message: &str) -> serde_json::Value {
|
||||
json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": id,
|
||||
"error": {
|
||||
"code": code,
|
||||
"message": message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn test_tools() -> Vec<ToolDefinition> {
|
||||
vec![
|
||||
ToolDefinition {
|
||||
name: "file_read".to_string(),
|
||||
description: "Read a file".to_string(),
|
||||
input_schema: json!({"type": "object", "properties": {"path": {"type": "string"}}}),
|
||||
},
|
||||
ToolDefinition {
|
||||
name: "web_fetch".to_string(),
|
||||
description: "Fetch a URL".to_string(),
|
||||
input_schema: json!({"type": "object"}),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mcp_server_tools_list() {
|
||||
let tools = test_tools();
|
||||
let request = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "tools/list",
|
||||
});
|
||||
|
||||
let response = handle_mcp_request(&request, &tools).await;
|
||||
assert_eq!(response["jsonrpc"], "2.0");
|
||||
assert_eq!(response["id"], 1);
|
||||
|
||||
let tool_list = response["result"]["tools"].as_array().unwrap();
|
||||
assert_eq!(tool_list.len(), 2);
|
||||
assert_eq!(tool_list[0]["name"], "file_read");
|
||||
assert_eq!(tool_list[1]["name"], "web_fetch");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mcp_server_unknown_method() {
|
||||
let tools = test_tools();
|
||||
let request = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 5,
|
||||
"method": "nonexistent/method",
|
||||
});
|
||||
|
||||
let response = handle_mcp_request(&request, &tools).await;
|
||||
assert_eq!(response["jsonrpc"], "2.0");
|
||||
assert_eq!(response["id"], 5);
|
||||
assert_eq!(response["error"]["code"], -32601);
|
||||
assert!(response["error"]["message"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("not found"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_mcp_server_initialize() {
|
||||
let tools = test_tools();
|
||||
let request = json!({
|
||||
"jsonrpc": "2.0",
|
||||
"id": 1,
|
||||
"method": "initialize",
|
||||
"params": {
|
||||
"protocolVersion": "2024-11-05",
|
||||
"capabilities": {},
|
||||
"clientInfo": {"name": "test"}
|
||||
}
|
||||
});
|
||||
|
||||
let response = handle_mcp_request(&request, &tools).await;
|
||||
assert_eq!(response["result"]["protocolVersion"], PROTOCOL_VERSION);
|
||||
assert!(response["result"]["serverInfo"]["name"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("openfang"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user