ruby-mcp-client 1.0.0 → 1.1.0
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/README.md +139 -4
- data/lib/mcp_client/auth/oauth_provider.rb +362 -46
- data/lib/mcp_client/auth.rb +79 -12
- data/lib/mcp_client/client.rb +197 -34
- data/lib/mcp_client/errors.rb +9 -0
- data/lib/mcp_client/http_transport_base.rb +5 -1
- data/lib/mcp_client/json_rpc_common.rb +19 -4
- data/lib/mcp_client/server_base.rb +73 -0
- data/lib/mcp_client/server_http.rb +27 -17
- data/lib/mcp_client/server_sse/json_rpc_transport.rb +4 -1
- data/lib/mcp_client/server_sse/reconnect_monitor.rb +4 -0
- data/lib/mcp_client/server_sse/sse_parser.rb +5 -2
- data/lib/mcp_client/server_sse.rb +49 -36
- data/lib/mcp_client/server_stdio/json_rpc_transport.rb +13 -3
- data/lib/mcp_client/server_stdio.rb +124 -20
- data/lib/mcp_client/server_streamable_http.rb +11 -30
- data/lib/mcp_client/task.rb +93 -61
- data/lib/mcp_client/tool.rb +48 -10
- data/lib/mcp_client/version.rb +1 -1
- metadata +18 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e99574cdccfbab39918f6576fc7c896b5208599313bd69e2f39df5b529c82c22
|
|
4
|
+
data.tar.gz: 823177bc1a531d773514b9d9da8d3d6f0f2aa15f6e8562e8c0123adcdb38911c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bc352d545dbabb90322ab473eaff7c6d52111ccd6e4812baa8b6507dc720024e23752b4a68481dc5b83722d2c9c4b6b3a36a17821998fc745cd8164e57ac8380
|
|
7
|
+
data.tar.gz: 72b2cdc279b54f2b10cbac22755086edee67792bd18e035d167f2855ad1d5dc5999c87d98e17bef83a70632459003bbd4af516414fe0e7b78b918fc2b2c5a8ce
|
data/README.md
CHANGED
|
@@ -38,7 +38,7 @@ Implements **MCP 2025-11-25** specification:
|
|
|
38
38
|
- **Sampling**: Server-requested LLM completions with modelPreferences
|
|
39
39
|
- **Completion**: Autocomplete for prompts/resources with context
|
|
40
40
|
- **Logging**: Server log messages with level filtering
|
|
41
|
-
- **Tasks**:
|
|
41
|
+
- **Tasks**: Task-augmented `tools/call` — create with a `ttl`, poll `tasks/get`, retrieve via `tasks/result`, plus `tasks/list` and `tasks/cancel`
|
|
42
42
|
- **Audio**: Audio content type support
|
|
43
43
|
- **OAuth 2.1**: PKCE, server discovery, dynamic registration
|
|
44
44
|
|
|
@@ -107,6 +107,11 @@ end
|
|
|
107
107
|
prompts = client.list_prompts
|
|
108
108
|
result = client.get_prompt('greeting', { name: 'Alice' })
|
|
109
109
|
|
|
110
|
+
# Pagination: list_tools and list_prompts automatically follow the server's
|
|
111
|
+
# nextCursor and return the COMPLETE set across all pages (with a per-call
|
|
112
|
+
# safety bound and an identical-cursor loop guard). No manual cursor handling
|
|
113
|
+
# is required.
|
|
114
|
+
|
|
110
115
|
# Resources
|
|
111
116
|
result = client.list_resources
|
|
112
117
|
contents = client.read_resource('file:///example.txt')
|
|
@@ -124,9 +129,12 @@ end
|
|
|
124
129
|
tool = client.find_tool('delete_user')
|
|
125
130
|
|
|
126
131
|
# Hint-style annotations (MCP 2025-11-25)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
132
|
+
# Defaults follow the MCP ToolAnnotations schema: when a hint is absent the
|
|
133
|
+
# client assumes the less-safe value, so an un-annotated tool is treated as
|
|
134
|
+
# writable, potentially destructive, and open-world.
|
|
135
|
+
tool.read_only_hint? # Defaults to false; tool may modify its environment
|
|
136
|
+
tool.destructive_hint? # Defaults to true; tool may perform destructive updates
|
|
137
|
+
tool.idempotent_hint? # Defaults to false; repeated calls may have additional effects
|
|
130
138
|
tool.open_world_hint? # Defaults to true; tool may interact with external entities
|
|
131
139
|
|
|
132
140
|
# Legacy annotations
|
|
@@ -200,6 +208,39 @@ client.on_notification do |server, method, params|
|
|
|
200
208
|
end
|
|
201
209
|
```
|
|
202
210
|
|
|
211
|
+
### Tasks (Long-running, task-augmented tools)
|
|
212
|
+
|
|
213
|
+
A task-capable server (one advertising `tasks.requests.tools.call`) can run a tool
|
|
214
|
+
whose `execution.taskSupport` is `optional` or `required` as a background task:
|
|
215
|
+
the call returns immediately with a task handle, and the result is fetched later.
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
tool = client.find_tool('long_job')
|
|
219
|
+
tool.supports_task? # execution.taskSupport is optional/required?
|
|
220
|
+
|
|
221
|
+
# Create the task (returns immediately); ttl is the requested lifetime in ms
|
|
222
|
+
task = client.call_tool_as_task('long_job', { input: 'data' }, ttl: 60_000)
|
|
223
|
+
|
|
224
|
+
# Poll until the task reaches a terminal (or input-required) status,
|
|
225
|
+
# honoring the server's suggested poll interval
|
|
226
|
+
until task.terminal? || task.input_required?
|
|
227
|
+
sleep((task.poll_interval || 1000) / 1000.0)
|
|
228
|
+
task = client.get_task(task.task_id) # tasks/get
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Retrieve the underlying result (e.g. a CallToolResult) via tasks/result
|
|
232
|
+
result = client.get_task_result(task.task_id)
|
|
233
|
+
|
|
234
|
+
# List and cancel tasks
|
|
235
|
+
page = client.list_tasks # { tasks: [...], next_cursor: ... }
|
|
236
|
+
client.cancel_task(task.task_id) # tasks/cancel
|
|
237
|
+
|
|
238
|
+
# React to server-pushed status updates
|
|
239
|
+
client.on_notification do |server, method, params|
|
|
240
|
+
puts "Task #{params['taskId']} -> #{params['status']}" if method == 'notifications/tasks/status'
|
|
241
|
+
end
|
|
242
|
+
```
|
|
243
|
+
|
|
203
244
|
### Elicitation (Server-initiated user interactions)
|
|
204
245
|
|
|
205
246
|
```ruby
|
|
@@ -243,6 +284,18 @@ client = MCPClient.create_client(
|
|
|
243
284
|
client = MCPClient.create_client(server_definition_file: 'servers.json')
|
|
244
285
|
```
|
|
245
286
|
|
|
287
|
+
### Retries
|
|
288
|
+
|
|
289
|
+
The `retries:` option controls automatic retry with exponential backoff. Only
|
|
290
|
+
failures where the request most likely did **not** complete at the server are
|
|
291
|
+
retried: transport/network errors and HTTP **5xx** responses. Application-level
|
|
292
|
+
failures — a JSON-RPC error response or an HTTP **4xx** — are **never** retried,
|
|
293
|
+
because the server already processed or rejected the request and re-sending
|
|
294
|
+
would risk re-executing a non-idempotent `tools/call`. Retryable server failures
|
|
295
|
+
raise `MCPClient::Errors::TransientServerError`, a subclass of
|
|
296
|
+
`MCPClient::Errors::ServerError`, so existing `rescue ServerError` handlers are
|
|
297
|
+
unaffected.
|
|
298
|
+
|
|
246
299
|
### Faraday Customization
|
|
247
300
|
|
|
248
301
|
```ruby
|
|
@@ -303,14 +356,96 @@ client = Anthropic::Client.new(access_token: ENV['ANTHROPIC_API_KEY'])
|
|
|
303
356
|
# Use tools with Claude API
|
|
304
357
|
```
|
|
305
358
|
|
|
359
|
+
### RubyLLM
|
|
360
|
+
|
|
361
|
+
```ruby
|
|
362
|
+
require 'mcp_client'
|
|
363
|
+
require 'ruby_llm'
|
|
364
|
+
|
|
365
|
+
RubyLLM.configure { |c| c.openai_api_key = ENV['OPENAI_API_KEY'] }
|
|
366
|
+
mcp = MCPClient.connect('http://localhost:8931/mcp') # Playwright MCP
|
|
367
|
+
|
|
368
|
+
# Wrap each MCP tool as a RubyLLM tool
|
|
369
|
+
tools = mcp.list_tools.map do |t|
|
|
370
|
+
tool_name = t.name
|
|
371
|
+
Class.new(RubyLLM::Tool) do
|
|
372
|
+
description t.description
|
|
373
|
+
params t.schema
|
|
374
|
+
define_method(:name) { tool_name }
|
|
375
|
+
define_method(:execute) { |**args| mcp.call_tool(tool_name, args) }
|
|
376
|
+
end.new
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
chat = RubyLLM.chat(model: 'gpt-4o-mini')
|
|
380
|
+
tools.each { |tool| chat.with_tool(tool) }
|
|
381
|
+
response = chat.ask('Navigate to google.com and tell me the page title')
|
|
382
|
+
```
|
|
383
|
+
|
|
306
384
|
See `examples/` for complete implementations:
|
|
307
385
|
- `ruby_openai_mcp.rb`, `openai_ruby_mcp.rb` - OpenAI integration
|
|
308
386
|
- `ruby_anthropic_mcp.rb` - Anthropic integration
|
|
309
387
|
- `gemini_ai_mcp.rb` - Google Vertex AI integration
|
|
388
|
+
- `ruby_llm_mcp.rb` - RubyLLM integration (OpenAI provider)
|
|
389
|
+
|
|
390
|
+
## Running the Examples
|
|
391
|
+
|
|
392
|
+
The `examples/run_all_examples.sh` harness runs every example that can run on the current machine — self-contained stdio servers, the Python/Flask/FastMCP echo and elicitation servers, `npx`-based MCP servers, and (optionally) the paid LLM integrations. It starts and tears down each server automatically and prints a `PASS`/`FAIL`/`SKIP` summary. `tasks_example.rb` is always skipped (it needs a task-capable remote server); `oauth_browser_auth.rb` is interactive and only runs when you opt in with `RUN_OAUTH=1`.
|
|
393
|
+
|
|
394
|
+
### Prerequisites
|
|
395
|
+
|
|
396
|
+
Run `bundle install` first. The script preflight-checks the following and prints a warning (it does **not** abort) for anything missing; affected examples are then skipped or fail:
|
|
397
|
+
|
|
398
|
+
- `ruby`, `bundle`, `curl`, `lsof` - on `PATH`
|
|
399
|
+
- `python3` (or `$PYTHON`) plus a separate `python` binary - on `PATH`
|
|
400
|
+
- Python packages `flask`, `fastmcp`, `mcp` - importable by `$PYTHON`
|
|
401
|
+
- `npx` (Node) - needed by the `npx`-based example (`json_input`) and by every LLM example, which spawn `npx` filesystem/Playwright servers
|
|
402
|
+
|
|
403
|
+
### Usage
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
examples/run_all_examples.sh # run everything runnable on this machine
|
|
407
|
+
RUN_AI=0 examples/run_all_examples.sh # skip the paid-LLM examples
|
|
408
|
+
RUN_NPX=0 examples/run_all_examples.sh # skip the npx-based example (json_input)
|
|
409
|
+
LOG_DIR=/path examples/run_all_examples.sh # write logs to a chosen dir
|
|
410
|
+
PYTHON=python3.12 TIMEOUT=180 examples/run_all_examples.sh # override interpreter and per-example timeout
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### Environment Knobs
|
|
414
|
+
|
|
415
|
+
| Variable | Default | Effect |
|
|
416
|
+
|----------|---------|--------|
|
|
417
|
+
| `RUN_AI` | `1` | Set to `0` to skip the LLM integrations, which make **real, paid** API calls. |
|
|
418
|
+
| `RUN_NPX` | `1` | Set to `0` (or leave `npx` off `PATH`) to skip the `npx`-based example (`json_input`). The LLM examples spawn `npx` servers too, but are gated by `RUN_AI` and their API keys instead. |
|
|
419
|
+
| `PYTHON` | `python3` | Interpreter used to launch the Python/Flask/FastMCP servers and run the import preflight checks. |
|
|
420
|
+
| `TIMEOUT` | `120` | Per-example wall-clock timeout in seconds; a timeout is reported as a `FAIL`. |
|
|
421
|
+
| `LOG_DIR` | fresh `mktemp` dir | Directory for per-example and per-server logs; the path is printed after preflight and in the summary. |
|
|
422
|
+
|
|
423
|
+
### Secrets and API Keys
|
|
424
|
+
|
|
425
|
+
Real secrets live in `examples/secrets.env`, which is **gitignored** and sourced automatically (every `KEY=value` line is exported) when present. Copy the tracked template to get started:
|
|
426
|
+
|
|
427
|
+
```bash
|
|
428
|
+
cp examples/secrets.env.example examples/secrets.env
|
|
429
|
+
# then set ZAPIER_MCP_TOKEN=... to enable the Zapier streamable-HTTP example
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Set `ZAPIER_MCP_TOKEN` (from the Zapier MCP setup page, "Option 1: Authorization header") to run `streamable_http_example.rb` and `oauth_example.rb` against Zapier; override `ZAPIER_MCP_URL` if your connect URL differs. To run the interactive `oauth_browser_auth.rb`, set `MCP_SERVER_URL` (e.g. an ngrok tunnel to your OAuth-protected MCP server) in `secrets.env` and pass `RUN_OAUTH=1`. The LLM examples each need their own credentials in the environment and are skipped without them:
|
|
433
|
+
|
|
434
|
+
- `ruby_anthropic_mcp.rb` - `ANTHROPIC_API_KEY` (+ `npx`)
|
|
435
|
+
- `openai_ruby_mcp.rb` - `OPENAI_API_KEY` (+ `npx`)
|
|
436
|
+
- `ruby_openai_mcp.rb`, `ruby_llm_mcp.rb` - `OPENAI_API_KEY` (+ `npx`, plus a Playwright MCP server on `:8931`)
|
|
437
|
+
- `gemini_ai_mcp.rb` - a Vertex service-account JSON at `VERTEX_CREDENTIALS_FILE` (default `examples/google-credentials.json`, + `npx`)
|
|
438
|
+
|
|
439
|
+
### How Pass/Fail Is Judged
|
|
440
|
+
|
|
441
|
+
Most examples print their own success/failure marks but exit `0` regardless, so the harness combines the exit code with a scan of the output rather than trusting the exit status alone. An example `FAIL`s when it exits nonzero, times out (exit `124`), prints a hard-error signature (a Ruby/Python traceback, `Connection refused`, `uninitialized constant`, and similar), prints a `❌` mark, or is missing its expected success marker; otherwise it `PASS`es. (The `❌` check is suppressed with `IGNORE_XMARK=1` for the interactive elicitation demos, where `❌` can be legitimate "declined" output.) The script exits `0` only if zero examples failed — `SKIP`s do not affect the exit status.
|
|
442
|
+
|
|
443
|
+
For deeper, per-topic walkthroughs see [`examples/README.md`](examples/README.md), [`examples/README_ECHO_SERVER.md`](examples/README_ECHO_SERVER.md), [`examples/STREAMABLE_HTTP_TESTING.md`](examples/STREAMABLE_HTTP_TESTING.md), and [`examples/elicitation/README.md`](examples/elicitation/README.md).
|
|
310
444
|
|
|
311
445
|
## OAuth 2.1 Authentication
|
|
312
446
|
|
|
313
447
|
```ruby
|
|
448
|
+
require 'mcp_client'
|
|
314
449
|
require 'mcp_client/auth/browser_oauth'
|
|
315
450
|
|
|
316
451
|
oauth = MCPClient::Auth::OAuthProvider.new(
|