Bring in mcp-* crates from goose as a starting point

This commit is contained in:
Alex Hancock 2025-02-20 13:15:20 -05:00
commit 4554644a3a
6 changed files with 3926 additions and 0 deletions

View file

@ -0,0 +1,22 @@
[package]
name = "mcp-server"
version = "0.1.0"
edition = "2021"
[dependencies]
anyhow = "1.0.94"
thiserror = "1.0"
mcp-core = { path = "../mcp-core" }
mcp-macros = { path = "../mcp-macros" }
serde = { version = "1.0.216", features = ["derive"] }
serde_json = "1.0.133"
schemars = "0.8"
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"
async-trait = "0.1"

View file

@ -0,0 +1,7 @@
### 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.

View file

@ -0,0 +1,104 @@
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 mcp_core::protocol::ErrorData {
fn from(err: RouterError) -> Self {
use mcp_core::protocol::*;
match err {
RouterError::MethodNotFound(msg) => ErrorData {
code: METHOD_NOT_FOUND,
message: msg,
data: None,
},
RouterError::InvalidParams(msg) => ErrorData {
code: INVALID_PARAMS,
message: msg,
data: None,
},
RouterError::Internal(msg) => ErrorData {
code: INTERNAL_ERROR,
message: msg,
data: None,
},
RouterError::ToolNotFound(msg) => ErrorData {
code: INVALID_REQUEST,
message: msg,
data: None,
},
RouterError::ResourceNotFound(msg) => ErrorData {
code: INVALID_REQUEST,
message: msg,
data: None,
},
RouterError::PromptNotFound(msg) => ErrorData {
code: INVALID_REQUEST,
message: 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()),
}
}
}

View file

@ -0,0 +1,269 @@
use std::{
pin::Pin,
task::{Context, Poll},
};
use futures::{Future, Stream};
use mcp_core::protocol::{JsonRpcError, JsonRpcMessage, JsonRpcRequest, JsonRpcResponse};
use pin_project::pin_project;
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncWrite, AsyncWriteExt, BufReader};
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<JsonRpcRequest, 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,
W: AsyncWrite + Unpin,
{
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) => {
// Serialize request for logging
let id = request.id;
let request_json = serde_json::to_string(&request)
.unwrap_or_else(|_| "Failed to serialize request".to_string());
tracing::info!(
request_id = ?id,
method = ?request.method,
json = %request_json,
"Received request"
);
// Process the request using our service
let response = match service.call(request).await {
Ok(resp) => resp,
Err(e) => {
let error_msg = e.into().to_string();
tracing::error!(error = %error_msg, "Request processing failed");
JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: None,
error: Some(mcp_core::protocol::ErrorData {
code: mcp_core::protocol::INTERNAL_ERROR,
message: error_msg,
data: None,
}),
}
}
};
// 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::Nil
| JsonRpcMessage::Error(_) => {
// Ignore responses, notifications and nil messages for now
continue;
}
}
}
Err(e) => {
// Convert transport error to JSON-RPC error response
let error = match e {
TransportError::Json(_) | TransportError::InvalidMessage(_) => {
mcp_core::protocol::ErrorData {
code: mcp_core::protocol::PARSE_ERROR,
message: e.to_string(),
data: None,
}
}
TransportError::Protocol(_) => mcp_core::protocol::ErrorData {
code: mcp_core::protocol::INVALID_REQUEST,
message: e.to_string(),
data: None,
},
_ => mcp_core::protocol::ErrorData {
code: mcp_core::protocol::INTERNAL_ERROR,
message: e.to_string(),
data: None,
},
};
let error_response = JsonRpcMessage::Error(JsonRpcError {
jsonrpc: "2.0".to_string(),
id: None,
error,
});
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<
JsonRpcRequest,
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<
JsonRpcRequest,
Response = JsonRpcResponse,
Error = BoxError,
Future = Pin<Box<dyn Future<Output = Result<JsonRpcResponse, BoxError>> + Send>>,
> + Send
+ 'static
{
}

View file

@ -0,0 +1,184 @@
use anyhow::Result;
use mcp_core::content::Content;
use mcp_core::handler::ResourceError;
use mcp_core::{handler::ToolError, protocol::ServerCapabilities, resource::Resource, tool::Tool};
use mcp_server::router::{CapabilitiesBuilder, RouterService};
use mcp_server::{ByteTransport, Router, Server};
use serde_json::Value;
use std::{future::Future, pin::Pin, sync::Arc};
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, ToolError> {
let mut counter = self.counter.lock().await;
*counter += 1;
Ok(*counter)
}
async fn decrement(&self) -> Result<i32, ToolError> {
let mut counter = self.counter.lock().await;
*counter -= 1;
Ok(*counter)
}
async fn get_value(&self) -> Result<i32, ToolError> {
let counter = self.counter.lock().await;
Ok(*counter)
}
fn _create_resource_text(&self, uri: &str, name: &str) -> Resource {
Resource::new(uri, Some("text/plain".to_string()), Some(name.to_string())).unwrap()
}
}
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)
.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(
&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))),
}
})
}
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
))),
}
})
}
}
#[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?)
}

View file

@ -0,0 +1,435 @@
use std::{
future::Future,
pin::Pin,
task::{Context, Poll},
};
type PromptFuture = Pin<Box<dyn Future<Output = Result<String, PromptError>> + Send + 'static>>;
use mcp_core::{
content::Content,
handler::{PromptError, ResourceError, ToolError},
prompt::{Prompt, PromptMessage, PromptMessageRole},
protocol::{
CallToolResult, GetPromptResult, Implementation, InitializeResult, JsonRpcRequest,
JsonRpcResponse, ListPromptsResult, ListResourcesResult, ListToolsResult,
PromptsCapability, ReadResourceResult, ResourcesCapability, ServerCapabilities,
ToolsCapability,
},
ResourceContents,
};
use serde_json::Value;
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<mcp_core::tool::Tool>;
fn call_tool(
&self,
tool_name: &str,
arguments: Value,
) -> Pin<Box<dyn Future<Output = Result<Vec<Content>, ToolError>> + Send + 'static>>;
fn list_resources(&self) -> Vec<mcp_core::resource::Resource>;
fn read_resource(
&self,
uri: &str,
) -> Pin<Box<dyn Future<Output = Result<String, ResourceError>> + Send + 'static>>;
fn list_prompts(&self) -> Option<Vec<Prompt>> {
None
}
fn get_prompt(&self, _prompt_name: &str) -> Option<PromptFuture> {
None
}
// Helper method to create base response
fn create_response(&self, id: Option<u64>) -> JsonRpcResponse {
JsonRpcResponse {
jsonrpc: "2.0".to_string(),
id,
result: None,
error: None,
}
}
fn handle_initialize(
&self,
req: JsonRpcRequest,
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
async move {
let result = InitializeResult {
protocol_version: "2024-11-05".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);
response.result =
Some(serde_json::to_value(result).map_err(|e| {
RouterError::Internal(format!("JSON serialization error: {}", e))
})?);
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);
response.result =
Some(serde_json::to_value(result).map_err(|e| {
RouterError::Internal(format!("JSON serialization error: {}", e))
})?);
Ok(response)
}
}
fn handle_tools_call(
&self,
req: JsonRpcRequest,
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
async move {
let params = req
.params
.ok_or_else(|| RouterError::InvalidParams("Missing parameters".into()))?;
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).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);
response.result =
Some(serde_json::to_value(result).map_err(|e| {
RouterError::Internal(format!("JSON serialization error: {}", e))
})?);
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);
response.result =
Some(serde_json::to_value(result).map_err(|e| {
RouterError::Internal(format!("JSON serialization error: {}", e))
})?);
Ok(response)
}
}
fn handle_resources_read(
&self,
req: JsonRpcRequest,
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
async move {
let params = req
.params
.ok_or_else(|| RouterError::InvalidParams("Missing parameters".into()))?;
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,
}],
};
let mut response = self.create_response(req.id);
response.result =
Some(serde_json::to_value(result).map_err(|e| {
RouterError::Internal(format!("JSON serialization error: {}", e))
})?);
Ok(response)
}
}
fn handle_prompts_list(
&self,
req: JsonRpcRequest,
) -> impl Future<Output = Result<JsonRpcResponse, RouterError>> + Send {
async move {
let prompts = self.list_prompts().unwrap_or_default();
let result = ListPromptsResult { prompts };
let mut response = self.create_response(req.id);
response.result =
Some(serde_json::to_value(result).map_err(|e| {
RouterError::Internal(format!("JSON serialization error: {}", e))
})?);
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
.params
.ok_or_else(|| RouterError::InvalidParams("Missing parameters".into()))?;
// 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 = match self.list_prompts() {
Some(prompts) => prompts
.into_iter()
.find(|p| p.name == prompt_name)
.ok_or_else(|| {
RouterError::PromptNotFound(format!("Prompt '{}' not found", prompt_name))
})?,
None => return Err(RouterError::PromptNotFound("No prompts available".into())),
};
// Validate required arguments
for arg in &prompt.arguments {
if arg.required
&& (!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)
.ok_or_else(|| RouterError::PromptNotFound("Prompt not found".into()))?
.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);
response.result = Some(
serde_json::to_value(GetPromptResult {
description: Some(description_filled),
messages,
})
.map_err(|e| RouterError::Internal(format!("JSON serialization error: {}", e)))?,
);
Ok(response)
}
}
}
pub struct RouterService<T>(pub T);
impl<T> Service<JsonRpcRequest> 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: JsonRpcRequest) -> Self::Future {
let this = self.0.clone();
Box::pin(async move {
let result = match req.method.as_str() {
"initialize" => this.handle_initialize(req).await,
"tools/list" => this.handle_tools_list(req).await,
"tools/call" => this.handle_tools_call(req).await,
"resources/list" => this.handle_resources_list(req).await,
"resources/read" => this.handle_resources_read(req).await,
"prompts/list" => this.handle_prompts_list(req).await,
"prompts/get" => this.handle_prompts_get(req).await,
_ => {
let mut response = this.create_response(req.id);
response.error = Some(RouterError::MethodNotFound(req.method).into());
Ok(response)
}
};
result.map_err(BoxError::from)
})
}
}