mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
Remove now unused mcp-server crate (#4773)
This commit is contained in:
Generated
-23
@@ -2775,7 +2775,6 @@ dependencies = [
|
||||
"jsonschema",
|
||||
"mcp-client",
|
||||
"mcp-core",
|
||||
"mcp-server",
|
||||
"nix 0.30.1",
|
||||
"once_cell",
|
||||
"rand 0.8.5",
|
||||
@@ -2829,7 +2828,6 @@ dependencies = [
|
||||
"lopdf",
|
||||
"lru",
|
||||
"mcp-core",
|
||||
"mcp-server",
|
||||
"mpatch",
|
||||
"oauth2",
|
||||
"once_cell",
|
||||
@@ -2887,7 +2885,6 @@ dependencies = [
|
||||
"goose-mcp",
|
||||
"http 1.2.0",
|
||||
"mcp-core",
|
||||
"mcp-server",
|
||||
"reqwest 0.12.12",
|
||||
"rmcp",
|
||||
"schemars",
|
||||
@@ -4110,26 +4107,6 @@ dependencies = [
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mcp-server"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures",
|
||||
"mcp-core",
|
||||
"pin-project",
|
||||
"rmcp",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
"tokio",
|
||||
"tower 0.4.13",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
"tracing-appender",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "md-5"
|
||||
version = "0.10.6"
|
||||
|
||||
@@ -19,7 +19,6 @@ goose = { path = "../goose" }
|
||||
goose-bench = { path = "../goose-bench" }
|
||||
goose-mcp = { path = "../goose-mcp" }
|
||||
mcp-client = { path = "../mcp-client" }
|
||||
mcp-server = { path = "../mcp-server" }
|
||||
mcp-core = { path = "../mcp-core" }
|
||||
rmcp = { workspace = true }
|
||||
agent-client-protocol = "0.4.0"
|
||||
|
||||
@@ -119,8 +119,6 @@ fn setup_logging_internal(
|
||||
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
// Set default levels for different modules
|
||||
EnvFilter::new("")
|
||||
// Set mcp-server module to DEBUG
|
||||
.add_directive("mcp_server=debug".parse().unwrap())
|
||||
// Set mcp-client to DEBUG
|
||||
.add_directive("mcp_client=debug".parse().unwrap())
|
||||
// Set goose module to DEBUG
|
||||
|
||||
@@ -13,7 +13,6 @@ workspace = true
|
||||
[dependencies]
|
||||
goose = { path = "../goose" }
|
||||
mcp-core = { path = "../mcp-core" }
|
||||
mcp-server = { path = "../mcp-server" }
|
||||
rmcp = { version = "0.6.0", features = ["server", "client", "transport-io", "macros"] }
|
||||
anyhow = "1.0.94"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
@@ -14,7 +14,6 @@ workspace = true
|
||||
goose = { path = "../goose" }
|
||||
mcp-core = { path = "../mcp-core" }
|
||||
goose-mcp = { path = "../goose-mcp" }
|
||||
mcp-server = { path = "../mcp-server" }
|
||||
rmcp = { workspace = true }
|
||||
schemars = "1.0"
|
||||
axum = { version = "0.8.1", features = ["ws", "macros"] }
|
||||
|
||||
@@ -57,8 +57,6 @@ pub fn setup_logging(name: Option<&str>) -> Result<()> {
|
||||
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
||||
// Set default levels for different modules
|
||||
EnvFilter::new("")
|
||||
// Set mcp-server module to DEBUG
|
||||
.add_directive("mcp_server=debug".parse().unwrap())
|
||||
// Set mcp-client to DEBUG
|
||||
.add_directive("mcp_client=debug".parse().unwrap())
|
||||
// Set goose module to DEBUG
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
[package]
|
||||
name = "mcp-server"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.94"
|
||||
thiserror = "1.0"
|
||||
mcp-core = { path = "../mcp-core" }
|
||||
rmcp = { workspace = true }
|
||||
serde = { version = "1.0.216", features = ["derive"] }
|
||||
serde_json = "1.0.133"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
tower = { version = "0.4", features = ["timeout"] }
|
||||
tower-service = "0.3"
|
||||
futures = "0.3"
|
||||
pin-project = "1.1"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
tracing-appender = "0.2"
|
||||
@@ -1,7 +0,0 @@
|
||||
### Test with MCP Inspector
|
||||
|
||||
```bash
|
||||
npx @modelcontextprotocol/inspector cargo run -p mcp-server
|
||||
```
|
||||
|
||||
Then visit the Inspector in the browser window and test the different endpoints.
|
||||
@@ -1,106 +0,0 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
pub type BoxError = Box<dyn std::error::Error + Sync + Send>;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum TransportError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("JSON serialization error: {0}")]
|
||||
Json(#[from] serde_json::Error),
|
||||
|
||||
#[error("Invalid UTF-8 sequence: {0}")]
|
||||
Utf8(#[from] std::string::FromUtf8Error),
|
||||
|
||||
#[error("Protocol error: {0}")]
|
||||
Protocol(String),
|
||||
|
||||
#[error("Invalid message format: {0}")]
|
||||
InvalidMessage(String),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ServerError {
|
||||
#[error("Transport error: {0}")]
|
||||
Transport(#[from] TransportError),
|
||||
|
||||
#[error("Service error: {0}")]
|
||||
Service(String),
|
||||
|
||||
#[error("Internal error: {0}")]
|
||||
Internal(String),
|
||||
|
||||
#[error("Request timed out")]
|
||||
Timeout(#[from] tower::timeout::error::Elapsed),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum RouterError {
|
||||
#[error("Method not found: {0}")]
|
||||
MethodNotFound(String),
|
||||
|
||||
#[error("Invalid parameters: {0}")]
|
||||
InvalidParams(String),
|
||||
|
||||
#[error("Internal error: {0}")]
|
||||
Internal(String),
|
||||
|
||||
#[error("Tool not found: {0}")]
|
||||
ToolNotFound(String),
|
||||
|
||||
#[error("Resource not found: {0}")]
|
||||
ResourceNotFound(String),
|
||||
|
||||
#[error("Not found: {0}")]
|
||||
PromptNotFound(String),
|
||||
}
|
||||
|
||||
impl From<RouterError> for rmcp::model::ErrorData {
|
||||
fn from(err: RouterError) -> Self {
|
||||
use rmcp::model::*;
|
||||
match err {
|
||||
RouterError::MethodNotFound(msg) => ErrorData {
|
||||
code: ErrorCode::METHOD_NOT_FOUND,
|
||||
message: Cow::from(msg),
|
||||
data: None,
|
||||
},
|
||||
RouterError::InvalidParams(msg) => ErrorData {
|
||||
code: ErrorCode::INVALID_PARAMS,
|
||||
message: Cow::from(msg),
|
||||
data: None,
|
||||
},
|
||||
RouterError::Internal(msg) => ErrorData {
|
||||
code: ErrorCode::INTERNAL_ERROR,
|
||||
message: Cow::from(msg),
|
||||
data: None,
|
||||
},
|
||||
RouterError::ToolNotFound(msg) => ErrorData {
|
||||
code: ErrorCode::INVALID_REQUEST,
|
||||
message: Cow::from(msg),
|
||||
data: None,
|
||||
},
|
||||
RouterError::ResourceNotFound(msg) => ErrorData {
|
||||
code: ErrorCode::INVALID_REQUEST,
|
||||
message: Cow::from(msg),
|
||||
data: None,
|
||||
},
|
||||
RouterError::PromptNotFound(msg) => ErrorData {
|
||||
code: ErrorCode::INVALID_REQUEST,
|
||||
message: Cow::from(msg),
|
||||
data: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<mcp_core::handler::ResourceError> for RouterError {
|
||||
fn from(err: mcp_core::handler::ResourceError) -> Self {
|
||||
match err {
|
||||
mcp_core::handler::ResourceError::NotFound(msg) => RouterError::ResourceNotFound(msg),
|
||||
_ => RouterError::Internal("Unknown resource error".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,289 +0,0 @@
|
||||
use std::{
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use futures::{Future, Stream};
|
||||
use pin_project::pin_project;
|
||||
use rmcp::model::{
|
||||
ErrorData, JsonRpcError, JsonRpcMessage, JsonRpcResponse, JsonRpcVersion2_0, RequestId,
|
||||
};
|
||||
use router::McpRequest;
|
||||
use tokio::{
|
||||
io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader},
|
||||
sync::mpsc,
|
||||
};
|
||||
use tower_service::Service;
|
||||
|
||||
mod errors;
|
||||
pub use errors::{BoxError, RouterError, ServerError, TransportError};
|
||||
|
||||
pub mod router;
|
||||
pub use router::Router;
|
||||
|
||||
/// A transport layer that handles JSON-RPC messages over byte
|
||||
#[pin_project]
|
||||
pub struct ByteTransport<R, W> {
|
||||
// Reader is a BufReader on the underlying stream (stdin or similar) buffering
|
||||
// the underlying data across poll calls, we clear one line (\n) during each
|
||||
// iteration of poll_next from this buffer
|
||||
#[pin]
|
||||
reader: BufReader<R>,
|
||||
#[pin]
|
||||
writer: W,
|
||||
}
|
||||
|
||||
impl<R, W> ByteTransport<R, W>
|
||||
where
|
||||
R: AsyncRead,
|
||||
W: AsyncWrite,
|
||||
{
|
||||
pub fn new(reader: R, writer: W) -> Self {
|
||||
Self {
|
||||
// Default BufReader capacity is 8 * 1024, increase this to 2MB to the file size limit
|
||||
// allows the buffer to have the capacity to read very large calls
|
||||
reader: BufReader::with_capacity(2 * 1024 * 1024, reader),
|
||||
writer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, W> Stream for ByteTransport<R, W>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
type Item = Result<JsonRpcMessage, TransportError>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let mut this = self.project();
|
||||
let mut buf = Vec::new();
|
||||
|
||||
let mut reader = this.reader.as_mut();
|
||||
let mut read_future = Box::pin(reader.read_until(b'\n', &mut buf));
|
||||
match read_future.as_mut().poll(cx) {
|
||||
Poll::Ready(Ok(0)) => Poll::Ready(None), // EOF
|
||||
Poll::Ready(Ok(_)) => {
|
||||
// Convert to UTF-8 string
|
||||
let line = match String::from_utf8(buf) {
|
||||
Ok(s) => s,
|
||||
Err(e) => return Poll::Ready(Some(Err(TransportError::Utf8(e)))),
|
||||
};
|
||||
// Log incoming message here before serde conversion to
|
||||
// track incomplete chunks which are not valid JSON
|
||||
tracing::info!(json = %line, "incoming message");
|
||||
|
||||
// Parse JSON and validate message format
|
||||
match serde_json::from_str::<serde_json::Value>(&line) {
|
||||
Ok(value) => {
|
||||
// Validate basic JSON-RPC structure
|
||||
if !value.is_object() {
|
||||
return Poll::Ready(Some(Err(TransportError::InvalidMessage(
|
||||
"Message must be a JSON object".into(),
|
||||
))));
|
||||
}
|
||||
let obj = value.as_object().unwrap(); // Safe due to check above
|
||||
|
||||
// Check jsonrpc version field
|
||||
if !obj.contains_key("jsonrpc") || obj["jsonrpc"] != "2.0" {
|
||||
return Poll::Ready(Some(Err(TransportError::InvalidMessage(
|
||||
"Missing or invalid jsonrpc version".into(),
|
||||
))));
|
||||
}
|
||||
|
||||
// Now try to parse as proper message
|
||||
match serde_json::from_value::<JsonRpcMessage>(value) {
|
||||
Ok(msg) => Poll::Ready(Some(Ok(msg))),
|
||||
Err(e) => Poll::Ready(Some(Err(TransportError::Json(e)))),
|
||||
}
|
||||
}
|
||||
Err(e) => Poll::Ready(Some(Err(TransportError::Json(e)))),
|
||||
}
|
||||
}
|
||||
Poll::Ready(Err(e)) => Poll::Ready(Some(Err(TransportError::Io(e)))),
|
||||
Poll::Pending => Poll::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R, W> ByteTransport<R, W>
|
||||
where
|
||||
R: AsyncRead + Unpin,
|
||||
W: AsyncWrite + Unpin,
|
||||
{
|
||||
pub async fn write_message(&mut self, msg: JsonRpcMessage) -> Result<(), std::io::Error> {
|
||||
let json = serde_json::to_string(&msg)?;
|
||||
Pin::new(&mut self.writer)
|
||||
.write_all(json.as_bytes())
|
||||
.await?;
|
||||
Pin::new(&mut self.writer).write_all(b"\n").await?;
|
||||
Pin::new(&mut self.writer).flush().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// The main server type that processes incoming requests
|
||||
pub struct Server<S> {
|
||||
service: S,
|
||||
}
|
||||
|
||||
impl<S> Server<S>
|
||||
where
|
||||
S: Service<McpRequest, Response = JsonRpcResponse> + Send,
|
||||
S::Error: Into<BoxError>,
|
||||
S::Future: Send,
|
||||
{
|
||||
pub fn new(service: S) -> Self {
|
||||
Self { service }
|
||||
}
|
||||
|
||||
// TODO transport trait instead of byte transport if we implement others
|
||||
pub async fn run<R, W>(self, mut transport: ByteTransport<R, W>) -> Result<(), ServerError>
|
||||
where
|
||||
R: AsyncRead + Unpin + Send + 'static,
|
||||
W: AsyncWrite + Unpin + Send + 'static,
|
||||
{
|
||||
use futures::StreamExt;
|
||||
let mut service = self.service;
|
||||
|
||||
tracing::info!("Server started");
|
||||
while let Some(msg_result) = transport.next().await {
|
||||
let _span = tracing::span!(tracing::Level::INFO, "message_processing").entered();
|
||||
match msg_result {
|
||||
Ok(msg) => {
|
||||
match msg {
|
||||
JsonRpcMessage::Request(request) => {
|
||||
let request_json = serde_json::to_string(&request)
|
||||
.unwrap_or_else(|_| "Failed to serialize request".to_string());
|
||||
|
||||
tracing::info!(
|
||||
method = ?request.request.method,
|
||||
json = %request_json,
|
||||
"Received request"
|
||||
);
|
||||
|
||||
// Process the request using our service
|
||||
let (notify_tx, mut notify_rx) = mpsc::channel(256);
|
||||
let mcp_request = McpRequest {
|
||||
request,
|
||||
notifier: notify_tx,
|
||||
};
|
||||
|
||||
let transport_fut = tokio::spawn(async move {
|
||||
while let Some(notification) = notify_rx.recv().await {
|
||||
if transport.write_message(notification).await.is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
transport
|
||||
});
|
||||
|
||||
let response = match service.call(mcp_request).await {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => {
|
||||
let error_msg = e.into().to_string();
|
||||
tracing::error!(error = %error_msg, "Request processing failed");
|
||||
|
||||
// Return an error response instead of a regular response
|
||||
return Err(ServerError::Transport(TransportError::Protocol(
|
||||
error_msg,
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
transport = match transport_fut.await {
|
||||
Ok(transport) => transport,
|
||||
Err(e) => {
|
||||
tracing::error!(error = %e, "Failed to spawn transport task");
|
||||
return Err(ServerError::Transport(TransportError::Io(
|
||||
e.into(),
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
// Serialize response for logging
|
||||
let response_json = serde_json::to_string(&response)
|
||||
.unwrap_or_else(|_| "Failed to serialize response".to_string());
|
||||
|
||||
tracing::info!(
|
||||
response_id = ?response.id,
|
||||
json = %response_json,
|
||||
"Sending response"
|
||||
);
|
||||
// Send the response back
|
||||
if let Err(e) = transport
|
||||
.write_message(JsonRpcMessage::Response(response))
|
||||
.await
|
||||
{
|
||||
return Err(ServerError::Transport(TransportError::Io(e)));
|
||||
}
|
||||
}
|
||||
JsonRpcMessage::Response(_)
|
||||
| JsonRpcMessage::Notification(_)
|
||||
| JsonRpcMessage::Error(_) => {
|
||||
// Ignore responses, notifications, batch messages and error messages for now
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
// Convert transport error to JSON-RPC error response
|
||||
let error_data = match e {
|
||||
TransportError::Json(_) | TransportError::InvalidMessage(_) => ErrorData {
|
||||
code: rmcp::model::ErrorCode::PARSE_ERROR,
|
||||
message: e.to_string().into(),
|
||||
data: None,
|
||||
},
|
||||
TransportError::Protocol(_) => ErrorData {
|
||||
code: rmcp::model::ErrorCode::INVALID_REQUEST,
|
||||
message: e.to_string().into(),
|
||||
data: None,
|
||||
},
|
||||
_ => ErrorData {
|
||||
code: rmcp::model::ErrorCode::INTERNAL_ERROR,
|
||||
message: e.to_string().into(),
|
||||
data: None,
|
||||
},
|
||||
};
|
||||
|
||||
let error_response = JsonRpcMessage::Error(JsonRpcError {
|
||||
jsonrpc: JsonRpcVersion2_0,
|
||||
id: RequestId::Number(0), // Use a default ID for transport errors
|
||||
error: error_data,
|
||||
});
|
||||
|
||||
if let Err(e) = transport.write_message(error_response).await {
|
||||
return Err(ServerError::Transport(TransportError::Io(e)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// Define a specific service implementation that we need for any
|
||||
// Any router implements this
|
||||
pub trait BoundedService:
|
||||
Service<
|
||||
McpRequest,
|
||||
Response = JsonRpcResponse,
|
||||
Error = BoxError,
|
||||
Future = Pin<Box<dyn Future<Output = Result<JsonRpcResponse, BoxError>> + Send>>,
|
||||
> + Send
|
||||
+ 'static
|
||||
{
|
||||
}
|
||||
|
||||
// Implement it for any type that meets the bounds
|
||||
impl<T> BoundedService for T where
|
||||
T: Service<
|
||||
McpRequest,
|
||||
Response = JsonRpcResponse,
|
||||
Error = BoxError,
|
||||
Future = Pin<Box<dyn Future<Output = Result<JsonRpcResponse, BoxError>> + Send>>,
|
||||
> + Send
|
||||
+ 'static
|
||||
{
|
||||
}
|
||||
@@ -1,248 +0,0 @@
|
||||
use anyhow::Result;
|
||||
use mcp_core::handler::{PromptError, ResourceError};
|
||||
use mcp_core::protocol::ServerCapabilities;
|
||||
use mcp_server::router::{CapabilitiesBuilder, RouterService};
|
||||
use mcp_server::{ByteTransport, Router, Server};
|
||||
use rmcp::model::{
|
||||
Content, ErrorCode, ErrorData, JsonRpcMessage, Prompt, PromptArgument, RawResource, Resource,
|
||||
Tool, ToolAnnotations,
|
||||
};
|
||||
use rmcp::object;
|
||||
use serde_json::Value;
|
||||
use std::borrow::Cow;
|
||||
use std::{future::Future, pin::Pin, sync::Arc};
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::{
|
||||
io::{stdin, stdout},
|
||||
sync::Mutex,
|
||||
};
|
||||
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||
use tracing_subscriber::{self, EnvFilter};
|
||||
|
||||
// A simple counter service that demonstrates the Router trait
|
||||
#[derive(Clone)]
|
||||
struct CounterRouter {
|
||||
counter: Arc<Mutex<i32>>,
|
||||
}
|
||||
|
||||
impl CounterRouter {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
counter: Arc::new(Mutex::new(0)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn increment(&self) -> Result<i32, ErrorData> {
|
||||
let mut counter = self.counter.lock().await;
|
||||
*counter += 1;
|
||||
Ok(*counter)
|
||||
}
|
||||
|
||||
async fn decrement(&self) -> Result<i32, ErrorData> {
|
||||
let mut counter = self.counter.lock().await;
|
||||
*counter -= 1;
|
||||
Ok(*counter)
|
||||
}
|
||||
|
||||
async fn get_value(&self) -> Result<i32, ErrorData> {
|
||||
let counter = self.counter.lock().await;
|
||||
Ok(*counter)
|
||||
}
|
||||
|
||||
fn _create_resource_text(&self, uri: &str, name: &str) -> Resource {
|
||||
Resource::new(RawResource::new(uri, name), None)
|
||||
}
|
||||
}
|
||||
|
||||
impl Router for CounterRouter {
|
||||
fn name(&self) -> String {
|
||||
"counter".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(),
|
||||
object!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}),
|
||||
)
|
||||
.annotate(ToolAnnotations {
|
||||
title: Some("Increment Tool".to_string()),
|
||||
read_only_hint: Some(false),
|
||||
destructive_hint: Some(false),
|
||||
idempotent_hint: Some(false),
|
||||
open_world_hint: Some(false),
|
||||
}),
|
||||
Tool::new(
|
||||
"decrement".to_string(),
|
||||
"Decrement the counter by 1".to_string(),
|
||||
object!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}),
|
||||
)
|
||||
.annotate(ToolAnnotations {
|
||||
title: Some("Decrement Tool".to_string()),
|
||||
read_only_hint: Some(false),
|
||||
destructive_hint: Some(false),
|
||||
idempotent_hint: Some(false),
|
||||
open_world_hint: Some(false),
|
||||
}),
|
||||
Tool::new(
|
||||
"get_value".to_string(),
|
||||
"Get the current counter value".to_string(),
|
||||
object!({
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": []
|
||||
}),
|
||||
)
|
||||
.annotate(ToolAnnotations {
|
||||
title: Some("Get Value Tool".to_string()),
|
||||
read_only_hint: Some(true),
|
||||
destructive_hint: Some(false),
|
||||
idempotent_hint: Some(false),
|
||||
open_world_hint: Some(false),
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
fn call_tool(
|
||||
&self,
|
||||
tool_name: &str,
|
||||
_arguments: Value,
|
||||
_notifier: mpsc::Sender<JsonRpcMessage>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ErrorData>> + 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(ErrorData {
|
||||
code: ErrorCode::INVALID_REQUEST,
|
||||
message: Cow::from(format!("Tool {} not found", tool_name)),
|
||||
data: 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"),
|
||||
]
|
||||
}
|
||||
|
||||
fn read_resource(
|
||||
&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
|
||||
))),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn list_prompts(&self) -> Vec<Prompt> {
|
||||
vec![Prompt::new(
|
||||
"example_prompt",
|
||||
Some("This is an example prompt that takes one required argument, 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]
|
||||
async fn main() -> Result<()> {
|
||||
// Set up file appender for logging
|
||||
let file_appender = RollingFileAppender::new(Rotation::DAILY, "logs", "mcp-server.log");
|
||||
|
||||
// Initialize the tracing subscriber with file and stdout logging
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(EnvFilter::from_default_env().add_directive(tracing::Level::INFO.into()))
|
||||
.with_writer(file_appender)
|
||||
.with_target(false)
|
||||
.with_thread_ids(true)
|
||||
.with_file(true)
|
||||
.with_line_number(true)
|
||||
.init();
|
||||
|
||||
tracing::info!("Starting MCP server");
|
||||
|
||||
// Create an instance of our counter router
|
||||
let router = RouterService(CounterRouter::new());
|
||||
|
||||
// Create and run the server
|
||||
let server = Server::new(router);
|
||||
let transport = ByteTransport::new(stdin(), stdout());
|
||||
|
||||
tracing::info!("Server initialized and ready to handle requests");
|
||||
Ok(server.run(transport).await?)
|
||||
}
|
||||
@@ -1,425 +0,0 @@
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
type PromptFuture = Pin<Box<dyn Future<Output = Result<String, PromptError>> + Send + 'static>>;
|
||||
use mcp_core::{
|
||||
handler::{PromptError, ResourceError},
|
||||
protocol::{
|
||||
CallToolResult, Implementation, InitializeResult, ListPromptsResult, ListResourcesResult,
|
||||
ListToolsResult, PromptsCapability, ReadResourceResult, ResourcesCapability,
|
||||
ServerCapabilities, ToolsCapability,
|
||||
},
|
||||
};
|
||||
use rmcp::model::{
|
||||
Content, ErrorData, GetPromptResult, JsonRpcMessage, JsonRpcRequest, JsonRpcResponse,
|
||||
JsonRpcVersion2_0, Prompt, PromptMessage, PromptMessageRole, RequestId, Resource,
|
||||
ResourceContents,
|
||||
};
|
||||
use serde_json::Value;
|
||||
use tokio::sync::mpsc;
|
||||
use tower_service::Service;
|
||||
|
||||
use crate::{BoxError, RouterError};
|
||||
|
||||
/// Builder for configuring and constructing capabilities
|
||||
pub struct CapabilitiesBuilder {
|
||||
tools: Option<ToolsCapability>,
|
||||
prompts: Option<PromptsCapability>,
|
||||
resources: Option<ResourcesCapability>,
|
||||
}
|
||||
|
||||
impl Default for CapabilitiesBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl CapabilitiesBuilder {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
tools: None,
|
||||
prompts: None,
|
||||
resources: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Add multiple tools to the router
|
||||
pub fn with_tools(mut self, list_changed: bool) -> Self {
|
||||
self.tools = Some(ToolsCapability {
|
||||
list_changed: Some(list_changed),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable prompts capability
|
||||
pub fn with_prompts(mut self, list_changed: bool) -> Self {
|
||||
self.prompts = Some(PromptsCapability {
|
||||
list_changed: Some(list_changed),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable resources capability
|
||||
pub fn with_resources(mut self, subscribe: bool, list_changed: bool) -> Self {
|
||||
self.resources = Some(ResourcesCapability {
|
||||
subscribe: Some(subscribe),
|
||||
list_changed: Some(list_changed),
|
||||
});
|
||||
self
|
||||
}
|
||||
|
||||
/// Build the router with automatic capability inference
|
||||
pub fn build(self) -> ServerCapabilities {
|
||||
// Create capabilities based on what's configured
|
||||
ServerCapabilities {
|
||||
tools: self.tools,
|
||||
prompts: self.prompts,
|
||||
resources: self.resources,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait Router: Send + Sync + 'static {
|
||||
fn name(&self) -> String;
|
||||
// in the protocol, instructions are optional but we make it required
|
||||
fn instructions(&self) -> String;
|
||||
fn capabilities(&self) -> ServerCapabilities;
|
||||
fn list_tools(&self) -> Vec<rmcp::model::Tool>;
|
||||
fn call_tool(
|
||||
&self,
|
||||
tool_name: &str,
|
||||
arguments: Value,
|
||||
notifier: mpsc::Sender<JsonRpcMessage>,
|
||||
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ErrorData>> + Send + 'static>>;
|
||||
fn list_resources(&self) -> Vec<Resource>;
|
||||
fn read_resource(
|
||||
&self,
|
||||
uri: &str,
|
||||
) -> Pin<Box<dyn Future<Output = Result<String, ResourceError>> + Send + 'static>>;
|
||||
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: RequestId) -> JsonRpcResponse {
|
||||
JsonRpcResponse {
|
||||
jsonrpc: JsonRpcVersion2_0,
|
||||
id,
|
||||
result: serde_json::Map::new(),
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to set result on response
|
||||
fn set_result<T: serde::Serialize>(
|
||||
&self,
|
||||
response: &mut JsonRpcResponse,
|
||||
result: T,
|
||||
) -> Result<(), RouterError> {
|
||||
let value = serde_json::to_value(result)
|
||||
.map_err(|e| RouterError::Internal(format!("JSON serialization error: {}", e)))?;
|
||||
|
||||
if let Some(obj) = value.as_object() {
|
||||
response.result = obj.clone();
|
||||
} else {
|
||||
return Err(RouterError::Internal("Result must be a JSON object".into()));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn handle_initialize(
|
||||
&self,
|
||||
req: JsonRpcRequest,
|
||||
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
|
||||
async move {
|
||||
let result = InitializeResult {
|
||||
protocol_version: "2025-03-26".to_string(),
|
||||
capabilities: self.capabilities().clone(),
|
||||
server_info: Implementation {
|
||||
name: self.name(),
|
||||
version: env!("CARGO_PKG_VERSION").to_string(),
|
||||
},
|
||||
instructions: Some(self.instructions()),
|
||||
};
|
||||
|
||||
let mut response = self.create_response(req.id);
|
||||
self.set_result(&mut response, result)?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tools_list(
|
||||
&self,
|
||||
req: JsonRpcRequest,
|
||||
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
|
||||
async move {
|
||||
let tools = self.list_tools();
|
||||
|
||||
let result = ListToolsResult {
|
||||
tools,
|
||||
next_cursor: None,
|
||||
};
|
||||
let mut response = self.create_response(req.id);
|
||||
self.set_result(&mut response, result)?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tools_call(
|
||||
&self,
|
||||
req: JsonRpcRequest,
|
||||
notifier: mpsc::Sender<JsonRpcMessage>,
|
||||
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
|
||||
async move {
|
||||
let params = &req.request.params;
|
||||
|
||||
let name = params
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| RouterError::InvalidParams("Missing tool name".into()))?;
|
||||
|
||||
let arguments = params.get("arguments").cloned().unwrap_or(Value::Null);
|
||||
|
||||
let result = match self.call_tool(name, arguments, notifier).await {
|
||||
Ok(result) => CallToolResult {
|
||||
content: result,
|
||||
is_error: None,
|
||||
},
|
||||
Err(err) => CallToolResult {
|
||||
content: vec![Content::text(err.to_string())],
|
||||
is_error: Some(true),
|
||||
},
|
||||
};
|
||||
|
||||
let mut response = self.create_response(req.id);
|
||||
self.set_result(&mut response, result)?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_resources_list(
|
||||
&self,
|
||||
req: JsonRpcRequest,
|
||||
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
|
||||
async move {
|
||||
let resources = self.list_resources();
|
||||
|
||||
let result = ListResourcesResult {
|
||||
resources,
|
||||
next_cursor: None,
|
||||
};
|
||||
let mut response = self.create_response(req.id);
|
||||
self.set_result(&mut response, result)?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_resources_read(
|
||||
&self,
|
||||
req: JsonRpcRequest,
|
||||
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
|
||||
async move {
|
||||
let params = &req.request.params;
|
||||
|
||||
let uri = params
|
||||
.get("uri")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| RouterError::InvalidParams("Missing resource URI".into()))?;
|
||||
|
||||
let contents = self.read_resource(uri).await.map_err(RouterError::from)?;
|
||||
|
||||
let result = ReadResourceResult {
|
||||
contents: vec![ResourceContents::TextResourceContents {
|
||||
uri: uri.to_string(),
|
||||
mime_type: Some("text/plain".to_string()),
|
||||
text: contents,
|
||||
meta: None,
|
||||
}],
|
||||
};
|
||||
|
||||
let mut response = self.create_response(req.id);
|
||||
self.set_result(&mut response, result)?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_prompts_list(
|
||||
&self,
|
||||
req: JsonRpcRequest,
|
||||
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
|
||||
async move {
|
||||
let prompts = self.list_prompts();
|
||||
|
||||
let result = ListPromptsResult { prompts };
|
||||
|
||||
let mut response = self.create_response(req.id);
|
||||
self.set_result(&mut response, result)?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_prompts_get(
|
||||
&self,
|
||||
req: JsonRpcRequest,
|
||||
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
|
||||
async move {
|
||||
// Validate and extract parameters
|
||||
let params = &req.request.params;
|
||||
|
||||
// Extract "name" field
|
||||
let prompt_name = params
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| RouterError::InvalidParams("Missing prompt name".into()))?;
|
||||
|
||||
// Extract "arguments" field
|
||||
let arguments = params
|
||||
.get("arguments")
|
||||
.and_then(Value::as_object)
|
||||
.ok_or_else(|| RouterError::InvalidParams("Missing arguments object".into()))?;
|
||||
|
||||
// Fetch the prompt definition first
|
||||
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
|
||||
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)
|
||||
.await
|
||||
.map_err(|e| RouterError::Internal(e.to_string()))?;
|
||||
|
||||
// Validate prompt arguments for potential security issues from user text input
|
||||
// Checks:
|
||||
// - Prompt must be less than 10000 total characters
|
||||
// - Argument keys must be less than 1000 characters
|
||||
// - Argument values must be less than 1000 characters
|
||||
// - Dangerous patterns, eg "../", "//", "\\\\", "<script>", "{{", "}}"
|
||||
for (key, value) in arguments.iter() {
|
||||
// Check for empty or overly long keys/values
|
||||
if key.is_empty() || key.len() > 1000 {
|
||||
return Err(RouterError::InvalidParams(
|
||||
"Argument keys must be between 1-1000 characters".into(),
|
||||
));
|
||||
}
|
||||
|
||||
let value_str = value.as_str().unwrap_or_default();
|
||||
if value_str.len() > 1000 {
|
||||
return Err(RouterError::InvalidParams(
|
||||
"Argument values must not exceed 1000 characters".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Check for potentially dangerous patterns
|
||||
let dangerous_patterns = ["../", "//", "\\\\", "<script>", "{{", "}}"];
|
||||
for pattern in dangerous_patterns {
|
||||
if key.contains(pattern) || value_str.contains(pattern) {
|
||||
return Err(RouterError::InvalidParams(format!(
|
||||
"Arguments contain potentially unsafe pattern: {}",
|
||||
pattern
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Validate the prompt description length
|
||||
if description.len() > 10000 {
|
||||
return Err(RouterError::Internal(
|
||||
"Prompt description exceeds maximum allowed length".into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Create a mutable copy of the description to fill in arguments
|
||||
let mut description_filled = description.clone();
|
||||
|
||||
// Replace each argument placeholder with its value from the arguments object
|
||||
for (key, value) in arguments {
|
||||
let placeholder = format!("{{{}}}", key);
|
||||
description_filled =
|
||||
description_filled.replace(&placeholder, value.as_str().unwrap_or_default());
|
||||
}
|
||||
|
||||
let messages = vec![PromptMessage::new_text(
|
||||
PromptMessageRole::User,
|
||||
description_filled.to_string(),
|
||||
)];
|
||||
|
||||
// Build the final response
|
||||
let mut response = self.create_response(req.id);
|
||||
let result = GetPromptResult {
|
||||
description: Some(description_filled),
|
||||
messages,
|
||||
};
|
||||
self.set_result(&mut response, result)?;
|
||||
Ok(response)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RouterService<T>(pub T);
|
||||
|
||||
pub struct McpRequest {
|
||||
pub request: JsonRpcRequest,
|
||||
pub notifier: mpsc::Sender<JsonRpcMessage>,
|
||||
}
|
||||
|
||||
impl<T> Service<McpRequest> for RouterService<T>
|
||||
where
|
||||
T: Router + Clone + Send + Sync + 'static,
|
||||
{
|
||||
type Response = JsonRpcResponse;
|
||||
type Error = BoxError;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
||||
|
||||
fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
|
||||
fn call(&mut self, req: McpRequest) -> Self::Future {
|
||||
let this = self.0.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
let result = match req.request.request.method.as_str() {
|
||||
"initialize" => this.handle_initialize(req.request).await,
|
||||
"tools/list" => this.handle_tools_list(req.request).await,
|
||||
"tools/call" => this.handle_tools_call(req.request, req.notifier).await,
|
||||
"resources/list" => this.handle_resources_list(req.request).await,
|
||||
"resources/read" => this.handle_resources_read(req.request).await,
|
||||
"prompts/list" => this.handle_prompts_list(req.request).await,
|
||||
"prompts/get" => this.handle_prompts_get(req.request).await,
|
||||
_ => {
|
||||
return Err(
|
||||
RouterError::MethodNotFound(req.request.request.method.clone()).into(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
result.map_err(BoxError::from)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user