-
Notifications
You must be signed in to change notification settings - Fork 3.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
context_server: Add support for SSE MCP servers #25693
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
use std::pin::Pin; | ||
use std::sync::Arc; | ||
use std::time::Duration; | ||
|
||
use anyhow::Result; | ||
use async_trait::async_trait; | ||
use futures::FutureExt; | ||
use futures::{io::BufReader, AsyncBufReadExt as _, Stream}; | ||
use gpui::http_client::HttpClient; | ||
use gpui::{AsyncApp, BackgroundExecutor}; | ||
use smol::channel; | ||
use smol::lock::Mutex; | ||
use url::Url; | ||
use util::ResultExt as _; | ||
|
||
use crate::transport::Transport; | ||
|
||
struct MessageUrl { | ||
url: Arc<Mutex<Option<String>>>, | ||
url_received: channel::Receiver<()>, | ||
} | ||
|
||
impl MessageUrl { | ||
fn new() -> (Self, channel::Sender<()>) { | ||
let (url_sender, url_received) = channel::bounded::<()>(1); | ||
( | ||
Self { | ||
url: Arc::new(Mutex::new(None)), | ||
url_received, | ||
}, | ||
url_sender, | ||
) | ||
} | ||
|
||
async fn url(&self) -> Result<String> { | ||
if let Some(url) = self.url.lock().await.clone() { | ||
return Ok(url); | ||
} | ||
self.url_received.recv().await?; | ||
Ok(self.url.lock().await.clone().unwrap()) | ||
} | ||
} | ||
|
||
pub struct SseTransport { | ||
message_url: MessageUrl, | ||
stdin_receiver: channel::Receiver<String>, | ||
stderr_receiver: channel::Receiver<String>, | ||
http_client: Arc<dyn HttpClient>, | ||
} | ||
|
||
impl SseTransport { | ||
pub fn new(endpoint: Url, cx: &AsyncApp) -> Result<Self> { | ||
let (stdin_sender, stdin_receiver) = channel::unbounded::<String>(); | ||
let (_stderr_sender, stderr_receiver) = channel::unbounded::<String>(); | ||
let (message_url, url_sender) = MessageUrl::new(); | ||
let http_client = cx.update(|cx| cx.http_client().clone())?; | ||
|
||
let message_url_clone = message_url.url.clone(); | ||
cx.spawn({ | ||
let http_client = http_client.clone(); | ||
move |cx| async move { | ||
Self::handle_sse_stream( | ||
cx.background_executor(), | ||
endpoint, | ||
message_url_clone, | ||
stdin_sender, | ||
url_sender, | ||
http_client, | ||
) | ||
.await | ||
.log_err() | ||
} | ||
}) | ||
.detach(); | ||
|
||
Ok(Self { | ||
message_url, | ||
stdin_receiver, | ||
stderr_receiver, | ||
http_client, | ||
}) | ||
} | ||
|
||
async fn handle_sse_stream( | ||
executor: &BackgroundExecutor, | ||
endpoint: Url, | ||
message_url: Arc<Mutex<Option<String>>>, | ||
stdin_sender: channel::Sender<String>, | ||
url_sender: channel::Sender<()>, | ||
http_client: Arc<dyn HttpClient>, | ||
) -> Result<()> { | ||
loop { | ||
let mut response = http_client | ||
.get(endpoint.as_str(), Default::default(), true) | ||
.await?; | ||
let mut reader = BufReader::new(response.body_mut()); | ||
let mut line = String::new(); | ||
|
||
loop { | ||
futures::select! { | ||
result = reader.read_line(&mut line).fuse() => { | ||
match result { | ||
Ok(0) => break, | ||
Ok(_) => { | ||
if line.starts_with("data: ") { | ||
let data = line.trim_start_matches("data: "); | ||
if data.starts_with("http") { | ||
*message_url.lock().await = Some(data.trim().to_string()); | ||
url_sender.send(()).await?; | ||
} else { | ||
stdin_sender.send(data.to_string()).await?; | ||
} | ||
} | ||
line.clear(); | ||
}, | ||
Err(_) => break, | ||
} | ||
}, | ||
_ = executor.timer(Duration::from_secs(30)).fuse() => { | ||
break; | ||
} | ||
Comment on lines
+119
to
+121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here I'm trying to account for computer sleep/wake-up, but it doesn't always work as expected. Is there a better way to handle these interruptions? |
||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
#[async_trait] | ||
impl Transport for SseTransport { | ||
async fn send(&self, message: String) -> Result<()> { | ||
let url = self.message_url.url().await?; | ||
self.http_client.post_json(&url, message.into()).await?; | ||
Ok(()) | ||
} | ||
|
||
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()) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -12,18 +12,34 @@ pub fn init(cx: &mut App) { | |
ContextServerSettings::register(cx); | ||
} | ||
|
||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug, Default)] | ||
pub struct ServerConfig { | ||
/// The command to run this context server. | ||
/// | ||
/// This will override the command set by an extension. | ||
pub command: Option<ServerCommand>, | ||
/// The settings for this context server. | ||
/// | ||
/// Consult the documentation for the context server to see what settings | ||
/// are supported. | ||
#[schemars(schema_with = "server_config_settings_json_schema")] | ||
pub settings: Option<serde_json::Value>, | ||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema, Debug)] | ||
#[serde(rename_all = "snake_case", tag = "type")] | ||
pub enum ServerConfig { | ||
Stdio { | ||
/// The command to run this context server. | ||
/// | ||
/// This will override the command set by an extension. | ||
command: Option<ServerCommand>, | ||
/// The settings for this context server. | ||
/// | ||
/// Consult the documentation for the context server to see what settings | ||
/// are supported. | ||
#[schemars(schema_with = "server_config_settings_json_schema")] | ||
settings: Option<serde_json::Value>, | ||
}, | ||
Sse { | ||
/// The remote SSE endpoint. | ||
endpoint: String, | ||
}, | ||
} | ||
Comment on lines
+17
to
+34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I initially tried to go the |
||
|
||
impl Default for ServerConfig { | ||
fn default() -> Self { | ||
ServerConfig::Stdio { | ||
command: None, | ||
settings: None, | ||
} | ||
} | ||
} | ||
|
||
fn server_config_settings_json_schema(_generator: &mut SchemaGenerator) -> Schema { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,7 @@ use anyhow::{anyhow, bail, Context, Result}; | |
use async_compression::futures::bufread::GzipDecoder; | ||
use async_tar::Archive; | ||
use async_trait::async_trait; | ||
use context_server_settings::ContextServerSettings; | ||
use context_server_settings::{ContextServerSettings, ServerConfig}; | ||
use extension::{ | ||
ExtensionLanguageServerProxy, KeyValueStoreDelegate, ProjectDelegate, WorktreeDelegate, | ||
}; | ||
|
@@ -664,14 +664,21 @@ impl ExtensionImports for WasmState { | |
}) | ||
.cloned() | ||
.unwrap_or_default(); | ||
Ok(serde_json::to_string(&settings::ContextServerSettings { | ||
command: settings.command.map(|command| settings::CommandSettings { | ||
path: Some(command.path), | ||
arguments: Some(command.args), | ||
env: command.env.map(|env| env.into_iter().collect()), | ||
}), | ||
settings: settings.settings, | ||
})?) | ||
match settings { | ||
ServerConfig::Stdio { command, settings } => { | ||
Ok(serde_json::to_string(&settings::ContextServerSettings { | ||
command: command.map(|command| settings::CommandSettings { | ||
path: Some(command.path), | ||
arguments: Some(command.args), | ||
env: command.env.map(|env| env.into_iter().collect()), | ||
}), | ||
settings, | ||
})?) | ||
} | ||
ServerConfig::Sse { .. } => { | ||
bail!("SSE server configuration is not supported") | ||
} | ||
} | ||
Comment on lines
+667
to
+681
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For now, I didn't add support in the extension, because I didn't want to mess around with the API. It seems that |
||
} | ||
_ => { | ||
bail!("Unknown settings category: {}", category); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not 100% sure what we are trying to do in this section, so I simply implemented a no-op, but I'm sure there's more to it... 😬