1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187//! 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"));
}
}