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
2 changes: 1 addition & 1 deletion crates/rmcp/src/handler/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ pub trait ServerHandler: Sized + Send + Sync + 'static {
request: CompleteRequestParam,
context: RequestContext<RoleServer>,
) -> impl Future<Output = Result<CompleteResult, McpError>> + Send + '_ {
std::future::ready(Err(McpError::method_not_found::<CompleteRequestMethod>()))
std::future::ready(Ok(CompleteResult::default()))
}
fn set_level(
&self,
Expand Down
156 changes: 154 additions & 2 deletions crates/rmcp/src/model.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1091,17 +1091,66 @@ pub struct ModelHint {
// COMPLETION AND AUTOCOMPLETE
// =============================================================================

/// Context for completion requests providing previously resolved arguments.
///
/// This enables context-aware completion where subsequent argument completions
/// can take into account the values of previously resolved arguments.
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CompletionContext {
/// Previously resolved argument values that can inform completion suggestions
#[serde(skip_serializing_if = "Option::is_none")]
pub arguments: Option<std::collections::HashMap<String, String>>,
}

impl CompletionContext {
/// Create a new empty completion context
pub fn new() -> Self {
Self::default()
}

/// Create a completion context with the given arguments
pub fn with_arguments(arguments: std::collections::HashMap<String, String>) -> Self {
Self {
arguments: Some(arguments),
}
}

/// Get a specific argument value by name
pub fn get_argument(&self, name: &str) -> Option<&String> {
self.arguments.as_ref()?.get(name)
}

/// Check if the context has any arguments
pub fn has_arguments(&self) -> bool {
self.arguments.as_ref().is_some_and(|args| !args.is_empty())
}

/// Get all argument names
pub fn argument_names(&self) -> impl Iterator<Item = &str> {
self.arguments
.as_ref()
.into_iter()
.flat_map(|args| args.keys())
.map(|k| k.as_str())
}
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CompleteRequestParam {
pub r#ref: Reference,
pub argument: ArgumentInfo,
/// Optional context containing previously resolved argument values
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<CompletionContext>,
}

pub type CompleteRequest = Request<CompleteRequestMethod, CompleteRequestParam>;

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CompletionInfo {
Expand All @@ -1112,7 +1161,74 @@ pub struct CompletionInfo {
pub has_more: Option<bool>,
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
impl CompletionInfo {
/// Maximum number of completion values allowed per response according to MCP specification
pub const MAX_VALUES: usize = 100;

/// Create a new CompletionInfo with validation for maximum values
pub fn new(values: Vec<String>) -> Result<Self, String> {
if values.len() > Self::MAX_VALUES {
return Err(format!(
"Too many completion values: {} (max: {})",
values.len(),
Self::MAX_VALUES
));
}
Ok(Self {
values,
total: None,
has_more: None,
})
}

/// Create CompletionInfo with all values and no pagination
pub fn with_all_values(values: Vec<String>) -> Result<Self, String> {
let completion = Self::new(values)?;
Ok(Self {
total: Some(completion.values.len() as u32),
has_more: Some(false),
..completion
})
}

/// Create CompletionInfo with pagination information
pub fn with_pagination(
values: Vec<String>,
total: Option<u32>,
has_more: bool,
) -> Result<Self, String> {
let completion = Self::new(values)?;
Ok(Self {
total,
has_more: Some(has_more),
..completion
})
}

/// Check if this completion response indicates more results are available
pub fn has_more_results(&self) -> bool {
self.has_more.unwrap_or(false)
}

/// Get the total number of available completions, if known
pub fn total_available(&self) -> Option<u32> {
self.total
}

/// Validate that the completion info complies with MCP specification
pub fn validate(&self) -> Result<(), String> {
if self.values.len() > Self::MAX_VALUES {
return Err(format!(
"Too many completion values: {} (max: {})",
self.values.len(),
Self::MAX_VALUES
));
}
Ok(())
}
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct CompleteResult {
Expand All @@ -1129,6 +1245,42 @@ pub enum Reference {
Prompt(PromptReference),
}

impl Reference {
/// Create a prompt reference
pub fn for_prompt(name: impl Into<String>) -> Self {
Self::Prompt(PromptReference { name: name.into() })
}

/// Create a resource reference
pub fn for_resource(uri: impl Into<String>) -> Self {
Self::Resource(ResourceReference { uri: uri.into() })
}

/// Get the reference type as a string
pub fn reference_type(&self) -> &'static str {
match self {
Self::Prompt(_) => "ref/prompt",
Self::Resource(_) => "ref/resource",
}
}

/// Extract prompt name if this is a prompt reference
pub fn as_prompt_name(&self) -> Option<&str> {
match self {
Self::Prompt(prompt_ref) => Some(&prompt_ref.name),
_ => None,
}
}

/// Extract resource URI if this is a resource reference
pub fn as_resource_uri(&self) -> Option<&str> {
match self {
Self::Resource(resource_ref) => Some(&resource_ref.uri),
_ => None,
}
}
}

#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ResourceReference {
Expand Down
112 changes: 102 additions & 10 deletions crates/rmcp/src/service/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,18 @@ use thiserror::Error;
use super::*;
use crate::{
model::{
CallToolRequest, CallToolRequestParam, CallToolResult, CancelledNotification,
ArgumentInfo, CallToolRequest, CallToolRequestParam, CallToolResult, CancelledNotification,
CancelledNotificationParam, ClientInfo, ClientJsonRpcMessage, ClientNotification,
ClientRequest, ClientResult, CompleteRequest, CompleteRequestParam, CompleteResult,
GetPromptRequest, GetPromptRequestParam, GetPromptResult, InitializeRequest,
InitializedNotification, JsonRpcResponse, ListPromptsRequest, ListPromptsResult,
ListResourceTemplatesRequest, ListResourceTemplatesResult, ListResourcesRequest,
ListResourcesResult, ListToolsRequest, ListToolsResult, PaginatedRequestParam,
ProgressNotification, ProgressNotificationParam, ReadResourceRequest,
ReadResourceRequestParam, ReadResourceResult, RequestId, RootsListChangedNotification,
ServerInfo, ServerJsonRpcMessage, ServerNotification, ServerRequest, ServerResult,
SetLevelRequest, SetLevelRequestParam, SubscribeRequest, SubscribeRequestParam,
UnsubscribeRequest, UnsubscribeRequestParam,
CompletionContext, CompletionInfo, GetPromptRequest, GetPromptRequestParam,
GetPromptResult, InitializeRequest, InitializedNotification, JsonRpcResponse,
ListPromptsRequest, ListPromptsResult, ListResourceTemplatesRequest,
ListResourceTemplatesResult, ListResourcesRequest, ListResourcesResult, ListToolsRequest,
ListToolsResult, PaginatedRequestParam, ProgressNotification, ProgressNotificationParam,
ReadResourceRequest, ReadResourceRequestParam, ReadResourceResult, Reference, RequestId,
RootsListChangedNotification, ServerInfo, ServerJsonRpcMessage, ServerNotification,
ServerRequest, ServerResult, SetLevelRequest, SetLevelRequestParam, SubscribeRequest,
SubscribeRequestParam, UnsubscribeRequest, UnsubscribeRequestParam,
},
transport::DynamicTransportError,
};
Expand Down Expand Up @@ -390,4 +390,96 @@ impl Peer<RoleClient> {
}
Ok(resource_templates)
}

/// Convenient method to get completion suggestions for a prompt argument
///
/// # Arguments
/// * `prompt_name` - Name of the prompt being completed
/// * `argument_name` - Name of the argument being completed
/// * `current_value` - Current partial value of the argument
/// * `context` - Optional context with previously resolved arguments
///
/// # Returns
/// CompletionInfo with suggestions for the specified prompt argument
pub async fn complete_prompt_argument(
&self,
prompt_name: impl Into<String>,
argument_name: impl Into<String>,
current_value: impl Into<String>,
context: Option<CompletionContext>,
) -> Result<CompletionInfo, ServiceError> {
let request = CompleteRequestParam {
r#ref: Reference::for_prompt(prompt_name),
argument: ArgumentInfo {
name: argument_name.into(),
value: current_value.into(),
},
context,
};

let result = self.complete(request).await?;
Ok(result.completion)
}

/// Convenient method to get completion suggestions for a resource URI argument
///
/// # Arguments
/// * `uri_template` - URI template pattern being completed
/// * `argument_name` - Name of the URI parameter being completed
/// * `current_value` - Current partial value of the parameter
/// * `context` - Optional context with previously resolved arguments
///
/// # Returns
/// CompletionInfo with suggestions for the specified resource URI argument
pub async fn complete_resource_argument(
&self,
uri_template: impl Into<String>,
argument_name: impl Into<String>,
current_value: impl Into<String>,
context: Option<CompletionContext>,
) -> Result<CompletionInfo, ServiceError> {
let request = CompleteRequestParam {
r#ref: Reference::for_resource(uri_template),
argument: ArgumentInfo {
name: argument_name.into(),
value: current_value.into(),
},
context,
};

let result = self.complete(request).await?;
Ok(result.completion)
}

/// Simple completion for a prompt argument without context
///
/// This is a convenience wrapper around `complete_prompt_argument` for
/// simple completion scenarios that don't require context awareness.
pub async fn complete_prompt_simple(
&self,
prompt_name: impl Into<String>,
argument_name: impl Into<String>,
current_value: impl Into<String>,
) -> Result<Vec<String>, ServiceError> {
let completion = self
.complete_prompt_argument(prompt_name, argument_name, current_value, None)
.await?;
Ok(completion.values)
}

/// Simple completion for a resource URI argument without context
///
/// This is a convenience wrapper around `complete_resource_argument` for
/// simple completion scenarios that don't require context awareness.
pub async fn complete_resource_simple(
&self,
uri_template: impl Into<String>,
argument_name: impl Into<String>,
current_value: impl Into<String>,
) -> Result<Vec<String>, ServiceError> {
let completion = self
.complete_resource_argument(uri_template, argument_name, current_value, None)
.await?;
Ok(completion.values)
}
}
Loading