refactor
This commit is contained in:
-375
@@ -1,375 +0,0 @@
|
||||
use crate::{
|
||||
AppState,
|
||||
api::auth::MaybeBearerToken,
|
||||
core::db::lookup_api_token,
|
||||
transport::execute::{ExecuteRequest, execute_for_token},
|
||||
};
|
||||
use rocket::{State, http::Status, post, response::content::RawJson, serde::json::Json};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
type ApiResponse = (Status, RawJson<String>);
|
||||
|
||||
const JSONRPC_VERSION: &str = "2.0";
|
||||
const MCP_PROTOCOL_VERSION: &str = "2025-11-25";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct JsonRpcRequest {
|
||||
pub jsonrpc: String,
|
||||
pub id: Option<Value>,
|
||||
pub method: String,
|
||||
#[serde(default)]
|
||||
pub params: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct JsonRpcResponse<T> {
|
||||
jsonrpc: &'static str,
|
||||
id: Option<Value>,
|
||||
result: T,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct JsonRpcErrorResponse {
|
||||
jsonrpc: &'static str,
|
||||
id: Option<Value>,
|
||||
error: JsonRpcError,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct JsonRpcError {
|
||||
code: i64,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct InitializeResult {
|
||||
#[serde(rename = "protocolVersion")]
|
||||
protocol_version: &'static str,
|
||||
capabilities: ServerCapabilities,
|
||||
#[serde(rename = "serverInfo")]
|
||||
server_info: ServerInfo,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ServerCapabilities {
|
||||
tools: ToolsCapability,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ToolsCapability {
|
||||
list_changed: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ServerInfo {
|
||||
name: &'static str,
|
||||
version: &'static str,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ToolsListResult {
|
||||
tools: Vec<ToolDefinition>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ToolDefinition {
|
||||
name: &'static str,
|
||||
description: &'static str,
|
||||
#[serde(rename = "inputSchema")]
|
||||
input_schema: Value,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ToolCallParams {
|
||||
name: String,
|
||||
arguments: Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ToolCallResult {
|
||||
content: Vec<TextContent>,
|
||||
#[serde(rename = "isError")]
|
||||
is_error: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ConnectedDevicesResult {
|
||||
device_ids: Vec<String>,
|
||||
}
|
||||
|
||||
enum ToolCall {
|
||||
Execute(ExecuteRequest),
|
||||
ListDevices,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct TextContent {
|
||||
#[serde(rename = "type")]
|
||||
kind: &'static str,
|
||||
text: String,
|
||||
}
|
||||
|
||||
#[post("/mcp", data = "<body>")]
|
||||
pub async fn route(
|
||||
body: Json<JsonRpcRequest>,
|
||||
token: MaybeBearerToken,
|
||||
state: &State<AppState>,
|
||||
) -> ApiResponse {
|
||||
let request = body.into_inner();
|
||||
|
||||
if request.jsonrpc != JSONRPC_VERSION {
|
||||
return jsonrpc_error(
|
||||
Status::BadRequest,
|
||||
request.id,
|
||||
-32600,
|
||||
"Invalid JSON-RPC version",
|
||||
);
|
||||
}
|
||||
|
||||
match request.method.as_str() {
|
||||
"initialize" => jsonrpc_ok(
|
||||
request.id,
|
||||
InitializeResult {
|
||||
protocol_version: MCP_PROTOCOL_VERSION,
|
||||
capabilities: ServerCapabilities {
|
||||
tools: ToolsCapability {
|
||||
list_changed: false,
|
||||
},
|
||||
},
|
||||
server_info: ServerInfo {
|
||||
name: env!("CARGO_PKG_NAME"),
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
},
|
||||
},
|
||||
),
|
||||
"notifications/initialized" => jsonrpc_notification_ok(),
|
||||
"tools/list" => match require_bearer_token(token.0.as_deref(), request.id.clone()) {
|
||||
Ok(_) => jsonrpc_ok(
|
||||
request.id,
|
||||
ToolsListResult {
|
||||
tools: tool_definitions(),
|
||||
},
|
||||
),
|
||||
Err(response) => response,
|
||||
},
|
||||
"tools/call" => {
|
||||
handle_tool_call(request.id, request.params, token.0.as_deref(), state).await
|
||||
}
|
||||
_ if request.id.is_none() => jsonrpc_notification_ok(),
|
||||
_ => jsonrpc_error(Status::BadRequest, request.id, -32601, "Method not found"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_tool_call(
|
||||
id: Option<Value>,
|
||||
params: Option<Value>,
|
||||
token: Option<&str>,
|
||||
state: &State<AppState>,
|
||||
) -> ApiResponse {
|
||||
let token = match require_bearer_token(token, id.clone()) {
|
||||
Ok(token) => token,
|
||||
Err(response) => return response,
|
||||
};
|
||||
|
||||
let Some(params) = params else {
|
||||
return jsonrpc_error(Status::BadRequest, id, -32602, "Missing params");
|
||||
};
|
||||
|
||||
let tool_call = match parse_tool_call(params) {
|
||||
Ok(call) => call,
|
||||
Err(_) => return jsonrpc_error(Status::BadRequest, id, -32602, "Invalid params"),
|
||||
};
|
||||
|
||||
match tool_call {
|
||||
ToolCall::Execute(request) => execute_tool_call(id, token, state, request).await,
|
||||
ToolCall::ListDevices => list_devices_tool_call(id, token, state).await,
|
||||
}
|
||||
}
|
||||
|
||||
fn require_bearer_token(token: Option<&str>, id: Option<Value>) -> Result<&str, ApiResponse> {
|
||||
token.ok_or_else(|| {
|
||||
jsonrpc_error(
|
||||
Status::Unauthorized,
|
||||
id,
|
||||
-32001,
|
||||
"Missing Authorization header",
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn tool_definitions() -> Vec<ToolDefinition> {
|
||||
vec![
|
||||
ToolDefinition {
|
||||
name: "execute",
|
||||
description: "Execute a shell command on a connected device.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"required": ["device_id", "command"],
|
||||
"properties": {
|
||||
"device_id": {
|
||||
"type": "string",
|
||||
"description": "Identifier of the connected device."
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Shell command to execute on the target device."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDefinition {
|
||||
name: "list_devices",
|
||||
description: "List currently connected device identifiers.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
fn parse_tool_call(params: Value) -> Result<ToolCall, &'static str> {
|
||||
let call: ToolCallParams = serde_json::from_value(params).map_err(|_| "Invalid params")?;
|
||||
|
||||
match call.name.as_str() {
|
||||
"execute" => serde_json::from_value(call.arguments)
|
||||
.map(ToolCall::Execute)
|
||||
.map_err(|_| "Invalid execute arguments"),
|
||||
"list_devices" => parse_list_devices_arguments(call.arguments),
|
||||
_ => Err("Unknown tool"),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_list_devices_arguments(arguments: Value) -> Result<ToolCall, &'static str> {
|
||||
match arguments {
|
||||
Value::Object(map) if map.is_empty() => Ok(ToolCall::ListDevices),
|
||||
_ => Err("Invalid list_devices arguments"),
|
||||
}
|
||||
}
|
||||
|
||||
fn connected_device_ids_for_user(state: &State<AppState>, user_id: &str) -> Vec<String> {
|
||||
state
|
||||
.registry
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
let ((entry_user_id, device_id), _) = entry.pair();
|
||||
(entry_user_id == user_id).then(|| device_id.clone())
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
async fn execute_tool_call(
|
||||
id: Option<Value>,
|
||||
token: &str,
|
||||
state: &State<AppState>,
|
||||
request: ExecuteRequest,
|
||||
) -> ApiResponse {
|
||||
match execute_for_token(state, token, request).await {
|
||||
Ok(result) => json_tool_result(id, &result),
|
||||
Err((status, message)) if status == Status::Unauthorized => {
|
||||
jsonrpc_error(Status::Unauthorized, id, -32001, message)
|
||||
}
|
||||
Err((_, message)) => jsonrpc_ok(id, tool_error(message)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_devices_tool_call(
|
||||
id: Option<Value>,
|
||||
token: &str,
|
||||
state: &State<AppState>,
|
||||
) -> ApiResponse {
|
||||
let user_id = match lookup_api_token(&state.database, token).await {
|
||||
Ok(user_id) => user_id,
|
||||
Err(message) => return jsonrpc_error(Status::Unauthorized, id, -32001, message),
|
||||
};
|
||||
|
||||
let mut device_ids = connected_device_ids_for_user(state, &user_id);
|
||||
device_ids.sort();
|
||||
|
||||
json_tool_result(id, &ConnectedDevicesResult { device_ids })
|
||||
}
|
||||
|
||||
fn json_tool_result<T: Serialize>(id: Option<Value>, payload: &T) -> ApiResponse {
|
||||
match serde_json::to_string(payload) {
|
||||
Ok(text) => jsonrpc_ok(id, ToolCallResult::success(text)),
|
||||
Err(error) => jsonrpc_error(Status::InternalServerError, id, -32603, error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn tool_error(message: String) -> ToolCallResult {
|
||||
ToolCallResult::error(message)
|
||||
}
|
||||
|
||||
impl ToolCallResult {
|
||||
fn success(text: String) -> Self {
|
||||
Self {
|
||||
content: vec![TextContent::text(text)],
|
||||
is_error: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn error(text: String) -> Self {
|
||||
Self {
|
||||
content: vec![TextContent::text(text)],
|
||||
is_error: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextContent {
|
||||
fn text(text: String) -> Self {
|
||||
Self { kind: "text", text }
|
||||
}
|
||||
}
|
||||
|
||||
fn jsonrpc_ok<T: Serialize>(id: Option<Value>, result: T) -> ApiResponse {
|
||||
match serde_json::to_string(&JsonRpcResponse {
|
||||
jsonrpc: JSONRPC_VERSION,
|
||||
id,
|
||||
result,
|
||||
}) {
|
||||
Ok(body) => (Status::Ok, RawJson(body)),
|
||||
Err(e) => jsonrpc_error(Status::InternalServerError, None, -32603, e.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn jsonrpc_notification_ok() -> ApiResponse {
|
||||
(Status::Accepted, RawJson(String::new()))
|
||||
}
|
||||
|
||||
fn jsonrpc_error(
|
||||
status: Status,
|
||||
id: Option<Value>,
|
||||
code: i64,
|
||||
message: impl Into<String>,
|
||||
) -> ApiResponse {
|
||||
let response = JsonRpcErrorResponse {
|
||||
jsonrpc: JSONRPC_VERSION,
|
||||
id,
|
||||
error: JsonRpcError {
|
||||
code,
|
||||
message: message.into(),
|
||||
},
|
||||
};
|
||||
|
||||
match serde_json::to_string(&response) {
|
||||
Ok(body) => (status, RawJson(body)),
|
||||
Err(e) => (
|
||||
Status::InternalServerError,
|
||||
RawJson(
|
||||
json!({
|
||||
"jsonrpc": JSONRPC_VERSION,
|
||||
"id": null,
|
||||
"error": {
|
||||
"code": -32603,
|
||||
"message": e.to_string()
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
mod protocol;
|
||||
mod tools;
|
||||
|
||||
use crate::{api::auth::MaybeBearerToken, app::state::AppState};
|
||||
use protocol::{
|
||||
ApiResponse, InitializeResult, JSONRPC_VERSION, jsonrpc_error, jsonrpc_notification_ok,
|
||||
jsonrpc_ok,
|
||||
};
|
||||
use rocket::{State, http::Status, post, serde::json::Json};
|
||||
use serde_json::Value;
|
||||
use tools::{handle_tool_call, tool_definitions};
|
||||
|
||||
pub use protocol::JsonRpcRequest;
|
||||
|
||||
#[post("/mcp", data = "<body>")]
|
||||
pub async fn route(
|
||||
body: Json<JsonRpcRequest>,
|
||||
token: MaybeBearerToken,
|
||||
state: &State<AppState>,
|
||||
) -> ApiResponse {
|
||||
let request = body.into_inner();
|
||||
|
||||
if request.jsonrpc != JSONRPC_VERSION {
|
||||
return jsonrpc_error(
|
||||
Status::BadRequest,
|
||||
request.id,
|
||||
-32600,
|
||||
"Invalid JSON-RPC version",
|
||||
);
|
||||
}
|
||||
|
||||
match request.method.as_str() {
|
||||
"initialize" => jsonrpc_ok(request.id, InitializeResult::new()),
|
||||
"notifications/initialized" => jsonrpc_notification_ok(),
|
||||
"tools/list" => match token.0 {
|
||||
Some(_) => jsonrpc_ok(request.id, tool_definitions()),
|
||||
None => missing_authorization(request.id),
|
||||
},
|
||||
"tools/call" => {
|
||||
handle_tool_call(request.id, request.params, token.0.as_deref(), state).await
|
||||
}
|
||||
_ if request.id.is_none() => jsonrpc_notification_ok(),
|
||||
_ => jsonrpc_error(Status::BadRequest, request.id, -32601, "Method not found"),
|
||||
}
|
||||
}
|
||||
|
||||
fn missing_authorization(id: Option<Value>) -> ApiResponse {
|
||||
jsonrpc_error(
|
||||
Status::Unauthorized,
|
||||
id,
|
||||
-32001,
|
||||
"Missing Authorization header",
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
use rocket::{http::Status, response::content::RawJson};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
pub type ApiResponse = (Status, RawJson<String>);
|
||||
|
||||
pub const JSONRPC_VERSION: &str = "2.0";
|
||||
const MCP_PROTOCOL_VERSION: &str = "2025-11-25";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct JsonRpcRequest {
|
||||
pub jsonrpc: String,
|
||||
pub id: Option<Value>,
|
||||
pub method: String,
|
||||
#[serde(default)]
|
||||
pub params: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct JsonRpcResponse<T> {
|
||||
jsonrpc: &'static str,
|
||||
id: Option<Value>,
|
||||
result: T,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct JsonRpcErrorResponse {
|
||||
jsonrpc: &'static str,
|
||||
id: Option<Value>,
|
||||
error: JsonRpcError,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct JsonRpcError {
|
||||
code: i64,
|
||||
message: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct InitializeResult {
|
||||
#[serde(rename = "protocolVersion")]
|
||||
protocol_version: &'static str,
|
||||
capabilities: ServerCapabilities,
|
||||
#[serde(rename = "serverInfo")]
|
||||
server_info: ServerInfo,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ServerCapabilities {
|
||||
tools: ToolsCapability,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ToolsCapability {
|
||||
list_changed: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ServerInfo {
|
||||
name: &'static str,
|
||||
version: &'static str,
|
||||
}
|
||||
|
||||
impl InitializeResult {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
protocol_version: MCP_PROTOCOL_VERSION,
|
||||
capabilities: ServerCapabilities {
|
||||
tools: ToolsCapability {
|
||||
list_changed: false,
|
||||
},
|
||||
},
|
||||
server_info: ServerInfo {
|
||||
name: env!("CARGO_PKG_NAME"),
|
||||
version: env!("CARGO_PKG_VERSION"),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn jsonrpc_ok<T: Serialize>(id: Option<Value>, result: T) -> ApiResponse {
|
||||
match serde_json::to_string(&JsonRpcResponse {
|
||||
jsonrpc: JSONRPC_VERSION,
|
||||
id,
|
||||
result,
|
||||
}) {
|
||||
Ok(body) => (Status::Ok, RawJson(body)),
|
||||
Err(error) => jsonrpc_error(Status::InternalServerError, None, -32603, error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn jsonrpc_notification_ok() -> ApiResponse {
|
||||
(Status::Accepted, RawJson(String::new()))
|
||||
}
|
||||
|
||||
pub fn jsonrpc_error(
|
||||
status: Status,
|
||||
id: Option<Value>,
|
||||
code: i64,
|
||||
message: impl Into<String>,
|
||||
) -> ApiResponse {
|
||||
let response = JsonRpcErrorResponse {
|
||||
jsonrpc: JSONRPC_VERSION,
|
||||
id,
|
||||
error: JsonRpcError {
|
||||
code,
|
||||
message: message.into(),
|
||||
},
|
||||
};
|
||||
|
||||
match serde_json::to_string(&response) {
|
||||
Ok(body) => (status, RawJson(body)),
|
||||
Err(error) => (
|
||||
Status::InternalServerError,
|
||||
RawJson(
|
||||
json!({
|
||||
"jsonrpc": JSONRPC_VERSION,
|
||||
"id": null,
|
||||
"error": {
|
||||
"code": -32603,
|
||||
"message": error.to_string()
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
),
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
use crate::{
|
||||
api::mcp::protocol::{ApiResponse, jsonrpc_error, jsonrpc_ok},
|
||||
app::state::AppState,
|
||||
core::db::lookup_api_token,
|
||||
transport::execute::{ExecuteRequest, execute_for_token},
|
||||
};
|
||||
use rocket::{State, http::Status};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct ToolsListResult {
|
||||
tools: Vec<ToolDefinition>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ToolDefinition {
|
||||
name: &'static str,
|
||||
description: &'static str,
|
||||
#[serde(rename = "inputSchema")]
|
||||
input_schema: Value,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ToolCallParams {
|
||||
name: String,
|
||||
arguments: Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ToolCallResult {
|
||||
content: Vec<TextContent>,
|
||||
#[serde(rename = "isError")]
|
||||
is_error: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct ConnectedDevicesResult {
|
||||
device_ids: Vec<String>,
|
||||
}
|
||||
|
||||
enum ToolCall {
|
||||
Execute(ExecuteRequest),
|
||||
ListDevices,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct TextContent {
|
||||
#[serde(rename = "type")]
|
||||
kind: &'static str,
|
||||
text: String,
|
||||
}
|
||||
|
||||
pub fn tool_definitions() -> ToolsListResult {
|
||||
ToolsListResult {
|
||||
tools: vec![
|
||||
ToolDefinition {
|
||||
name: "execute",
|
||||
description: "Execute a shell command on a connected device.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"required": ["device_id", "command"],
|
||||
"properties": {
|
||||
"device_id": {
|
||||
"type": "string",
|
||||
"description": "Identifier of the connected device."
|
||||
},
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Shell command to execute on the target device."
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
ToolDefinition {
|
||||
name: "list_devices",
|
||||
description: "List currently connected device identifiers.",
|
||||
input_schema: json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"additionalProperties": false
|
||||
}),
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn handle_tool_call(
|
||||
id: Option<Value>,
|
||||
params: Option<Value>,
|
||||
token: Option<&str>,
|
||||
state: &State<AppState>,
|
||||
) -> ApiResponse {
|
||||
let Some(token) = token else {
|
||||
return jsonrpc_error(
|
||||
Status::Unauthorized,
|
||||
id,
|
||||
-32001,
|
||||
"Missing Authorization header",
|
||||
);
|
||||
};
|
||||
|
||||
let Some(params) = params else {
|
||||
return jsonrpc_error(Status::BadRequest, id, -32602, "Missing params");
|
||||
};
|
||||
|
||||
let tool_call = match parse_tool_call(params) {
|
||||
Ok(call) => call,
|
||||
Err(_) => return jsonrpc_error(Status::BadRequest, id, -32602, "Invalid params"),
|
||||
};
|
||||
|
||||
match tool_call {
|
||||
ToolCall::Execute(request) => execute_tool_call(id, token, state, request).await,
|
||||
ToolCall::ListDevices => list_devices_tool_call(id, token, state).await,
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_tool_call(params: Value) -> Result<ToolCall, ()> {
|
||||
let call: ToolCallParams = serde_json::from_value(params).map_err(|_| ())?;
|
||||
|
||||
match call.name.as_str() {
|
||||
"execute" => serde_json::from_value(call.arguments)
|
||||
.map(ToolCall::Execute)
|
||||
.map_err(|_| ()),
|
||||
"list_devices" => match call.arguments {
|
||||
Value::Object(map) if map.is_empty() => Ok(ToolCall::ListDevices),
|
||||
_ => Err(()),
|
||||
},
|
||||
_ => Err(()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn execute_tool_call(
|
||||
id: Option<Value>,
|
||||
token: &str,
|
||||
state: &State<AppState>,
|
||||
request: ExecuteRequest,
|
||||
) -> ApiResponse {
|
||||
match execute_for_token(state, token, request).await {
|
||||
Ok(result) => json_tool_result(id, &result),
|
||||
Err((status, message)) if status == Status::Unauthorized => {
|
||||
jsonrpc_error(Status::Unauthorized, id, -32001, message)
|
||||
}
|
||||
Err((_, message)) => jsonrpc_ok(id, ToolCallResult::error(message)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn list_devices_tool_call(
|
||||
id: Option<Value>,
|
||||
token: &str,
|
||||
state: &State<AppState>,
|
||||
) -> ApiResponse {
|
||||
let user_id = match lookup_api_token(&state.database, token).await {
|
||||
Ok(user_id) => user_id,
|
||||
Err(message) => return jsonrpc_error(Status::Unauthorized, id, -32001, message),
|
||||
};
|
||||
|
||||
let mut device_ids = state
|
||||
.registry
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
let ((entry_user_id, device_id), _) = entry.pair();
|
||||
(entry_user_id == &user_id).then(|| device_id.clone())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
device_ids.sort();
|
||||
|
||||
json_tool_result(id, &ConnectedDevicesResult { device_ids })
|
||||
}
|
||||
|
||||
fn json_tool_result<T: Serialize>(id: Option<Value>, payload: &T) -> ApiResponse {
|
||||
match serde_json::to_string(payload) {
|
||||
Ok(text) => jsonrpc_ok(id, ToolCallResult::success(text)),
|
||||
Err(error) => jsonrpc_error(Status::InternalServerError, id, -32603, error.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
impl ToolCallResult {
|
||||
fn success(text: String) -> Self {
|
||||
Self {
|
||||
content: vec![TextContent::text(text)],
|
||||
is_error: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn error(text: String) -> Self {
|
||||
Self {
|
||||
content: vec![TextContent::text(text)],
|
||||
is_error: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TextContent {
|
||||
fn text(text: String) -> Self {
|
||||
Self { kind: "text", text }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod auth;
|
||||
pub mod catchers;
|
||||
pub mod mcp;
|
||||
Reference in New Issue
Block a user