Merge pull request #2 from kalvinnchau/mcp-prompt-spec

This commit is contained in:
Alex Hancock 2025-02-22 11:41:26 -05:00 • committed by GitHub
commit 8f3cb34f6e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 159 additions and 51 deletions

View file

@ -82,5 +82,16 @@ async fn main() -> Result<(), ClientError> {
let resource = client.read_resource("memo://insights").await?; let resource = client.read_resource("memo://insights").await?;
println!("Resource: {resource:?}\n"); println!("Resource: {resource:?}\n");
let prompts = client.list_prompts(None).await?;
println!("Prompts: {prompts:?}\n");
let prompt = client
.get_prompt(
"example_prompt",
serde_json::json!({"message": "hello there!"}),
)
.await?;
println!("Prompt: {prompt:?}\n");
Ok(()) Ok(())
} }

View file

@ -1,7 +1,7 @@
use mcp_core::protocol::{ use mcp_core::protocol::{
CallToolResult, Implementation, InitializeResult, JsonRpcError, JsonRpcMessage, CallToolResult, GetPromptResult, Implementation, InitializeResult, JsonRpcError,
JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListResourcesResult, ListToolsResult, JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListPromptsResult,
ReadResourceResult, ServerCapabilities, METHOD_NOT_FOUND, ListResourcesResult, ListToolsResult, ReadResourceResult, ServerCapabilities, METHOD_NOT_FOUND,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
@ -93,6 +93,10 @@ pub trait McpClientTrait: Send + Sync {
async fn list_tools(&self, next_cursor: Option<String>) -> Result<ListToolsResult, Error>; async fn list_tools(&self, next_cursor: Option<String>) -> Result<ListToolsResult, Error>;
async fn call_tool(&self, name: &str, arguments: Value) -> Result<CallToolResult, Error>; async fn call_tool(&self, name: &str, arguments: Value) -> Result<CallToolResult, Error>;
async fn list_prompts(&self, next_cursor: Option<String>) -> Result<ListPromptsResult, Error>;
async fn get_prompt(&self, name: &str, arguments: Value) -> Result<GetPromptResult, Error>;
} }
/// The MCP client is the interface for MCP operations. /// The MCP client is the interface for MCP operations.
@ -346,4 +350,42 @@ where
// https://modelcontextprotocol.io/docs/concepts/tools#error-handling-2 // https://modelcontextprotocol.io/docs/concepts/tools#error-handling-2
self.send_request("tools/call", params).await self.send_request("tools/call", params).await
} }
async fn list_prompts(&self, next_cursor: Option<String>) -> Result<ListPromptsResult, Error> {
if !self.completed_initialization() {
return Err(Error::NotInitialized);
}
// If prompts is not supported, return an error
if self.server_capabilities.as_ref().unwrap().prompts.is_none() {
return Err(Error::RpcError {
code: METHOD_NOT_FOUND,
message: "Server does not support 'prompts' capability".to_string(),
});
}
let payload = next_cursor
.map(|cursor| serde_json::json!({"cursor": cursor}))
.unwrap_or_else(|| serde_json::json!({}));
self.send_request("prompts/list", payload).await
}
async fn get_prompt(&self, name: &str, arguments: Value) -> Result<GetPromptResult, Error> {
if !self.completed_initialization() {
return Err(Error::NotInitialized);
}
// If prompts is not supported, return an error
if self.server_capabilities.as_ref().unwrap().prompts.is_none() {
return Err(Error::RpcError {
code: METHOD_NOT_FOUND,
message: "Server does not support 'prompts' capability".to_string(),
});
}
let params = serde_json::json!({ "name": name, "arguments": arguments });
self.send_request("prompts/get", params).await
}
} }

View file

@ -111,13 +111,23 @@ impl SseActor {
// Attempt to parse the SSE data as a JsonRpcMessage // Attempt to parse the SSE data as a JsonRpcMessage
match serde_json::from_str::<JsonRpcMessage>(&e.data) { match serde_json::from_str::<JsonRpcMessage>(&e.data) {
Ok(message) => { Ok(message) => {
// If it's a response, complete the pending request match &message {
if let JsonRpcMessage::Response(resp) = &message { JsonRpcMessage::Response(response) => {
if let Some(id) = &resp.id { if let Some(id) = &response.id {
pending_requests.respond(&id.to_string(), Ok(message)).await; pending_requests
.respond(&id.to_string(), Ok(message))
.await;
} }
} }
// If it's something else (notification, etc.), handle as needed JsonRpcMessage::Error(error) => {
if let Some(id) = &error.id {
pending_requests
.respond(&id.to_string(), Ok(message))
.await;
}
}
_ => {} // TODO: Handle other variants (Request, etc.)
}
} }
Err(err) => { Err(err) => {
warn!("Failed to parse SSE message: {err}"); warn!("Failed to parse SSE message: {err}");

View file

@ -87,11 +87,19 @@ impl StdioActor {
"Received incoming message" "Received incoming message"
); );
if let JsonRpcMessage::Response(response) = &message { match &message {
JsonRpcMessage::Response(response) => {
if let Some(id) = &response.id { if let Some(id) = &response.id {
pending_requests.respond(&id.to_string(), Ok(message)).await; pending_requests.respond(&id.to_string(), Ok(message)).await;
} }
} }
JsonRpcMessage::Error(error) => {
if let Some(id) = &error.id {
pending_requests.respond(&id.to_string(), Ok(message)).await;
}
}
_ => {} // TODO: Handle other variants (Request, etc.)
}
} }
line.clear(); line.clear();
} }

View file

@ -10,22 +10,28 @@ use serde::{Deserialize, Serialize};
pub struct Prompt { pub struct Prompt {
/// The name of the prompt /// The name of the prompt
pub name: String, pub name: String,
/// A description of what the prompt does /// Optional description of what the prompt does
pub description: String, #[serde(skip_serializing_if = "Option::is_none")]
/// The arguments that can be passed to customize the prompt pub description: Option<String>,
pub arguments: Vec<PromptArgument>, /// Optional arguments that can be passed to customize the prompt
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Vec<PromptArgument>>,
} }
impl Prompt { impl Prompt {
/// Create a new prompt with the given name, description and arguments /// Create a new prompt with the given name, description and arguments
pub fn new<N, D>(name: N, description: D, arguments: Vec<PromptArgument>) -> Self pub fn new<N, D>(
name: N,
description: Option<D>,
arguments: Option<Vec<PromptArgument>>,
) -> Self
where where
N: Into<String>, N: Into<String>,
D: Into<String>, D: Into<String>,
{ {
Prompt { Prompt {
name: name.into(), name: name.into(),
description: description.into(), description: description.map(Into::into),
arguments, arguments,
} }
} }
@ -37,9 +43,11 @@ pub struct PromptArgument {
/// The name of the argument /// The name of the argument
pub name: String, pub name: String,
/// A description of what the argument is used for /// A description of what the argument is used for
pub description: String, #[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Whether this argument is required /// Whether this argument is required
pub required: bool, #[serde(skip_serializing_if = "Option::is_none")]
pub required: Option<bool>,
} }
/// Represents the role of a message sender in a prompt conversation /// Represents the role of a message sender in a prompt conversation
@ -151,6 +159,6 @@ pub struct PromptTemplate {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct PromptArgumentTemplate { pub struct PromptArgumentTemplate {
pub name: String, pub name: String,
pub description: String, pub description: Option<String>,
pub required: bool, pub required: Option<bool>,
} }

View file

@ -1,6 +1,7 @@
use anyhow::Result; use anyhow::Result;
use mcp_core::content::Content; use mcp_core::content::Content;
use mcp_core::handler::ResourceError; use mcp_core::handler::{PromptError, ResourceError};
use mcp_core::prompt::{Prompt, PromptArgument};
use mcp_core::{handler::ToolError, protocol::ServerCapabilities, resource::Resource, tool::Tool}; use mcp_core::{handler::ToolError, protocol::ServerCapabilities, resource::Resource, tool::Tool};
use mcp_server::router::{CapabilitiesBuilder, RouterService}; use mcp_server::router::{CapabilitiesBuilder, RouterService};
use mcp_server::{ByteTransport, Router, Server}; use mcp_server::{ByteTransport, Router, Server};
@ -61,6 +62,7 @@ impl Router for CounterRouter {
CapabilitiesBuilder::new() CapabilitiesBuilder::new()
.with_tools(false) .with_tools(false)
.with_resources(false, false) .with_resources(false, false)
.with_prompts(false)
.build() .build()
} }
@ -153,6 +155,37 @@ impl Router for CounterRouter {
} }
}) })
} }
fn list_prompts(&self) -> Vec<Prompt> {
vec![Prompt::new(
"example_prompt",
Some("This is an example prompt that takes one required agrument, message"),
Some(vec![PromptArgument {
name: "message".to_string(),
description: Some("A message to put in the prompt".to_string()),
required: Some(true),
}]),
)]
}
fn get_prompt(
&self,
prompt_name: &str,
) -> Pin<Box<dyn Future<Output = Result<String, PromptError>> + Send + 'static>> {
let prompt_name = prompt_name.to_string();
Box::pin(async move {
match prompt_name.as_str() {
"example_prompt" => {
let prompt = "This is an example prompt with your message here: '{message}'";
Ok(prompt.to_string())
}
_ => Err(PromptError::NotFound(format!(
"Prompt {} not found",
prompt_name
))),
}
})
}
} }
#[tokio::main] #[tokio::main]

View file

@ -97,12 +97,8 @@ pub trait Router: Send + Sync + 'static {
&self, &self,
uri: &str, uri: &str,
) -> Pin<Box<dyn Future<Output = Result<String, ResourceError>> + Send + 'static>>; ) -> Pin<Box<dyn Future<Output = Result<String, ResourceError>> + Send + 'static>>;
fn list_prompts(&self) -> Option<Vec<Prompt>> { fn list_prompts(&self) -> Vec<Prompt>;
None fn get_prompt(&self, prompt_name: &str) -> PromptFuture;
}
fn get_prompt(&self, _prompt_name: &str) -> Option<PromptFuture> {
None
}
// Helper method to create base response // Helper method to create base response
fn create_response(&self, id: Option<u64>) -> JsonRpcResponse { fn create_response(&self, id: Option<u64>) -> JsonRpcResponse {
@ -257,7 +253,7 @@ pub trait Router: Send + Sync + 'static {
req: JsonRpcRequest, req: JsonRpcRequest,
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send { ) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
async move { async move {
let prompts = self.list_prompts().unwrap_or_default(); let prompts = self.list_prompts();
let result = ListPromptsResult { prompts }; let result = ListPromptsResult { prompts };
@ -294,19 +290,19 @@ pub trait Router: Send + Sync + 'static {
.ok_or_else(|| RouterError::InvalidParams("Missing arguments object".into()))?; .ok_or_else(|| RouterError::InvalidParams("Missing arguments object".into()))?;
// Fetch the prompt definition first // Fetch the prompt definition first
let prompt = match self.list_prompts() { let prompt = self
Some(prompts) => prompts .list_prompts()
.into_iter() .into_iter()
.find(|p| p.name == prompt_name) .find(|p| p.name == prompt_name)
.ok_or_else(|| { .ok_or_else(|| {
RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name)) RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name))
})?, })?;
None => return Err(RouterError::PromptNotFound("No prompts available".into())),
};
// Validate required arguments // Validate required arguments
for arg in &prompt.arguments { if let Some(args) = &prompt.arguments {
if arg.required for arg in args {
if arg.required.is_some()
&& arg.required.unwrap()
&& (!arguments.contains_key(&arg.name) && (!arguments.contains_key(&arg.name)
|| arguments || arguments
.get(&arg.name) .get(&arg.name)
@ -319,11 +315,11 @@ pub trait Router: Send + Sync + 'static {
))); )));
} }
} }
}
// Now get the prompt content // Now get the prompt content
let description = self let description = self
.get_prompt(prompt_name) .get_prompt(prompt_name)
.ok_or_else(|| RouterError::PromptNotFound("Prompt not found".into()))?
.await .await
.map_err(|e| RouterError::Internal(e.to_string()))?; .map_err(|e| RouterError::Internal(e.to_string()))?;