Compare commits

..

No commits in common. "ead72d3b4a8cb458f32e26860a0c3939c8fff03a" and "397d87f32b5f9f4fe0cd6eb24649a14acb003210" have entirely different histories.

13 changed files with 183 additions and 278 deletions

View File

@ -1 +0,0 @@
3.4.1

View File

@ -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"

View File

@ -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
View File

@ -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",

View File

@ -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"

View File

@ -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(())
} }

View File

@ -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(())
} }

View File

@ -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?)
} }
} }

View File

@ -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);

View File

@ -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))
} }
} }

View File

@ -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

View File

@ -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.

View File

@ -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