Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions crates/mcp-client/examples/stdio_integration.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,5 +82,16 @@ async fn main() -> Result<(), ClientError> {
let resource = client.read_resource("memo://insights").await?;
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(())
}
48 changes: 45 additions & 3 deletions crates/mcp-client/src/client.rs
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
use mcp_core::protocol::{
CallToolResult, Implementation, InitializeResult, JsonRpcError, JsonRpcMessage,
JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListResourcesResult, ListToolsResult,
ReadResourceResult, ServerCapabilities, METHOD_NOT_FOUND,
CallToolResult, GetPromptResult, Implementation, InitializeResult, JsonRpcError,
JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListPromptsResult,
ListResourcesResult, ListToolsResult, ReadResourceResult, ServerCapabilities, METHOD_NOT_FOUND,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
Expand DownExpand Up@@ -93,6 +93,10 @@ pub trait McpClientTrait: Send + Sync {
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 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.
Expand DownExpand Up@@ -346,4 +350,42 @@ where
// https://modelcontextprotocol.io/docs/concepts/tools#error-handling-2
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
}
}
20 changes: 15 additions & 5 deletions crates/mcp-client/src/transport/sse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,13 +111,23 @@ impl SseActor {
// Attempt to parse the SSE data as a JsonRpcMessage
match serde_json::from_str::<JsonRpcMessage>(&e.data) {
Ok(message) => {
// If it's a response, complete the pending request
if let JsonRpcMessage::Response(resp) = &message {
if let Some(id) = &resp.id {
pending_requests.respond(&id.to_string(), Ok(message)).await;
match &message {
JsonRpcMessage::Response(response) => {
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.)
}
// If it's something else (notification, etc.), handle as needed
}
Err(err) => {
warn!("Failed to parse SSE message: {err}");
Expand Down
14 changes: 11 additions & 3 deletions crates/mcp-client/src/transport/stdio.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -87,10 +87,18 @@ impl StdioActor {
"Received incoming message"
);

if let JsonRpcMessage::Response(response) = &message {
if let Some(id) = &response.id {
pending_requests.respond(&id.to_string(), Ok(message)).await;
match &message {
JsonRpcMessage::Response(response) => {
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();
Expand Down
28 changes: 18 additions & 10 deletions crates/mcp-core/src/prompt.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,22 +10,28 @@ use serde::{Deserialize, Serialize};
pub struct Prompt {
/// The name of the prompt
pub name: String,
/// A description of what the prompt does
pub description: String,
/// The arguments that can be passed to customize the prompt
pub arguments: Vec<PromptArgument>,
/// Optional description of what the prompt does
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Optional arguments that can be passed to customize the prompt
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Vec<PromptArgument>>,
}

impl Prompt {
/// 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
N: Into<String>,
D: Into<String>,
{
Prompt {
name: name.into(),
description: description.into(),
description: description.map(Into::into),
arguments,
}
}
Expand All@@ -37,9 +43,11 @@ pub struct PromptArgument {
/// The name of the argument
pub name: String,
/// 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
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
Expand DownExpand Up@@ -151,6 +159,6 @@ pub struct PromptTemplate {
#[derive(Debug, Serialize, Deserialize)]
pub struct PromptArgumentTemplate {
pub name: String,
pub description: String,
pub required: bool,
pub description: Option<String>,
pub required: Option<bool>,
}
35 changes: 34 additions & 1 deletion crates/mcp-server/src/main.rs
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
use anyhow::Result;
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_server::router::{CapabilitiesBuilder, RouterService};
use mcp_server::{ByteTransport, Router, Server};
Expand DownExpand Up@@ -61,6 +62,7 @@ impl Router for CounterRouter {
CapabilitiesBuilder::new()
.with_tools(false)
.with_resources(false, false)
.with_prompts(false)
.build()
}

Expand DownExpand Up@@ -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]
Expand Down
54 changes: 25 additions & 29 deletions crates/mcp-server/src/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -97,12 +97,8 @@ pub trait Router: Send + Sync + 'static {
&self,
uri: &str,
) -> Pin<Box<dyn Future<Output = Result<String, ResourceError>> + Send + 'static>>;
fn list_prompts(&self) -> Option<Vec<Prompt>> {
None
}
fn get_prompt(&self, _prompt_name: &str) -> Option<PromptFuture> {
None
}
fn list_prompts(&self) -> Vec<Prompt>;
fn get_prompt(&self, prompt_name: &str) -> PromptFuture;

// Helper method to create base response
fn create_response(&self, id: Option<u64>) -> JsonRpcResponse {
Expand DownExpand Up@@ -257,7 +253,7 @@ pub trait Router: Send + Sync + 'static {
req: JsonRpcRequest,
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
async move {
let prompts = self.list_prompts().unwrap_or_default();
let prompts = self.list_prompts();

let result = ListPromptsResult { prompts };

Expand DownExpand Up@@ -294,36 +290,36 @@ pub trait Router: Send + Sync + 'static {
.ok_or_else(|| RouterError::InvalidParams("Missing arguments object".into()))?;

// Fetch the prompt definition first
let prompt = match self.list_prompts() {
Some(prompts) => prompts
.into_iter()
.find(|p| p.name == prompt_name)
.ok_or_else(|| {
RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name))
})?,
None => return Err(RouterError::PromptNotFound("No prompts available".into())),
};
let prompt = self
.list_prompts()
.into_iter()
.find(|p| p.name == prompt_name)
.ok_or_else(|| {
RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name))
})?;

// Validate required arguments
for arg in &prompt.arguments {
if arg.required
&& (!arguments.contains_key(&arg.name)
|| arguments
.get(&arg.name)
.and_then(Value::as_str)
.is_none_or(str::is_empty))
{
return Err(RouterError::InvalidParams(format!(
"Missing required argument: '{}'",
arg.name
)));
if let Some(args) = &prompt.arguments {
for arg in args {
if arg.required.is_some()
&& arg.required.unwrap()
&& (!arguments.contains_key(&arg.name)
|| arguments
.get(&arg.name)
.and_then(Value::as_str)
.is_none_or(str::is_empty))
{
return Err(RouterError::InvalidParams(format!(
"Missing required argument: '{}'",
arg.name
)));
}
}
}

// Now get the prompt content
let description = self
.get_prompt(prompt_name)
.ok_or_else(|| RouterError::PromptNotFound("Prompt not found".into()))?
.await
.map_err(|e| RouterError::Internal(e.to_string()))?;

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat: update mcp-core prompt to match spec, add mcp list/get to mcp-client by kalvinnchau · Pull Request #2 · modelcontextprotocol/rust-sdk · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions crates/mcp-client/examples/stdio_integration.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,5 +82,16 @@ async fn main() -> Result<(), ClientError> {
let resource = client.read_resource("memo://insights").await?;
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(())
}
48 changes: 45 additions & 3 deletions crates/mcp-client/src/client.rs
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
use mcp_core::protocol::{
CallToolResult, Implementation, InitializeResult, JsonRpcError, JsonRpcMessage,
JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListResourcesResult, ListToolsResult,
ReadResourceResult, ServerCapabilities, METHOD_NOT_FOUND,
CallToolResult, GetPromptResult, Implementation, InitializeResult, JsonRpcError,
JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListPromptsResult,
ListResourcesResult, ListToolsResult, ReadResourceResult, ServerCapabilities, METHOD_NOT_FOUND,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
Expand DownExpand Up@@ -93,6 +93,10 @@ pub trait McpClientTrait: Send + Sync {
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 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.
Expand DownExpand Up@@ -346,4 +350,42 @@ where
// https://modelcontextprotocol.io/docs/concepts/tools#error-handling-2
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
}
}
20 changes: 15 additions & 5 deletions crates/mcp-client/src/transport/sse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,13 +111,23 @@ impl SseActor {
// Attempt to parse the SSE data as a JsonRpcMessage
match serde_json::from_str::<JsonRpcMessage>(&e.data) {
Ok(message) => {
// If it's a response, complete the pending request
if let JsonRpcMessage::Response(resp) = &message {
if let Some(id) = &resp.id {
pending_requests.respond(&id.to_string(), Ok(message)).await;
match &message {
JsonRpcMessage::Response(response) => {
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.)
}
// If it's something else (notification, etc.), handle as needed
}
Err(err) => {
warn!("Failed to parse SSE message: {err}");
Expand Down
14 changes: 11 additions & 3 deletions crates/mcp-client/src/transport/stdio.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -87,10 +87,18 @@ impl StdioActor {
"Received incoming message"
);

if let JsonRpcMessage::Response(response) = &message {
if let Some(id) = &response.id {
pending_requests.respond(&id.to_string(), Ok(message)).await;
match &message {
JsonRpcMessage::Response(response) => {
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();
Expand Down
28 changes: 18 additions & 10 deletions crates/mcp-core/src/prompt.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,22 +10,28 @@ use serde::{Deserialize, Serialize};
pub struct Prompt {
/// The name of the prompt
pub name: String,
/// A description of what the prompt does
pub description: String,
/// The arguments that can be passed to customize the prompt
pub arguments: Vec<PromptArgument>,
/// Optional description of what the prompt does
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Optional arguments that can be passed to customize the prompt
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Vec<PromptArgument>>,
}

impl Prompt {
/// 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
N: Into<String>,
D: Into<String>,
{
Prompt {
name: name.into(),
description: description.into(),
description: description.map(Into::into),
arguments,
}
}
Expand All@@ -37,9 +43,11 @@ pub struct PromptArgument {
/// The name of the argument
pub name: String,
/// 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
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
Expand DownExpand Up@@ -151,6 +159,6 @@ pub struct PromptTemplate {
#[derive(Debug, Serialize, Deserialize)]
pub struct PromptArgumentTemplate {
pub name: String,
pub description: String,
pub required: bool,
pub description: Option<String>,
pub required: Option<bool>,
}
35 changes: 34 additions & 1 deletion crates/mcp-server/src/main.rs
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
use anyhow::Result;
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_server::router::{CapabilitiesBuilder, RouterService};
use mcp_server::{ByteTransport, Router, Server};
Expand DownExpand Up@@ -61,6 +62,7 @@ impl Router for CounterRouter {
CapabilitiesBuilder::new()
.with_tools(false)
.with_resources(false, false)
.with_prompts(false)
.build()
}

Expand DownExpand Up@@ -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]
Expand Down
54 changes: 25 additions & 29 deletions crates/mcp-server/src/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -97,12 +97,8 @@ pub trait Router: Send + Sync + 'static {
&self,
uri: &str,
) -> Pin<Box<dyn Future<Output = Result<String, ResourceError>> + Send + 'static>>;
fn list_prompts(&self) -> Option<Vec<Prompt>> {
None
}
fn get_prompt(&self, _prompt_name: &str) -> Option<PromptFuture> {
None
}
fn list_prompts(&self) -> Vec<Prompt>;
fn get_prompt(&self, prompt_name: &str) -> PromptFuture;

// Helper method to create base response
fn create_response(&self, id: Option<u64>) -> JsonRpcResponse {
Expand DownExpand Up@@ -257,7 +253,7 @@ pub trait Router: Send + Sync + 'static {
req: JsonRpcRequest,
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
async move {
let prompts = self.list_prompts().unwrap_or_default();
let prompts = self.list_prompts();

let result = ListPromptsResult { prompts };

Expand DownExpand Up@@ -294,36 +290,36 @@ pub trait Router: Send + Sync + 'static {
.ok_or_else(|| RouterError::InvalidParams("Missing arguments object".into()))?;

// Fetch the prompt definition first
let prompt = match self.list_prompts() {
Some(prompts) => prompts
.into_iter()
.find(|p| p.name == prompt_name)
.ok_or_else(|| {
RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name))
})?,
None => return Err(RouterError::PromptNotFound("No prompts available".into())),
};
let prompt = self
.list_prompts()
.into_iter()
.find(|p| p.name == prompt_name)
.ok_or_else(|| {
RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name))
})?;

// Validate required arguments
for arg in &prompt.arguments {
if arg.required
&& (!arguments.contains_key(&arg.name)
|| arguments
.get(&arg.name)
.and_then(Value::as_str)
.is_none_or(str::is_empty))
{
return Err(RouterError::InvalidParams(format!(
"Missing required argument: '{}'",
arg.name
)));
if let Some(args) = &prompt.arguments {
for arg in args {
if arg.required.is_some()
&& arg.required.unwrap()
&& (!arguments.contains_key(&arg.name)
|| arguments
.get(&arg.name)
.and_then(Value::as_str)
.is_none_or(str::is_empty))
{
return Err(RouterError::InvalidParams(format!(
"Missing required argument: '{}'",
arg.name
)));
}
}
}

// Now get the prompt content
let description = self
.get_prompt(prompt_name)
.ok_or_else(|| RouterError::PromptNotFound("Prompt not found".into()))?
.await
.map_err(|e| RouterError::Internal(e.to_string()))?;

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: update mcp-core prompt to match spec, add mcp list/get to mcp-client by kalvinnchau · Pull Request #2 · modelcontextprotocol/rust-sdk · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions crates/mcp-client/examples/stdio_integration.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,5 +82,16 @@ async fn main() -> Result<(), ClientError> {
let resource = client.read_resource("memo://insights").await?;
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(())
}
48 changes: 45 additions & 3 deletions crates/mcp-client/src/client.rs
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
use mcp_core::protocol::{
CallToolResult, Implementation, InitializeResult, JsonRpcError, JsonRpcMessage,
JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListResourcesResult, ListToolsResult,
ReadResourceResult, ServerCapabilities, METHOD_NOT_FOUND,
CallToolResult, GetPromptResult, Implementation, InitializeResult, JsonRpcError,
JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListPromptsResult,
ListResourcesResult, ListToolsResult, ReadResourceResult, ServerCapabilities, METHOD_NOT_FOUND,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
Expand DownExpand Up@@ -93,6 +93,10 @@ pub trait McpClientTrait: Send + Sync {
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 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.
Expand DownExpand Up@@ -346,4 +350,42 @@ where
// https://modelcontextprotocol.io/docs/concepts/tools#error-handling-2
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
}
}
20 changes: 15 additions & 5 deletions crates/mcp-client/src/transport/sse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,13 +111,23 @@ impl SseActor {
// Attempt to parse the SSE data as a JsonRpcMessage
match serde_json::from_str::<JsonRpcMessage>(&e.data) {
Ok(message) => {
// If it's a response, complete the pending request
if let JsonRpcMessage::Response(resp) = &message {
if let Some(id) = &resp.id {
pending_requests.respond(&id.to_string(), Ok(message)).await;
match &message {
JsonRpcMessage::Response(response) => {
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.)
}
// If it's something else (notification, etc.), handle as needed
}
Err(err) => {
warn!("Failed to parse SSE message: {err}");
Expand Down
14 changes: 11 additions & 3 deletions crates/mcp-client/src/transport/stdio.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -87,10 +87,18 @@ impl StdioActor {
"Received incoming message"
);

if let JsonRpcMessage::Response(response) = &message {
if let Some(id) = &response.id {
pending_requests.respond(&id.to_string(), Ok(message)).await;
match &message {
JsonRpcMessage::Response(response) => {
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();
Expand Down
28 changes: 18 additions & 10 deletions crates/mcp-core/src/prompt.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,22 +10,28 @@ use serde::{Deserialize, Serialize};
pub struct Prompt {
/// The name of the prompt
pub name: String,
/// A description of what the prompt does
pub description: String,
/// The arguments that can be passed to customize the prompt
pub arguments: Vec<PromptArgument>,
/// Optional description of what the prompt does
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Optional arguments that can be passed to customize the prompt
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Vec<PromptArgument>>,
}

impl Prompt {
/// 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
N: Into<String>,
D: Into<String>,
{
Prompt {
name: name.into(),
description: description.into(),
description: description.map(Into::into),
arguments,
}
}
Expand All@@ -37,9 +43,11 @@ pub struct PromptArgument {
/// The name of the argument
pub name: String,
/// 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
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
Expand DownExpand Up@@ -151,6 +159,6 @@ pub struct PromptTemplate {
#[derive(Debug, Serialize, Deserialize)]
pub struct PromptArgumentTemplate {
pub name: String,
pub description: String,
pub required: bool,
pub description: Option<String>,
pub required: Option<bool>,
}
35 changes: 34 additions & 1 deletion crates/mcp-server/src/main.rs
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
use anyhow::Result;
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_server::router::{CapabilitiesBuilder, RouterService};
use mcp_server::{ByteTransport, Router, Server};
Expand DownExpand Up@@ -61,6 +62,7 @@ impl Router for CounterRouter {
CapabilitiesBuilder::new()
.with_tools(false)
.with_resources(false, false)
.with_prompts(false)
.build()
}

Expand DownExpand Up@@ -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]
Expand Down
54 changes: 25 additions & 29 deletions crates/mcp-server/src/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -97,12 +97,8 @@ pub trait Router: Send + Sync + 'static {
&self,
uri: &str,
) -> Pin<Box<dyn Future<Output = Result<String, ResourceError>> + Send + 'static>>;
fn list_prompts(&self) -> Option<Vec<Prompt>> {
None
}
fn get_prompt(&self, _prompt_name: &str) -> Option<PromptFuture> {
None
}
fn list_prompts(&self) -> Vec<Prompt>;
fn get_prompt(&self, prompt_name: &str) -> PromptFuture;

// Helper method to create base response
fn create_response(&self, id: Option<u64>) -> JsonRpcResponse {
Expand DownExpand Up@@ -257,7 +253,7 @@ pub trait Router: Send + Sync + 'static {
req: JsonRpcRequest,
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
async move {
let prompts = self.list_prompts().unwrap_or_default();
let prompts = self.list_prompts();

let result = ListPromptsResult { prompts };

Expand DownExpand Up@@ -294,36 +290,36 @@ pub trait Router: Send + Sync + 'static {
.ok_or_else(|| RouterError::InvalidParams("Missing arguments object".into()))?;

// Fetch the prompt definition first
let prompt = match self.list_prompts() {
Some(prompts) => prompts
.into_iter()
.find(|p| p.name == prompt_name)
.ok_or_else(|| {
RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name))
})?,
None => return Err(RouterError::PromptNotFound("No prompts available".into())),
};
let prompt = self
.list_prompts()
.into_iter()
.find(|p| p.name == prompt_name)
.ok_or_else(|| {
RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name))
})?;

// Validate required arguments
for arg in &prompt.arguments {
if arg.required
&& (!arguments.contains_key(&arg.name)
|| arguments
.get(&arg.name)
.and_then(Value::as_str)
.is_none_or(str::is_empty))
{
return Err(RouterError::InvalidParams(format!(
"Missing required argument: '{}'",
arg.name
)));
if let Some(args) = &prompt.arguments {
for arg in args {
if arg.required.is_some()
&& arg.required.unwrap()
&& (!arguments.contains_key(&arg.name)
|| arguments
.get(&arg.name)
.and_then(Value::as_str)
.is_none_or(str::is_empty))
{
return Err(RouterError::InvalidParams(format!(
"Missing required argument: '{}'",
arg.name
)));
}
}
}

// Now get the prompt content
let description = self
.get_prompt(prompt_name)
.ok_or_else(|| RouterError::PromptNotFound("Prompt not found".into()))?
.await
.map_err(|e| RouterError::Internal(e.to_string()))?;

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: update mcp-core prompt to match spec, add mcp list/get to mcp-client by kalvinnchau · Pull Request #2 · modelcontextprotocol/rust-sdk · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions crates/mcp-client/examples/stdio_integration.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,5 +82,16 @@ async fn main() -> Result<(), ClientError> {
let resource = client.read_resource("memo://insights").await?;
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(())
}
48 changes: 45 additions & 3 deletions crates/mcp-client/src/client.rs
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
use mcp_core::protocol::{
CallToolResult, Implementation, InitializeResult, JsonRpcError, JsonRpcMessage,
JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListResourcesResult, ListToolsResult,
ReadResourceResult, ServerCapabilities, METHOD_NOT_FOUND,
CallToolResult, GetPromptResult, Implementation, InitializeResult, JsonRpcError,
JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListPromptsResult,
ListResourcesResult, ListToolsResult, ReadResourceResult, ServerCapabilities, METHOD_NOT_FOUND,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
Expand DownExpand Up@@ -93,6 +93,10 @@ pub trait McpClientTrait: Send + Sync {
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 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.
Expand DownExpand Up@@ -346,4 +350,42 @@ where
// https://modelcontextprotocol.io/docs/concepts/tools#error-handling-2
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
}
}
20 changes: 15 additions & 5 deletions crates/mcp-client/src/transport/sse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,13 +111,23 @@ impl SseActor {
// Attempt to parse the SSE data as a JsonRpcMessage
match serde_json::from_str::<JsonRpcMessage>(&e.data) {
Ok(message) => {
// If it's a response, complete the pending request
if let JsonRpcMessage::Response(resp) = &message {
if let Some(id) = &resp.id {
pending_requests.respond(&id.to_string(), Ok(message)).await;
match &message {
JsonRpcMessage::Response(response) => {
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.)
}
// If it's something else (notification, etc.), handle as needed
}
Err(err) => {
warn!("Failed to parse SSE message: {err}");
Expand Down
14 changes: 11 additions & 3 deletions crates/mcp-client/src/transport/stdio.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -87,10 +87,18 @@ impl StdioActor {
"Received incoming message"
);

if let JsonRpcMessage::Response(response) = &message {
if let Some(id) = &response.id {
pending_requests.respond(&id.to_string(), Ok(message)).await;
match &message {
JsonRpcMessage::Response(response) => {
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();
Expand Down
28 changes: 18 additions & 10 deletions crates/mcp-core/src/prompt.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,22 +10,28 @@ use serde::{Deserialize, Serialize};
pub struct Prompt {
/// The name of the prompt
pub name: String,
/// A description of what the prompt does
pub description: String,
/// The arguments that can be passed to customize the prompt
pub arguments: Vec<PromptArgument>,
/// Optional description of what the prompt does
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Optional arguments that can be passed to customize the prompt
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Vec<PromptArgument>>,
}

impl Prompt {
/// 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
N: Into<String>,
D: Into<String>,
{
Prompt {
name: name.into(),
description: description.into(),
description: description.map(Into::into),
arguments,
}
}
Expand All@@ -37,9 +43,11 @@ pub struct PromptArgument {
/// The name of the argument
pub name: String,
/// 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
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
Expand DownExpand Up@@ -151,6 +159,6 @@ pub struct PromptTemplate {
#[derive(Debug, Serialize, Deserialize)]
pub struct PromptArgumentTemplate {
pub name: String,
pub description: String,
pub required: bool,
pub description: Option<String>,
pub required: Option<bool>,
}
35 changes: 34 additions & 1 deletion crates/mcp-server/src/main.rs
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
use anyhow::Result;
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_server::router::{CapabilitiesBuilder, RouterService};
use mcp_server::{ByteTransport, Router, Server};
Expand DownExpand Up@@ -61,6 +62,7 @@ impl Router for CounterRouter {
CapabilitiesBuilder::new()
.with_tools(false)
.with_resources(false, false)
.with_prompts(false)
.build()
}

Expand DownExpand Up@@ -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]
Expand Down
54 changes: 25 additions & 29 deletions crates/mcp-server/src/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -97,12 +97,8 @@ pub trait Router: Send + Sync + 'static {
&self,
uri: &str,
) -> Pin<Box<dyn Future<Output = Result<String, ResourceError>> + Send + 'static>>;
fn list_prompts(&self) -> Option<Vec<Prompt>> {
None
}
fn get_prompt(&self, _prompt_name: &str) -> Option<PromptFuture> {
None
}
fn list_prompts(&self) -> Vec<Prompt>;
fn get_prompt(&self, prompt_name: &str) -> PromptFuture;

// Helper method to create base response
fn create_response(&self, id: Option<u64>) -> JsonRpcResponse {
Expand DownExpand Up@@ -257,7 +253,7 @@ pub trait Router: Send + Sync + 'static {
req: JsonRpcRequest,
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
async move {
let prompts = self.list_prompts().unwrap_or_default();
let prompts = self.list_prompts();

let result = ListPromptsResult { prompts };

Expand DownExpand Up@@ -294,36 +290,36 @@ pub trait Router: Send + Sync + 'static {
.ok_or_else(|| RouterError::InvalidParams("Missing arguments object".into()))?;

// Fetch the prompt definition first
let prompt = match self.list_prompts() {
Some(prompts) => prompts
.into_iter()
.find(|p| p.name == prompt_name)
.ok_or_else(|| {
RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name))
})?,
None => return Err(RouterError::PromptNotFound("No prompts available".into())),
};
let prompt = self
.list_prompts()
.into_iter()
.find(|p| p.name == prompt_name)
.ok_or_else(|| {
RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name))
})?;

// Validate required arguments
for arg in &prompt.arguments {
if arg.required
&& (!arguments.contains_key(&arg.name)
|| arguments
.get(&arg.name)
.and_then(Value::as_str)
.is_none_or(str::is_empty))
{
return Err(RouterError::InvalidParams(format!(
"Missing required argument: '{}'",
arg.name
)));
if let Some(args) = &prompt.arguments {
for arg in args {
if arg.required.is_some()
&& arg.required.unwrap()
&& (!arguments.contains_key(&arg.name)
|| arguments
.get(&arg.name)
.and_then(Value::as_str)
.is_none_or(str::is_empty))
{
return Err(RouterError::InvalidParams(format!(
"Missing required argument: '{}'",
arg.name
)));
}
}
}

// Now get the prompt content
let description = self
.get_prompt(prompt_name)
.ok_or_else(|| RouterError::PromptNotFound("Prompt not found".into()))?
.await
.map_err(|e| RouterError::Internal(e.to_string()))?;

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat: update mcp-core prompt to match spec, add mcp list/get to mcp-client by kalvinnchau · Pull Request #2 · modelcontextprotocol/rust-sdk · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions crates/mcp-client/examples/stdio_integration.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,5 +82,16 @@ async fn main() -> Result<(), ClientError> {
let resource = client.read_resource("memo://insights").await?;
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(())
}
48 changes: 45 additions & 3 deletions crates/mcp-client/src/client.rs
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
use mcp_core::protocol::{
CallToolResult, Implementation, InitializeResult, JsonRpcError, JsonRpcMessage,
JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListResourcesResult, ListToolsResult,
ReadResourceResult, ServerCapabilities, METHOD_NOT_FOUND,
CallToolResult, GetPromptResult, Implementation, InitializeResult, JsonRpcError,
JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListPromptsResult,
ListResourcesResult, ListToolsResult, ReadResourceResult, ServerCapabilities, METHOD_NOT_FOUND,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
Expand DownExpand Up@@ -93,6 +93,10 @@ pub trait McpClientTrait: Send + Sync {
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 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.
Expand DownExpand Up@@ -346,4 +350,42 @@ where
// https://modelcontextprotocol.io/docs/concepts/tools#error-handling-2
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
}
}
20 changes: 15 additions & 5 deletions crates/mcp-client/src/transport/sse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,13 +111,23 @@ impl SseActor {
// Attempt to parse the SSE data as a JsonRpcMessage
match serde_json::from_str::<JsonRpcMessage>(&e.data) {
Ok(message) => {
// If it's a response, complete the pending request
if let JsonRpcMessage::Response(resp) = &message {
if let Some(id) = &resp.id {
pending_requests.respond(&id.to_string(), Ok(message)).await;
match &message {
JsonRpcMessage::Response(response) => {
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.)
}
// If it's something else (notification, etc.), handle as needed
}
Err(err) => {
warn!("Failed to parse SSE message: {err}");
Expand Down
14 changes: 11 additions & 3 deletions crates/mcp-client/src/transport/stdio.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -87,10 +87,18 @@ impl StdioActor {
"Received incoming message"
);

if let JsonRpcMessage::Response(response) = &message {
if let Some(id) = &response.id {
pending_requests.respond(&id.to_string(), Ok(message)).await;
match &message {
JsonRpcMessage::Response(response) => {
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();
Expand Down
28 changes: 18 additions & 10 deletions crates/mcp-core/src/prompt.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,22 +10,28 @@ use serde::{Deserialize, Serialize};
pub struct Prompt {
/// The name of the prompt
pub name: String,
/// A description of what the prompt does
pub description: String,
/// The arguments that can be passed to customize the prompt
pub arguments: Vec<PromptArgument>,
/// Optional description of what the prompt does
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Optional arguments that can be passed to customize the prompt
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Vec<PromptArgument>>,
}

impl Prompt {
/// 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
N: Into<String>,
D: Into<String>,
{
Prompt {
name: name.into(),
description: description.into(),
description: description.map(Into::into),
arguments,
}
}
Expand All@@ -37,9 +43,11 @@ pub struct PromptArgument {
/// The name of the argument
pub name: String,
/// 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
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
Expand DownExpand Up@@ -151,6 +159,6 @@ pub struct PromptTemplate {
#[derive(Debug, Serialize, Deserialize)]
pub struct PromptArgumentTemplate {
pub name: String,
pub description: String,
pub required: bool,
pub description: Option<String>,
pub required: Option<bool>,
}
35 changes: 34 additions & 1 deletion crates/mcp-server/src/main.rs
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
use anyhow::Result;
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_server::router::{CapabilitiesBuilder, RouterService};
use mcp_server::{ByteTransport, Router, Server};
Expand DownExpand Up@@ -61,6 +62,7 @@ impl Router for CounterRouter {
CapabilitiesBuilder::new()
.with_tools(false)
.with_resources(false, false)
.with_prompts(false)
.build()
}

Expand DownExpand Up@@ -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]
Expand Down
54 changes: 25 additions & 29 deletions crates/mcp-server/src/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -97,12 +97,8 @@ pub trait Router: Send + Sync + 'static {
&self,
uri: &str,
) -> Pin<Box<dyn Future<Output = Result<String, ResourceError>> + Send + 'static>>;
fn list_prompts(&self) -> Option<Vec<Prompt>> {
None
}
fn get_prompt(&self, _prompt_name: &str) -> Option<PromptFuture> {
None
}
fn list_prompts(&self) -> Vec<Prompt>;
fn get_prompt(&self, prompt_name: &str) -> PromptFuture;

// Helper method to create base response
fn create_response(&self, id: Option<u64>) -> JsonRpcResponse {
Expand DownExpand Up@@ -257,7 +253,7 @@ pub trait Router: Send + Sync + 'static {
req: JsonRpcRequest,
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
async move {
let prompts = self.list_prompts().unwrap_or_default();
let prompts = self.list_prompts();

let result = ListPromptsResult { prompts };

Expand DownExpand Up@@ -294,36 +290,36 @@ pub trait Router: Send + Sync + 'static {
.ok_or_else(|| RouterError::InvalidParams("Missing arguments object".into()))?;

// Fetch the prompt definition first
let prompt = match self.list_prompts() {
Some(prompts) => prompts
.into_iter()
.find(|p| p.name == prompt_name)
.ok_or_else(|| {
RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name))
})?,
None => return Err(RouterError::PromptNotFound("No prompts available".into())),
};
let prompt = self
.list_prompts()
.into_iter()
.find(|p| p.name == prompt_name)
.ok_or_else(|| {
RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name))
})?;

// Validate required arguments
for arg in &prompt.arguments {
if arg.required
&& (!arguments.contains_key(&arg.name)
|| arguments
.get(&arg.name)
.and_then(Value::as_str)
.is_none_or(str::is_empty))
{
return Err(RouterError::InvalidParams(format!(
"Missing required argument: '{}'",
arg.name
)));
if let Some(args) = &prompt.arguments {
for arg in args {
if arg.required.is_some()
&& arg.required.unwrap()
&& (!arguments.contains_key(&arg.name)
|| arguments
.get(&arg.name)
.and_then(Value::as_str)
.is_none_or(str::is_empty))
{
return Err(RouterError::InvalidParams(format!(
"Missing required argument: '{}'",
arg.name
)));
}
}
}

// Now get the prompt content
let description = self
.get_prompt(prompt_name)
.ok_or_else(|| RouterError::PromptNotFound("Prompt not found".into()))?
.await
.map_err(|e| RouterError::Internal(e.to_string()))?;

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: update mcp-core prompt to match spec, add mcp list/get to mcp-client by kalvinnchau · Pull Request #2 · modelcontextprotocol/rust-sdk · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions crates/mcp-client/examples/stdio_integration.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,5 +82,16 @@ async fn main() -> Result<(), ClientError> {
let resource = client.read_resource("memo://insights").await?;
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(())
}
48 changes: 45 additions & 3 deletions crates/mcp-client/src/client.rs
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
use mcp_core::protocol::{
CallToolResult, Implementation, InitializeResult, JsonRpcError, JsonRpcMessage,
JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListResourcesResult, ListToolsResult,
ReadResourceResult, ServerCapabilities, METHOD_NOT_FOUND,
CallToolResult, GetPromptResult, Implementation, InitializeResult, JsonRpcError,
JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListPromptsResult,
ListResourcesResult, ListToolsResult, ReadResourceResult, ServerCapabilities, METHOD_NOT_FOUND,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
Expand DownExpand Up@@ -93,6 +93,10 @@ pub trait McpClientTrait: Send + Sync {
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 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.
Expand DownExpand Up@@ -346,4 +350,42 @@ where
// https://modelcontextprotocol.io/docs/concepts/tools#error-handling-2
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
}
}
20 changes: 15 additions & 5 deletions crates/mcp-client/src/transport/sse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,13 +111,23 @@ impl SseActor {
// Attempt to parse the SSE data as a JsonRpcMessage
match serde_json::from_str::<JsonRpcMessage>(&e.data) {
Ok(message) => {
// If it's a response, complete the pending request
if let JsonRpcMessage::Response(resp) = &message {
if let Some(id) = &resp.id {
pending_requests.respond(&id.to_string(), Ok(message)).await;
match &message {
JsonRpcMessage::Response(response) => {
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.)
}
// If it's something else (notification, etc.), handle as needed
}
Err(err) => {
warn!("Failed to parse SSE message: {err}");
Expand Down
14 changes: 11 additions & 3 deletions crates/mcp-client/src/transport/stdio.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -87,10 +87,18 @@ impl StdioActor {
"Received incoming message"
);

if let JsonRpcMessage::Response(response) = &message {
if let Some(id) = &response.id {
pending_requests.respond(&id.to_string(), Ok(message)).await;
match &message {
JsonRpcMessage::Response(response) => {
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();
Expand Down
28 changes: 18 additions & 10 deletions crates/mcp-core/src/prompt.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,22 +10,28 @@ use serde::{Deserialize, Serialize};
pub struct Prompt {
/// The name of the prompt
pub name: String,
/// A description of what the prompt does
pub description: String,
/// The arguments that can be passed to customize the prompt
pub arguments: Vec<PromptArgument>,
/// Optional description of what the prompt does
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Optional arguments that can be passed to customize the prompt
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Vec<PromptArgument>>,
}

impl Prompt {
/// 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
N: Into<String>,
D: Into<String>,
{
Prompt {
name: name.into(),
description: description.into(),
description: description.map(Into::into),
arguments,
}
}
Expand All@@ -37,9 +43,11 @@ pub struct PromptArgument {
/// The name of the argument
pub name: String,
/// 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
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
Expand DownExpand Up@@ -151,6 +159,6 @@ pub struct PromptTemplate {
#[derive(Debug, Serialize, Deserialize)]
pub struct PromptArgumentTemplate {
pub name: String,
pub description: String,
pub required: bool,
pub description: Option<String>,
pub required: Option<bool>,
}
35 changes: 34 additions & 1 deletion crates/mcp-server/src/main.rs
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
use anyhow::Result;
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_server::router::{CapabilitiesBuilder, RouterService};
use mcp_server::{ByteTransport, Router, Server};
Expand DownExpand Up@@ -61,6 +62,7 @@ impl Router for CounterRouter {
CapabilitiesBuilder::new()
.with_tools(false)
.with_resources(false, false)
.with_prompts(false)
.build()
}

Expand DownExpand Up@@ -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]
Expand Down
54 changes: 25 additions & 29 deletions crates/mcp-server/src/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -97,12 +97,8 @@ pub trait Router: Send + Sync + 'static {
&self,
uri: &str,
) -> Pin<Box<dyn Future<Output = Result<String, ResourceError>> + Send + 'static>>;
fn list_prompts(&self) -> Option<Vec<Prompt>> {
None
}
fn get_prompt(&self, _prompt_name: &str) -> Option<PromptFuture> {
None
}
fn list_prompts(&self) -> Vec<Prompt>;
fn get_prompt(&self, prompt_name: &str) -> PromptFuture;

// Helper method to create base response
fn create_response(&self, id: Option<u64>) -> JsonRpcResponse {
Expand DownExpand Up@@ -257,7 +253,7 @@ pub trait Router: Send + Sync + 'static {
req: JsonRpcRequest,
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
async move {
let prompts = self.list_prompts().unwrap_or_default();
let prompts = self.list_prompts();

let result = ListPromptsResult { prompts };

Expand DownExpand Up@@ -294,36 +290,36 @@ pub trait Router: Send + Sync + 'static {
.ok_or_else(|| RouterError::InvalidParams("Missing arguments object".into()))?;

// Fetch the prompt definition first
let prompt = match self.list_prompts() {
Some(prompts) => prompts
.into_iter()
.find(|p| p.name == prompt_name)
.ok_or_else(|| {
RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name))
})?,
None => return Err(RouterError::PromptNotFound("No prompts available".into())),
};
let prompt = self
.list_prompts()
.into_iter()
.find(|p| p.name == prompt_name)
.ok_or_else(|| {
RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name))
})?;

// Validate required arguments
for arg in &prompt.arguments {
if arg.required
&& (!arguments.contains_key(&arg.name)
|| arguments
.get(&arg.name)
.and_then(Value::as_str)
.is_none_or(str::is_empty))
{
return Err(RouterError::InvalidParams(format!(
"Missing required argument: '{}'",
arg.name
)));
if let Some(args) = &prompt.arguments {
for arg in args {
if arg.required.is_some()
&& arg.required.unwrap()
&& (!arguments.contains_key(&arg.name)
|| arguments
.get(&arg.name)
.and_then(Value::as_str)
.is_none_or(str::is_empty))
{
return Err(RouterError::InvalidParams(format!(
"Missing required argument: '{}'",
arg.name
)));
}
}
}

// Now get the prompt content
let description = self
.get_prompt(prompt_name)
.ok_or_else(|| RouterError::PromptNotFound("Prompt not found".into()))?
.await
.map_err(|e| RouterError::Internal(e.to_string()))?;

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat: update mcp-core prompt to match spec, add mcp list/get to mcp-client by kalvinnchau · Pull Request #2 · modelcontextprotocol/rust-sdk · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions crates/mcp-client/examples/stdio_integration.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,5 +82,16 @@ async fn main() -> Result<(), ClientError> {
let resource = client.read_resource("memo://insights").await?;
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(())
}
48 changes: 45 additions & 3 deletions crates/mcp-client/src/client.rs
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
use mcp_core::protocol::{
CallToolResult, Implementation, InitializeResult, JsonRpcError, JsonRpcMessage,
JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListResourcesResult, ListToolsResult,
ReadResourceResult, ServerCapabilities, METHOD_NOT_FOUND,
CallToolResult, GetPromptResult, Implementation, InitializeResult, JsonRpcError,
JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListPromptsResult,
ListResourcesResult, ListToolsResult, ReadResourceResult, ServerCapabilities, METHOD_NOT_FOUND,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
Expand DownExpand Up@@ -93,6 +93,10 @@ pub trait McpClientTrait: Send + Sync {
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 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.
Expand DownExpand Up@@ -346,4 +350,42 @@ where
// https://modelcontextprotocol.io/docs/concepts/tools#error-handling-2
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
}
}
20 changes: 15 additions & 5 deletions crates/mcp-client/src/transport/sse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,13 +111,23 @@ impl SseActor {
// Attempt to parse the SSE data as a JsonRpcMessage
match serde_json::from_str::<JsonRpcMessage>(&e.data) {
Ok(message) => {
// If it's a response, complete the pending request
if let JsonRpcMessage::Response(resp) = &message {
if let Some(id) = &resp.id {
pending_requests.respond(&id.to_string(), Ok(message)).await;
match &message {
JsonRpcMessage::Response(response) => {
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.)
}
// If it's something else (notification, etc.), handle as needed
}
Err(err) => {
warn!("Failed to parse SSE message: {err}");
Expand Down
14 changes: 11 additions & 3 deletions crates/mcp-client/src/transport/stdio.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -87,10 +87,18 @@ impl StdioActor {
"Received incoming message"
);

if let JsonRpcMessage::Response(response) = &message {
if let Some(id) = &response.id {
pending_requests.respond(&id.to_string(), Ok(message)).await;
match &message {
JsonRpcMessage::Response(response) => {
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();
Expand Down
28 changes: 18 additions & 10 deletions crates/mcp-core/src/prompt.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,22 +10,28 @@ use serde::{Deserialize, Serialize};
pub struct Prompt {
/// The name of the prompt
pub name: String,
/// A description of what the prompt does
pub description: String,
/// The arguments that can be passed to customize the prompt
pub arguments: Vec<PromptArgument>,
/// Optional description of what the prompt does
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Optional arguments that can be passed to customize the prompt
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Vec<PromptArgument>>,
}

impl Prompt {
/// 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
N: Into<String>,
D: Into<String>,
{
Prompt {
name: name.into(),
description: description.into(),
description: description.map(Into::into),
arguments,
}
}
Expand All@@ -37,9 +43,11 @@ pub struct PromptArgument {
/// The name of the argument
pub name: String,
/// 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
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
Expand DownExpand Up@@ -151,6 +159,6 @@ pub struct PromptTemplate {
#[derive(Debug, Serialize, Deserialize)]
pub struct PromptArgumentTemplate {
pub name: String,
pub description: String,
pub required: bool,
pub description: Option<String>,
pub required: Option<bool>,
}
35 changes: 34 additions & 1 deletion crates/mcp-server/src/main.rs
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
use anyhow::Result;
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_server::router::{CapabilitiesBuilder, RouterService};
use mcp_server::{ByteTransport, Router, Server};
Expand DownExpand Up@@ -61,6 +62,7 @@ impl Router for CounterRouter {
CapabilitiesBuilder::new()
.with_tools(false)
.with_resources(false, false)
.with_prompts(false)
.build()
}

Expand DownExpand Up@@ -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]
Expand Down
54 changes: 25 additions & 29 deletions crates/mcp-server/src/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -97,12 +97,8 @@ pub trait Router: Send + Sync + 'static {
&self,
uri: &str,
) -> Pin<Box<dyn Future<Output = Result<String, ResourceError>> + Send + 'static>>;
fn list_prompts(&self) -> Option<Vec<Prompt>> {
None
}
fn get_prompt(&self, _prompt_name: &str) -> Option<PromptFuture> {
None
}
fn list_prompts(&self) -> Vec<Prompt>;
fn get_prompt(&self, prompt_name: &str) -> PromptFuture;

// Helper method to create base response
fn create_response(&self, id: Option<u64>) -> JsonRpcResponse {
Expand DownExpand Up@@ -257,7 +253,7 @@ pub trait Router: Send + Sync + 'static {
req: JsonRpcRequest,
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
async move {
let prompts = self.list_prompts().unwrap_or_default();
let prompts = self.list_prompts();

let result = ListPromptsResult { prompts };

Expand DownExpand Up@@ -294,36 +290,36 @@ pub trait Router: Send + Sync + 'static {
.ok_or_else(|| RouterError::InvalidParams("Missing arguments object".into()))?;

// Fetch the prompt definition first
let prompt = match self.list_prompts() {
Some(prompts) => prompts
.into_iter()
.find(|p| p.name == prompt_name)
.ok_or_else(|| {
RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name))
})?,
None => return Err(RouterError::PromptNotFound("No prompts available".into())),
};
let prompt = self
.list_prompts()
.into_iter()
.find(|p| p.name == prompt_name)
.ok_or_else(|| {
RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name))
})?;

// Validate required arguments
for arg in &prompt.arguments {
if arg.required
&& (!arguments.contains_key(&arg.name)
|| arguments
.get(&arg.name)
.and_then(Value::as_str)
.is_none_or(str::is_empty))
{
return Err(RouterError::InvalidParams(format!(
"Missing required argument: '{}'",
arg.name
)));
if let Some(args) = &prompt.arguments {
for arg in args {
if arg.required.is_some()
&& arg.required.unwrap()
&& (!arguments.contains_key(&arg.name)
|| arguments
.get(&arg.name)
.and_then(Value::as_str)
.is_none_or(str::is_empty))
{
return Err(RouterError::InvalidParams(format!(
"Missing required argument: '{}'",
arg.name
)));
}
}
}

// Now get the prompt content
let description = self
.get_prompt(prompt_name)
.ok_or_else(|| RouterError::PromptNotFound("Prompt not found".into()))?
.await
.map_err(|e| RouterError::Internal(e.to_string()))?;

Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat: update mcp-core prompt to match spec, add mcp list/get to mcp-client by kalvinnchau · Pull Request #2 · modelcontextprotocol/rust-sdk · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions crates/mcp-client/examples/stdio_integration.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -82,5 +82,16 @@ async fn main() -> Result<(), ClientError> {
let resource = client.read_resource("memo://insights").await?;
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(())
}
48 changes: 45 additions & 3 deletions crates/mcp-client/src/client.rs
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
use mcp_core::protocol::{
CallToolResult, Implementation, InitializeResult, JsonRpcError, JsonRpcMessage,
JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListResourcesResult, ListToolsResult,
ReadResourceResult, ServerCapabilities, METHOD_NOT_FOUND,
CallToolResult, GetPromptResult, Implementation, InitializeResult, JsonRpcError,
JsonRpcMessage, JsonRpcNotification, JsonRpcRequest, JsonRpcResponse, ListPromptsResult,
ListResourcesResult, ListToolsResult, ReadResourceResult, ServerCapabilities, METHOD_NOT_FOUND,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
Expand DownExpand Up@@ -93,6 +93,10 @@ pub trait McpClientTrait: Send + Sync {
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 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.
Expand DownExpand Up@@ -346,4 +350,42 @@ where
// https://modelcontextprotocol.io/docs/concepts/tools#error-handling-2
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
}
}
20 changes: 15 additions & 5 deletions crates/mcp-client/src/transport/sse.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -111,13 +111,23 @@ impl SseActor {
// Attempt to parse the SSE data as a JsonRpcMessage
match serde_json::from_str::<JsonRpcMessage>(&e.data) {
Ok(message) => {
// If it's a response, complete the pending request
if let JsonRpcMessage::Response(resp) = &message {
if let Some(id) = &resp.id {
pending_requests.respond(&id.to_string(), Ok(message)).await;
match &message {
JsonRpcMessage::Response(response) => {
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.)
}
// If it's something else (notification, etc.), handle as needed
}
Err(err) => {
warn!("Failed to parse SSE message: {err}");
Expand Down
14 changes: 11 additions & 3 deletions crates/mcp-client/src/transport/stdio.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -87,10 +87,18 @@ impl StdioActor {
"Received incoming message"
);

if let JsonRpcMessage::Response(response) = &message {
if let Some(id) = &response.id {
pending_requests.respond(&id.to_string(), Ok(message)).await;
match &message {
JsonRpcMessage::Response(response) => {
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();
Expand Down
28 changes: 18 additions & 10 deletions crates/mcp-core/src/prompt.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -10,22 +10,28 @@ use serde::{Deserialize, Serialize};
pub struct Prompt {
/// The name of the prompt
pub name: String,
/// A description of what the prompt does
pub description: String,
/// The arguments that can be passed to customize the prompt
pub arguments: Vec<PromptArgument>,
/// Optional description of what the prompt does
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
/// Optional arguments that can be passed to customize the prompt
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<Vec<PromptArgument>>,
}

impl Prompt {
/// 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
N: Into<String>,
D: Into<String>,
{
Prompt {
name: name.into(),
description: description.into(),
description: description.map(Into::into),
arguments,
}
}
Expand All@@ -37,9 +43,11 @@ pub struct PromptArgument {
/// The name of the argument
pub name: String,
/// 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
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
Expand DownExpand Up@@ -151,6 +159,6 @@ pub struct PromptTemplate {
#[derive(Debug, Serialize, Deserialize)]
pub struct PromptArgumentTemplate {
pub name: String,
pub description: String,
pub required: bool,
pub description: Option<String>,
pub required: Option<bool>,
}
35 changes: 34 additions & 1 deletion crates/mcp-server/src/main.rs
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
use anyhow::Result;
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_server::router::{CapabilitiesBuilder, RouterService};
use mcp_server::{ByteTransport, Router, Server};
Expand DownExpand Up@@ -61,6 +62,7 @@ impl Router for CounterRouter {
CapabilitiesBuilder::new()
.with_tools(false)
.with_resources(false, false)
.with_prompts(false)
.build()
}

Expand DownExpand Up@@ -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]
Expand Down
54 changes: 25 additions & 29 deletions crates/mcp-server/src/router.rs
Original file line numberDiff line numberDiff line change
Expand Up@@ -97,12 +97,8 @@ pub trait Router: Send + Sync + 'static {
&self,
uri: &str,
) -> Pin<Box<dyn Future<Output = Result<String, ResourceError>> + Send + 'static>>;
fn list_prompts(&self) -> Option<Vec<Prompt>> {
None
}
fn get_prompt(&self, _prompt_name: &str) -> Option<PromptFuture> {
None
}
fn list_prompts(&self) -> Vec<Prompt>;
fn get_prompt(&self, prompt_name: &str) -> PromptFuture;

// Helper method to create base response
fn create_response(&self, id: Option<u64>) -> JsonRpcResponse {
Expand DownExpand Up@@ -257,7 +253,7 @@ pub trait Router: Send + Sync + 'static {
req: JsonRpcRequest,
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
async move {
let prompts = self.list_prompts().unwrap_or_default();
let prompts = self.list_prompts();

let result = ListPromptsResult { prompts };

Expand DownExpand Up@@ -294,36 +290,36 @@ pub trait Router: Send + Sync + 'static {
.ok_or_else(|| RouterError::InvalidParams("Missing arguments object".into()))?;

// Fetch the prompt definition first
let prompt = match self.list_prompts() {
Some(prompts) => prompts
.into_iter()
.find(|p| p.name == prompt_name)
.ok_or_else(|| {
RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name))
})?,
None => return Err(RouterError::PromptNotFound("No prompts available".into())),
};
let prompt = self
.list_prompts()
.into_iter()
.find(|p| p.name == prompt_name)
.ok_or_else(|| {
RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name))
})?;

// Validate required arguments
for arg in &prompt.arguments {
if arg.required
&& (!arguments.contains_key(&arg.name)
|| arguments
.get(&arg.name)
.and_then(Value::as_str)
.is_none_or(str::is_empty))
{
return Err(RouterError::InvalidParams(format!(
"Missing required argument: '{}'",
arg.name
)));
if let Some(args) = &prompt.arguments {
for arg in args {
if arg.required.is_some()
&& arg.required.unwrap()
&& (!arguments.contains_key(&arg.name)
|| arguments
.get(&arg.name)
.and_then(Value::as_str)
.is_none_or(str::is_empty))
{
return Err(RouterError::InvalidParams(format!(
"Missing required argument: '{}'",
arg.name
)));
}
}
}

// Now get the prompt content
let description = self
.get_prompt(prompt_name)
.ok_or_else(|| RouterError::PromptNotFound("Prompt not found".into()))?
.await
.map_err(|e| RouterError::Internal(e.to_string()))?;

Expand Down