context_servers: Add initial implementation (#16103)
This commit proposes the addition of "context serveres" and the underlying protocol (model context protocol). Context servers allow simple definition of slash commands in another language and running local on the user machines. This aims to quickly prototype new commands, and provide a way to add personal (or company wide) customizations to the assistant panel, without having to maintain an extension. We can use this to reuse our existing codebase, with authenticators, etc and easily have it provide context into the assistant panel. As such it occupies a different design space as extensions, which I think are more aimed towards long-term, well maintained pieces of code that can be easily distributed. It's implemented as a central crate for easy reusability across the codebase and to easily hook into the assistant panel at all points. Design wise there are a few pieces: 1. client.rs: A simple JSON-RPC client talking over stdio to a spawned server. This is very close to how LSP work and likely there could be a combined client down the line. 2. types.rs: Serialization and deserialization client for the underlying model context protocol. 3. protocol.rs: Handling the session between client and server. 4. manager.rs: Manages settings and adding and deleting servers from a central pool. A server can be defined in the settings.json as: ``` "context_servers": [ {"id": "test", "executable": "python", "args": ["-m", "context_server"] ] ``` ## Quick Example A quick example of how a theoretical backend site can look like. With roughly 100 lines of code (nicely generated by Claude) and a bit of decorator magic (200 lines in total), one can come up with a framework that makes it as easy as: ```python @context_server.slash_command(name="rot13", description="Perform a rot13 transformation") @context_server.argument(name="input", type=str, help="String to rot13") async def rot13(input: str) -> str: return ''.join(chr((ord(c) - 97 + 13) % 26 + 97) if c.isalpha() else c for c in echo.lower()) ``` to define a new slash_command. ## Todo: - Allow context servers to be defined in workspace settings. - Allow passing env variables to context_servers Release Notes: - N/A --------- Co-authored-by: Marshall Bowers <elliott.codes@gmail.com>
This commit is contained in:
parent
d54818fd9e
commit
02ea6ac845
16 changed files with 1433 additions and 7 deletions
140
crates/context_servers/src/protocol.rs
Normal file
140
crates/context_servers/src/protocol.rs
Normal file
|
@ -0,0 +1,140 @@
|
|||
//! 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 anyhow::Result;
|
||||
use collections::HashMap;
|
||||
|
||||
use crate::client::Client;
|
||||
use crate::types;
|
||||
|
||||
pub use types::PromptInfo;
|
||||
|
||||
const PROTOCOL_VERSION: u32 = 1;
|
||||
|
||||
pub struct ModelContextProtocol {
|
||||
inner: Client,
|
||||
}
|
||||
|
||||
impl ModelContextProtocol {
|
||||
pub fn new(inner: Client) -> Self {
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
pub async fn initialize(
|
||||
self,
|
||||
client_info: types::EntityInfo,
|
||||
) -> Result<InitializedContextServerProtocol> {
|
||||
let params = types::InitializeParams {
|
||||
protocol_version: PROTOCOL_VERSION,
|
||||
capabilities: types::ClientCapabilities {
|
||||
experimental: None,
|
||||
sampling: None,
|
||||
},
|
||||
client_info,
|
||||
};
|
||||
|
||||
let response: types::InitializeResponse = self
|
||||
.inner
|
||||
.request(types::RequestType::Initialize.as_str(), params)
|
||||
.await?;
|
||||
|
||||
log::trace!("mcp server info {:?}", response.server_info);
|
||||
|
||||
self.inner.notify(
|
||||
types::NotificationType::Initialized.as_str(),
|
||||
serde_json::json!({}),
|
||||
)?;
|
||||
|
||||
let initialized_protocol = InitializedContextServerProtocol {
|
||||
inner: self.inner,
|
||||
initialize: response,
|
||||
};
|
||||
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
fn check_capability(&self, capability: ServerCapability) -> Result<()> {
|
||||
if self.capable(capability) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"Server does not support {:?} capability",
|
||||
capability
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
/// List the MCP prompts.
|
||||
pub async fn list_prompts(&self) -> Result<Vec<types::PromptInfo>> {
|
||||
self.check_capability(ServerCapability::Prompts)?;
|
||||
|
||||
let response: types::PromptsListResponse = self
|
||||
.inner
|
||||
.request(types::RequestType::PromptsList.as_str(), ())
|
||||
.await?;
|
||||
|
||||
Ok(response.prompts)
|
||||
}
|
||||
|
||||
/// Executes a prompt with the given arguments and returns the result.
|
||||
pub async fn run_prompt<P: AsRef<str>>(
|
||||
&self,
|
||||
prompt: P,
|
||||
arguments: HashMap<String, String>,
|
||||
) -> Result<String> {
|
||||
self.check_capability(ServerCapability::Prompts)?;
|
||||
|
||||
let params = types::PromptsGetParams {
|
||||
name: prompt.as_ref().to_string(),
|
||||
arguments: Some(arguments),
|
||||
};
|
||||
|
||||
let response: types::PromptsGetResponse = self
|
||||
.inner
|
||||
.request(types::RequestType::PromptsGet.as_str(), params)
|
||||
.await?;
|
||||
|
||||
Ok(response.prompt)
|
||||
}
|
||||
}
|
||||
|
||||
impl InitializedContextServerProtocol {
|
||||
pub async fn request<R: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
method: &str,
|
||||
params: impl serde::Serialize,
|
||||
) -> Result<R> {
|
||||
self.inner.request(method, params).await
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue