micro_mcp 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.devcontainer/devcontainer.json +2 -2
- data/.tool-versions +1 -0
- data/AGENTS.md +5 -1
- data/CHANGELOG.md +12 -0
- data/Cargo.lock +278 -558
- data/README.md +39 -8
- data/docs/changes/ERGONOMIC_IMPROVEMENTS.md +133 -0
- data/ext/micro_mcp/AGENTS.md +66 -0
- data/ext/micro_mcp/Cargo.toml +3 -1
- data/ext/micro_mcp/src/lib.rs +21 -5
- data/ext/micro_mcp/src/server.rs +470 -50
- data/ext/micro_mcp/src/utils.rs +1 -1
- data/lib/micro_mcp/runtime_helpers.rb +104 -0
- data/lib/micro_mcp/schema.rb +125 -0
- data/lib/micro_mcp/server.rb +2 -2
- data/lib/micro_mcp/tool_registry.rb +65 -2
- data/lib/micro_mcp/validation_helpers.rb +59 -0
- data/lib/micro_mcp/version.rb +1 -1
- data/lib/micro_mcp.rb +3 -0
- metadata +7 -2
- data/sig/micro_mcp.rbs +0 -4
data/ext/micro_mcp/src/server.rs
CHANGED
@@ -1,22 +1,32 @@
|
|
1
|
+
use async_trait::async_trait;
|
1
2
|
use rust_mcp_sdk::{
|
2
3
|
mcp_server::{server_runtime, ServerHandler, ServerRuntime},
|
3
4
|
schema::{
|
4
5
|
schema_utils::CallToolError, CallToolRequest, CallToolResult, Implementation,
|
5
6
|
InitializeResult, ListToolsRequest, ListToolsResult, RpcError, ServerCapabilities,
|
6
|
-
ServerCapabilitiesTools,
|
7
|
+
ServerCapabilitiesTools, Tool, ToolInputSchema, LATEST_PROTOCOL_VERSION,
|
7
8
|
},
|
8
9
|
McpServer, StdioTransport, TransportOptions,
|
9
10
|
};
|
10
|
-
use
|
11
|
-
use async_trait::async_trait;
|
12
|
-
use std::sync::{Mutex, OnceLock};
|
11
|
+
use serde_json::{Map as JsonMap, Value as JsonValue};
|
13
12
|
use std::collections::HashMap;
|
13
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
14
|
+
use std::sync::{Arc, Mutex, OnceLock};
|
15
|
+
use tokio::runtime::Runtime;
|
14
16
|
|
15
|
-
use magnus::{block::Proc, Error, Ruby};
|
17
|
+
use magnus::{block::Proc, value::ReprValue, Error, Ruby, Value};
|
18
|
+
use magnus::{typed_data::DataTypeFunctions, TypedData};
|
19
|
+
use std::cell::RefCell;
|
20
|
+
use std::rc::Rc;
|
16
21
|
|
17
22
|
use crate::utils::nogvl;
|
18
23
|
|
19
24
|
static RUNTIME: OnceLock<Runtime> = OnceLock::new();
|
25
|
+
static SHUTDOWN_FLAG: OnceLock<Arc<AtomicBool>> = OnceLock::new();
|
26
|
+
|
27
|
+
fn shutdown_flag() -> &'static Arc<AtomicBool> {
|
28
|
+
SHUTDOWN_FLAG.get_or_init(|| Arc::new(AtomicBool::new(false)))
|
29
|
+
}
|
20
30
|
|
21
31
|
type ToolHandler = RubyHandler;
|
22
32
|
|
@@ -39,17 +49,149 @@ fn tools() -> &'static Mutex<HashMap<String, ToolEntry>> {
|
|
39
49
|
TOOLS.get_or_init(|| Mutex::new(HashMap::new()))
|
40
50
|
}
|
41
51
|
|
42
|
-
|
52
|
+
fn ruby_value_to_json_value(ruby: &Ruby, val: Value) -> Result<JsonValue, Error> {
|
53
|
+
let json_str: String = magnus::eval!(ruby, "require 'json'; JSON.generate(obj)", obj = val)?;
|
54
|
+
serde_json::from_str(&json_str)
|
55
|
+
.map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))
|
56
|
+
}
|
57
|
+
|
58
|
+
fn json_value_to_ruby_value(ruby: &Ruby, val: &JsonValue) -> Result<Value, Error> {
|
59
|
+
let json_str = serde_json::to_string(val)
|
60
|
+
.map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
|
61
|
+
Ok(magnus::eval!(
|
62
|
+
ruby,
|
63
|
+
"require 'json'; JSON.parse(str)",
|
64
|
+
str = json_str
|
65
|
+
)?)
|
66
|
+
}
|
67
|
+
|
68
|
+
fn parse_tool_input_schema(json: JsonValue) -> ToolInputSchema {
|
69
|
+
if let JsonValue::Object(obj) = json {
|
70
|
+
let required = obj
|
71
|
+
.get("required")
|
72
|
+
.and_then(|v| v.as_array())
|
73
|
+
.map(|arr| {
|
74
|
+
arr.iter()
|
75
|
+
.filter_map(|v| v.as_str().map(|s| s.to_string()))
|
76
|
+
.collect::<Vec<_>>()
|
77
|
+
})
|
78
|
+
.unwrap_or_default();
|
79
|
+
|
80
|
+
let properties = obj
|
81
|
+
.get("properties")
|
82
|
+
.and_then(|v| v.as_object())
|
83
|
+
.map(|props| {
|
84
|
+
props
|
85
|
+
.iter()
|
86
|
+
.filter_map(|(k, v)| match v {
|
87
|
+
JsonValue::Object(map) => Some((k.clone(), map.clone())),
|
88
|
+
_ => None,
|
89
|
+
})
|
90
|
+
.collect::<HashMap<String, JsonMap<String, JsonValue>>>()
|
91
|
+
});
|
92
|
+
|
93
|
+
ToolInputSchema::new(required, properties)
|
94
|
+
} else {
|
95
|
+
ToolInputSchema::new(Vec::new(), None)
|
96
|
+
}
|
97
|
+
}
|
98
|
+
|
99
|
+
#[derive(Clone, TypedData)]
|
100
|
+
#[magnus(class = "MicroMcp::Runtime", free_immediately, unsafe_generics)]
|
101
|
+
pub struct RubyMcpServer<'a> {
|
102
|
+
inner: Rc<RefCell<Option<&'a dyn McpServer>>>,
|
103
|
+
}
|
104
|
+
|
105
|
+
impl<'a> DataTypeFunctions for RubyMcpServer<'a> {}
|
106
|
+
|
107
|
+
// SAFETY: the wrapped reference is only used while valid
|
108
|
+
unsafe impl<'a> Send for RubyMcpServer<'a> {}
|
109
|
+
|
110
|
+
impl<'a> RubyMcpServer<'a> {
|
111
|
+
fn new(runtime: &'a dyn McpServer) -> Self {
|
112
|
+
Self {
|
113
|
+
inner: Rc::new(RefCell::new(Some(runtime))),
|
114
|
+
}
|
115
|
+
}
|
116
|
+
|
117
|
+
fn invalidate(&self) {
|
118
|
+
*self.inner.borrow_mut() = None;
|
119
|
+
}
|
120
|
+
|
121
|
+
fn runtime(&self) -> Result<&'a dyn McpServer, Error> {
|
122
|
+
match *self.inner.borrow() {
|
123
|
+
Some(ptr) => Ok(ptr),
|
124
|
+
None => {
|
125
|
+
let ruby = Ruby::get().unwrap();
|
126
|
+
Err(Error::new(
|
127
|
+
ruby.exception_runtime_error(),
|
128
|
+
"McpServer reference is no longer valid",
|
129
|
+
))
|
130
|
+
}
|
131
|
+
}
|
132
|
+
}
|
133
|
+
|
134
|
+
pub fn is_initialized(&self) -> Result<bool, Error> {
|
135
|
+
Ok(self.runtime()?.is_initialized())
|
136
|
+
}
|
137
|
+
|
138
|
+
pub fn client_supports_sampling(&self) -> Result<Option<bool>, Error> {
|
139
|
+
Ok(self.runtime()?.client_supports_sampling())
|
140
|
+
}
|
141
|
+
|
142
|
+
pub fn create_message(&self, params: Value) -> Result<Value, Error> {
|
143
|
+
let ruby = Ruby::get().unwrap();
|
144
|
+
let runtime = self.runtime()?;
|
145
|
+
let json_value = ruby_value_to_json_value(&ruby, params)?;
|
146
|
+
let request_params: rust_mcp_sdk::schema::CreateMessageRequestParams =
|
147
|
+
serde_json::from_value(json_value)
|
148
|
+
.map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
|
149
|
+
|
150
|
+
let runtime_handle = RUNTIME.get().expect("Tokio not initialised");
|
151
|
+
let handle = runtime_handle.handle();
|
152
|
+
|
153
|
+
let result = if tokio::runtime::Handle::try_current().is_ok() {
|
154
|
+
tokio::task::block_in_place(|| {
|
155
|
+
handle.block_on(async { runtime.create_message(request_params).await })
|
156
|
+
})
|
157
|
+
} else {
|
158
|
+
handle.block_on(async { runtime.create_message(request_params).await })
|
159
|
+
}
|
160
|
+
.map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
|
161
|
+
|
162
|
+
let json_result = serde_json::to_value(result)
|
163
|
+
.map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
|
164
|
+
json_value_to_ruby_value(&ruby, &json_result)
|
165
|
+
}
|
166
|
+
}
|
167
|
+
|
168
|
+
pub fn register_tool(
|
169
|
+
ruby: &Ruby,
|
170
|
+
name: String,
|
171
|
+
description: Option<String>,
|
172
|
+
arg_schema: Option<Value>,
|
173
|
+
handler: Proc,
|
174
|
+
) -> Result<(), Error> {
|
175
|
+
let schema = match arg_schema {
|
176
|
+
Some(val) => {
|
177
|
+
let json = ruby_value_to_json_value(ruby, val)?;
|
178
|
+
parse_tool_input_schema(json)
|
179
|
+
}
|
180
|
+
None => ToolInputSchema::new(Vec::new(), None),
|
181
|
+
};
|
182
|
+
|
43
183
|
let tool = Tool {
|
44
184
|
annotations: None,
|
45
185
|
description,
|
46
|
-
input_schema:
|
186
|
+
input_schema: schema,
|
47
187
|
name: name.clone(),
|
48
188
|
};
|
49
189
|
|
50
190
|
let handler_fn = RubyHandler(handler);
|
51
191
|
|
52
|
-
let mut map = tools()
|
192
|
+
let mut map = tools()
|
193
|
+
.lock()
|
194
|
+
.map_err(|_| Error::new(ruby.exception_runtime_error(), "tools mutex poisoned"))?;
|
53
195
|
map.insert(
|
54
196
|
name,
|
55
197
|
ToolEntry {
|
@@ -60,7 +202,6 @@ pub fn register_tool(_ruby: &Ruby, name: String, description: Option<String>, ha
|
|
60
202
|
Ok(())
|
61
203
|
}
|
62
204
|
|
63
|
-
|
64
205
|
pub struct MyServerHandler;
|
65
206
|
|
66
207
|
#[async_trait]
|
@@ -71,7 +212,9 @@ impl ServerHandler for MyServerHandler {
|
|
71
212
|
_runtime: &dyn McpServer,
|
72
213
|
) -> Result<ListToolsResult, RpcError> {
|
73
214
|
let tools = {
|
74
|
-
let map = tools().lock().
|
215
|
+
let map = tools().lock().map_err(|_| {
|
216
|
+
RpcError::internal_error().with_message("tools mutex poisoned".to_string())
|
217
|
+
})?;
|
75
218
|
map.values().map(|t| t.tool.clone()).collect()
|
76
219
|
};
|
77
220
|
Ok(ListToolsResult {
|
@@ -84,14 +227,35 @@ impl ServerHandler for MyServerHandler {
|
|
84
227
|
async fn handle_call_tool_request(
|
85
228
|
&self,
|
86
229
|
request: CallToolRequest,
|
87
|
-
|
230
|
+
runtime: &dyn McpServer,
|
88
231
|
) -> Result<CallToolResult, CallToolError> {
|
89
|
-
let map = tools()
|
232
|
+
let map = tools()
|
233
|
+
.lock()
|
234
|
+
.map_err(|_| CallToolError::new(std::io::Error::other("tools mutex poisoned")))?;
|
90
235
|
match map.get(request.tool_name()) {
|
91
236
|
Some(entry) => {
|
92
237
|
let proc = entry.handler.0;
|
93
|
-
let
|
94
|
-
|
238
|
+
let wrapper = RubyMcpServer::new(runtime);
|
239
|
+
let args_value = if let Some(map) = &request.params.arguments {
|
240
|
+
let json = JsonValue::Object(map.clone());
|
241
|
+
Some(
|
242
|
+
crate::utils::with_gvl(|| {
|
243
|
+
let ruby = Ruby::get().unwrap();
|
244
|
+
json_value_to_ruby_value(&ruby, &json)
|
245
|
+
})
|
246
|
+
.map_err(|e: Error| {
|
247
|
+
CallToolError::new(std::io::Error::other(e.to_string()))
|
248
|
+
})?,
|
249
|
+
)
|
250
|
+
} else {
|
251
|
+
None
|
252
|
+
};
|
253
|
+
let text_result: Result<String, Error> = crate::utils::with_gvl(|| {
|
254
|
+
let ruby = Ruby::get().unwrap();
|
255
|
+
let args = args_value.unwrap_or_else(|| ruby.qnil().as_value());
|
256
|
+
proc.call::<_, String>((args, wrapper.clone()))
|
257
|
+
});
|
258
|
+
wrapper.invalidate();
|
95
259
|
match text_result {
|
96
260
|
Ok(text) => Ok(CallToolResult::text_content(text, None)),
|
97
261
|
Err(e) => Err(CallToolError::new(std::io::Error::other(e.to_string()))),
|
@@ -103,50 +267,115 @@ impl ServerHandler for MyServerHandler {
|
|
103
267
|
}
|
104
268
|
|
105
269
|
pub fn start_server() -> String {
|
106
|
-
let runtime = RUNTIME
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
270
|
+
let runtime = RUNTIME.get_or_init(|| Runtime::new().expect("Failed to create Tokio runtime"));
|
271
|
+
|
272
|
+
// Reset shutdown flag for new server start
|
273
|
+
shutdown_flag().store(false, Ordering::Relaxed);
|
274
|
+
|
275
|
+
let _ = nogvl(|| {
|
276
|
+
runtime.block_on(async {
|
277
|
+
let server_details = InitializeResult {
|
278
|
+
server_info: Implementation {
|
279
|
+
name: "Hello World MCP Server".to_string(),
|
280
|
+
version: "0.1.0".to_string(),
|
281
|
+
},
|
282
|
+
capabilities: ServerCapabilities {
|
283
|
+
tools: Some(ServerCapabilitiesTools { list_changed: None }),
|
284
|
+
..Default::default()
|
285
|
+
},
|
286
|
+
meta: None,
|
287
|
+
instructions: Some("server instructions...".to_string()),
|
288
|
+
protocol_version: LATEST_PROTOCOL_VERSION.to_string(),
|
289
|
+
};
|
290
|
+
|
291
|
+
let handler = MyServerHandler {};
|
292
|
+
let transport = StdioTransport::new(TransportOptions::default())?;
|
293
|
+
let server: ServerRuntime =
|
294
|
+
server_runtime::create_server(server_details, transport, handler);
|
295
|
+
|
296
|
+
// Use select! to wait for either server completion or shutdown signal
|
297
|
+
tokio::select! {
|
298
|
+
result = server.start() => {
|
299
|
+
result
|
300
|
+
}
|
301
|
+
_ = shutdown_monitor() => {
|
302
|
+
// Server was requested to shutdown
|
303
|
+
Ok(())
|
304
|
+
}
|
305
|
+
_ = signal_handler() => {
|
306
|
+
// System signal received
|
307
|
+
Ok(())
|
308
|
+
}
|
309
|
+
}
|
310
|
+
})
|
311
|
+
});
|
312
|
+
|
313
|
+
"Ok".into()
|
314
|
+
}
|
315
|
+
|
316
|
+
async fn signal_handler() {
|
317
|
+
use tokio::signal;
|
318
|
+
|
319
|
+
let mut sigint = signal::unix::signal(signal::unix::SignalKind::interrupt()).unwrap();
|
320
|
+
let mut sigterm = signal::unix::signal(signal::unix::SignalKind::terminate()).unwrap();
|
123
321
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
322
|
+
tokio::select! {
|
323
|
+
_ = sigint.recv() => {},
|
324
|
+
_ = sigterm.recv() => {},
|
325
|
+
}
|
326
|
+
}
|
128
327
|
|
129
|
-
|
130
|
-
|
328
|
+
async fn shutdown_monitor() {
|
329
|
+
let flag = shutdown_flag();
|
330
|
+
loop {
|
331
|
+
if flag.load(Ordering::Relaxed) {
|
332
|
+
break;
|
333
|
+
}
|
334
|
+
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
|
335
|
+
}
|
336
|
+
}
|
131
337
|
|
338
|
+
pub fn shutdown_server() -> String {
|
339
|
+
shutdown_flag().store(true, Ordering::Relaxed);
|
132
340
|
"Ok".into()
|
133
341
|
}
|
134
342
|
|
135
343
|
#[cfg(test)]
|
136
344
|
mod tests {
|
345
|
+
use async_trait::async_trait;
|
137
346
|
use rust_mcp_sdk::{
|
138
347
|
mcp_client::client_runtime,
|
139
|
-
schema::{
|
348
|
+
schema::{
|
349
|
+
CallToolRequestParams, ClientCapabilities, CreateMessageRequest, CreateMessageResult,
|
350
|
+
Implementation, InitializeRequestParams, Role, RpcError, TextContent,
|
351
|
+
LATEST_PROTOCOL_VERSION,
|
352
|
+
},
|
140
353
|
McpClient, StdioTransport, TransportOptions,
|
141
354
|
};
|
142
|
-
use
|
355
|
+
use serde_json::json;
|
143
356
|
|
144
357
|
struct TestClientHandler;
|
145
358
|
#[async_trait]
|
146
|
-
impl rust_mcp_sdk::mcp_client::ClientHandler for TestClientHandler {
|
359
|
+
impl rust_mcp_sdk::mcp_client::ClientHandler for TestClientHandler {
|
360
|
+
async fn handle_create_message_request(
|
361
|
+
&self,
|
362
|
+
_request: CreateMessageRequest,
|
363
|
+
_runtime: &dyn McpClient,
|
364
|
+
) -> std::result::Result<CreateMessageResult, RpcError> {
|
365
|
+
Ok(CreateMessageResult {
|
366
|
+
content: TextContent::new("hello".to_string(), None).into(),
|
367
|
+
meta: None,
|
368
|
+
model: "test-model".to_string(),
|
369
|
+
role: Role::Assistant,
|
370
|
+
stop_reason: None,
|
371
|
+
})
|
372
|
+
}
|
373
|
+
}
|
374
|
+
|
375
|
+
use rust_mcp_sdk::error::SdkResult;
|
147
376
|
|
148
377
|
#[tokio::test]
|
149
|
-
async fn hello_world_tool_works() {
|
378
|
+
async fn hello_world_tool_works() -> SdkResult<()> {
|
150
379
|
let transport = StdioTransport::create_with_server_launch(
|
151
380
|
"ruby",
|
152
381
|
vec![
|
@@ -157,8 +386,7 @@ mod tests {
|
|
157
386
|
],
|
158
387
|
None,
|
159
388
|
TransportOptions::default(),
|
160
|
-
)
|
161
|
-
.unwrap();
|
389
|
+
)?;
|
162
390
|
|
163
391
|
let client_details = InitializeRequestParams {
|
164
392
|
capabilities: ClientCapabilities::default(),
|
@@ -171,18 +399,210 @@ mod tests {
|
|
171
399
|
|
172
400
|
let client = client_runtime::create_client(client_details, transport, TestClientHandler);
|
173
401
|
|
174
|
-
client.clone().start().await
|
402
|
+
client.clone().start().await?;
|
175
403
|
|
176
|
-
let tools = client.list_tools(None).await
|
404
|
+
let tools = client.list_tools(None).await?;
|
177
405
|
assert_eq!(tools.tools.len(), 1);
|
178
406
|
assert_eq!(tools.tools[0].name, "say_hello_world");
|
179
407
|
|
180
408
|
let result = client
|
181
|
-
.call_tool(CallToolRequestParams {
|
182
|
-
|
183
|
-
|
184
|
-
|
409
|
+
.call_tool(CallToolRequestParams {
|
410
|
+
name: "say_hello_world".into(),
|
411
|
+
arguments: None,
|
412
|
+
})
|
413
|
+
.await?;
|
414
|
+
let text = result.content[0].as_text_content()?.text.clone();
|
185
415
|
assert_eq!(text, "Hello World!");
|
416
|
+
Ok(())
|
186
417
|
}
|
187
|
-
}
|
188
418
|
|
419
|
+
#[tokio::test]
|
420
|
+
async fn tools_with_arguments_work() -> SdkResult<()> {
|
421
|
+
let transport = StdioTransport::create_with_server_launch(
|
422
|
+
"ruby",
|
423
|
+
vec![
|
424
|
+
"-I".into(),
|
425
|
+
"../../lib".into(),
|
426
|
+
"../../bin/mcp".into(),
|
427
|
+
"../../test/support/argument_tools.rb".into(),
|
428
|
+
],
|
429
|
+
None,
|
430
|
+
TransportOptions::default(),
|
431
|
+
)?;
|
432
|
+
|
433
|
+
let client_details = InitializeRequestParams {
|
434
|
+
capabilities: ClientCapabilities::default(),
|
435
|
+
client_info: Implementation {
|
436
|
+
name: "test-client".into(),
|
437
|
+
version: "0.1.0".into(),
|
438
|
+
},
|
439
|
+
protocol_version: LATEST_PROTOCOL_VERSION.into(),
|
440
|
+
};
|
441
|
+
|
442
|
+
let client = client_runtime::create_client(client_details, transport, TestClientHandler);
|
443
|
+
|
444
|
+
client.clone().start().await?;
|
445
|
+
|
446
|
+
let tools = client.list_tools(None).await?;
|
447
|
+
assert_eq!(tools.tools.len(), 2);
|
448
|
+
assert!(tools.tools.iter().any(|t| t.name == "add_numbers"));
|
449
|
+
assert!(tools.tools.iter().any(|t| t.name == "echo_message"));
|
450
|
+
|
451
|
+
let result = client
|
452
|
+
.call_tool(CallToolRequestParams {
|
453
|
+
name: "add_numbers".into(),
|
454
|
+
arguments: Some(
|
455
|
+
[("a".to_string(), json!(5)), ("b".to_string(), json!(7))]
|
456
|
+
.into_iter()
|
457
|
+
.collect(),
|
458
|
+
),
|
459
|
+
})
|
460
|
+
.await?;
|
461
|
+
let text = result.content[0].as_text_content()?.text.clone();
|
462
|
+
assert_eq!(text, "12");
|
463
|
+
|
464
|
+
let result = client
|
465
|
+
.call_tool(CallToolRequestParams {
|
466
|
+
name: "echo_message".into(),
|
467
|
+
arguments: Some([("message".to_string(), json!("hi"))].into_iter().collect()),
|
468
|
+
})
|
469
|
+
.await?;
|
470
|
+
let text = result.content[0].as_text_content()?.text.clone();
|
471
|
+
assert_eq!(text, "hi");
|
472
|
+
Ok(())
|
473
|
+
}
|
474
|
+
|
475
|
+
#[tokio::test]
|
476
|
+
async fn runtime_lifetime_enforced() -> SdkResult<()> {
|
477
|
+
let transport = StdioTransport::create_with_server_launch(
|
478
|
+
"ruby",
|
479
|
+
vec![
|
480
|
+
"-I".into(),
|
481
|
+
"../../lib".into(),
|
482
|
+
"../../bin/mcp".into(),
|
483
|
+
"../../test/support/runtime_lifetime_tool.rb".into(),
|
484
|
+
],
|
485
|
+
None,
|
486
|
+
TransportOptions::default(),
|
487
|
+
)?;
|
488
|
+
|
489
|
+
let client_details = InitializeRequestParams {
|
490
|
+
capabilities: ClientCapabilities::default(),
|
491
|
+
client_info: Implementation {
|
492
|
+
name: "test-client".into(),
|
493
|
+
version: "0.1.0".into(),
|
494
|
+
},
|
495
|
+
protocol_version: LATEST_PROTOCOL_VERSION.into(),
|
496
|
+
};
|
497
|
+
|
498
|
+
let client = client_runtime::create_client(client_details, transport, TestClientHandler);
|
499
|
+
|
500
|
+
client.clone().start().await?;
|
501
|
+
|
502
|
+
let tools = client.list_tools(None).await?;
|
503
|
+
assert_eq!(tools.tools.len(), 2);
|
504
|
+
|
505
|
+
// first call stores the runtime
|
506
|
+
let result = client
|
507
|
+
.call_tool(CallToolRequestParams {
|
508
|
+
name: "capture_runtime".into(),
|
509
|
+
arguments: None,
|
510
|
+
})
|
511
|
+
.await?;
|
512
|
+
let text = result.content[0].as_text_content()?.text.clone();
|
513
|
+
assert_eq!(text, "true");
|
514
|
+
|
515
|
+
// second call should fail as runtime was invalidated
|
516
|
+
let result = client
|
517
|
+
.call_tool(CallToolRequestParams {
|
518
|
+
name: "use_captured_runtime".into(),
|
519
|
+
arguments: None,
|
520
|
+
})
|
521
|
+
.await?;
|
522
|
+
assert!(result.is_error.unwrap_or(false));
|
523
|
+
let text = result.content[0].as_text_content()?.text.clone();
|
524
|
+
assert!(text.contains("McpServer reference"));
|
525
|
+
|
526
|
+
Ok(())
|
527
|
+
}
|
528
|
+
|
529
|
+
#[tokio::test]
|
530
|
+
async fn client_supports_sampling_exposed() -> SdkResult<()> {
|
531
|
+
let transport = StdioTransport::create_with_server_launch(
|
532
|
+
"ruby",
|
533
|
+
vec![
|
534
|
+
"-I".into(),
|
535
|
+
"../../lib".into(),
|
536
|
+
"../../bin/mcp".into(),
|
537
|
+
"../../test/support/client_capabilities_tool.rb".into(),
|
538
|
+
],
|
539
|
+
None,
|
540
|
+
TransportOptions::default(),
|
541
|
+
)?;
|
542
|
+
|
543
|
+
let client_details = InitializeRequestParams {
|
544
|
+
capabilities: ClientCapabilities::default(),
|
545
|
+
client_info: Implementation {
|
546
|
+
name: "test-client".into(),
|
547
|
+
version: "0.1.0".into(),
|
548
|
+
},
|
549
|
+
protocol_version: LATEST_PROTOCOL_VERSION.into(),
|
550
|
+
};
|
551
|
+
|
552
|
+
let client = client_runtime::create_client(client_details, transport, TestClientHandler);
|
553
|
+
|
554
|
+
client.clone().start().await?;
|
555
|
+
|
556
|
+
let result = client
|
557
|
+
.call_tool(CallToolRequestParams {
|
558
|
+
name: "client_sampling_supported".into(),
|
559
|
+
arguments: None,
|
560
|
+
})
|
561
|
+
.await?;
|
562
|
+
let text = result.content[0].as_text_content()?.text.clone();
|
563
|
+
assert_eq!(text, "false");
|
564
|
+
|
565
|
+
Ok(())
|
566
|
+
}
|
567
|
+
|
568
|
+
#[tokio::test]
|
569
|
+
async fn create_message_exposed() -> SdkResult<()> {
|
570
|
+
let transport = StdioTransport::create_with_server_launch(
|
571
|
+
"ruby",
|
572
|
+
vec![
|
573
|
+
"-I".into(),
|
574
|
+
"../../lib".into(),
|
575
|
+
"../../bin/mcp".into(),
|
576
|
+
"../../test/support/create_message_tool.rb".into(),
|
577
|
+
],
|
578
|
+
None,
|
579
|
+
TransportOptions::default(),
|
580
|
+
)?;
|
581
|
+
|
582
|
+
let client_details = InitializeRequestParams {
|
583
|
+
capabilities: ClientCapabilities::default(),
|
584
|
+
client_info: Implementation {
|
585
|
+
name: "test-client".into(),
|
586
|
+
version: "0.1.0".into(),
|
587
|
+
},
|
588
|
+
protocol_version: LATEST_PROTOCOL_VERSION.into(),
|
589
|
+
};
|
590
|
+
|
591
|
+
let client = client_runtime::create_client(client_details, transport, TestClientHandler);
|
592
|
+
|
593
|
+
client.clone().start().await?;
|
594
|
+
|
595
|
+
let result = client
|
596
|
+
.call_tool(CallToolRequestParams {
|
597
|
+
name: "create_message_error".into(),
|
598
|
+
arguments: None,
|
599
|
+
})
|
600
|
+
.await?;
|
601
|
+
|
602
|
+
assert!(result.is_error.unwrap_or(false));
|
603
|
+
let text = result.content[0].as_text_content()?.text.clone();
|
604
|
+
assert!(text.contains("missing field"));
|
605
|
+
|
606
|
+
Ok(())
|
607
|
+
}
|
608
|
+
}
|
data/ext/micro_mcp/src/utils.rs
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
use std::{ffi::c_void, mem::MaybeUninit, ptr::null_mut};
|
2
1
|
use rb_sys::{rb_thread_call_with_gvl, rb_thread_call_without_gvl};
|
2
|
+
use std::{ffi::c_void, mem::MaybeUninit, ptr::null_mut};
|
3
3
|
|
4
4
|
unsafe extern "C" fn call_without_gvl<F, R>(arg: *mut c_void) -> *mut c_void
|
5
5
|
where
|