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:
4t145 2025-03-28 05:33:41 +08:00 • committed by GitHub
commit 0be6067326
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 8676 additions and 4466 deletions

View file

@ -0,0 +1,44 @@
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, Default)]
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()
}
}
}

View file

@ -0,0 +1 @@
pub mod calculator;

View file

@ -0,0 +1,63 @@
use rmcp::{Error as McpError, model::*, schemars, tool};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub enum ChatRole {
System,
User,
Assistant,
Tool,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ChatMessage {
pub role: ChatRole,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, schemars::JsonSchema)]
pub struct ChatRequest {
pub system: Option<String>,
pub messages: Vec<ChatMessage>,
}
#[derive(Clone, Default)]
pub struct Demo;
#[tool(tool_box)]
impl Demo {
pub fn new() -> Self {
Self
}
#[tool(description = "LLM")]
async fn chat(
&self,
#[tool(aggr)] chat_request: ChatRequest,
) -> Result<CallToolResult, McpError> {
let content = Content::json(chat_request)?;
Ok(CallToolResult::success(vec![content]))
}
}
#[test]
fn test_complex_schema() {
let attr = Demo::chat_tool_attr();
let input_schema = attr.input_schema;
let enum_numer = input_schema
.get("definitions")
.unwrap()
.as_object()
.unwrap()
.get("ChatRole")
.unwrap()
.as_object()
.unwrap()
.get("enum")
.unwrap()
.as_array()
.unwrap()
.len();
assert_eq!(enum_numer, 4);
println!("{}", serde_json::to_string_pretty(&input_schema).unwrap());
}

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,56 @@
use rmcp::{ServerHandler, handler::server::tool::ToolCallContext, tool};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, JsonSchema)]
pub struct GetWeatherRequest {
pub city: String,
pub date: String,
}
impl ServerHandler for Server {
async fn call_tool(
&self,
request: rmcp::model::CallToolRequestParam,
context: rmcp::service::RequestContext<rmcp::RoleServer>,
) -> Result<rmcp::model::CallToolResult, rmcp::Error> {
let tcc = ToolCallContext::new(self, request, context);
match tcc.name() {
"get-weather" => Self::get_weather_tool_call(tcc).await,
_ => Err(rmcp::Error::invalid_params("method not found", None)),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct Server {}
impl Server {
/// This tool is used to get the weather of a city.
#[tool(name = "get-weather", description = "Get the weather of a city.", vis = )]
pub async fn get_weather(&self, #[tool(param)] city: String) -> String {
drop(city);
"rain".to_string()
}
#[tool(description = "Empty Parameter")]
async fn empty_param(&self) {}
}
#[tokio::test]
async fn test_tool_macros() {
let server = Server::default();
let _attr = Server::get_weather_tool_attr();
let _get_weather_call_fn = Server::get_weather_tool_call;
let _get_weather_fn = Server::get_weather;
server.get_weather("harbin".into()).await;
}
#[tokio::test]
async fn test_tool_macros_with_empty_param() {
let _attr = Server::empty_param_tool_attr();
println!("{_attr:?}");
assert_eq!(_attr.input_schema.get("type").unwrap(), "object");
assert!(_attr.input_schema.get("properties").is_none());
}
impl GetWeatherRequest {}

View file

@ -0,0 +1,36 @@
use rmcp::transport::sse_server::SseServer;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
mod common;
use common::calculator::Calculator;
const BIND_ADDRESS: &str = "127.0.0.1:8000";
#[tokio::test]
async fn test_with_js_client() -> anyhow::Result<()> {
tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "debug".to_string().into()),
)
.with(tracing_subscriber::fmt::layer())
.init();
tokio::process::Command::new("npm")
.arg("install")
.current_dir("tests/test_with_js")
.spawn()?
.wait()
.await?;
let ct = SseServer::serve(BIND_ADDRESS.parse()?)
.await?
.with_service(Calculator::default);
let exit_status = tokio::process::Command::new("node")
.arg("tests/test_with_js/client.js")
.spawn()?
.wait()
.await?;
assert!(exit_status.success());
ct.cancel();
Ok(())
}

View file

@ -0,0 +1,2 @@
/node_modules
package-lock.json

View file

@ -0,0 +1,28 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
const transport = new SSEClientTransport( new URL(`http://127.0.0.1:8000/sse`));
const client = new Client(
{
name: "example-client",
version: "1.0.0"
},
{
capabilities: {
prompts: {},
resources: {},
tools: {}
}
}
);
await client.connect(transport);
const tools = await client.listTools();
console.log(tools);
const resources = await client.listResources();
console.log(resources);
const templates = await client.listResourceTemplates();
console.log(templates);
const prompts = await client.listPrompts();
console.log(prompts);
await client.close();

View file

@ -0,0 +1,16 @@
{
"dependencies": {
"@modelcontextprotocol/sdk": "^1.7.0"
},
"type": "module",
"name": "test_with_ts",
"version": "1.0.0",
"main": "index.js",
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"description": ""
}

View file

@ -0,0 +1,70 @@
use rmcp::{
ServiceExt,
transport::{SseServer, TokioChildProcess},
};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
mod common;
use common::calculator::Calculator;
const BIND_ADDRESS: &str = "127.0.0.1:8000";
#[tokio::test]
async fn test_with_python_client() -> anyhow::Result<()> {
let _ = tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "debug".to_string().into()),
)
.with(tracing_subscriber::fmt::layer())
.try_init();
tokio::process::Command::new("uv")
.args(["pip", "install", "-r", "pyproject.toml"])
.current_dir("tests/test_with_python")
.spawn()?
.wait()
.await?;
let ct = SseServer::serve(BIND_ADDRESS.parse()?)
.await?
.with_service(Calculator::default);
let status = tokio::process::Command::new("uv")
.arg("run")
.arg("tests/test_with_python/client.py")
.spawn()?
.wait()
.await?;
assert!(status.success());
ct.cancel();
Ok(())
}
#[tokio::test]
async fn test_with_python_server() -> anyhow::Result<()> {
let _ = tracing_subscriber::registry()
.with(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "debug".to_string().into()),
)
.with(tracing_subscriber::fmt::layer())
.try_init();
tokio::process::Command::new("uv")
.args(["pip", "install", "-r", "pyproject.toml"])
.current_dir("tests/test_with_python")
.spawn()?
.wait()
.await?;
let transport = TokioChildProcess::new(
tokio::process::Command::new("uv")
.arg("run")
.arg("tests/test_with_python/server.py"),
)?;
let client = ().serve(transport).await?;
let resources = client.list_all_resources().await?;
tracing::info!("{:#?}", resources);
let tools = client.list_all_tools().await?;
tracing::info!("{:#?}", tools);
client.cancel().await?;
Ok(())
}

View file

View file

@ -0,0 +1,28 @@
from mcp import ClientSession, StdioServerParameters, types
from mcp.client.sse import sse_client
async def run():
async with sse_client("http://localhost:8000/sse") as (read, write):
async with ClientSession(
read, write
) as session:
# Initialize the connection
await session.initialize()
# List available prompts
prompts = await session.list_prompts()
print(prompts)
# List available resources
resources = await session.list_resources()
print(resources)
# List available tools
tools = await session.list_tools()
print(tools)
if __name__ == "__main__":
import asyncio
asyncio.run(run())

View file

@ -0,0 +1,14 @@
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "test_with_python"
version = "0.1.0"
description = "Test Python client for RMCP"
dependencies = [
"mcp",
]
[tool.setuptools]
py-modules = ["client", "server"]

View file

@ -0,0 +1,20 @@
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Demo")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two numbers"""
return a + b
# Add a dynamic greeting resource
@mcp.resource("greeting://{name}")
def get_greeting(name: str) -> str:
"""Get a personalized greeting"""
return f"Hello, {name}!"
if __name__ == "__main__":
mcp.run()