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
14 changed files with 8676 additions and 4466 deletions
44
crates/rmcp/tests/common/calculator.rs
Normal file
44
crates/rmcp/tests/common/calculator.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
1
crates/rmcp/tests/common/mod.rs
Normal file
1
crates/rmcp/tests/common/mod.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
pub mod calculator;
|
||||
63
crates/rmcp/tests/test_complex_schema.rs
Normal file
63
crates/rmcp/tests/test_complex_schema.rs
Normal 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());
|
||||
}
|
||||
1
crates/rmcp/tests/test_tool_handler.rs
Normal file
1
crates/rmcp/tests/test_tool_handler.rs
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
56
crates/rmcp/tests/test_tool_macros.rs
Normal file
56
crates/rmcp/tests/test_tool_macros.rs
Normal 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 {}
|
||||
36
crates/rmcp/tests/test_with_js.rs
Normal file
36
crates/rmcp/tests/test_with_js.rs
Normal 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(())
|
||||
}
|
||||
2
crates/rmcp/tests/test_with_js/.gitignore
vendored
Normal file
2
crates/rmcp/tests/test_with_js/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
/node_modules
|
||||
package-lock.json
|
||||
28
crates/rmcp/tests/test_with_js/client.js
Normal file
28
crates/rmcp/tests/test_with_js/client.js
Normal 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();
|
||||
16
crates/rmcp/tests/test_with_js/package.json
Normal file
16
crates/rmcp/tests/test_with_js/package.json
Normal 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": ""
|
||||
}
|
||||
70
crates/rmcp/tests/test_with_python.rs
Normal file
70
crates/rmcp/tests/test_with_python.rs
Normal 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(())
|
||||
}
|
||||
0
crates/rmcp/tests/test_with_python/.gitignore
vendored
Normal file
0
crates/rmcp/tests/test_with_python/.gitignore
vendored
Normal file
28
crates/rmcp/tests/test_with_python/client.py
Normal file
28
crates/rmcp/tests/test_with_python/client.py
Normal 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())
|
||||
14
crates/rmcp/tests/test_with_python/pyproject.toml
Normal file
14
crates/rmcp/tests/test_with_python/pyproject.toml
Normal 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"]
|
||||
20
crates/rmcp/tests/test_with_python/server.py
Normal file
20
crates/rmcp/tests/test_with_python/server.py
Normal 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()
|
||||
Loading…
Reference in a new issue