logiguard fork v3: full patch set on verified 8c74db0 tree

Includes prior-session patches (carry forward so the app compiles):
  - crates/gpui/build.rs: cross-compile manifest fix
  - crates/gpui/src/platform.rs: PlatformWindow::activate_with_token trait method
  - crates/gpui/src/window.rs: Window::activate_with_token public API
  - crates/gpui_linux/src/linux/wayland/window.rs: WaylandWindow::activate_with_token + activate() keyboard-serial fix

Plus the focus-serial fix:
  - serial.rs: SerialKind::KeyboardEnter
  - client.rs: store wl_keyboard.enter serial; latest_serial_of()

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mohamad Khani
2026-07-14 01:52:12 +03:30
commit b9819977a5
3984 changed files with 1487015 additions and 0 deletions

View File

@@ -0,0 +1,594 @@
use anyhow::{Context as _, Result, anyhow};
use collections::HashMap;
use futures::{FutureExt, StreamExt, channel::oneshot, future, select};
use futures_lite::future::yield_now;
use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, Task};
use parking_lot::Mutex;
use postage::barrier;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use serde_json::{Value, value::RawValue};
use slotmap::SlotMap;
use std::{
fmt,
path::PathBuf,
pin::pin,
sync::{
Arc,
atomic::{AtomicI32, Ordering::SeqCst},
},
time::{Duration, Instant},
};
use util::{ResultExt, TryFutureExt};
use crate::{
transport::{StdioTransport, Transport},
types::{CancelledParams, ClientNotification, Notification as _, notifications::Cancelled},
};
const JSON_RPC_VERSION: &str = "2.0";
const DEFAULT_REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
// Standard JSON-RPC error codes
pub const PARSE_ERROR: i32 = -32700;
pub const INVALID_REQUEST: i32 = -32600;
pub const METHOD_NOT_FOUND: i32 = -32601;
pub const INVALID_PARAMS: i32 = -32602;
pub const INTERNAL_ERROR: i32 = -32603;
type ResponseHandler = Box<dyn Send + FnOnce(String)>;
type NotificationHandler = Box<dyn Send + FnMut(Value, AsyncApp)>;
type RequestHandler = Box<dyn Send + FnMut(RequestId, &RawValue, AsyncApp)>;
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RequestId {
Int(i32),
Str(String),
}
pub(crate) struct Client {
server_id: ContextServerId,
next_id: AtomicI32,
outbound_tx: async_channel::Sender<String>,
name: Arc<str>,
subscription_set: Arc<Mutex<NotificationSubscriptionSet>>,
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
#[allow(clippy::type_complexity)]
#[allow(dead_code)]
io_tasks: Mutex<Option<(Task<Option<()>>, Task<Option<()>>)>>,
#[allow(dead_code)]
output_done_rx: Mutex<Option<barrier::Receiver>>,
executor: BackgroundExecutor,
#[allow(dead_code)]
transport: Arc<dyn Transport>,
request_timeout: Option<Duration>,
/// Single-slot side channel for the last transport-level error. When the
/// output task encounters a send failure it stashes the error here and
/// exits; the next request to observe cancellation `.take()`s it so it can
/// propagate a typed error (e.g. `TransportError::AuthRequired`) instead
/// of a generic "cancelled". This works because `initialize` is the sole
/// in-flight request at startup, but would need rethinking if concurrent
/// requests are ever issued during that phase.
last_transport_error: Arc<Mutex<Option<anyhow::Error>>>,
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub(crate) struct ContextServerId(pub Arc<str>);
fn is_null_value<T: Serialize>(value: &T) -> bool {
matches!(serde_json::to_value(value), Ok(Value::Null))
}
#[derive(Serialize, Deserialize)]
pub struct Request<'a, T> {
pub jsonrpc: &'static str,
pub id: RequestId,
pub method: &'a str,
#[serde(skip_serializing_if = "is_null_value")]
pub params: T,
}
#[derive(Serialize, Deserialize)]
pub struct AnyRequest<'a> {
pub jsonrpc: &'a str,
pub id: RequestId,
pub method: &'a str,
#[serde(skip_serializing_if = "is_null_value")]
pub params: Option<&'a RawValue>,
}
#[derive(Serialize, Deserialize)]
struct AnyResponse<'a> {
jsonrpc: &'a str,
id: RequestId,
#[serde(default)]
error: Option<Error>,
#[serde(borrow)]
result: Option<&'a RawValue>,
}
#[derive(Serialize, Deserialize)]
#[allow(dead_code)]
pub(crate) struct Response<T> {
pub jsonrpc: &'static str,
pub id: RequestId,
#[serde(flatten)]
pub value: CspResult<T>,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum CspResult<T> {
#[serde(rename = "result")]
Ok(Option<T>),
#[allow(dead_code)]
Error(Option<Error>),
}
#[derive(Serialize, Deserialize)]
struct Notification<'a, T> {
jsonrpc: &'static str,
#[serde(borrow)]
method: &'a str,
#[serde(skip_serializing_if = "is_null_value")]
params: T,
}
#[derive(Debug, Clone, Deserialize)]
struct AnyNotification<'a> {
#[expect(
unused,
reason = "Part of the JSON-RPC protocol - we expect the field to be present in a valid JSON-RPC notification"
)]
jsonrpc: &'a str,
method: String,
#[serde(default)]
params: Option<Value>,
}
#[derive(Debug, Serialize, Deserialize)]
pub(crate) struct Error {
pub message: String,
pub code: i32,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ModelContextServerBinary {
pub executable: PathBuf,
pub args: Vec<String>,
pub env: Option<HashMap<String, String>>,
pub timeout: Option<u64>,
}
impl Client {
/// Creates a new Client instance for a context server.
///
/// This function initializes a new Client by spawning a child process for the context server,
/// setting up communication channels, and initializing handlers for input/output operations.
/// It takes a server ID, binary information, and an async app context as input.
pub fn stdio(
server_id: ContextServerId,
binary: ModelContextServerBinary,
working_directory: &Option<PathBuf>,
cx: AsyncApp,
) -> Result<Self> {
log::debug!(
"starting context server (executable={:?}, args={:?})",
binary.executable,
&binary.args
);
let server_name = binary
.executable
.file_name()
.map(|name| name.to_string_lossy().into_owned())
.unwrap_or_else(String::new);
let timeout = binary.timeout.map(Duration::from_secs);
let transport = Arc::new(StdioTransport::new(binary, working_directory, &cx)?);
Self::new(server_id, server_name.into(), transport, timeout, cx)
}
/// Creates a new Client instance for a context server.
pub fn new(
server_id: ContextServerId,
server_name: Arc<str>,
transport: Arc<dyn Transport>,
request_timeout: Option<Duration>,
cx: AsyncApp,
) -> Result<Self> {
let (outbound_tx, outbound_rx) = async_channel::unbounded::<String>();
let (output_done_tx, output_done_rx) = barrier::channel();
let subscription_set = Arc::new(Mutex::new(NotificationSubscriptionSet::default()));
let response_handlers =
Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default())));
let request_handlers = Arc::new(Mutex::new(HashMap::<_, RequestHandler>::default()));
let receive_input_task = cx.spawn({
let subscription_set = subscription_set.clone();
let response_handlers = response_handlers.clone();
let request_handlers = request_handlers.clone();
let transport = transport.clone();
async move |cx| {
Self::handle_input(
transport,
subscription_set,
request_handlers,
response_handlers,
cx,
)
.log_err()
.await
}
});
let receive_err_task = cx.spawn({
let transport = transport.clone();
async move |_| Self::handle_err(transport).log_err().await
});
let input_task = cx.spawn(async move |_| {
let (input, err) = futures::join!(receive_input_task, receive_err_task);
input.or(err)
});
let last_transport_error: Arc<Mutex<Option<anyhow::Error>>> = Arc::new(Mutex::new(None));
let output_task = cx.background_spawn({
let transport = transport.clone();
let last_transport_error = last_transport_error.clone();
Self::handle_output(
transport,
outbound_rx,
output_done_tx,
response_handlers.clone(),
last_transport_error,
)
.log_err()
});
Ok(Self {
server_id,
subscription_set,
response_handlers,
name: server_name,
next_id: Default::default(),
outbound_tx,
executor: cx.background_executor().clone(),
io_tasks: Mutex::new(Some((input_task, output_task))),
output_done_rx: Mutex::new(Some(output_done_rx)),
transport,
request_timeout,
last_transport_error,
})
}
/// Handles input from the server's stdout.
///
/// This function continuously reads lines from the provided stdout stream,
/// parses them as JSON-RPC responses or notifications, and dispatches them
/// to the appropriate handlers. It processes both responses (which are matched
/// to pending requests) and notifications (which trigger registered handlers).
async fn handle_input(
transport: Arc<dyn Transport>,
subscription_set: Arc<Mutex<NotificationSubscriptionSet>>,
request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>,
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
cx: &mut AsyncApp,
) -> anyhow::Result<()> {
let mut receiver = transport.receive();
while let Some(message) = receiver.next().await {
log::trace!("recv: {}", &message);
if let Ok(request) = serde_json::from_str::<AnyRequest>(&message) {
let mut request_handlers = request_handlers.lock();
if let Some(handler) = request_handlers.get_mut(request.method) {
handler(
request.id,
request.params.unwrap_or(RawValue::NULL),
cx.clone(),
);
}
} else if let Ok(response) = serde_json::from_str::<AnyResponse>(&message) {
if let Some(handlers) = response_handlers.lock().as_mut()
&& let Some(handler) = handlers.remove(&response.id)
{
handler(message.to_string());
}
} else if let Ok(notification) = serde_json::from_str::<AnyNotification>(&message) {
subscription_set.lock().notify(
&notification.method,
notification.params.unwrap_or(Value::Null),
cx,
)
} else {
log::error!("Unhandled JSON from context_server: {}", message);
}
}
yield_now().await;
Ok(())
}
/// Handles the stderr output from the context server.
/// Continuously reads and logs any error messages from the server.
async fn handle_err(transport: Arc<dyn Transport>) -> anyhow::Result<()> {
while let Some(err) = transport.receive_err().next().await {
log::debug!("context server stderr: {}", err.trim());
}
Ok(())
}
/// Handles the output to the context server's stdin.
/// This function continuously receives messages from the outbound channel,
/// writes them to the server's stdin, and manages the lifecycle of response handlers.
async fn handle_output(
transport: Arc<dyn Transport>,
outbound_rx: async_channel::Receiver<String>,
output_done_tx: barrier::Sender,
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
last_transport_error: Arc<Mutex<Option<anyhow::Error>>>,
) -> anyhow::Result<()> {
let _clear_response_handlers = util::defer({
let response_handlers = response_handlers.clone();
move || {
response_handlers.lock().take();
}
});
while let Ok(message) = outbound_rx.recv().await {
log::trace!("outgoing message: {}", message);
if let Err(err) = transport.send(message).await {
log::debug!("transport send failed: {:#}", err);
*last_transport_error.lock() = Some(err);
return Ok(());
}
}
drop(output_done_tx);
Ok(())
}
/// Sends a JSON-RPC request to the context server and waits for a response.
/// This function handles serialization, deserialization, timeout, and error handling.
pub async fn request<T: DeserializeOwned>(
&self,
method: &str,
params: impl Serialize,
) -> Result<T> {
self.request_with(
method,
params,
None,
self.request_timeout.or(Some(DEFAULT_REQUEST_TIMEOUT)),
)
.await
}
pub async fn request_with<T: DeserializeOwned>(
&self,
method: &str,
params: impl Serialize,
cancel_rx: Option<oneshot::Receiver<()>>,
timeout: Option<Duration>,
) -> Result<T> {
let id = self.next_id.fetch_add(1, SeqCst);
let request = serde_json::to_string(&Request {
jsonrpc: JSON_RPC_VERSION,
id: RequestId::Int(id),
method,
params,
})
.unwrap();
let (tx, rx) = oneshot::channel();
let handle_response = self
.response_handlers
.lock()
.as_mut()
.context("server shut down")
.map(|handlers| {
handlers.insert(
RequestId::Int(id),
Box::new(move |result| {
let _ = tx.send(result);
}),
);
});
let send = self
.outbound_tx
.try_send(request)
.context("failed to write to context server's stdin");
let executor = self.executor.clone();
let started = Instant::now();
handle_response?;
send?;
let mut timeout_fut = pin!(
match timeout {
Some(timeout) => future::Either::Left(executor.timer(timeout)),
None => future::Either::Right(future::pending()),
}
.fuse()
);
let mut cancel_fut = pin!(
match cancel_rx {
Some(rx) => future::Either::Left(async {
rx.await.log_err();
}),
None => future::Either::Right(future::pending()),
}
.fuse()
);
select! {
response = rx.fuse() => {
let elapsed = started.elapsed();
log::trace!("took {elapsed:?} to receive response to {method:?} id {id}");
match response {
Ok(response) => {
let parsed: AnyResponse = serde_json::from_str(&response)?;
if let Some(error) = parsed.error {
Err(anyhow!(error.message))
} else if let Some(result) = parsed.result {
Ok(serde_json::from_str(result.get())?)
} else {
anyhow::bail!("Invalid response: no result or error");
}
}
Err(_canceled) => {
if let Some(err) = self.last_transport_error.lock().take() {
return Err(err);
}
anyhow::bail!("cancelled")
}
}
}
_ = cancel_fut => {
self.notify(
Cancelled::METHOD,
ClientNotification::Cancelled(CancelledParams {
request_id: RequestId::Int(id),
reason: None
})
).log_err();
anyhow::bail!(RequestCanceled)
}
_ = timeout_fut => {
log::error!("cancelled csp request task for {method:?} id {id} which took over {:?}", timeout.unwrap());
anyhow::bail!("Context server request timeout");
}
}
}
/// Sends a notification to the context server without expecting a response.
/// This function serializes the notification and sends it through the outbound channel.
pub fn notify(&self, method: &str, params: impl Serialize) -> Result<()> {
let notification = serde_json::to_string(&Notification {
jsonrpc: JSON_RPC_VERSION,
method,
params,
})
.unwrap();
self.outbound_tx.try_send(notification)?;
Ok(())
}
/// Notify the underlying transport of the negotiated MCP protocol version
/// so it can stamp subsequent requests (e.g. HTTP's `MCP-Protocol-Version`
/// header required from 2025-06-18 onward).
pub(crate) fn set_protocol_version(&self, version: &str) {
self.transport.set_protocol_version(version);
}
#[must_use]
pub fn on_notification(
&self,
method: &'static str,
f: Box<dyn 'static + Send + FnMut(Value, AsyncApp)>,
) -> NotificationSubscription {
let mut notification_subscriptions = self.subscription_set.lock();
NotificationSubscription {
id: notification_subscriptions.add_handler(method, f),
set: self.subscription_set.clone(),
}
}
}
#[derive(Debug)]
pub struct RequestCanceled;
impl std::error::Error for RequestCanceled {}
impl std::fmt::Display for RequestCanceled {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("Context server request was canceled")
}
}
impl fmt::Display for ContextServerId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl fmt::Debug for Client {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Context Server Client")
.field("id", &self.server_id.0)
.field("name", &self.name)
.finish_non_exhaustive()
}
}
slotmap::new_key_type! {
struct NotificationSubscriptionId;
}
#[derive(Default)]
pub struct NotificationSubscriptionSet {
// we have very few subscriptions at the moment
methods: Vec<(&'static str, Vec<NotificationSubscriptionId>)>,
handlers: SlotMap<NotificationSubscriptionId, NotificationHandler>,
}
impl NotificationSubscriptionSet {
#[must_use]
fn add_handler(
&mut self,
method: &'static str,
handler: NotificationHandler,
) -> NotificationSubscriptionId {
let id = self.handlers.insert(handler);
if let Some((_, handler_ids)) = self
.methods
.iter_mut()
.find(|(probe_method, _)| method == *probe_method)
{
debug_assert!(
handler_ids.len() < 20,
"Too many MCP handlers for {}. Consider using a different data structure.",
method
);
handler_ids.push(id);
} else {
self.methods.push((method, vec![id]));
};
id
}
fn notify(&mut self, method: &str, payload: Value, cx: &mut AsyncApp) {
let Some((_, handler_ids)) = self
.methods
.iter_mut()
.find(|(probe_method, _)| method == *probe_method)
else {
return;
};
for handler_id in handler_ids {
if let Some(handler) = self.handlers.get_mut(*handler_id) {
handler(payload.clone(), cx.clone());
}
}
}
}
pub struct NotificationSubscription {
id: NotificationSubscriptionId,
set: Arc<Mutex<NotificationSubscriptionSet>>,
}
impl Drop for NotificationSubscription {
fn drop(&mut self) {
let mut set = self.set.lock();
set.handlers.remove(self.id);
set.methods.retain_mut(|(_, handler_ids)| {
handler_ids.retain(|id| *id != self.id);
!handler_ids.is_empty()
});
}
}

View File

@@ -0,0 +1,164 @@
pub mod client;
pub mod listener;
pub mod oauth;
pub mod protocol;
#[cfg(any(test, feature = "test-support"))]
pub mod test;
pub mod transport;
pub mod types;
use collections::HashMap;
use http_client::HttpClient;
use std::path::Path;
use std::sync::Arc;
use std::time::Duration;
use std::{fmt::Display, path::PathBuf};
use anyhow::Result;
use client::Client;
use gpui::AsyncApp;
use parking_lot::RwLock;
pub use settings::ContextServerCommand;
use url::Url;
use crate::transport::HttpTransport;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct ContextServerId(pub Arc<str>);
impl Display for ContextServerId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
enum ContextServerTransport {
Stdio(ContextServerCommand, Option<PathBuf>),
Custom(Arc<dyn crate::transport::Transport>),
}
pub struct ContextServer {
id: ContextServerId,
client: RwLock<Option<Arc<crate::protocol::InitializedContextServerProtocol>>>,
configuration: ContextServerTransport,
request_timeout: Option<Duration>,
}
impl ContextServer {
pub fn stdio(
id: ContextServerId,
command: ContextServerCommand,
working_directory: Option<Arc<Path>>,
) -> Self {
Self {
id,
client: RwLock::new(None),
configuration: ContextServerTransport::Stdio(
command,
working_directory.map(|directory| directory.to_path_buf()),
),
request_timeout: None,
}
}
pub fn http(
id: ContextServerId,
endpoint: &Url,
headers: HashMap<String, String>,
http_client: Arc<dyn HttpClient>,
executor: gpui::BackgroundExecutor,
request_timeout: Option<Duration>,
) -> Result<Self> {
let transport = match endpoint.scheme() {
"http" | "https" => {
log::info!("Using HTTP transport for {}", endpoint);
let transport =
HttpTransport::new(http_client, endpoint.to_string(), headers, executor);
Arc::new(transport) as _
}
_ => anyhow::bail!("unsupported MCP url scheme {}", endpoint.scheme()),
};
Ok(Self::new_with_timeout(id, transport, request_timeout))
}
pub fn new(id: ContextServerId, transport: Arc<dyn crate::transport::Transport>) -> Self {
Self::new_with_timeout(id, transport, None)
}
pub fn new_with_timeout(
id: ContextServerId,
transport: Arc<dyn crate::transport::Transport>,
request_timeout: Option<Duration>,
) -> Self {
Self {
id,
client: RwLock::new(None),
configuration: ContextServerTransport::Custom(transport),
request_timeout,
}
}
pub fn id(&self) -> ContextServerId {
self.id.clone()
}
pub fn client(&self) -> Option<Arc<crate::protocol::InitializedContextServerProtocol>> {
self.client.read().clone()
}
pub async fn start(&self, cx: &AsyncApp) -> Result<()> {
self.initialize(self.new_client(cx)?).await
}
fn new_client(&self, cx: &AsyncApp) -> Result<Client> {
Ok(match &self.configuration {
ContextServerTransport::Stdio(command, working_directory) => Client::stdio(
client::ContextServerId(self.id.0.clone()),
client::ModelContextServerBinary {
executable: Path::new(&command.path).to_path_buf(),
args: command.args.clone(),
env: command.env.clone(),
timeout: command.timeout,
},
working_directory,
cx.clone(),
)?,
ContextServerTransport::Custom(transport) => Client::new(
client::ContextServerId(self.id.0.clone()),
self.id().0,
transport.clone(),
self.request_timeout,
cx.clone(),
)?,
})
}
async fn initialize(&self, client: Client) -> Result<()> {
log::debug!("starting context server {}", self.id);
let protocol = crate::protocol::ModelContextProtocol::new(client);
let client_info = types::Implementation {
name: "Zed".to_string(),
title: None,
version: env!("CARGO_PKG_VERSION").to_string(),
description: None,
};
let initialized_protocol = protocol.initialize(client_info).await?;
log::debug!(
"context server {} initialized: {:?}",
self.id,
initialized_protocol.initialize,
);
*self.client.write() = Some(Arc::new(initialized_protocol));
Ok(())
}
pub fn stop(&self) -> Result<()> {
let mut client = self.client.write();
if let Some(protocol) = client.take() {
drop(protocol);
}
Ok(())
}
}

View File

@@ -0,0 +1,438 @@
use ::serde::{Deserialize, Serialize};
use anyhow::{Context as _, Result};
use collections::HashMap;
use futures::AsyncReadExt;
use futures::stream::StreamExt;
use futures::{
AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, FutureExt,
channel::mpsc::{UnboundedReceiver, UnboundedSender, unbounded},
io::BufReader,
select_biased,
};
use gpui::{App, AppContext, AsyncApp, Task};
use net::async_net::{UnixListener, UnixStream};
use schemars::JsonSchema;
use serde::de::DeserializeOwned;
use serde_json::{json, value::RawValue};
use std::{
any::TypeId,
cell::RefCell,
path::{Path, PathBuf},
rc::Rc,
};
use util::ResultExt;
use crate::{
client::{CspResult, RequestId, Response},
types::{
CallToolParams, CallToolResponse, ListToolsResponse, Request, Tool, ToolAnnotations,
ToolResponseContent,
requests::{CallTool, ListTools},
},
};
pub struct McpServer {
socket_path: PathBuf,
tools: Rc<RefCell<HashMap<&'static str, RegisteredTool>>>,
handlers: Rc<RefCell<HashMap<&'static str, RequestHandler>>>,
_server_task: Task<()>,
}
struct RegisteredTool {
tool: Tool,
handler: ToolHandler,
}
type ToolHandler = Box<
dyn Fn(
Option<serde_json::Value>,
&mut AsyncApp,
) -> Task<Result<ToolResponse<serde_json::Value>>>,
>;
type RequestHandler = Box<dyn Fn(RequestId, Option<Box<RawValue>>, &App) -> Task<String>>;
impl McpServer {
pub fn new(cx: &AsyncApp) -> Task<Result<Self>> {
let task = cx.background_spawn(async move {
let temp_dir = tempfile::Builder::new().prefix("zed-mcp").tempdir()?;
let socket_path = temp_dir.path().join("mcp.sock");
let listener = UnixListener::bind(&socket_path).context("creating mcp socket")?;
anyhow::Ok((temp_dir, socket_path, listener))
});
cx.spawn(async move |cx| {
let (temp_dir, socket_path, listener) = task.await?;
let tools = Rc::new(RefCell::new(HashMap::default()));
let handlers = Rc::new(RefCell::new(HashMap::default()));
let server_task = cx.spawn({
let tools = tools.clone();
let handlers = handlers.clone();
async move |cx| {
while let Ok((stream, _)) = listener.accept().await {
Self::serve_connection(stream, tools.clone(), handlers.clone(), cx);
}
drop(temp_dir)
}
});
Ok(Self {
socket_path,
_server_task: server_task,
tools,
handlers,
})
})
}
pub fn add_tool<T: McpServerTool + Clone + 'static>(&mut self, tool: T) {
let mut settings = schemars::generate::SchemaSettings::draft07();
settings.inline_subschemas = true;
let mut generator = settings.into_generator();
let input_schema = generator.root_schema_for::<T::Input>();
let description = input_schema
.get("description")
.and_then(|desc| desc.as_str())
.map(|desc| desc.to_string());
debug_assert!(
description.is_some(),
"Input schema struct must include a doc comment for the tool description"
);
let registered_tool = RegisteredTool {
tool: Tool {
name: T::NAME.into(),
title: None,
description,
input_schema: input_schema.into(),
output_schema: if TypeId::of::<T::Output>() == TypeId::of::<()>() {
None
} else {
Some(generator.root_schema_for::<T::Output>().into())
},
annotations: Some(tool.annotations()),
},
handler: Box::new({
move |input_value, cx| {
let input = match input_value {
Some(input) => serde_json::from_value(input),
None => serde_json::from_value(serde_json::Value::Null),
};
let tool = tool.clone();
match input {
Ok(input) => cx.spawn(async move |cx| {
let output = tool.run(input, cx).await?;
Ok(ToolResponse {
content: output.content,
structured_content: serde_json::to_value(output.structured_content)
.unwrap_or_default(),
})
}),
Err(err) => Task::ready(Err(err.into())),
}
}
}),
};
self.tools.borrow_mut().insert(T::NAME, registered_tool);
}
pub fn handle_request<R: Request>(
&mut self,
f: impl Fn(R::Params, &App) -> Task<Result<R::Response>> + 'static,
) {
let f = Box::new(f);
self.handlers.borrow_mut().insert(
R::METHOD,
Box::new(move |req_id, opt_params, cx| {
let result = match opt_params {
Some(params) => serde_json::from_str(params.get()),
None => serde_json::from_value(serde_json::Value::Null),
};
let params: R::Params = match result {
Ok(params) => params,
Err(e) => {
return Task::ready(
serde_json::to_string(&Response::<R::Response> {
jsonrpc: "2.0",
id: req_id,
value: CspResult::Error(Some(crate::client::Error {
message: format!("{e}"),
code: -32700,
})),
})
.unwrap(),
);
}
};
let task = f(params, cx);
cx.background_spawn(async move {
match task.await {
Ok(result) => serde_json::to_string(&Response {
jsonrpc: "2.0",
id: req_id,
value: CspResult::Ok(Some(result)),
})
.unwrap(),
Err(e) => serde_json::to_string(&Response {
jsonrpc: "2.0",
id: req_id,
value: CspResult::Error::<R::Response>(Some(crate::client::Error {
message: format!("{e}"),
code: -32603,
})),
})
.unwrap(),
}
})
}),
);
}
pub fn socket_path(&self) -> &Path {
&self.socket_path
}
fn serve_connection(
stream: UnixStream,
tools: Rc<RefCell<HashMap<&'static str, RegisteredTool>>>,
handlers: Rc<RefCell<HashMap<&'static str, RequestHandler>>>,
cx: &mut AsyncApp,
) {
let (read, write) = stream.split();
let (incoming_tx, mut incoming_rx) = unbounded();
let (outgoing_tx, outgoing_rx) = unbounded();
cx.background_spawn(Self::handle_io(outgoing_rx, incoming_tx, write, read))
.detach();
cx.spawn(async move |cx| {
while let Some(request) = incoming_rx.next().await {
let Some(request_id) = request.id.clone() else {
continue;
};
if request.method == CallTool::METHOD {
Self::handle_call_tool(request_id, request.params, &tools, &outgoing_tx, cx)
.await;
} else if request.method == ListTools::METHOD {
Self::handle_list_tools(request.id.unwrap(), &tools, &outgoing_tx);
} else if let Some(handler) = handlers.borrow().get(&request.method.as_ref()) {
let outgoing_tx = outgoing_tx.clone();
let task = cx.update(|cx| handler(request_id, request.params, cx));
cx.spawn(async move |_| {
let response = task.await;
outgoing_tx.unbounded_send(response).ok();
})
.detach();
} else {
Self::send_err(
request_id,
format!("unhandled method {}", request.method),
&outgoing_tx,
);
}
}
})
.detach();
}
fn handle_list_tools(
request_id: RequestId,
tools: &Rc<RefCell<HashMap<&'static str, RegisteredTool>>>,
outgoing_tx: &UnboundedSender<String>,
) {
let response = ListToolsResponse {
tools: tools.borrow().values().map(|t| t.tool.clone()).collect(),
next_cursor: None,
meta: None,
};
outgoing_tx
.unbounded_send(
serde_json::to_string(&Response {
jsonrpc: "2.0",
id: request_id,
value: CspResult::Ok(Some(response)),
})
.unwrap_or_default(),
)
.ok();
}
async fn handle_call_tool(
request_id: RequestId,
params: Option<Box<RawValue>>,
tools: &Rc<RefCell<HashMap<&'static str, RegisteredTool>>>,
outgoing_tx: &UnboundedSender<String>,
cx: &mut AsyncApp,
) {
let result: Result<CallToolParams, serde_json::Error> = match params.as_ref() {
Some(params) => serde_json::from_str(params.get()),
None => serde_json::from_value(serde_json::Value::Null),
};
match result {
Ok(params) => {
if let Some(tool) = tools.borrow().get(&params.name.as_ref()) {
let outgoing_tx = outgoing_tx.clone();
let task = (tool.handler)(params.arguments, cx);
cx.spawn(async move |_| {
let response = match task.await {
Ok(result) => CallToolResponse {
content: result.content,
is_error: Some(false),
meta: None,
structured_content: if result.structured_content.is_null() {
None
} else {
Some(result.structured_content)
},
},
Err(err) => CallToolResponse {
content: vec![ToolResponseContent::Text {
text: err.to_string(),
}],
is_error: Some(true),
meta: None,
structured_content: None,
},
};
outgoing_tx
.unbounded_send(
serde_json::to_string(&Response {
jsonrpc: "2.0",
id: request_id,
value: CspResult::Ok(Some(response)),
})
.unwrap_or_default(),
)
.ok();
})
.detach();
} else {
Self::send_err(
request_id,
format!("Tool not found: {}", params.name),
outgoing_tx,
);
}
}
Err(err) => {
Self::send_err(request_id, err.to_string(), outgoing_tx);
}
}
}
fn send_err(
request_id: RequestId,
message: impl Into<String>,
outgoing_tx: &UnboundedSender<String>,
) {
outgoing_tx
.unbounded_send(
serde_json::to_string(&Response::<()> {
jsonrpc: "2.0",
id: request_id,
value: CspResult::Error(Some(crate::client::Error {
message: message.into(),
code: -32601,
})),
})
.unwrap(),
)
.ok();
}
async fn handle_io(
mut outgoing_rx: UnboundedReceiver<String>,
incoming_tx: UnboundedSender<RawRequest>,
mut outgoing_bytes: impl Unpin + AsyncWrite,
incoming_bytes: impl Unpin + AsyncRead,
) -> Result<()> {
let mut output_reader = BufReader::new(incoming_bytes);
let mut incoming_line = String::new();
loop {
select_biased! {
message = outgoing_rx.next().fuse() => {
if let Some(message) = message {
log::trace!("send: {}", &message);
outgoing_bytes.write_all(message.as_bytes()).await?;
outgoing_bytes.write_all(&[b'\n']).await?;
} else {
break;
}
}
bytes_read = output_reader.read_line(&mut incoming_line).fuse() => {
if bytes_read? == 0 {
break
}
log::trace!("recv: {}", &incoming_line);
match serde_json::from_str(&incoming_line) {
Ok(message) => {
incoming_tx.unbounded_send(message).log_err();
}
Err(error) => {
outgoing_bytes.write_all(serde_json::to_string(&json!({
"jsonrpc": "2.0",
"error": json!({
"code": -32603,
"message": format!("Failed to parse: {error}"),
}),
}))?.as_bytes()).await?;
outgoing_bytes.write_all(&[b'\n']).await?;
log::error!("failed to parse incoming message: {error}. Raw: {incoming_line}");
}
}
incoming_line.clear();
}
}
}
Ok(())
}
}
pub trait McpServerTool {
type Input: DeserializeOwned + JsonSchema;
type Output: Serialize + JsonSchema;
const NAME: &'static str;
fn annotations(&self) -> ToolAnnotations {
ToolAnnotations {
title: None,
read_only_hint: None,
destructive_hint: None,
idempotent_hint: None,
open_world_hint: None,
}
}
fn run(
&self,
input: Self::Input,
cx: &mut AsyncApp,
) -> impl Future<Output = Result<ToolResponse<Self::Output>>>;
}
#[derive(Debug)]
pub struct ToolResponse<T> {
pub content: Vec<ToolResponseContent>,
pub structured_content: T,
}
#[derive(Debug, Serialize, Deserialize)]
struct RawRequest {
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<RequestId>,
method: String,
#[serde(skip_serializing_if = "Option::is_none")]
params: Option<Box<serde_json::value::RawValue>>,
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,132 @@
//! This module implements parts of the Model Context Protocol.
//!
//! It handles the lifecycle messages, and provides a general interface to
//! interacting with an MCP server. It uses the generic JSON-RPC client to
//! read/write messages and the types from types.rs for serialization/deserialization
//! of messages.
use std::time::Duration;
use anyhow::Result;
use futures::channel::oneshot;
use gpui::AsyncApp;
use serde_json::Value;
use crate::client::{Client, NotificationSubscription};
use crate::types::{self, Notification, Request};
pub struct ModelContextProtocol {
inner: Client,
}
impl ModelContextProtocol {
pub(crate) fn new(inner: Client) -> Self {
Self { inner }
}
fn supported_protocols() -> Vec<types::ProtocolVersion> {
vec![
types::ProtocolVersion(types::LATEST_PROTOCOL_VERSION.to_string()),
types::ProtocolVersion(types::VERSION_2025_06_18.to_string()),
types::ProtocolVersion(types::VERSION_2025_03_26.to_string()),
types::ProtocolVersion(types::VERSION_2024_11_05.to_string()),
]
}
pub async fn initialize(
self,
client_info: types::Implementation,
) -> Result<InitializedContextServerProtocol> {
let params = types::InitializeParams {
protocol_version: types::ProtocolVersion(types::LATEST_PROTOCOL_VERSION.to_string()),
capabilities: types::ClientCapabilities {
experimental: None,
sampling: None,
roots: None,
},
meta: None,
client_info,
};
let response: types::InitializeResponse = self
.inner
.request(types::requests::Initialize::METHOD, params)
.await?;
anyhow::ensure!(
Self::supported_protocols().contains(&response.protocol_version),
"Unsupported protocol version: {:?}",
response.protocol_version
);
log::trace!("mcp server info {:?}", response.server_info);
// Per MCP 2025-06-18, HTTP transport must attach the negotiated version
// as `MCP-Protocol-Version` on every post-initialize request.
self.inner
.set_protocol_version(&response.protocol_version.0);
let initialized_protocol = InitializedContextServerProtocol {
inner: self.inner,
initialize: response,
};
initialized_protocol.notify::<types::notifications::Initialized>(())?;
Ok(initialized_protocol)
}
}
pub struct InitializedContextServerProtocol {
inner: Client,
pub initialize: types::InitializeResponse,
}
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum ServerCapability {
Experimental,
Logging,
Prompts,
Resources,
Tools,
}
impl InitializedContextServerProtocol {
/// Check if the server supports a specific capability
pub fn capable(&self, capability: ServerCapability) -> bool {
match capability {
ServerCapability::Experimental => self.initialize.capabilities.experimental.is_some(),
ServerCapability::Logging => self.initialize.capabilities.logging.is_some(),
ServerCapability::Prompts => self.initialize.capabilities.prompts.is_some(),
ServerCapability::Resources => self.initialize.capabilities.resources.is_some(),
ServerCapability::Tools => self.initialize.capabilities.tools.is_some(),
}
}
pub async fn request<T: Request>(&self, params: T::Params) -> Result<T::Response> {
self.inner.request(T::METHOD, params).await
}
pub async fn request_with<T: Request>(
&self,
params: T::Params,
cancel_rx: Option<oneshot::Receiver<()>>,
timeout: Option<Duration>,
) -> Result<T::Response> {
self.inner
.request_with(T::METHOD, params, cancel_rx, timeout)
.await
}
pub fn notify<T: Notification>(&self, params: T::Params) -> Result<()> {
self.inner.notify(T::METHOD, params)
}
pub fn on_notification(
&self,
method: &'static str,
f: Box<dyn 'static + Send + FnMut(Value, AsyncApp)>,
) -> NotificationSubscription {
self.inner.on_notification(method, f)
}
}

View File

@@ -0,0 +1,132 @@
use anyhow::Context as _;
use collections::HashMap;
use futures::{FutureExt, Stream, StreamExt as _, future::BoxFuture, lock::Mutex};
use gpui::BackgroundExecutor;
use std::{pin::Pin, sync::Arc};
use crate::{
transport::Transport,
types::{Implementation, InitializeResponse, ProtocolVersion, ServerCapabilities},
};
pub fn create_fake_transport(
name: impl Into<String>,
executor: BackgroundExecutor,
) -> FakeTransport {
let name = name.into();
FakeTransport::new(executor).on_request::<crate::types::requests::Initialize, _>(
move |_params| {
let name = name.clone();
async move { create_initialize_response(name.clone()) }
},
)
}
fn create_initialize_response(server_name: String) -> InitializeResponse {
InitializeResponse {
protocol_version: ProtocolVersion(crate::types::LATEST_PROTOCOL_VERSION.to_string()),
server_info: Implementation {
name: server_name,
title: None,
version: "1.0.0".to_string(),
description: None,
},
capabilities: ServerCapabilities::default(),
meta: None,
}
}
pub struct FakeTransport {
request_handlers: HashMap<
&'static str,
Arc<dyn Send + Sync + Fn(serde_json::Value) -> BoxFuture<'static, serde_json::Value>>,
>,
tx: futures::channel::mpsc::UnboundedSender<String>,
rx: Arc<Mutex<futures::channel::mpsc::UnboundedReceiver<String>>>,
executor: BackgroundExecutor,
}
impl FakeTransport {
pub fn new(executor: BackgroundExecutor) -> Self {
let (tx, rx) = futures::channel::mpsc::unbounded();
Self {
request_handlers: Default::default(),
tx,
rx: Arc::new(Mutex::new(rx)),
executor,
}
}
pub fn on_request<T, Fut>(
mut self,
handler: impl 'static + Send + Sync + Fn(T::Params) -> Fut,
) -> Self
where
T: crate::types::Request,
Fut: 'static + Send + Future<Output = T::Response>,
{
self.request_handlers.insert(
T::METHOD,
Arc::new(move |value| {
let params = value
.get("params")
.cloned()
.unwrap_or(serde_json::Value::Null);
let params: T::Params =
serde_json::from_value(params).expect("Invalid parameters received");
let response = handler(params);
async move { serde_json::to_value(response.await).unwrap() }.boxed()
}),
);
self
}
}
#[async_trait::async_trait]
impl Transport for FakeTransport {
async fn send(&self, message: String) -> anyhow::Result<()> {
if let Ok(msg) = serde_json::from_str::<serde_json::Value>(&message) {
let id = msg.get("id").and_then(|id| id.as_u64()).unwrap_or(0);
if let Some(method) = msg.get("method") {
let method = method.as_str().expect("Invalid method received");
if let Some(handler) = self.request_handlers.get(method) {
let payload = handler(msg).await;
let response = serde_json::json!({
"jsonrpc": "2.0",
"id": id,
"result": payload
});
self.tx
.unbounded_send(response.to_string())
.context("sending a message")?;
} else {
log::debug!("No handler registered for MCP request '{method}'");
}
}
}
Ok(())
}
fn receive(&self) -> Pin<Box<dyn Stream<Item = String> + Send>> {
let rx = self.rx.clone();
let executor = self.executor.clone();
Box::pin(futures::stream::unfold(rx, move |rx| {
let executor = executor.clone();
async move {
let mut rx_guard = rx.lock().await;
executor.simulate_random_delay().await;
if let Some(message) = rx_guard.next().await {
drop(rx_guard);
Some((message, rx))
} else {
None
}
}
}))
}
fn receive_err(&self) -> Pin<Box<dyn Stream<Item = String> + Send>> {
Box::pin(futures::stream::empty())
}
}

View File

@@ -0,0 +1,22 @@
pub mod http;
mod stdio_transport;
use anyhow::Result;
use async_trait::async_trait;
use futures::Stream;
use std::pin::Pin;
pub use http::*;
pub use stdio_transport::*;
#[async_trait]
pub trait Transport: Send + Sync {
async fn send(&self, message: String) -> Result<()>;
fn receive(&self) -> Pin<Box<dyn Stream<Item = String> + Send>>;
fn receive_err(&self) -> Pin<Box<dyn Stream<Item = String> + Send>>;
/// Called after the MCP initialize handshake completes so transports that
/// need the negotiated version (currently only HTTP, which must attach an
/// `MCP-Protocol-Version` header from 2025-06-18 onward) can pick it up.
fn set_protocol_version(&self, _version: &str) {}
}

View File

@@ -0,0 +1,786 @@
use anyhow::{Result, anyhow};
use async_trait::async_trait;
use collections::HashMap;
use futures::{Stream, StreamExt};
use gpui::BackgroundExecutor;
use http_client::{AsyncBody, HttpClient, Request, Response, http::Method};
use parking_lot::Mutex as SyncMutex;
use std::{pin::Pin, sync::Arc};
use crate::oauth::{self, OAuthTokenProvider, WwwAuthenticate};
use crate::transport::Transport;
use crate::types;
/// Typed errors returned by the HTTP transport that callers can downcast from
/// `anyhow::Error` to handle specific failure modes.
#[derive(Debug)]
pub enum TransportError {
/// The server returned 401 and token refresh either wasn't possible or
/// failed. The caller should initiate the OAuth authorization flow.
AuthRequired { www_authenticate: WwwAuthenticate },
}
impl std::fmt::Display for TransportError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
TransportError::AuthRequired { .. } => {
write!(f, "OAuth authorization required")
}
}
}
}
impl std::error::Error for TransportError {}
// Constants from MCP spec
const HEADER_SESSION_ID: &str = "Mcp-Session-Id";
const HEADER_PROTOCOL_VERSION: &str = "MCP-Protocol-Version";
const EVENT_STREAM_MIME_TYPE: &str = "text/event-stream";
const JSON_MIME_TYPE: &str = "application/json";
/// HTTP Transport with session management and SSE support
pub struct HttpTransport {
http_client: Arc<dyn HttpClient>,
endpoint: String,
session_id: Arc<SyncMutex<Option<String>>>,
/// Negotiated MCP protocol version, populated by `set_protocol_version`
/// after the initialize handshake. From 2025-06-18 onward the server
/// requires clients to echo this in the `MCP-Protocol-Version` header on
/// every subsequent request.
protocol_version: Arc<SyncMutex<Option<String>>>,
executor: BackgroundExecutor,
response_tx: async_channel::Sender<String>,
response_rx: async_channel::Receiver<String>,
error_tx: async_channel::Sender<String>,
error_rx: async_channel::Receiver<String>,
/// Static headers to include in every request (e.g. from server config).
headers: HashMap<String, String>,
/// When set, the transport attaches `Authorization: Bearer` headers and
/// handles 401 responses with token refresh + retry.
token_provider: Option<Arc<dyn OAuthTokenProvider>>,
}
impl HttpTransport {
pub fn new(
http_client: Arc<dyn HttpClient>,
endpoint: String,
headers: HashMap<String, String>,
executor: BackgroundExecutor,
) -> Self {
Self::new_with_token_provider(http_client, endpoint, headers, executor, None)
}
pub fn new_with_token_provider(
http_client: Arc<dyn HttpClient>,
endpoint: String,
headers: HashMap<String, String>,
executor: BackgroundExecutor,
token_provider: Option<Arc<dyn OAuthTokenProvider>>,
) -> Self {
let (response_tx, response_rx) = async_channel::unbounded();
let (error_tx, error_rx) = async_channel::unbounded();
Self {
http_client,
executor,
endpoint,
session_id: Arc::new(SyncMutex::new(None)),
protocol_version: Arc::new(SyncMutex::new(None)),
response_tx,
response_rx,
error_tx,
error_rx,
headers,
token_provider,
}
}
/// Build a POST request for the given message body, attaching all standard
/// headers (content-type, accept, session ID, static headers, and bearer
/// token if available).
fn build_request(&self, message: &[u8]) -> Result<http_client::Request<AsyncBody>> {
let mut request_builder = Request::builder()
.method(Method::POST)
.uri(&self.endpoint)
.header("Content-Type", JSON_MIME_TYPE)
.header(
"Accept",
format!("{}, {}", JSON_MIME_TYPE, EVENT_STREAM_MIME_TYPE),
);
for (key, value) in &self.headers {
request_builder = request_builder.header(key.as_str(), value.as_str());
}
// Attach bearer token when a token provider is present.
if let Some(token) = self.token_provider.as_ref().and_then(|p| p.access_token()) {
request_builder = request_builder.header("Authorization", format!("Bearer {}", token));
}
// Add session ID if we have one (except for initialize).
if let Some(ref session_id) = *self.session_id.lock() {
request_builder = request_builder.header(HEADER_SESSION_ID, session_id.as_str());
}
// Echo the negotiated protocol version once initialization has
// completed. Required by servers speaking MCP 2025-06-18 or later.
if let Some(ref version) = *self.protocol_version.lock()
&& types::requires_protocol_version_header(version)
{
request_builder = request_builder.header(HEADER_PROTOCOL_VERSION, version.as_str());
}
Ok(request_builder.body(AsyncBody::from(message.to_vec()))?)
}
/// Send a message and handle the response based on content type.
async fn send_message(&self, message: String) -> Result<()> {
let is_notification =
!message.contains("\"id\":") || message.contains("notifications/initialized");
// If we currently have no access token, try refreshing before sending
// the request so restored but expired sessions do not need an initial
// 401 round-trip before they can recover.
if let Some(ref provider) = self.token_provider {
if provider.access_token().is_none() {
provider.try_refresh().await.unwrap_or(false);
}
}
let request = self.build_request(message.as_bytes())?;
let mut response = self.http_client.send(request).await?;
// On 401, try refreshing the token and retry once.
if response.status().as_u16() == 401 {
let www_auth_header = response
.headers()
.get("www-authenticate")
.and_then(|v| v.to_str().ok())
.unwrap_or("Bearer");
let www_authenticate =
oauth::parse_www_authenticate(www_auth_header).unwrap_or(WwwAuthenticate {
resource_metadata: None,
scope: None,
error: None,
error_description: None,
});
if let Some(ref provider) = self.token_provider {
if provider.try_refresh().await.unwrap_or(false) {
// Retry with the refreshed token.
let retry_request = self.build_request(message.as_bytes())?;
response = self.http_client.send(retry_request).await?;
// If still 401 after refresh, give up.
if response.status().as_u16() == 401 {
return Err(TransportError::AuthRequired { www_authenticate }.into());
}
} else {
return Err(TransportError::AuthRequired { www_authenticate }.into());
}
} else {
return Err(TransportError::AuthRequired { www_authenticate }.into());
}
}
// Handle different response types based on status and content-type.
match response.status() {
status if status.is_success() => {
// Check content type
let content_type = response
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok());
// Extract session ID from response headers if present
if let Some(session_id) = response
.headers()
.get(HEADER_SESSION_ID)
.and_then(|v| v.to_str().ok())
{
*self.session_id.lock() = Some(session_id.to_string());
log::debug!("Session ID set: {}", session_id);
}
match content_type {
Some(ct) if ct.starts_with(JSON_MIME_TYPE) => {
// JSON response - read and forward immediately
let mut body = String::new();
futures::AsyncReadExt::read_to_string(response.body_mut(), &mut body)
.await?;
// Only send non-empty responses
if !body.is_empty() {
self.response_tx
.send(body)
.await
.map_err(|_| anyhow!("Failed to send JSON response"))?;
}
}
Some(ct) if ct.starts_with(EVENT_STREAM_MIME_TYPE) => {
// SSE stream - set up streaming
self.setup_sse_stream(response).await?;
}
_ => {
// For notifications, 202 Accepted with no content type is ok
if is_notification && status.as_u16() == 202 {
log::debug!("Notification accepted");
} else {
return Err(anyhow!("Unexpected content type: {:?}", content_type));
}
}
}
}
status if status.as_u16() == 202 => {
// Accepted - notification acknowledged, no response needed
log::debug!("Notification accepted");
}
_ => {
let mut error_body = String::new();
futures::AsyncReadExt::read_to_string(response.body_mut(), &mut error_body).await?;
self.error_tx
.send(format!("HTTP {}: {}", response.status(), error_body))
.await
.map_err(|_| anyhow!("Failed to send error"))?;
}
}
Ok(())
}
/// Set up SSE streaming from the response
async fn setup_sse_stream(&self, mut response: Response<AsyncBody>) -> Result<()> {
let response_tx = self.response_tx.clone();
let error_tx = self.error_tx.clone();
// Spawn a task to handle the SSE stream
self.executor
.spawn(async move {
let reader = futures::io::BufReader::new(response.body_mut());
let mut lines = futures::AsyncBufReadExt::lines(reader);
let mut data_buffer = Vec::new();
let mut in_message = false;
while let Some(line_result) = lines.next().await {
match line_result {
Ok(line) => {
if line.is_empty() {
// Empty line signals end of event
if !data_buffer.is_empty() {
let message = data_buffer.join("\n");
// Filter out ping messages and empty data
if !message.trim().is_empty() && message != "ping" {
if let Err(e) = response_tx.send(message).await {
log::error!("Failed to send SSE message: {}", e);
break;
}
}
data_buffer.clear();
}
in_message = false;
} else if let Some(data) = line.strip_prefix("data: ") {
// Handle data lines
let data = data.trim();
if !data.is_empty() {
// Check if this is a ping message
if data == "ping" {
log::trace!("Received SSE ping");
continue;
}
data_buffer.push(data.to_string());
in_message = true;
}
} else if line.starts_with("event:")
|| line.starts_with("id:")
|| line.starts_with("retry:")
{
// Ignore other SSE fields
continue;
} else if in_message {
// Continuation of data
data_buffer.push(line);
}
}
Err(e) => {
let _ = error_tx.send(format!("SSE stream error: {}", e)).await;
break;
}
}
}
})
.detach();
Ok(())
}
}
#[async_trait]
impl Transport for HttpTransport {
async fn send(&self, message: String) -> Result<()> {
self.send_message(message).await
}
fn receive(&self) -> Pin<Box<dyn Stream<Item = String> + Send>> {
Box::pin(self.response_rx.clone())
}
fn receive_err(&self) -> Pin<Box<dyn Stream<Item = String> + Send>> {
Box::pin(self.error_rx.clone())
}
fn set_protocol_version(&self, version: &str) {
*self.protocol_version.lock() = Some(version.to_string());
}
}
impl Drop for HttpTransport {
fn drop(&mut self) {
// Try to cleanup session on drop
let http_client = self.http_client.clone();
let endpoint = self.endpoint.clone();
let session_id = self.session_id.lock().clone();
let protocol_version = self.protocol_version.lock().clone();
let headers = self.headers.clone();
let access_token = self.token_provider.as_ref().and_then(|p| p.access_token());
if let Some(session_id) = session_id {
self.executor
.spawn(async move {
let mut request_builder = Request::builder()
.method(Method::DELETE)
.uri(&endpoint)
.header(HEADER_SESSION_ID, &session_id);
// Add static authentication headers.
for (key, value) in headers {
request_builder = request_builder.header(key.as_str(), value.as_str());
}
// Attach bearer token if available.
if let Some(token) = access_token {
request_builder =
request_builder.header("Authorization", format!("Bearer {}", token));
}
// Stamp the negotiated MCP protocol version on the DELETE
// too, matching what `build_request` does for POSTs.
if let Some(ref version) = protocol_version
&& types::requires_protocol_version_header(version)
{
request_builder =
request_builder.header(HEADER_PROTOCOL_VERSION, version.as_str());
}
let request = request_builder.body(AsyncBody::empty());
if let Ok(request) = request {
let _ = http_client.send(request).await;
}
})
.detach();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use gpui::TestAppContext;
use parking_lot::Mutex as SyncMutex;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
/// A mock token provider that returns a configurable token and tracks
/// refresh attempts.
struct FakeTokenProvider {
token: SyncMutex<Option<String>>,
refreshed_token: SyncMutex<Option<String>>,
refresh_succeeds: AtomicBool,
refresh_count: AtomicUsize,
}
impl FakeTokenProvider {
fn new(token: Option<&str>, refresh_succeeds: bool) -> Arc<Self> {
Self::with_refreshed_token(token, None, refresh_succeeds)
}
fn with_refreshed_token(
token: Option<&str>,
refreshed_token: Option<&str>,
refresh_succeeds: bool,
) -> Arc<Self> {
Arc::new(Self {
token: SyncMutex::new(token.map(String::from)),
refreshed_token: SyncMutex::new(refreshed_token.map(String::from)),
refresh_succeeds: AtomicBool::new(refresh_succeeds),
refresh_count: AtomicUsize::new(0),
})
}
fn set_token(&self, token: &str) {
*self.token.lock() = Some(token.to_string());
}
fn refresh_count(&self) -> usize {
self.refresh_count.load(Ordering::SeqCst)
}
}
#[async_trait]
impl OAuthTokenProvider for FakeTokenProvider {
fn access_token(&self) -> Option<String> {
self.token.lock().clone()
}
async fn try_refresh(&self) -> Result<bool> {
self.refresh_count.fetch_add(1, Ordering::SeqCst);
let refresh_succeeds = self.refresh_succeeds.load(Ordering::SeqCst);
if refresh_succeeds {
if let Some(token) = self.refreshed_token.lock().clone() {
*self.token.lock() = Some(token);
}
}
Ok(refresh_succeeds)
}
}
fn make_fake_http_client(
handler: impl Fn(
http_client::Request<AsyncBody>,
) -> std::pin::Pin<
Box<dyn std::future::Future<Output = anyhow::Result<Response<AsyncBody>>> + Send>,
> + Send
+ Sync
+ 'static,
) -> Arc<dyn HttpClient> {
http_client::FakeHttpClient::create(handler) as Arc<dyn HttpClient>
}
fn json_response(status: u16, body: &str) -> anyhow::Result<Response<AsyncBody>> {
Ok(Response::builder()
.status(status)
.header("Content-Type", "application/json")
.body(AsyncBody::from(body.as_bytes().to_vec()))
.unwrap())
}
#[gpui::test]
async fn test_bearer_token_attached_to_requests(cx: &mut TestAppContext) {
// Capture the Authorization header from the request.
let captured_auth = Arc::new(SyncMutex::new(None::<String>));
let captured_auth_clone = captured_auth.clone();
let client = make_fake_http_client(move |req| {
let auth = req
.headers()
.get("Authorization")
.map(|v| v.to_str().unwrap().to_string());
*captured_auth_clone.lock() = auth;
Box::pin(async { json_response(200, r#"{"jsonrpc":"2.0","id":1,"result":{}}"#) })
});
let provider = FakeTokenProvider::new(Some("test-access-token"), false);
let transport = HttpTransport::new_with_token_provider(
client,
"http://mcp.example.com/mcp".to_string(),
HashMap::default(),
cx.background_executor.clone(),
Some(provider),
);
transport
.send(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#.to_string())
.await
.expect("send should succeed");
assert_eq!(
captured_auth.lock().as_deref(),
Some("Bearer test-access-token"),
);
}
#[gpui::test]
async fn test_no_bearer_token_without_provider(cx: &mut TestAppContext) {
let captured_auth = Arc::new(SyncMutex::new(None::<String>));
let captured_auth_clone = captured_auth.clone();
let client = make_fake_http_client(move |req| {
let auth = req
.headers()
.get("Authorization")
.map(|v| v.to_str().unwrap().to_string());
*captured_auth_clone.lock() = auth;
Box::pin(async { json_response(200, r#"{"jsonrpc":"2.0","id":1,"result":{}}"#) })
});
let transport = HttpTransport::new(
client,
"http://mcp.example.com/mcp".to_string(),
HashMap::default(),
cx.background_executor.clone(),
);
transport
.send(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#.to_string())
.await
.expect("send should succeed");
assert!(captured_auth.lock().is_none());
}
#[gpui::test]
async fn test_missing_token_triggers_refresh_before_first_request(cx: &mut TestAppContext) {
let captured_auth = Arc::new(SyncMutex::new(None::<String>));
let captured_auth_clone = captured_auth.clone();
let client = make_fake_http_client(move |req| {
let auth = req
.headers()
.get("Authorization")
.map(|v| v.to_str().unwrap().to_string());
*captured_auth_clone.lock() = auth;
Box::pin(async { json_response(200, r#"{"jsonrpc":"2.0","id":1,"result":{}}"#) })
});
let provider = FakeTokenProvider::with_refreshed_token(None, Some("refreshed-token"), true);
let transport = HttpTransport::new_with_token_provider(
client,
"http://mcp.example.com/mcp".to_string(),
HashMap::default(),
cx.background_executor.clone(),
Some(provider.clone()),
);
transport
.send(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#.to_string())
.await
.expect("send should succeed after proactive refresh");
assert_eq!(provider.refresh_count(), 1);
assert_eq!(
captured_auth.lock().as_deref(),
Some("Bearer refreshed-token"),
);
}
#[gpui::test]
async fn test_invalid_token_still_triggers_refresh_and_retry(cx: &mut TestAppContext) {
let request_count = Arc::new(AtomicUsize::new(0));
let request_count_clone = request_count.clone();
let client = make_fake_http_client(move |_req| {
let count = request_count_clone.fetch_add(1, Ordering::SeqCst);
Box::pin(async move {
if count == 0 {
Ok(Response::builder()
.status(401)
.header(
"WWW-Authenticate",
r#"Bearer error="invalid_token", resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource""#,
)
.body(AsyncBody::from(b"Unauthorized".to_vec()))
.unwrap())
} else {
json_response(200, r#"{"jsonrpc":"2.0","id":1,"result":{}}"#)
}
})
});
let provider = FakeTokenProvider::with_refreshed_token(
Some("old-token"),
Some("refreshed-token"),
true,
);
let transport = HttpTransport::new_with_token_provider(
client,
"http://mcp.example.com/mcp".to_string(),
HashMap::default(),
cx.background_executor.clone(),
Some(provider.clone()),
);
transport
.send(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#.to_string())
.await
.expect("send should succeed after refresh");
assert_eq!(provider.refresh_count(), 1);
assert_eq!(request_count.load(Ordering::SeqCst), 2);
}
#[gpui::test]
async fn test_401_triggers_refresh_and_retry(cx: &mut TestAppContext) {
let request_count = Arc::new(AtomicUsize::new(0));
let request_count_clone = request_count.clone();
let client = make_fake_http_client(move |_req| {
let count = request_count_clone.fetch_add(1, Ordering::SeqCst);
Box::pin(async move {
if count == 0 {
// First request: 401.
Ok(Response::builder()
.status(401)
.header(
"WWW-Authenticate",
r#"Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource""#,
)
.body(AsyncBody::from(b"Unauthorized".to_vec()))
.unwrap())
} else {
// Retry after refresh: 200.
json_response(200, r#"{"jsonrpc":"2.0","id":1,"result":{}}"#)
}
})
});
let provider = FakeTokenProvider::new(Some("old-token"), true);
// Simulate the refresh updating the token.
let provider_ref = provider.clone();
let transport = HttpTransport::new_with_token_provider(
client,
"http://mcp.example.com/mcp".to_string(),
HashMap::default(),
cx.background_executor.clone(),
Some(provider.clone()),
);
// Set the new token that will be used on retry.
provider_ref.set_token("refreshed-token");
transport
.send(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#.to_string())
.await
.expect("send should succeed after refresh");
assert_eq!(provider_ref.refresh_count(), 1);
assert_eq!(request_count.load(Ordering::SeqCst), 2);
}
#[gpui::test]
async fn test_401_returns_auth_required_when_refresh_fails(cx: &mut TestAppContext) {
let client = make_fake_http_client(|_req| {
Box::pin(async {
Ok(Response::builder()
.status(401)
.header(
"WWW-Authenticate",
r#"Bearer resource_metadata="https://mcp.example.com/.well-known/oauth-protected-resource", scope="read write""#,
)
.body(AsyncBody::from(b"Unauthorized".to_vec()))
.unwrap())
})
});
// Refresh returns false — no new token available.
let provider = FakeTokenProvider::new(Some("stale-token"), false);
let transport = HttpTransport::new_with_token_provider(
client,
"http://mcp.example.com/mcp".to_string(),
HashMap::default(),
cx.background_executor.clone(),
Some(provider.clone()),
);
let err = transport
.send(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#.to_string())
.await
.unwrap_err();
let transport_err = err
.downcast_ref::<TransportError>()
.expect("error should be TransportError");
match transport_err {
TransportError::AuthRequired { www_authenticate } => {
assert_eq!(
www_authenticate
.resource_metadata
.as_ref()
.map(|u| u.as_str()),
Some("https://mcp.example.com/.well-known/oauth-protected-resource"),
);
assert_eq!(
www_authenticate.scope,
Some(vec!["read".to_string(), "write".to_string()]),
);
}
}
assert_eq!(provider.refresh_count(), 1);
}
#[gpui::test]
async fn test_401_returns_auth_required_without_provider(cx: &mut TestAppContext) {
let client = make_fake_http_client(|_req| {
Box::pin(async {
Ok(Response::builder()
.status(401)
.header("WWW-Authenticate", "Bearer")
.body(AsyncBody::from(b"Unauthorized".to_vec()))
.unwrap())
})
});
// No token provider at all.
let transport = HttpTransport::new(
client,
"http://mcp.example.com/mcp".to_string(),
HashMap::default(),
cx.background_executor.clone(),
);
let err = transport
.send(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#.to_string())
.await
.unwrap_err();
let transport_err = err
.downcast_ref::<TransportError>()
.expect("error should be TransportError");
match transport_err {
TransportError::AuthRequired { www_authenticate } => {
assert!(www_authenticate.resource_metadata.is_none());
assert!(www_authenticate.scope.is_none());
}
}
}
#[gpui::test]
async fn test_401_after_successful_refresh_still_returns_auth_required(
cx: &mut TestAppContext,
) {
// Both requests return 401 — the server rejects the refreshed token too.
let client = make_fake_http_client(|_req| {
Box::pin(async {
Ok(Response::builder()
.status(401)
.header("WWW-Authenticate", "Bearer")
.body(AsyncBody::from(b"Unauthorized".to_vec()))
.unwrap())
})
});
let provider = FakeTokenProvider::new(Some("token"), true);
let transport = HttpTransport::new_with_token_provider(
client,
"http://mcp.example.com/mcp".to_string(),
HashMap::default(),
cx.background_executor.clone(),
Some(provider.clone()),
);
let err = transport
.send(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#.to_string())
.await
.unwrap_err();
err.downcast_ref::<TransportError>()
.expect("error should be TransportError");
// Refresh was attempted exactly once.
assert_eq!(provider.refresh_count(), 1);
}
}

View File

@@ -0,0 +1,148 @@
use std::path::PathBuf;
use std::pin::Pin;
use anyhow::Result;
use async_trait::async_trait;
use futures::io::{BufReader, BufWriter};
use futures::{
AsyncBufReadExt as _, AsyncRead, AsyncWrite, AsyncWriteExt as _, Stream, StreamExt as _,
};
use gpui::AsyncApp;
use util::TryFutureExt as _;
use util::process::Child;
use util::shell::Shell;
use util::shell_builder::ShellBuilder;
use crate::client::ModelContextServerBinary;
use crate::transport::Transport;
pub struct StdioTransport {
stdout_sender: async_channel::Sender<String>,
stdin_receiver: async_channel::Receiver<String>,
stderr_receiver: async_channel::Receiver<String>,
server: Child,
}
impl StdioTransport {
pub fn new(
binary: ModelContextServerBinary,
working_directory: &Option<PathBuf>,
cx: &AsyncApp,
) -> Result<Self> {
let builder = ShellBuilder::new(&Shell::System, cfg!(windows)).non_interactive();
let mut command =
builder.build_std_command(Some(binary.executable.display().to_string()), &binary.args);
command.envs(binary.env.unwrap_or_default());
if let Some(working_directory) = working_directory {
command.current_dir(working_directory);
}
let mut server = Child::spawn(
command,
std::process::Stdio::piped(),
std::process::Stdio::piped(),
std::process::Stdio::piped(),
)?;
let stdin = server.stdin.take().unwrap();
let stdout = server.stdout.take().unwrap();
let stderr = server.stderr.take().unwrap();
let (stdin_sender, stdin_receiver) = async_channel::unbounded::<String>();
let (stdout_sender, stdout_receiver) = async_channel::unbounded::<String>();
let (stderr_sender, stderr_receiver) = async_channel::unbounded::<String>();
cx.spawn(async move |_| Self::handle_output(stdin, stdout_receiver).log_err().await)
.detach();
cx.spawn(async move |_| Self::handle_input(stdout, stdin_sender).await)
.detach();
cx.spawn(async move |_| Self::handle_err(stderr, stderr_sender).await)
.detach();
Ok(Self {
stdout_sender,
stdin_receiver,
stderr_receiver,
server,
})
}
async fn handle_input<Stdout>(stdin: Stdout, inbound_rx: async_channel::Sender<String>)
where
Stdout: AsyncRead + Unpin + Send + 'static,
{
let mut stdin = BufReader::new(stdin);
let mut line = String::new();
while let Ok(n) = stdin.read_line(&mut line).await {
if n == 0 {
break;
}
if inbound_rx.send(line.clone()).await.is_err() {
break;
}
line.clear();
}
}
async fn handle_output<Stdin>(
stdin: Stdin,
outbound_rx: async_channel::Receiver<String>,
) -> Result<()>
where
Stdin: AsyncWrite + Unpin + Send + 'static,
{
let mut stdin = BufWriter::new(stdin);
let mut pinned_rx = Box::pin(outbound_rx);
while let Some(message) = pinned_rx.next().await {
log::trace!("outgoing message: {}", message);
stdin.write_all(message.as_bytes()).await?;
stdin.write_all(b"\n").await?;
stdin.flush().await?;
}
Ok(())
}
async fn handle_err<Stderr>(stderr: Stderr, stderr_tx: async_channel::Sender<String>)
where
Stderr: AsyncRead + Unpin + Send + 'static,
{
let mut stderr = BufReader::new(stderr);
let mut line = String::new();
while let Ok(n) = stderr.read_line(&mut line).await {
if n == 0 {
break;
}
if stderr_tx.send(line.clone()).await.is_err() {
break;
}
line.clear();
}
}
}
#[async_trait]
impl Transport for StdioTransport {
async fn send(&self, message: String) -> Result<()> {
Ok(self.stdout_sender.send(message).await?)
}
fn receive(&self) -> Pin<Box<dyn Stream<Item = String> + Send>> {
Box::pin(self.stdin_receiver.clone())
}
fn receive_err(&self) -> Pin<Box<dyn Stream<Item = String> + Send>> {
Box::pin(self.stderr_receiver.clone())
}
}
impl Drop for StdioTransport {
fn drop(&mut self) {
let _ = self.server.kill();
}
}

View File

@@ -0,0 +1,811 @@
use collections::HashMap;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use url::Url;
use crate::client::RequestId;
pub const VERSION_2024_11_05: &str = "2024-11-05";
pub const VERSION_2025_03_26: &str = "2025-03-26";
pub const VERSION_2025_06_18: &str = "2025-06-18";
pub const LATEST_PROTOCOL_VERSION: &str = "2025-11-25";
/// Protocol versions that include the streamable HTTP transport's
/// `MCP-Protocol-Version` header requirement on post-initialize requests.
pub fn requires_protocol_version_header(version: &str) -> bool {
matches!(version, VERSION_2025_06_18 | LATEST_PROTOCOL_VERSION)
}
pub mod requests {
use super::*;
macro_rules! request {
($method:expr, $name:ident, $params:ty, $response:ty) => {
pub struct $name;
impl Request for $name {
type Params = $params;
type Response = $response;
const METHOD: &'static str = $method;
}
};
}
request!(
"initialize",
Initialize,
InitializeParams,
InitializeResponse
);
request!("tools/call", CallTool, CallToolParams, CallToolResponse);
request!(
"resources/unsubscribe",
ResourcesUnsubscribe,
ResourcesUnsubscribeParams,
()
);
request!(
"resources/subscribe",
ResourcesSubscribe,
ResourcesSubscribeParams,
()
);
request!(
"resources/read",
ResourcesRead,
ResourcesReadParams,
ResourcesReadResponse
);
request!("resources/list", ResourcesList, (), ResourcesListResponse);
request!(
"logging/setLevel",
LoggingSetLevel,
LoggingSetLevelParams,
()
);
request!(
"prompts/get",
PromptsGet,
PromptsGetParams,
PromptsGetResponse
);
request!("prompts/list", PromptsList, (), PromptsListResponse);
request!(
"completion/complete",
CompletionComplete,
CompletionCompleteParams,
CompletionCompleteResponse
);
request!("ping", Ping, (), ());
request!("tools/list", ListTools, (), ListToolsResponse);
request!(
"resources/templates/list",
ListResourceTemplates,
(),
ListResourceTemplatesResponse
);
request!("roots/list", ListRoots, (), ListRootsResponse);
}
pub trait Request {
type Params: DeserializeOwned + Serialize + Send + Sync + 'static;
type Response: DeserializeOwned + Serialize + Send + Sync + 'static;
const METHOD: &'static str;
}
pub mod notifications {
use super::*;
macro_rules! notification {
($method:expr, $name:ident, $params:ty) => {
pub struct $name;
impl Notification for $name {
type Params = $params;
const METHOD: &'static str = $method;
}
};
}
notification!("notifications/initialized", Initialized, ());
notification!("notifications/progress", Progress, ProgressParams);
notification!("notifications/message", Message, MessageParams);
notification!("notifications/cancelled", Cancelled, CancelledParams);
notification!(
"notifications/resources/updated",
ResourcesUpdated,
ResourcesUpdatedParams
);
notification!(
"notifications/resources/list_changed",
ResourcesListChanged,
()
);
notification!("notifications/tools/list_changed", ToolsListChanged, ());
notification!("notifications/prompts/list_changed", PromptsListChanged, ());
notification!("notifications/roots/list_changed", RootsListChanged, ());
}
pub trait Notification {
type Params: DeserializeOwned + Serialize + Send + Sync + 'static;
const METHOD: &'static str;
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageParams {
pub level: LoggingLevel,
pub logger: Option<String>,
pub data: serde_json::Value,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesUpdatedParams {
pub uri: String,
}
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ProtocolVersion(pub String);
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeParams {
pub protocol_version: ProtocolVersion,
pub capabilities: ClientCapabilities,
pub client_info: Implementation,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallToolParams {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<serde_json::Value>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesUnsubscribeParams {
pub uri: Url,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesSubscribeParams {
pub uri: Url,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesReadParams {
pub uri: Url,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct LoggingSetLevelParams {
pub level: LoggingLevel,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptsGetParams {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<HashMap<String, String>>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionCompleteParams {
#[serde(rename = "ref")]
pub reference: CompletionReference,
pub argument: CompletionArgument,
/// Previously-resolved argument values so the server can provide
/// context-sensitive completions (added in MCP 2025-06-18).
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<CompletionContext>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionContext {
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<HashMap<String, String>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum CompletionReference {
Prompt(PromptReference),
Resource(ResourceReference),
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptReference {
#[serde(rename = "type")]
pub ty: PromptReferenceType,
pub name: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceReference {
#[serde(rename = "type")]
pub ty: PromptReferenceType,
pub uri: Url,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PromptReferenceType {
#[serde(rename = "ref/prompt")]
Prompt,
#[serde(rename = "ref/resource")]
Resource,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionArgument {
pub name: String,
pub value: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeResponse {
pub protocol_version: ProtocolVersion,
pub capabilities: ServerCapabilities,
pub server_info: Implementation,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesReadResponse {
pub contents: Vec<ResourceContentsType>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ResourceContentsType {
Text(TextResourceContents),
Blob(BlobResourceContents),
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesListResponse {
pub resources: Vec<Resource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SamplingMessage {
pub role: Role,
pub content: MessageContent,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateMessageRequest {
pub messages: Vec<SamplingMessage>,
#[serde(skip_serializing_if = "Option::is_none")]
pub model_preferences: Option<ModelPreferences>,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_prompt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub include_context: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f64>,
pub max_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_sequences: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateMessageResult {
pub role: Role,
pub content: MessageContent,
pub model: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub stop_reason: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptMessage {
pub role: Role,
pub content: MessageContent,
}
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Assistant,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum MessageContent {
#[serde(rename = "text")]
Text {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<MessageAnnotations>,
},
#[serde(rename = "image", rename_all = "camelCase")]
Image {
data: String,
mime_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<MessageAnnotations>,
},
#[serde(rename = "audio", rename_all = "camelCase")]
Audio {
data: String,
mime_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<MessageAnnotations>,
},
#[serde(rename = "resource")]
Resource {
resource: ResourceContents,
#[serde(skip_serializing_if = "Option::is_none")]
annotations: Option<MessageAnnotations>,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MessageAnnotations {
#[serde(skip_serializing_if = "Option::is_none")]
pub audience: Option<Vec<Role>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub priority: Option<f64>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptsGetResponse {
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub messages: Vec<PromptMessage>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptsListResponse {
pub prompts: Vec<Prompt>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionCompleteResponse {
pub completion: CompletionResult,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionResult {
pub values: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub has_more: Option<bool>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Prompt {
pub name: String,
/// Human-readable display name (added in MCP 2025-06-18).
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Vec<PromptArgument>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptArgument {
pub name: String,
/// Human-readable display name (added in MCP 2025-06-18).
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub experimental: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sampling: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub roots: Option<RootsCapabilities>,
}
#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ServerCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub experimental: Option<HashMap<String, serde_json::Value>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub logging: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completions: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub prompts: Option<PromptsCapabilities>,
#[serde(skip_serializing_if = "Option::is_none")]
pub resources: Option<ResourcesCapabilities>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<ToolsCapabilities>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PromptsCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub list_changed: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourcesCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub subscribe: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub list_changed: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolsCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub list_changed: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RootsCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
pub list_changed: Option<bool>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Tool {
pub name: String,
/// Human-readable display name (added in MCP 2025-06-18).
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub input_schema: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output_schema: Option<serde_json::Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub annotations: Option<ToolAnnotations>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ToolAnnotations {
/// A human-readable title for the tool.
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
/// If true, the tool does not modify its environment.
#[serde(skip_serializing_if = "Option::is_none")]
pub read_only_hint: Option<bool>,
/// If true, the tool may perform destructive updates to its environment.
#[serde(skip_serializing_if = "Option::is_none")]
pub destructive_hint: Option<bool>,
/// If true, calling the tool repeatedly with the same arguments will have no additional effect on its environment.
#[serde(skip_serializing_if = "Option::is_none")]
pub idempotent_hint: Option<bool>,
/// If true, this tool may interact with an "open world" of external entities.
#[serde(skip_serializing_if = "Option::is_none")]
pub open_world_hint: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Implementation {
pub name: String,
/// Human-readable display name (added in MCP 2025-06-18).
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
pub version: String,
/// Human-readable description of the implementation (added in MCP 2025-11-25).
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Resource {
pub uri: Url,
pub name: String,
/// Human-readable display name (added in MCP 2025-06-18).
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceContents {
pub uri: Url,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TextResourceContents {
pub uri: Url,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
pub text: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BlobResourceContents {
pub uri: Url,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
pub blob: String,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceTemplate {
pub uri_template: String,
pub name: String,
/// Human-readable display name (added in MCP 2025-06-18).
#[serde(skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mime_type: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum LoggingLevel {
Debug,
Info,
Notice,
Warning,
Error,
Critical,
Alert,
Emergency,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelPreferences {
#[serde(skip_serializing_if = "Option::is_none")]
pub hints: Option<Vec<ModelHint>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cost_priority: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub speed_priority: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub intelligence_priority: Option<f64>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelHint {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(untagged)]
pub enum ClientNotification {
Initialized,
Progress(ProgressParams),
RootsListChanged,
Cancelled(CancelledParams),
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CancelledParams {
pub request_id: RequestId,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ProgressToken {
String(String),
Number(f64),
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ProgressParams {
pub progress_token: ProgressToken,
pub progress: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total: Option<f64>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
pub enum CompletionTotal {
Exact(u32),
HasMore,
Unknown,
}
impl CompletionTotal {
pub fn from_options(has_more: Option<bool>, total: Option<u32>) -> Self {
match (has_more, total) {
(_, Some(count)) => CompletionTotal::Exact(count),
(Some(true), _) => CompletionTotal::HasMore,
_ => CompletionTotal::Unknown,
}
}
}
pub struct Completion {
pub values: Vec<String>,
pub total: CompletionTotal,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CallToolResponse {
pub content: Vec<ToolResponseContent>,
#[serde(skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub structured_content: Option<serde_json::Value>,
}
impl CallToolResponse {
pub fn text_contents(&self) -> String {
let mut text = String::new();
for chunk in &self.content {
if let ToolResponseContent::Text { text: chunk } = chunk {
text.push_str(chunk)
};
}
text
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum ToolResponseContent {
#[serde(rename = "text")]
Text { text: String },
#[serde(rename = "image", rename_all = "camelCase")]
Image { data: String, mime_type: String },
#[serde(rename = "audio", rename_all = "camelCase")]
Audio { data: String, mime_type: String },
#[serde(rename = "resource")]
Resource { resource: ResourceContents },
/// Link to an MCP resource on the server, without inlining its contents.
/// Added in MCP 2025-06-18.
#[serde(rename = "resource_link", rename_all = "camelCase")]
ResourceLink {
uri: Url,
name: String,
#[serde(skip_serializing_if = "Option::is_none")]
title: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
mime_type: Option<String>,
},
}
impl ToolResponseContent {
pub fn text(&self) -> Option<&str> {
if let ToolResponseContent::Text { text } = self {
Some(text)
} else {
None
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListToolsResponse {
pub tools: Vec<Tool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListResourceTemplatesResponse {
pub resource_templates: Vec<ResourceTemplate>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_cursor: Option<String>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ListRootsResponse {
pub roots: Vec<Root>,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<HashMap<String, serde_json::Value>>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Root {
pub uri: Url,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
}