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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e7e861ecba26ec407886fbb6d169b85c78ecba0768552b512cbe6c9c1f6216ad
4
- data.tar.gz: d71bab971376849e2bf72e2598116087cdb4f072c556368471d1d9adfe1bd7b5
3
+ metadata.gz: e99574cdccfbab39918f6576fc7c896b5208599313bd69e2f39df5b529c82c22
4
+ data.tar.gz: 823177bc1a531d773514b9d9da8d3d6f0f2aa15f6e8562e8c0123adcdb38911c
5
5
  SHA512:
6
- metadata.gz: 20d61bad3b57193dbc1a8019b87ec85758c55f61edc7c37808e7f1bc3a4fb60f35532f694aa68334273da31245ef6541cddbf2c4d0dc720d122bfae09488b415
7
- data.tar.gz: e3e3ec6840cf41e0b18d6112a7ff602a798cdcb6108b1c75afd5ecaf68724adee2109dca6782e884e7b2db51d8dd8ef09fd414e20d3d7ec0c1bc072225e33427
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**: Structured task management with progress tracking
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
- tool.read_only_hint? # Defaults to true; tool does not modify environment
128
- tool.destructive_hint? # Defaults to false; tool may perform destructive updates
129
- tool.idempotent_hint? # Defaults to false; repeated calls have no additional effect
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(