Move whole rmcp crate to offcial rust sdk (#44)
* get rid of async-trait * move rmcp to official-rust-sdk * replace master with main * update readme, change license * fix typo * Update README.md Co-authored-by: Jefry Dewangga <jefrydco@gmail.com> * fix typos --------- Co-authored-by: = <=> Co-authored-by: Jefry Dewangga <jefrydco@gmail.com>
This commit is contained in:
parent
31493cfbcc
commit
0be6067326
4 changed files with 8676 additions and 4466 deletions
45
examples/servers/src/common/calculator.rs
Normal file
45
examples/servers/src/common/calculator.rs
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
use rmcp::{
|
||||
ServerHandler,
|
||||
model::{ServerCapabilities, ServerInfo},
|
||||
schemars, tool,
|
||||
};
|
||||
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct SumRequest {
|
||||
#[schemars(description = "the left hand side number")]
|
||||
pub a: i32,
|
||||
pub b: i32,
|
||||
}
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Calculator;
|
||||
#[tool(tool_box)]
|
||||
impl Calculator {
|
||||
#[tool(description = "Calculate the sum of two numbers")]
|
||||
fn sum(&self, #[tool(aggr)] SumRequest { a, b }: SumRequest) -> String {
|
||||
(a + b).to_string()
|
||||
}
|
||||
|
||||
#[tool(description = "Calculate the sum of two numbers")]
|
||||
fn sub(
|
||||
&self,
|
||||
#[tool(param)]
|
||||
#[schemars(description = "the left hand side number")]
|
||||
a: i32,
|
||||
#[tool(param)]
|
||||
#[schemars(description = "the left hand side number")]
|
||||
b: i32,
|
||||
) -> String {
|
||||
(a - b).to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[tool(tool_box)]
|
||||
impl ServerHandler for Calculator {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
instructions: Some("A simple calculator".into()),
|
||||
capabilities: ServerCapabilities::builder().enable_tools().build(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,184 +1,191 @@
|
|||
use std::{future::Future, pin::Pin, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
|
||||
use mcp_core::{
|
||||
handler::{PromptError, ResourceError},
|
||||
prompt::{Prompt, PromptArgument},
|
||||
protocol::ServerCapabilities,
|
||||
Content, Resource, Tool, ToolError,
|
||||
use rmcp::{
|
||||
Error as McpError, RoleServer, ServerHandler, const_string, model::*, schemars,
|
||||
service::RequestContext, tool,
|
||||
};
|
||||
use mcp_server::router::CapabilitiesBuilder;
|
||||
use serde_json::Value;
|
||||
|
||||
use serde_json::json;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct CounterRouter {
|
||||
counter: Arc<Mutex<i32>>,
|
||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||
pub struct StructRequest {
|
||||
pub a: i32,
|
||||
pub b: i32,
|
||||
}
|
||||
|
||||
impl CounterRouter {
|
||||
#[derive(Clone)]
|
||||
pub struct Counter {
|
||||
counter: Arc<Mutex<i32>>,
|
||||
}
|
||||
#[tool(tool_box)]
|
||||
impl Counter {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
counter: Arc::new(Mutex::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn increment(&self) -> Result<i32, ToolError> {
|
||||
fn _create_resource_text(&self, uri: &str, name: &str) -> Resource {
|
||||
RawResource::new(uri, name.to_string()).no_annotation()
|
||||
}
|
||||
|
||||
#[tool(description = "Increment the counter by 1")]
|
||||
async fn increment(&self) -> Result<CallToolResult, McpError> {
|
||||
let mut counter = self.counter.lock().await;
|
||||
*counter += 1;
|
||||
Ok(*counter)
|
||||
Ok(CallToolResult::success(vec![Content::text(
|
||||
counter.to_string(),
|
||||
)]))
|
||||
}
|
||||
|
||||
async fn decrement(&self) -> Result<i32, ToolError> {
|
||||
#[tool(description = "Decrement the counter by 1")]
|
||||
async fn decrement(&self) -> Result<CallToolResult, McpError> {
|
||||
let mut counter = self.counter.lock().await;
|
||||
*counter -= 1;
|
||||
Ok(*counter)
|
||||
Ok(CallToolResult::success(vec![Content::text(
|
||||
counter.to_string(),
|
||||
)]))
|
||||
}
|
||||
|
||||
async fn get_value(&self) -> Result<i32, ToolError> {
|
||||
let counter = self.counter.lock().await;
|
||||
Ok(*counter)
|
||||
#[tool(description = "Get the current counter value")]
|
||||
async fn get_value(&self) -> Result<CallToolResult, McpError> {
|
||||
let mut counter = self.counter.lock().await;
|
||||
*counter -= 1;
|
||||
Ok(CallToolResult::success(vec![Content::text(
|
||||
counter.to_string(),
|
||||
)]))
|
||||
}
|
||||
|
||||
fn _create_resource_text(&self, uri: &str, name: &str) -> Resource {
|
||||
Resource::new(uri, Some("text/plain".to_string()), Some(name.to_string())).unwrap()
|
||||
#[tool(description = "Say hello to the client")]
|
||||
fn say_hello(&self) -> Result<CallToolResult, McpError> {
|
||||
Ok(CallToolResult::success(vec![Content::text("hello")]))
|
||||
}
|
||||
|
||||
#[tool(description = "Repeat what you say")]
|
||||
fn echo(
|
||||
&self,
|
||||
#[tool(param)]
|
||||
#[schemars(description = "Repeat what you say")]
|
||||
saying: String,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
Ok(CallToolResult::success(vec![Content::text(saying)]))
|
||||
}
|
||||
|
||||
#[tool(description = "Calculate the sum of two numbers")]
|
||||
fn sum(
|
||||
&self,
|
||||
#[tool(aggr)] StructRequest { a, b }: StructRequest,
|
||||
) -> Result<CallToolResult, McpError> {
|
||||
Ok(CallToolResult::success(vec![Content::text(
|
||||
(a + b).to_string(),
|
||||
)]))
|
||||
}
|
||||
}
|
||||
|
||||
impl mcp_server::Router for CounterRouter {
|
||||
fn name(&self) -> String {
|
||||
"counter".to_string()
|
||||
const_string!(Echo = "echo");
|
||||
#[tool(tool_box)]
|
||||
impl ServerHandler for Counter {
|
||||
fn get_info(&self) -> ServerInfo {
|
||||
ServerInfo {
|
||||
protocol_version: ProtocolVersion::V_2024_11_05,
|
||||
capabilities: ServerCapabilities::builder()
|
||||
.enable_prompts()
|
||||
.enable_resources()
|
||||
.enable_tools()
|
||||
.build(),
|
||||
server_info: Implementation::from_build_env(),
|
||||
instructions: Some("This server provides a counter tool that can increment and decrement values. The counter starts at 0 and can be modified using the 'increment' and 'decrement' tools. Use 'get_value' to check the current count.".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn instructions(&self) -> String {
|
||||
"This server provides a counter tool that can increment and decrement values. The counter starts at 0 and can be modified using the 'increment' and 'decrement' tools. Use 'get_value' to check the current count.".to_string()
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> ServerCapabilities {
|
||||
CapabilitiesBuilder::new()
|
||||
.with_tools(false)
|
||||
.with_resources(false, false)
|
||||
.with_prompts(false)
|
||||
.build()
|
||||
}
|
||||
|
||||
fn list_tools(&self) -> Vec<Tool> {
|
||||
vec![
|
||||
Tool::new(
|
||||
"increment".to_string(),
|
||||
"Increment the counter by 1".to_string(),
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}),
|
||||
),
|
||||
Tool::new(
|
||||
"decrement".to_string(),
|
||||
"Decrement the counter by 1".to_string(),
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}),
|
||||
),
|
||||
Tool::new(
|
||||
"get_value".to_string(),
|
||||
"Get the current counter value".to_string(),
|
||||
serde_json::json!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}),
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
fn call_tool(
|
||||
async fn list_resources(
|
||||
&self,
|
||||
tool_name: &str,
|
||||
_arguments: Value,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ToolError>> + Send + 'static>> {
|
||||
let this = self.clone();
|
||||
let tool_name = tool_name.to_string();
|
||||
|
||||
Box::pin(async move {
|
||||
match tool_name.as_str() {
|
||||
"increment" => {
|
||||
let value = this.increment().await?;
|
||||
Ok(vec![Content::text(value.to_string())])
|
||||
}
|
||||
"decrement" => {
|
||||
let value = this.decrement().await?;
|
||||
Ok(vec![Content::text(value.to_string())])
|
||||
}
|
||||
"get_value" => {
|
||||
let value = this.get_value().await?;
|
||||
Ok(vec![Content::text(value.to_string())])
|
||||
}
|
||||
_ => Err(ToolError::NotFound(format!("Tool {} not found", tool_name))),
|
||||
}
|
||||
_request: PaginatedRequestParam,
|
||||
_: RequestContext<RoleServer>,
|
||||
) -> Result<ListResourcesResult, McpError> {
|
||||
Ok(ListResourcesResult {
|
||||
resources: vec![
|
||||
self._create_resource_text("str:////Users/to/some/path/", "cwd"),
|
||||
self._create_resource_text("memo://insights", "memo-name"),
|
||||
],
|
||||
next_cursor: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn list_resources(&self) -> Vec<Resource> {
|
||||
vec![
|
||||
self._create_resource_text("str:////Users/to/some/path/", "cwd"),
|
||||
self._create_resource_text("memo://insights", "memo-name"),
|
||||
]
|
||||
async fn read_resource(
|
||||
&self,
|
||||
ReadResourceRequestParam { uri }: ReadResourceRequestParam,
|
||||
_: RequestContext<RoleServer>,
|
||||
) -> Result<ReadResourceResult, McpError> {
|
||||
match uri.as_str() {
|
||||
"str:////Users/to/some/path/" => {
|
||||
let cwd = "/Users/to/some/path/";
|
||||
Ok(ReadResourceResult {
|
||||
contents: vec![ResourceContents::text(cwd, uri)],
|
||||
})
|
||||
}
|
||||
"memo://insights" => {
|
||||
let memo = "Business Intelligence Memo\n\nAnalysis has revealed 5 key insights ...";
|
||||
Ok(ReadResourceResult {
|
||||
contents: vec![ResourceContents::text(memo, uri)],
|
||||
})
|
||||
}
|
||||
_ => Err(McpError::resource_not_found(
|
||||
"resource_not_found",
|
||||
Some(json!({
|
||||
"uri": uri
|
||||
})),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn read_resource(
|
||||
async fn list_prompts(
|
||||
&self,
|
||||
uri: &str,
|
||||
) -> Pin<Box<dyn Future<Output = Result<String, ResourceError>> + Send + 'static>> {
|
||||
let uri = uri.to_string();
|
||||
Box::pin(async move {
|
||||
match uri.as_str() {
|
||||
"str:////Users/to/some/path/" => {
|
||||
let cwd = "/Users/to/some/path/";
|
||||
Ok(cwd.to_string())
|
||||
}
|
||||
"memo://insights" => {
|
||||
let memo =
|
||||
"Business Intelligence Memo\n\nAnalysis has revealed 5 key insights ...";
|
||||
Ok(memo.to_string())
|
||||
}
|
||||
_ => Err(ResourceError::NotFound(format!(
|
||||
"Resource {} not found",
|
||||
uri
|
||||
))),
|
||||
}
|
||||
_request: PaginatedRequestParam,
|
||||
_: RequestContext<RoleServer>,
|
||||
) -> Result<ListPromptsResult, McpError> {
|
||||
Ok(ListPromptsResult {
|
||||
next_cursor: None,
|
||||
prompts: 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 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),
|
||||
}]),
|
||||
)]
|
||||
async fn get_prompt(
|
||||
&self,
|
||||
GetPromptRequestParam { name, arguments: _ }: GetPromptRequestParam,
|
||||
_: RequestContext<RoleServer>,
|
||||
) -> Result<GetPromptResult, McpError> {
|
||||
match name.as_str() {
|
||||
"example_prompt" => {
|
||||
let prompt = "This is an example prompt with your message here: '{message}'";
|
||||
Ok(GetPromptResult {
|
||||
description: None,
|
||||
messages: vec![PromptMessage {
|
||||
role: PromptMessageRole::User,
|
||||
content: PromptMessageContent::text(prompt),
|
||||
}],
|
||||
})
|
||||
}
|
||||
_ => Err(McpError::invalid_params("prompt not found", None)),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_prompt(
|
||||
async fn list_resource_templates(
|
||||
&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
|
||||
))),
|
||||
}
|
||||
_request: PaginatedRequestParam,
|
||||
_: RequestContext<RoleServer>,
|
||||
) -> Result<ListResourceTemplatesResult, McpError> {
|
||||
Ok(ListResourceTemplatesResult {
|
||||
next_cursor: None,
|
||||
resource_templates: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
use tokio_util::codec::Decoder;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct JsonRpcFrameCodec;
|
||||
impl Decoder for JsonRpcFrameCodec {
|
||||
type Item = tokio_util::bytes::Bytes;
|
||||
type Error = tokio::io::Error;
|
||||
fn decode(
|
||||
&mut self,
|
||||
src: &mut tokio_util::bytes::BytesMut,
|
||||
) -> Result<Option<Self::Item>, Self::Error> {
|
||||
if let Some(end) = src
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(idx, &b)| (b == b'\n').then_some(idx))
|
||||
{
|
||||
let line = src.split_to(end);
|
||||
let _char_next_line = src.split_to(1);
|
||||
Ok(Some(line.freeze()))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,2 +1,2 @@
|
|||
pub mod calculator;
|
||||
pub mod counter;
|
||||
pub mod jsonrpc_frame_codec;
|
||||
|
|
|
|||
Loading…
Reference in a new issue