use crate::auth::McpAuth;
use crate::error::{McpError, ProtoError};
use crate::proto::{self, McpToolDef, ToolOutput};
use serde_json::Value;
use std::time::Duration;
use url::Url;
const SESSION_HEADER: &str = "Mcp-Session-Id";
#[derive(Debug, Clone)]
pub struct McpClientOptions {
pub timeout: Duration,
pub client_name: String,
pub client_version: String,
}
impl Default for McpClientOptions {
fn default() -> Self {
Self {
timeout: Duration::from_secs(30),
client_name: "greentic-mcp-client".to_string(),
client_version: env!("CARGO_PKG_VERSION").to_string(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ServerInfo {
pub name: String,
pub version: String,
pub protocol_version: String,
}
pub struct McpHttpClient {
http: reqwest::Client,
endpoint: Url,
auth: Option<McpAuth>,
opts: McpClientOptions,
session_id: Option<String>,
next_id: u64,
}
impl McpHttpClient {
pub fn new(
endpoint: Url,
auth: Option<McpAuth>,
opts: McpClientOptions,
) -> Result<Self, McpError> {
let http = reqwest::Client::builder().timeout(opts.timeout).build()?;
Ok(Self {
http,
endpoint,
auth,
opts,
session_id: None,
next_id: 1,
})
}
fn take_id(&mut self) -> u64 {
let id = self.next_id;
self.next_id += 1;
id
}
async fn post(
&mut self,
payload: &Value,
expected_id: Option<u64>,
) -> Result<Option<Value>, McpError> {
let mut req = self
.http
.post(self.endpoint.clone())
.header("Content-Type", "application/json")
.header("Accept", "application/json, text/event-stream")
.body(payload.to_string());
if let Some(auth) = &self.auth {
let (name, value) = auth.header();
req = req.header(name, value);
}
if let Some(session) = &self.session_id {
req = req.header(SESSION_HEADER, session.clone());
}
let resp = req.send().await?;
if let Some(session) = resp
.headers()
.get(SESSION_HEADER)
.and_then(|v| v.to_str().ok())
{
self.session_id = Some(session.to_string());
}
let resp = resp.error_for_status()?;
let Some(expected_id) = expected_id else {
return Ok(None);
};
let content_type = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or("application/json")
.to_string();
let body = resp.bytes().await?;
let envelope = proto::parse_jsonrpc_response(&content_type, &body, expected_id)?;
Ok(Some(envelope))
}
pub async fn initialize(&mut self) -> Result<ServerInfo, McpError> {
let id = self.take_id();
let payload = proto::build_initialize(
id,
proto::PROTOCOL_VERSION,
&self.opts.client_name,
&self.opts.client_version,
);
let envelope = self
.post(&payload, Some(id))
.await?
.ok_or(McpError::Proto(ProtoError::NoEnvelope))?;
let result = proto::extract_result(&envelope)?;
let server_info = result.get("serverInfo").ok_or_else(|| {
McpError::BadInitialize("missing serverInfo in initialize result".to_string())
})?;
let info = ServerInfo {
name: server_info
.get("name")
.and_then(Value::as_str)
.unwrap_or("")
.to_string(),
version: server_info
.get("version")
.and_then(Value::as_str)
.unwrap_or("")
.to_string(),
protocol_version: result
.get("protocolVersion")
.and_then(Value::as_str)
.unwrap_or("")
.to_string(),
};
self.post(&proto::build_initialized(), None).await?;
Ok(info)
}
pub async fn list_tools(&mut self) -> Result<Vec<McpToolDef>, McpError> {
let id = self.take_id();
let envelope = self
.post(&proto::build_tools_list(id), Some(id))
.await?
.ok_or(McpError::Proto(ProtoError::NoEnvelope))?;
let result = proto::extract_result(&envelope)?;
Ok(proto::map_tools_list(&result))
}
pub async fn call_tool(&mut self, name: &str, args: &Value) -> Result<ToolOutput, McpError> {
let id = self.take_id();
let envelope = self
.post(&proto::build_tools_call(id, name, args), Some(id))
.await?
.ok_or(McpError::Proto(ProtoError::NoEnvelope))?;
let result = proto::extract_result(&envelope)?;
Ok(proto::extract_tool_output(&result)?)
}
}