Compare commits
No commits in common. "ead72d3b4a8cb458f32e26860a0c3939c8fff03a" and "397d87f32b5f9f4fe0cd6eb24649a14acb003210" have entirely different histories.
ead72d3b4a
...
397d87f32b
@ -1 +0,0 @@
|
|||||||
3.4.1
|
|
||||||
2
Gemfile
2
Gemfile
@ -5,8 +5,6 @@ source "https://rubygems.org"
|
|||||||
# Specify your gem's dependencies in mcp.gemspec
|
# Specify your gem's dependencies in mcp.gemspec
|
||||||
gemspec
|
gemspec
|
||||||
|
|
||||||
gem "pry"
|
|
||||||
|
|
||||||
gem "rake", "~> 13.0"
|
gem "rake", "~> 13.0"
|
||||||
|
|
||||||
gem "rake-compiler", "~> 1.2.0"
|
gem "rake-compiler", "~> 1.2.0"
|
||||||
|
|||||||
@ -8,19 +8,14 @@ GEM
|
|||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
ast (2.4.2)
|
ast (2.4.2)
|
||||||
coderay (1.1.3)
|
|
||||||
diff-lcs (1.6.0)
|
diff-lcs (1.6.0)
|
||||||
json (2.10.2)
|
json (2.10.2)
|
||||||
language_server-protocol (3.17.0.4)
|
language_server-protocol (3.17.0.4)
|
||||||
lint_roller (1.1.0)
|
lint_roller (1.1.0)
|
||||||
method_source (1.1.0)
|
|
||||||
parallel (1.26.3)
|
parallel (1.26.3)
|
||||||
parser (3.3.7.1)
|
parser (3.3.7.1)
|
||||||
ast (~> 2.4.1)
|
ast (~> 2.4.1)
|
||||||
racc
|
racc
|
||||||
pry (0.15.2)
|
|
||||||
coderay (~> 1.1)
|
|
||||||
method_source (~> 1.0)
|
|
||||||
racc (1.8.1)
|
racc (1.8.1)
|
||||||
rainbow (3.1.1)
|
rainbow (3.1.1)
|
||||||
rake (13.2.1)
|
rake (13.2.1)
|
||||||
@ -83,7 +78,6 @@ PLATFORMS
|
|||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
mcp!
|
mcp!
|
||||||
pry
|
|
||||||
rake (~> 13.0)
|
rake (~> 13.0)
|
||||||
rake-compiler (~> 1.2.0)
|
rake-compiler (~> 1.2.0)
|
||||||
rspec (~> 3.0)
|
rspec (~> 3.0)
|
||||||
|
|||||||
11
ext/mcp/Cargo.lock
generated
11
ext/mcp/Cargo.lock
generated
@ -251,16 +251,6 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "keepcalm"
|
|
||||||
version = "0.3.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "031ddc7e27bbb011c78958881a3723873608397b8b10e146717fc05cf3364d78"
|
|
||||||
dependencies = [
|
|
||||||
"once_cell",
|
|
||||||
"parking_lot",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@ -343,7 +333,6 @@ version = "0.1.0"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"jsonrpsee",
|
"jsonrpsee",
|
||||||
"keepcalm",
|
|
||||||
"magnus",
|
"magnus",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@ -21,4 +21,3 @@ serde_json = "1.0.140"
|
|||||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
serde_magnus = "0.9.0"
|
serde_magnus = "0.9.0"
|
||||||
keepcalm = "0.3.5"
|
|
||||||
|
|||||||
@ -1,23 +1,21 @@
|
|||||||
use crate::types::{
|
use std::process::Stdio;
|
||||||
CallToolRequestParams, ClientCapabilities, ListToolsRequestParams, ListToolsResult,
|
|
||||||
};
|
|
||||||
use jsonrpsee::core::client::{
|
use jsonrpsee::core::client::{
|
||||||
Client, ClientBuilder, ClientT, TransportReceiverT, TransportSenderT,
|
Client, ClientBuilder, ClientT, TransportReceiverT, TransportSenderT,
|
||||||
};
|
};
|
||||||
use jsonrpsee::core::traits::ToRpcParams;
|
use jsonrpsee::core::traits::ToRpcParams;
|
||||||
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::process::Stdio;
|
|
||||||
use tokio::io::{AsyncBufReadExt, AsyncWriteExt};
|
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use types::{Implementation, InitializeRequestParams, InitializeResult};
|
use types::{Implementation, InitializeRequestParams, InitializeResult};
|
||||||
|
use crate::types::{CallToolRequestParams, ClientCapabilities, ListToolsRequestParams, ListToolsResult};
|
||||||
|
|
||||||
mod mcp_client;
|
mod types;
|
||||||
mod rpc_helpers;
|
mod rpc_helpers;
|
||||||
mod stdio_transport;
|
mod stdio_transport;
|
||||||
mod types;
|
|
||||||
use crate::mcp_client::McpClientConnection;
|
|
||||||
use rpc_helpers::*;
|
use rpc_helpers::*;
|
||||||
|
use stdio_transport::StdioTransport;
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
@ -31,54 +29,51 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.with_level(true)
|
.with_level(true)
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let client = McpClientConnection::new_stdio(
|
|
||||||
"/Users/joshuacoles/.local/bin/mcp-server-fetch".to_string(),
|
|
||||||
vec![],
|
|
||||||
InitializeRequestParams {
|
|
||||||
capabilities: Default::default(),
|
|
||||||
client_info: Implementation {
|
|
||||||
name: "ABC".to_string(),
|
|
||||||
version: "0.0.1".to_string(),
|
|
||||||
},
|
|
||||||
protocol_version: "2024-11-05".to_string(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
dbg!(client.list_tools());
|
let mut cmd = tokio::process::Command::new("/Users/joshuacoles/.local/bin/mcp-server-fetch")
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
// let response: InitializeResult = client.request("initialize", InitializeRequestParams {
|
let transport = StdioTransport::new(&mut cmd);
|
||||||
// capabilities: ClientCapabilities::default(),
|
|
||||||
// client_info: Implementation { name: "Rust MCP".to_string(), version: "0.1.0".to_string() },
|
let client: Client = ClientBuilder::default().build_with_tokio(
|
||||||
// protocol_version: "2024-11-05".to_string(),
|
transport.clone(),
|
||||||
|
transport.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let response: InitializeResult = client.request("initialize", InitializeRequestParams {
|
||||||
|
capabilities: ClientCapabilities::default(),
|
||||||
|
client_info: Implementation { name: "Rust MCP".to_string(), version: "0.1.0".to_string() },
|
||||||
|
protocol_version: "2024-11-05".to_string(),
|
||||||
|
}.to_rpc()).await?;
|
||||||
|
|
||||||
|
client.notification("notifications/initialized", NoParams).await?;
|
||||||
|
|
||||||
|
println!("Hey");
|
||||||
|
|
||||||
|
// drop(transport.stdin);
|
||||||
|
|
||||||
|
select! {
|
||||||
|
_ = cmd.wait() => {
|
||||||
|
println!("Command exited");
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {
|
||||||
|
cmd.kill().await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||||
|
|
||||||
|
// let response: ListToolsResult = client.request("tools/list", ListToolsRequestParams::default().to_rpc()).await?;
|
||||||
|
|
||||||
|
// let response: serde_json::Value = client.request("tools/call", CallToolRequestParams {
|
||||||
|
// arguments: json!({ "url": "http://example.com" }).as_object().unwrap().clone(),
|
||||||
|
// name: "fetch".to_string(),
|
||||||
// }.to_rpc()).await?;
|
// }.to_rpc()).await?;
|
||||||
//
|
|
||||||
// client.notification("notifications/initialized", NoParams).await?;
|
// println!("Response: {:#?}", response);
|
||||||
//
|
|
||||||
// println!("Hey");
|
|
||||||
//
|
|
||||||
// // drop(transport.stdin);
|
|
||||||
//
|
|
||||||
// select! {
|
|
||||||
// _ = cmd.wait() => {
|
|
||||||
// println!("Command exited");
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// _ = tokio::time::sleep(std::time::Duration::from_secs(1)) => {
|
|
||||||
// cmd.kill().await?;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
|
||||||
//
|
|
||||||
// // let response: ListToolsResult = client.request("tools/list", ListToolsRequestParams::default().to_rpc()).await?;
|
|
||||||
//
|
|
||||||
// // let response: serde_json::Value = client.request("tools/call", CallToolRequestParams {
|
|
||||||
// // arguments: json!({ "url": "http://example.com" }).as_object().unwrap().clone(),
|
|
||||||
// // name: "fetch".to_string(),
|
|
||||||
// // }.to_rpc()).await?;
|
|
||||||
//
|
|
||||||
// // println!("Response: {:#?}", response);
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,73 +1,75 @@
|
|||||||
use crate::mcp_client::McpClientConnection;
|
use jsonrpsee::async_client::{Client, ClientBuilder};
|
||||||
use magnus::prelude::*;
|
use tokio::process::Command;
|
||||||
|
use crate::mcp_client::McpClient;
|
||||||
|
use jsonrpsee::core::client::ClientT;
|
||||||
use once_cell::sync::Lazy;
|
use once_cell::sync::Lazy;
|
||||||
|
use magnus::prelude::*;
|
||||||
|
|
||||||
mod mcp_client;
|
mod mcp_client;
|
||||||
|
mod types;
|
||||||
mod rpc_helpers;
|
mod rpc_helpers;
|
||||||
mod stdio_transport;
|
mod stdio_transport;
|
||||||
mod types;
|
|
||||||
|
|
||||||
use crate::types::{CallToolRequestParams, Implementation, InitializeRequestParams};
|
use std::{
|
||||||
use magnus::{
|
hash::{Hash, Hasher},
|
||||||
eval, function, method, prelude::*, Error, ExceptionClass, RHash, Ruby, TryConvert, Value,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use magnus::{function, method, prelude::*, scan_args::{get_kwargs, scan_args}, typed_data, Error, RHash, Ruby, Symbol, TryConvert, Value};
|
||||||
use serde_magnus::serialize;
|
use serde_magnus::serialize;
|
||||||
use tokio::sync::Mutex;
|
use crate::types::{CallToolRequestParams, Implementation, InitializeRequestParams, InitializeResult};
|
||||||
|
|
||||||
// Create global runtime
|
// Create global runtime
|
||||||
static RUNTIME: Lazy<tokio::runtime::Runtime> =
|
static RUNTIME: Lazy<tokio::runtime::Runtime> = Lazy::new(|| {
|
||||||
Lazy::new(|| tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime"));
|
tokio::runtime::Runtime::new().expect("Failed to create Tokio runtime")
|
||||||
|
});
|
||||||
|
|
||||||
#[magnus::wrap(class = "Mcp::Client", free_immediately, size)]
|
#[magnus::wrap(class = "Mcp::Client", free_immediately, size)]
|
||||||
struct McpClientRb {
|
struct McpClientRb {
|
||||||
client: Mutex<Option<McpClientConnection>>,
|
client: McpClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl McpClientRb {
|
impl McpClientRb {
|
||||||
fn new(command: String, args: Vec<String>) -> Result<Self, magnus::Error> {
|
fn new(command: String, args: Vec<String>) -> Result<Self, magnus::Error> {
|
||||||
let client = RUNTIME
|
let client = RUNTIME.block_on(async {
|
||||||
.block_on(async {
|
let child = Command::new(command)
|
||||||
McpClientConnection::new_stdio(
|
.args(args)
|
||||||
command,
|
.stdin(std::process::Stdio::piped())
|
||||||
args,
|
.stdout(std::process::Stdio::piped())
|
||||||
InitializeRequestParams {
|
.spawn()
|
||||||
capabilities: Default::default(),
|
.unwrap();
|
||||||
client_info: Implementation {
|
|
||||||
name: "ABC".to_string(),
|
|
||||||
version: "0.0.1".to_string(),
|
|
||||||
},
|
|
||||||
protocol_version: "2024-11-05".to_string(),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
.map_err(|err| Error::new(magnus::exception::runtime_error(), err.to_string()))?;
|
|
||||||
|
|
||||||
Ok(Self {
|
let transport = stdio_transport::StdioTransport::new(child);
|
||||||
client: Mutex::new(Some(client)),
|
|
||||||
})
|
ClientBuilder::default().build_with_tokio(
|
||||||
|
transport.clone(),
|
||||||
|
transport.clone(),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(Self { client: McpClient { client } })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn disconnect(&self) {
|
fn connect(&self) -> Result<bool, magnus::Error> {
|
||||||
RUNTIME.block_on(async {
|
RUNTIME.block_on(async {
|
||||||
self.client.lock().await.take();
|
let a = self.client.initialize(InitializeRequestParams {
|
||||||
|
capabilities: Default::default(),
|
||||||
|
client_info: Implementation { name: "ABC".to_string(), version: "0.0.1".to_string() },
|
||||||
|
protocol_version: "2024-11-05".to_string(),
|
||||||
|
}).await;
|
||||||
|
|
||||||
|
match a {
|
||||||
|
Ok(_) => Ok(true),
|
||||||
|
Err(e) => Err(magnus::Error::new(
|
||||||
|
magnus::exception::runtime_error(),
|
||||||
|
e.to_string(),
|
||||||
|
)),
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn list_tools(&self) -> Result<Value, magnus::Error> {
|
fn list_tools(&self) -> Result<Value, magnus::Error> {
|
||||||
RUNTIME.block_on(async {
|
RUNTIME.block_on(async {
|
||||||
let a = self
|
let a = self.client.list_tools().await;
|
||||||
.client
|
|
||||||
.lock()
|
|
||||||
.await
|
|
||||||
.as_ref()
|
|
||||||
.ok_or(Error::new(
|
|
||||||
ExceptionClass::from_value(eval("Mcp::ClientDisconnectedError").unwrap())
|
|
||||||
.unwrap(),
|
|
||||||
"Client is not connected",
|
|
||||||
))?
|
|
||||||
.list_tools()
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match a {
|
match a {
|
||||||
Ok(tools) => serialize::<_, Value>(&tools),
|
Ok(tools) => serialize::<_, Value>(&tools),
|
||||||
@ -79,25 +81,22 @@ impl McpClientRb {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn call_tool(&self, name: String, rhash: RHash) -> Result<Value, magnus::Error> {
|
fn call_tool(&self, values: &[Value]) -> Result<Value, magnus::Error> {
|
||||||
let kwargs: serde_json::Map<String, serde_json::Value> = serde_magnus::deserialize(rhash)?;
|
let args = scan_args::<(Value,), (), (), (), RHash, ()>(values)?;
|
||||||
|
let ((name,)) = args.required;
|
||||||
|
let kwargs: RHash = args.keywords;
|
||||||
|
let kwargs: serde_json::Map<String, serde_json::Value> = serde_magnus::deserialize(kwargs)?;
|
||||||
|
|
||||||
|
let name = match Symbol::from_value(name) {
|
||||||
|
Some(symbol) => symbol.name()?.to_string(),
|
||||||
|
None => String::try_convert(name)?,
|
||||||
|
};
|
||||||
|
|
||||||
RUNTIME.block_on(async {
|
RUNTIME.block_on(async {
|
||||||
let a = self
|
let a = self.client.call_tool::<serde_json::Value>(CallToolRequestParams {
|
||||||
.client
|
name,
|
||||||
.lock()
|
arguments: kwargs,
|
||||||
.await
|
}).await;
|
||||||
.as_ref()
|
|
||||||
.ok_or(Error::new(
|
|
||||||
ExceptionClass::from_value(eval("Mcp::ClientDisconnectedError").unwrap())
|
|
||||||
.unwrap(),
|
|
||||||
"Client is not connected",
|
|
||||||
))?
|
|
||||||
.call_tool::<serde_json::Value>(CallToolRequestParams {
|
|
||||||
name,
|
|
||||||
arguments: kwargs,
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
match a {
|
match a {
|
||||||
Ok(a) => Ok(serde_magnus::serialize(&a)?),
|
Ok(a) => Ok(serde_magnus::serialize(&a)?),
|
||||||
@ -112,23 +111,13 @@ impl McpClientRb {
|
|||||||
|
|
||||||
#[magnus::init]
|
#[magnus::init]
|
||||||
fn init(ruby: &Ruby) -> Result<(), Error> {
|
fn init(ruby: &Ruby) -> Result<(), Error> {
|
||||||
tracing_subscriber::fmt()
|
|
||||||
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
|
|
||||||
.with_file(true)
|
|
||||||
.with_line_number(true)
|
|
||||||
.with_thread_ids(true)
|
|
||||||
.with_thread_names(true)
|
|
||||||
.with_target(true)
|
|
||||||
.with_level(true)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
let module = ruby.define_module("Mcp")?;
|
let module = ruby.define_module("Mcp")?;
|
||||||
let client_class = module.define_class("Client", ruby.class_object())?;
|
let client_class = module.define_class("Client", ruby.class_object())?;
|
||||||
|
|
||||||
client_class.define_singleton_method("new", function!(McpClientRb::new, 2))?;
|
client_class.define_singleton_method("new", function!(McpClientRb::new, 2))?;
|
||||||
|
client_class.define_method("connect", method!(McpClientRb::connect, 0))?;
|
||||||
client_class.define_method("list_tools", method!(McpClientRb::list_tools, 0))?;
|
client_class.define_method("list_tools", method!(McpClientRb::list_tools, 0))?;
|
||||||
client_class.define_method("call_tool", method!(McpClientRb::call_tool, 2))?;
|
client_class.define_method("call_tool", method!(McpClientRb::call_tool, -1))?;
|
||||||
client_class.define_method("disconnect", method!(McpClientRb::disconnect, 0))?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,72 +1,25 @@
|
|||||||
use crate::rpc_helpers::{NoParams, ToRpcArg};
|
use jsonrpsee::async_client::Client;
|
||||||
use crate::stdio_transport::Adapter;
|
|
||||||
use crate::types::{
|
|
||||||
CallToolRequestParams, InitializeRequestParams, InitializeResult, ListToolsRequestParams,
|
|
||||||
ListToolsResult, Tool,
|
|
||||||
};
|
|
||||||
use jsonrpsee::async_client::{Client, ClientBuilder};
|
|
||||||
use jsonrpsee::core::client::ClientT;
|
use jsonrpsee::core::client::ClientT;
|
||||||
use std::sync::Arc;
|
use tokio::process::Child;
|
||||||
use tokio::io::BufReader;
|
use stdio_transport::StdioTransport;
|
||||||
use tokio::process::{Child, ChildStdin, ChildStdout, Command};
|
use crate::rpc_helpers::{NoParams, ToRpcArg};
|
||||||
use tokio::sync::Mutex;
|
use crate::stdio_transport;
|
||||||
|
use crate::types::{CallToolRequestParams, InitializeRequestParams, InitializeResult, ListToolsRequestParams, ListToolsResult, Tool};
|
||||||
|
|
||||||
enum TransportHandle {
|
pub struct McpClient {
|
||||||
Stdio {
|
pub(crate) transport: StdioTransport,
|
||||||
child: Child,
|
|
||||||
stdin: Arc<Mutex<ChildStdin>>,
|
|
||||||
stdout: Arc<Mutex<BufReader<ChildStdout>>>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// This represents a live MCP connection to an MCP server. It will close the connection when dropped on a best effort basis.
|
|
||||||
pub struct McpClientConnection {
|
|
||||||
pub(crate) transport: TransportHandle,
|
|
||||||
pub(crate) client: Client,
|
pub(crate) client: Client,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl McpClientConnection {
|
impl McpClient {
|
||||||
pub async fn new_stdio(
|
pub async fn initialize(&self, params: InitializeRequestParams) -> Result<InitializeResult, anyhow::Error> {
|
||||||
command: String,
|
let result: InitializeResult = self.client.request("initialize", params.to_rpc()).await?;
|
||||||
args: Vec<String>,
|
self.client.notification("notifications/initialized", NoParams).await?;
|
||||||
init_params: InitializeRequestParams,
|
Ok(result)
|
||||||
) -> Result<Self, anyhow::Error> {
|
|
||||||
let mut child = Command::new(command)
|
|
||||||
.args(args)
|
|
||||||
.stdin(std::process::Stdio::piped())
|
|
||||||
.stdout(std::process::Stdio::piped())
|
|
||||||
.kill_on_drop(true)
|
|
||||||
.spawn()?;
|
|
||||||
|
|
||||||
// We take ownership of the stdin and stdout here to pass them to the transport, wrapping them in an Arc and Mutex to allow them to be shared between threads in an async context.
|
|
||||||
let stdin = Arc::new(Mutex::new(child.stdin.take().unwrap()));
|
|
||||||
let stdout = Arc::new(Mutex::new(BufReader::new(child.stdout.take().unwrap())));
|
|
||||||
|
|
||||||
let client = ClientBuilder::default()
|
|
||||||
.build_with_tokio(Adapter(stdin.clone()), Adapter(stdout.clone()));
|
|
||||||
|
|
||||||
let new_client = Self {
|
|
||||||
transport: TransportHandle::Stdio {
|
|
||||||
child,
|
|
||||||
stdin,
|
|
||||||
stdout,
|
|
||||||
},
|
|
||||||
client,
|
|
||||||
};
|
|
||||||
|
|
||||||
new_client.initialize(init_params).await?;
|
|
||||||
Ok(new_client)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn initialize(
|
pub async fn shutdown(mut self) {
|
||||||
&self,
|
self.transport.shutdown();
|
||||||
params: InitializeRequestParams,
|
|
||||||
) -> Result<InitializeResult, anyhow::Error> {
|
|
||||||
let result: InitializeResult = self.client.request("initialize", params.to_rpc()).await?;
|
|
||||||
self.client
|
|
||||||
.notification("notifications/initialized", NoParams)
|
|
||||||
.await?;
|
|
||||||
Ok(result)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn list_tools(&self) -> Result<Vec<Tool>, anyhow::Error> {
|
pub async fn list_tools(&self) -> Result<Vec<Tool>, anyhow::Error> {
|
||||||
@ -76,26 +29,14 @@ impl McpClientConnection {
|
|||||||
tools.extend(result.tools);
|
tools.extend(result.tools);
|
||||||
|
|
||||||
while let Some(cursor) = result.next_cursor.as_ref() {
|
while let Some(cursor) = result.next_cursor.as_ref() {
|
||||||
let result: ListToolsResult = self
|
let result: ListToolsResult = self.client.request("tools/list", ListToolsRequestParams { cursor: Some(cursor.clone()) }.to_rpc()).await?;
|
||||||
.client
|
|
||||||
.request(
|
|
||||||
"tools/list",
|
|
||||||
ListToolsRequestParams {
|
|
||||||
cursor: Some(cursor.clone()),
|
|
||||||
}
|
|
||||||
.to_rpc(),
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
tools.extend(result.tools);
|
tools.extend(result.tools);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(tools)
|
Ok(tools)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn call_tool<T: serde::de::DeserializeOwned>(
|
pub async fn call_tool<T: serde::de::DeserializeOwned>(&self, params: CallToolRequestParams) -> Result<T, anyhow::Error> {
|
||||||
&self,
|
|
||||||
params: CallToolRequestParams,
|
|
||||||
) -> Result<T, anyhow::Error> {
|
|
||||||
Ok(self.client.request("tools/call", params.to_rpc()).await?)
|
Ok(self.client.request("tools/call", params.to_rpc()).await?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use jsonrpsee::core::traits::ToRpcParams;
|
use jsonrpsee::core::traits::ToRpcParams;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::value::RawValue;
|
|
||||||
use serde_json::Error;
|
use serde_json::Error;
|
||||||
|
use serde_json::value::RawValue;
|
||||||
|
|
||||||
pub struct RpcArg<T>(T);
|
pub struct RpcArg<T>(T);
|
||||||
|
|
||||||
|
|||||||
@ -1,33 +1,53 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
use tokio::process::{Child, ChildStdin, ChildStdout, Command};
|
||||||
|
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
|
||||||
use jsonrpsee::core::async_trait;
|
use jsonrpsee::core::async_trait;
|
||||||
use jsonrpsee::core::client::{ReceivedMessage, TransportReceiverT, TransportSenderT};
|
use jsonrpsee::core::client::{ReceivedMessage, TransportReceiverT, TransportSenderT};
|
||||||
use std::sync::Arc;
|
|
||||||
use tokio::io::{AsyncBufRead, AsyncBufReadExt, AsyncWriteExt};
|
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
pub struct Adapter<T>(pub Arc<Mutex<T>>);
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct StdioTransport {
|
||||||
|
pub stdin: Arc<Mutex<ChildStdin>>,
|
||||||
|
pub stdout: Arc<Mutex<BufReader<ChildStdout>>>,
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
impl StdioTransport {
|
||||||
impl<T: Unpin + Send + 'static + AsyncWriteExt> TransportSenderT for Adapter<T> {
|
pub fn new(child: &mut Child) -> Self {
|
||||||
type Error = tokio::io::Error;
|
let stdin = Arc::new(Mutex::new(child.stdin.take().unwrap()));
|
||||||
|
let stdout = Arc::new(Mutex::new(BufReader::new(child.stdout.take().unwrap())));
|
||||||
|
Self { stdin, stdout }
|
||||||
|
}
|
||||||
|
|
||||||
#[tracing::instrument(skip(self), level = "trace")]
|
pub(crate) async fn shutdown(mut self) -> Result<(), tokio::io::Error> {
|
||||||
async fn send(&mut self, msg: String) -> Result<(), Self::Error> {
|
|
||||||
let mut guard = self.0.lock().await;
|
|
||||||
|
|
||||||
guard.write_all(msg.as_bytes()).await?;
|
|
||||||
guard.write_all(b"\n").await?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<T: AsyncBufRead + Unpin + Send + 'static> TransportReceiverT for Adapter<T> {
|
impl TransportSenderT for StdioTransport {
|
||||||
|
type Error = tokio::io::Error;
|
||||||
|
|
||||||
|
#[tracing::instrument(skip(self), level = "trace")]
|
||||||
|
async fn send(&mut self, msg: String) -> Result<(), Self::Error> {
|
||||||
|
debug!("Sending: {}", msg);
|
||||||
|
let mut stdin = self.stdin.lock().await;
|
||||||
|
stdin.write_all(msg.as_bytes()).await?;
|
||||||
|
stdin.write_all(b"\n").await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl TransportReceiverT for StdioTransport {
|
||||||
type Error = tokio::io::Error;
|
type Error = tokio::io::Error;
|
||||||
|
|
||||||
#[tracing::instrument(skip(self), level = "trace")]
|
#[tracing::instrument(skip(self), level = "trace")]
|
||||||
async fn receive(&mut self) -> Result<ReceivedMessage, Self::Error> {
|
async fn receive(&mut self) -> Result<ReceivedMessage, Self::Error> {
|
||||||
|
let mut stdout = self.stdout.lock().await;
|
||||||
let mut str = String::new();
|
let mut str = String::new();
|
||||||
self.0.lock().await.read_line(&mut str).await?;
|
stdout.read_line(&mut str).await?;
|
||||||
|
debug!("Received: {}", str);
|
||||||
Ok(ReceivedMessage::Text(str))
|
Ok(ReceivedMessage::Text(str))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
17
lib/mcp.rb
17
lib/mcp.rb
@ -5,18 +5,7 @@ require_relative "mcp/version"
|
|||||||
module Mcp
|
module Mcp
|
||||||
class Error < StandardError; end
|
class Error < StandardError; end
|
||||||
|
|
||||||
class ClientDisconnectedError < Error; end
|
|
||||||
|
|
||||||
class Client
|
class Client
|
||||||
def initialize(command, args)
|
|
||||||
# This is implemented in rust
|
|
||||||
raise NotImplementedError
|
|
||||||
end
|
|
||||||
|
|
||||||
def list_tools
|
|
||||||
raise NotImplementedError
|
|
||||||
end
|
|
||||||
|
|
||||||
def tools
|
def tools
|
||||||
ToolsProxy.new(self)
|
ToolsProxy.new(self)
|
||||||
end
|
end
|
||||||
@ -30,10 +19,12 @@ module Mcp
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def respond_to_missing?(name, include_private = false) end
|
def respond_to_missing?(name, include_private = false)
|
||||||
|
@tools.any? { |tool| tool["name"] == name.to_s } || super
|
||||||
|
end
|
||||||
|
|
||||||
def method_missing(name, **kwargs)
|
def method_missing(name, **kwargs)
|
||||||
@client.call_tool(name.to_s, kwargs)
|
@client.call_tool(name, **kwargs)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
18
mcp.gemspec
18
mcp.gemspec
@ -6,19 +6,19 @@ Gem::Specification.new do |spec|
|
|||||||
spec.name = "mcp"
|
spec.name = "mcp"
|
||||||
spec.version = Mcp::VERSION
|
spec.version = Mcp::VERSION
|
||||||
spec.authors = ["Joshua Coles"]
|
spec.authors = ["Joshua Coles"]
|
||||||
spec.email = ["joshuac@amphora-research.com"]
|
spec.email = ["josh@coles.to"]
|
||||||
|
|
||||||
spec.summary = "MCP Client implementation"
|
spec.summary = "TODO: Write a short summary, because RubyGems requires one."
|
||||||
# spec.description = "TODO: Write a longer description or delete this line."
|
spec.description = "TODO: Write a longer description or delete this line."
|
||||||
# spec.homepage = "TODO: Put your gem's website or public repo URL here."
|
spec.homepage = "TODO: Put your gem's website or public repo URL here."
|
||||||
spec.license = "MIT"
|
spec.license = "MIT"
|
||||||
spec.required_ruby_version = ">= 3.1.0"
|
spec.required_ruby_version = ">= 3.1.0"
|
||||||
|
|
||||||
# spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
|
spec.metadata["allowed_push_host"] = "TODO: Set to your gem server 'https://example.com'"
|
||||||
#
|
|
||||||
# spec.metadata["homepage_uri"] = spec.homepage
|
spec.metadata["homepage_uri"] = spec.homepage
|
||||||
# spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
|
spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
|
||||||
# spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
|
||||||
|
|
||||||
# Specify which files should be added to the gem when it is released.
|
# Specify which files should be added to the gem when it is released.
|
||||||
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
||||||
|
|||||||
@ -1,24 +1,14 @@
|
|||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
RSpec.describe Mcp do
|
RSpec.describe Mcp do
|
||||||
it "can list tools" do
|
it "has a version number" do
|
||||||
client = Mcp::Client.new("/Users/joshuacoles/.local/bin/mcp-server-fetch", [])
|
expect(distance([1, 1], [1, 2])).to eq(1)
|
||||||
expect(client.list_tools).to_not be_nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'can call tools' do
|
it "does something useful" do
|
||||||
client = Mcp::Client.new("/Users/joshuacoles/.local/bin/mcp-server-fetch", [])
|
a = Mcp::Client.new("/Users/joshuacoles/.local/bin/mcp-server-fetch", [])
|
||||||
result = client.tools.fetch(url: 'http://example.com')
|
expect(a.connect).to eq(true)
|
||||||
|
puts a.list_tools
|
||||||
expect(result).to be_a(Hash)
|
puts a.tools.fetch(url: 'http://example.com')
|
||||||
expect(result['content'][0]).to be_a(Hash)
|
|
||||||
expect(result['content'][0]['text']).to include('Contents of http://example.com/')
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'handles calls after disconnect' do
|
|
||||||
client = Mcp::Client.new("/Users/joshuacoles/.local/bin/mcp-server-fetch", [])
|
|
||||||
client.disconnect
|
|
||||||
|
|
||||||
expect { client.tools.fetch(url: 'http://example.com') }.to raise_error(Mcp::ClientDisconnectedError)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user