Merge pull request #2 from kalvinnchau/mcp-prompt-spec
This commit is contained in:
commit
8f3cb34f6e
7 changed files with 159 additions and 51 deletions
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
JsonRpcMessage::Error(error) => {
|
||||||
|
if let Some(id) = &error.id {
|
||||||
|
pending_requests
|
||||||
|
.respond(&id.to_string(), Ok(message))
|
||||||
|
.await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {} // TODO: Handle other variants (Request, etc.)
|
||||||
}
|
}
|
||||||
// If it's something else (notification, etc.), handle as needed
|
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
warn!("Failed to parse SSE message: {err}");
|
warn!("Failed to parse SSE message: {err}");
|
||||||
|
|
|
||||||
|
|
@ -87,10 +87,18 @@ impl StdioActor {
|
||||||
"Received incoming message"
|
"Received incoming message"
|
||||||
);
|
);
|
||||||
|
|
||||||
if let JsonRpcMessage::Response(response) = &message {
|
match &message {
|
||||||
if let Some(id) = &response.id {
|
JsonRpcMessage::Response(response) => {
|
||||||
pending_requests.respond(&id.to_string(), Ok(message)).await;
|
if let Some(id) = &response.id {
|
||||||
|
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();
|
||||||
|
|
|
||||||
|
|
@ -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>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
|
|
|
||||||
|
|
@ -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,36 +290,36 @@ 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 {
|
||||||
&& (!arguments.contains_key(&arg.name)
|
if arg.required.is_some()
|
||||||
|| arguments
|
&& arg.required.unwrap()
|
||||||
.get(&arg.name)
|
&& (!arguments.contains_key(&arg.name)
|
||||||
.and_then(Value::as_str)
|
|| arguments
|
||||||
.is_none_or(str::is_empty))
|
.get(&arg.name)
|
||||||
{
|
.and_then(Value::as_str)
|
||||||
return Err(RouterError::InvalidParams(format!(
|
.is_none_or(str::is_empty))
|
||||||
"Missing required argument: '{}'",
|
{
|
||||||
arg.name
|
return Err(RouterError::InvalidParams(format!(
|
||||||
)));
|
"Missing required argument: '{}'",
|
||||||
|
arg.name
|
||||||
|
)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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()))?;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue