## Summary This PR adds support for the MCP (Model Context Protocol) `notifications/tools/list_changed` notification, enabling dynamic tool discovery when MCP servers add, remove, or modify their available tools at runtime. ## Release Notes: - Improved: MCP tools are now automatically reloaded when a context server sends a `tools/list_changed` notification, eliminating the need to restart the server to discover new tools. ## Changes - Register a notification handler for `notifications/tools/list_changed` in `ContextServerRegistry` - Automatically reload tools when the notification is received - Handler is registered both on initial server startup and when a server transitions to `Running` status ## Motivation The MCP specification includes a `notifications/tools/list_changed` notification to inform clients when the list of available tools has changed. Previously, Zed's agent would only load tools once when a context server started. This meant that: 1. If an MCP server dynamically registered new tools after initialization, they would not be available to the agent 2. The only way to refresh tools was to restart the entire context server 3. Tools that were removed or modified would remain in the old state until restart ## Implementation Details The implementation follows these steps: 1. When a context server transitions to `Running` status, register a notification handler for `notifications/tools/list_changed` 2. The handler captures a weak reference to the `ContextServerRegistry` entity 3. When the notification is received, spawn a task that calls `reload_tools_for_server` with the server ID 4. The existing `reload_tools_for_server` method handles fetching the updated tool list and notifying observers This approach is minimal and reuses existing tool-loading infrastructure. ## Testing - [x] Code compiles with `./script/clippy -p agent` - The notification handler infrastructure already exists and is tested in the codebase - The `reload_tools_for_server` method is already tested and working ## Benefits - Improves developer experience by enabling hot-reloading of MCP tools - Aligns with the MCP specification's capability negotiation system - No breaking changes to existing functionality - Enables more flexible and dynamic MCP server implementations ## Related Issues This implements part of the MCP specification that was already defined in the type system but not wired up to actually handle the notifications. --------- Co-authored-by: Agus Zubiaga <agus@zed.dev>
126 lines
3.8 KiB
Rust
126 lines
3.8 KiB
Rust
//! 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_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);
|
|
|
|
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)
|
|
}
|
|
}
|