use std::borrow::Cow;
use std::sync::Arc;
use futures::FutureExt;
use http::request::Parts;
use objectiveai_sdk::filesystem::plugins::ManifestWithNameAndSource as PluginManifest;
use objectiveai_sdk::filesystem::tools::ManifestWithNameAndSource as ToolManifest;
use rmcp::{
ServerHandler,
handler::server::router::tool::{ToolRoute, ToolRouter},
handler::server::tool::{Extension, parse_json_object, schema_for_type},
handler::server::wrapper::Parameters,
model::{
CallToolResult, Content, Implementation, ProtocolVersion, ServerCapabilities, ServerInfo,
Tool,
},
schemars, tool, tool_handler, tool_router,
};
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct ObjectiveAiRequest {
#[schemars(description = "The command arguments to pass to the ObjectiveAI CLI (e.g. [\"agents\", \"list\"] or [\"functions\", \"executions\", \"create\", \"--help\"])")]
pub command: Vec<String>,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct PluginRequest {
#[schemars(description = "Args forwarded to the plugin's argv (prefixed automatically with `plugins <name>` when invoking the CLI).")]
pub args: Vec<String>,
}
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
pub struct ToolRequest {
#[schemars(description = "Args forwarded to the tool's argv (prefixed automatically with `tools <name>` when invoking the CLI).")]
pub args: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct ObjectiveAiMcpCli {
pub tool_router: ToolRouter<Self>,
pub cli_config: Arc<objectiveai_cli::Config>,
pub test_mode: bool,
}
#[tool_router]
impl ObjectiveAiMcpCli {
pub fn with_plugins_and_tools(
cli_config: Arc<objectiveai_cli::Config>,
plugins: Vec<PluginManifest>,
tools: Vec<ToolManifest>,
test_mode: bool,
) -> Self {
let mut tool_router = Self::tool_router();
for plugin in plugins {
if plugin.name == "ObjectiveAI" {
continue;
}
let plugin_name = plugin.name.clone();
let cli_config_for_route = cli_config.clone();
let tool = Tool::new(
Cow::Owned(plugin.name.clone()),
Cow::Owned(plugin.manifest.description.clone()),
schema_for_type::<PluginRequest>(),
);
tool_router.add_route(ToolRoute::new_dyn(tool, move |ctx| {
let cli_config = cli_config_for_route.clone();
let plugin_name = plugin_name.clone();
async move {
let arguments = ctx.arguments.unwrap_or_default();
let req: PluginRequest = parse_json_object(arguments)?;
let parts = ctx
.request_context
.extensions
.get::<Parts>()
.cloned()
.unwrap_or_else(|| http::Request::new(()).into_parts().0);
let args: Vec<String> =
["objectiveai".to_string(), "plugins".to_string(), plugin_name]
.into_iter()
.chain(req.args.into_iter())
.collect();
let buf = run_cli_and_collect(&cli_config, &parts, args, test_mode).await;
Ok(CallToolResult::success(vec![Content::text(buf)]))
}
.boxed()
}));
}
for cli_tool in tools {
if cli_tool.name == "ObjectiveAI" {
continue;
}
let tool_name = cli_tool.name.clone();
let cli_config_for_route = cli_config.clone();
let tool = Tool::new(
Cow::Owned(cli_tool.name.clone()),
Cow::Owned(cli_tool.manifest.description.clone()),
schema_for_type::<ToolRequest>(),
);
tool_router.add_route(ToolRoute::new_dyn(tool, move |ctx| {
let cli_config = cli_config_for_route.clone();
let tool_name = tool_name.clone();
async move {
let arguments = ctx.arguments.unwrap_or_default();
let req: ToolRequest = parse_json_object(arguments)?;
let parts = ctx
.request_context
.extensions
.get::<Parts>()
.cloned()
.unwrap_or_else(|| http::Request::new(()).into_parts().0);
let args: Vec<String> =
["objectiveai".to_string(), "tools".to_string(), tool_name]
.into_iter()
.chain(req.args.into_iter())
.collect();
let buf = run_cli_and_collect(&cli_config, &parts, args, test_mode).await;
Ok(CallToolResult::success(vec![Content::text(buf)]))
}
.boxed()
}));
}
Self {
tool_router,
cli_config,
test_mode,
}
}
#[tool(
name = "ObjectiveAI",
description = "Run an ObjectiveAI command."
)]
async fn objectiveai(
&self,
Parameters(req): Parameters<ObjectiveAiRequest>,
Extension(parts): Extension<Parts>,
) -> String {
let args: Vec<String> = std::iter::once("objectiveai".to_string())
.chain(req.command)
.collect();
run_cli_and_collect(&self.cli_config, &parts, args, self.test_mode).await
}
}
async fn run_cli_and_collect(
cli_config: &Arc<objectiveai_cli::Config>,
parts: &Parts,
args: Vec<String>,
test_mode: bool,
) -> String {
let cli_config: Arc<objectiveai_cli::Config> = match parts
.headers
.get("X-OBJECTIVEAI-AGENT-ID")
.and_then(|h| h.to_str().ok())
{
Some(agent_id) => {
let mut cfg = (**cli_config).clone();
cfg.agent_id = Some(agent_id.to_string());
Arc::new(cfg)
}
None => cli_config.clone(),
};
let collected = Arc::new(tokio::sync::Mutex::new(Vec::new()));
let handle = objectiveai_sdk::cli::output::Handle {
destination: objectiveai_sdk::cli::output::HandleDestination::Collect(collected.clone()),
agent_id: cli_config.agent_id.clone(),
};
let _ = objectiveai_cli::run(args, &cli_config, handle).await;
let outputs = collected.lock().await;
let mut buf = String::new();
for output in outputs.iter() {
match serde_json::to_string(output) {
Ok(line) => buf.push_str(&line),
Err(e) => buf.push_str(&format!("error serializing output: {e}")),
}
buf.push('\n');
}
if test_mode {
buf = objectiveai_sdk::cli::output::strip_agent_id_lines(&buf);
}
buf
}
#[tool_handler]
impl ServerHandler for ObjectiveAiMcpCli {
fn get_info(&self) -> ServerInfo {
ServerInfo {
protocol_version: ProtocolVersion::V_2025_06_18,
capabilities: ServerCapabilities::builder().enable_tools().build(),
server_info: Implementation {
name: "oai".into(),
title: None,
version: env!("CARGO_PKG_VERSION").into(),
description: None,
icons: None,
website_url: None,
},
instructions: None,
}
}
}