use std::sync::Arc;
use std::time::Duration;
use elicitation::ElicitPlugin;
use futures::future::BoxFuture;
use rmcp::{
ErrorData,
model::{CallToolRequestParams, CallToolResult, Content, Tool},
service::RequestContext,
};
use schemars::JsonSchema;
use serde::Deserialize;
use tracing::instrument;
use crate::plugins::util::{parse_args, typed_tool};
pub struct Plugin {
client: Arc<reqwest::Client>,
}
impl Plugin {
pub fn new() -> Self {
Self {
client: Arc::new(reqwest::Client::new()),
}
}
pub fn with_client(client: reqwest::Client) -> Self {
Self {
client: Arc::new(client),
}
}
}
impl Default for Plugin {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Deserialize, JsonSchema)]
struct HttpParams {
url: String,
body: Option<String>,
content_type: Option<String>,
timeout_secs: Option<f64>,
bearer_token: Option<String>,
headers: Option<Vec<String>>,
}
fn apply_options(mut builder: reqwest::RequestBuilder, p: &HttpParams) -> reqwest::RequestBuilder {
if let Some(t) = p.timeout_secs {
builder = builder.timeout(Duration::from_secs_f64(t));
}
if let Some(token) = &p.bearer_token {
builder = builder.bearer_auth(token);
}
if let Some(ct) = &p.content_type {
builder = builder.header(reqwest::header::CONTENT_TYPE, ct.as_str());
}
for h in p.headers.iter().flatten() {
if let Some((k, v)) = h.split_once(':') {
builder = builder.header(k.trim(), v.trim());
}
}
if let Some(body) = &p.body {
builder = builder.body(body.clone());
}
builder
}
async fn execute(builder: reqwest::RequestBuilder) -> Result<CallToolResult, ErrorData> {
match builder.send().await {
Ok(resp) => {
let status = resp.status().as_u16();
let url = resp.url().to_string();
let body = resp.text().await.unwrap_or_default();
let json = serde_json::json!({ "status": status, "url": url, "body": body });
Ok(CallToolResult::success(vec![Content::text(
json.to_string(),
)]))
}
Err(e) => Ok(CallToolResult::error(vec![Content::text(e.to_string())])),
}
}
async fn execute_head(builder: reqwest::RequestBuilder) -> Result<CallToolResult, ErrorData> {
match builder.send().await {
Ok(resp) => {
let status = resp.status().as_u16();
let url = resp.url().to_string();
let json = serde_json::json!({ "status": status, "url": url });
Ok(CallToolResult::success(vec![Content::text(
json.to_string(),
)]))
}
Err(e) => Ok(CallToolResult::error(vec![Content::text(e.to_string())])),
}
}
impl ElicitPlugin for Plugin {
fn name(&self) -> &'static str {
"http"
}
fn list_tools(&self) -> Vec<Tool> {
vec![
typed_tool::<HttpParams>(
"get",
"Send an HTTP GET request; returns status, URL, and response body.",
),
typed_tool::<HttpParams>(
"post",
"Send an HTTP POST request with optional body; returns status, URL, and response body.",
),
typed_tool::<HttpParams>(
"put",
"Send an HTTP PUT request with optional body; returns status, URL, and response body.",
),
typed_tool::<HttpParams>(
"delete",
"Send an HTTP DELETE request; returns status, URL, and response body.",
),
typed_tool::<HttpParams>(
"patch",
"Send an HTTP PATCH request with optional body; returns status, URL, and response body.",
),
typed_tool::<HttpParams>(
"head",
"Send an HTTP HEAD request; returns status and URL only (no body).",
),
]
}
#[instrument(skip(self, _ctx), fields(tool = %params.name))]
fn call_tool<'a>(
&'a self,
params: CallToolRequestParams,
_ctx: RequestContext<rmcp::RoleServer>,
) -> BoxFuture<'a, Result<CallToolResult, ErrorData>> {
Box::pin(async move {
let p: HttpParams = parse_args(¶ms)?;
let client = Arc::clone(&self.client);
let builder = match params.name.as_ref() {
"get" => client.get(&p.url),
"post" => client.post(&p.url),
"put" => client.put(&p.url),
"delete" => client.delete(&p.url),
"patch" => client.patch(&p.url),
"head" => {
let b = apply_options(client.head(&p.url), &p);
return execute_head(b).await;
}
other => {
return Err(ErrorData::invalid_params(
format!("unknown tool: {other}"),
None,
));
}
};
execute(apply_options(builder, &p)).await
})
}
}