ruby-mcp-client 1.0.1 → 2.0.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: 8e56ef84106ab19bf3028b31dd7ce362cfb58f244743057271f14a83409153f0
4
- data.tar.gz: 96f0e42875d80032c24056c1dbd9c6417b3db994538838736fa52f303e0d4f85
3
+ metadata.gz: 5687a9c54b7e5c4814e80bd2e5e97d2072dd0f994e3bebe680259a1a9db64c25
4
+ data.tar.gz: 05b3bce582f21e57ae07ac862daf6a74812f3ba79f6e4b806868850686c33467
5
5
  SHA512:
6
- metadata.gz: 48b7cd77ab406967bc4cd5fe79c5c1511a8eb2c9e4c9ebc27b31b8386e175a435d1a620ffcfa7871044188061b3f0718875d009f2a4ef7d030786ad11bcbde35
7
- data.tar.gz: 11f845c21895b67d0a308267972936fcf07df042e33de6417b69ee486f1739a8768f25b698bbbad92fdbef3acf0e6ec42eaffa525896b9e77e50e86960c7daf8
6
+ metadata.gz: 3aef7d0842eab30de7e84a5507298efd30ae78de3ea3223cc8f00bd31892e7665f55f533952df771a2d3ab38f874d3905e029b0d19b8147d07ffd030772106fe
7
+ data.tar.gz: 324f5121352512f827f05fa55f78917b3218b7d031c418cfa884a8718b2b06fda027b8b811df2083361200ff17682dbfea8998505db0e2932db3a545f605c35a
data/README.md CHANGED
@@ -28,7 +28,10 @@ Built-in API conversions: `to_openai_tools()`, `to_anthropic_tools()`, `to_googl
28
28
 
29
29
  ## MCP Protocol Support
30
30
 
31
- Implements **MCP 2025-11-25** specification:
31
+ Implements the **MCP 2025-11-25** specification. The client negotiates the
32
+ protocol version during `initialize` and disconnects if the server answers
33
+ with a revision it cannot speak (supported: `2025-11-25`, `2025-06-18`,
34
+ `2025-03-26`, `2024-11-05`):
32
35
 
33
36
  - **Tools**: list, call, streaming, annotations (hint-style), structured outputs, title
34
37
  - **Prompts**: list, get with parameters
@@ -38,9 +41,11 @@ Implements **MCP 2025-11-25** specification:
38
41
  - **Sampling**: Server-requested LLM completions with modelPreferences
39
42
  - **Completion**: Autocomplete for prompts/resources with context
40
43
  - **Logging**: Server log messages with level filtering
41
- - **Tasks**: Structured task management with progress tracking
44
+ - **Tasks**: Task-augmented `tools/call` create with a `ttl`, poll `tasks/get`, retrieve via `tasks/result`, plus `tasks/list` and `tasks/cancel`
42
45
  - **Audio**: Audio content type support
43
- - **OAuth 2.1**: PKCE, server discovery, dynamic registration
46
+ - **Progress & Cancellation**: `progressToken` plumbing with per-call callbacks; automatic `notifications/cancelled` for abandoned requests
47
+ - **Metadata**: `icons`, `title` and `_meta` parsed on tools, prompts and resources
48
+ - **OAuth 2.1**: PKCE (S256 required), RFC 8414/9728 discovery, dynamic registration, Client ID Metadata Documents, scope step-up challenges
44
49
 
45
50
  ## Quick Connect API (Recommended)
46
51
 
@@ -107,6 +112,11 @@ end
107
112
  prompts = client.list_prompts
108
113
  result = client.get_prompt('greeting', { name: 'Alice' })
109
114
 
115
+ # Pagination: list_tools and list_prompts automatically follow the server's
116
+ # nextCursor and return the COMPLETE set across all pages (with a per-call
117
+ # safety bound and an identical-cursor loop guard). No manual cursor handling
118
+ # is required.
119
+
110
120
  # Resources
111
121
  result = client.list_resources
112
122
  contents = client.read_resource('file:///example.txt')
@@ -124,9 +134,12 @@ end
124
134
  tool = client.find_tool('delete_user')
125
135
 
126
136
  # 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
137
+ # Defaults follow the MCP ToolAnnotations schema: when a hint is absent the
138
+ # client assumes the less-safe value, so an un-annotated tool is treated as
139
+ # writable, potentially destructive, and open-world.
140
+ tool.read_only_hint? # Defaults to false; tool may modify its environment
141
+ tool.destructive_hint? # Defaults to true; tool may perform destructive updates
142
+ tool.idempotent_hint? # Defaults to false; repeated calls may have additional effects
130
143
  tool.open_world_hint? # Defaults to true; tool may interact with external entities
131
144
 
132
145
  # Legacy annotations
@@ -144,6 +157,25 @@ tool.output_schema # JSON Schema for output
144
157
 
145
158
  result = client.call_tool('get_weather', { location: 'SF' })
146
159
  data = result['structuredContent'] # Type-safe structured data
160
+
161
+ # Per MCP 2025-11-25, clients SHOULD validate structured results against the
162
+ # tool's output schema, and a tool that declares an outputSchema must return
163
+ # structuredContent in successful results. call_tool checks both automatically
164
+ # for the common JSON Schema keywords (type, properties, required, items, enum,
165
+ # numeric/string bounds). The full 2020-12 vocabulary ($ref/$dynamicRef/$defs,
166
+ # allOf/anyOf/oneOf/not, if/then/else, additionalProperties, patternProperties,
167
+ # propertyNames, prefixItems, contains/minContains/maxContains, uniqueItems,
168
+ # multipleOf, format, dependentRequired/dependentSchemas, minProperties/
169
+ # maxProperties, unevaluated*) is NOT evaluated: when a schema uses any of
170
+ # those keywords, call_tool logs a "validation is partial" warning naming them
171
+ # (in both modes), since data may pass this check that a full validator would
172
+ # reject. By default a violation (mismatch, or missing structuredContent on a
173
+ # successful result) logs a warning; opt in to strict mode to raise instead:
174
+ client = MCPClient::Client.new(
175
+ mcp_server_configs: [...],
176
+ validate_structured_content: :strict # raises MCPClient::Errors::ValidationError on violation
177
+ )
178
+ # Task-delivered results (get_task_result) are not validated yet.
147
179
  ```
148
180
 
149
181
  ### Roots
@@ -176,6 +208,81 @@ client = MCPClient.connect('http://server/mcp',
176
208
  )
177
209
  ```
178
210
 
211
+ Sampling tool calling (SEP-1577) is opt-in: pass `sampling_supports_tools: true`
212
+ to declare the `sampling.tools` capability. The handler then receives the full
213
+ request params (including `tools`/`toolChoice`) as an optional fifth argument;
214
+ without the opt-in, tool-enabled sampling requests are rejected with `-32602`
215
+ as the spec requires:
216
+
217
+ ```ruby
218
+ client = MCPClient::Client.new(
219
+ mcp_server_configs: [...],
220
+ sampling_supports_tools: true,
221
+ sampling_handler: ->(messages, prefs, system_prompt, max_tokens, params = nil) {
222
+ tools = params && params['tools'] # ToolUseContent may be returned in content
223
+ # ...
224
+ }
225
+ )
226
+ ```
227
+
228
+ ### Progress Tracking
229
+
230
+ Attach a per-call progress callback — the client generates a unique
231
+ `progressToken`, places it in the request `_meta`, and routes matching
232
+ `notifications/progress` to your block while the request is active (stale
233
+ tokens after completion are dropped):
234
+
235
+ ```ruby
236
+ client.call_tool('long_running', args, progress: ->(progress, total, message) {
237
+ puts "#{message}: #{progress}/#{total}"
238
+ })
239
+ ```
240
+
241
+ A request-level `_meta` (e.g. a hand-picked `progressToken`) can also be passed
242
+ inside the arguments under the `'_meta'` key on every transport — it is hoisted
243
+ to the JSON-RPC params level on the wire, never sent as a tool argument.
244
+
245
+ ### Timeouts and Cancellation
246
+
247
+ Timeouts are configurable per request in addition to the per-server
248
+ `read_timeout`. A timed-out request raises
249
+ `MCPClient::Errors::RequestTimeoutError` (a `TransportError` subclass), is
250
+ **never** silently re-sent by the retry layer, and a best-effort
251
+ `notifications/cancelled` is sent for the abandoned request (never for
252
+ `initialize`, and task-augmented calls use `tasks/cancel` instead):
253
+
254
+ ```ruby
255
+ client.send_rpc('tools/call', params: { name: 'slow', arguments: {} }, timeout: 300)
256
+ server.rpc_request('tools/list', {}, timeout: 5)
257
+ ```
258
+
259
+ ### Client Identity and Server Instructions
260
+
261
+ Hosts can present their own `Implementation` info (sent as `clientInfo` during
262
+ initialize; `name` and `version` required — `title`, `description`,
263
+ `websiteUrl`, `icons` optional), and read the server's `instructions` hint
264
+ after connecting:
265
+
266
+ ```ruby
267
+ client = MCPClient::Client.new(
268
+ mcp_server_configs: [...],
269
+ client_info: { 'name' => 'my-ide', 'version' => '2.0.0', 'description' => 'An MCP-powered IDE' }
270
+ )
271
+ client.servers.first.connect
272
+ puts client.servers.first.instructions # e.g. "Use the search tool before answering."
273
+ ```
274
+
275
+ ### Capability Gating
276
+
277
+ Optional server features (`logging/setLevel`, `resources/subscribe`,
278
+ `completion/complete`, `tasks/list`, `tasks/cancel`) are only sent to servers
279
+ that negotiated the corresponding capability; otherwise
280
+ `MCPClient::Errors::CapabilityError` is raised (the lifecycle forbids using
281
+ capabilities that were not negotiated). `Client#log_level=` skips
282
+ non-logging servers instead of failing. Declared *client* capabilities are
283
+ derived from what the host actually registered (handlers, roots), never
284
+ hardcoded.
285
+
179
286
  ### Completion (Autocomplete)
180
287
 
181
288
  ```ruby
@@ -200,6 +307,42 @@ client.on_notification do |server, method, params|
200
307
  end
201
308
  ```
202
309
 
310
+ ### Tasks (Long-running, task-augmented tools)
311
+
312
+ A task-capable server (one advertising `tasks.requests.tools.call`) can run a tool
313
+ whose `execution.taskSupport` is `optional` or `required` as a background task:
314
+ the call returns immediately with a task handle, and the result is fetched later.
315
+ Try it locally: `python3 examples/echo_server_streamable.py &` then
316
+ `./examples/tasks_example.rb` runs the full lifecycle against a task-capable
317
+ demo server.
318
+
319
+ ```ruby
320
+ tool = client.find_tool('long_job')
321
+ tool.supports_task? # execution.taskSupport is optional/required?
322
+
323
+ # Create the task (returns immediately); ttl is the requested lifetime in ms
324
+ task = client.call_tool_as_task('long_job', { input: 'data' }, ttl: 60_000)
325
+
326
+ # Poll until the task reaches a terminal (or input-required) status,
327
+ # honoring the server's suggested poll interval
328
+ until task.terminal? || task.input_required?
329
+ sleep((task.poll_interval || 1000) / 1000.0)
330
+ task = client.get_task(task.task_id) # tasks/get
331
+ end
332
+
333
+ # Retrieve the underlying result (e.g. a CallToolResult) via tasks/result
334
+ result = client.get_task_result(task.task_id)
335
+
336
+ # List and cancel tasks
337
+ page = client.list_tasks # { tasks: [...], next_cursor: ... }
338
+ client.cancel_task(task.task_id) # tasks/cancel
339
+
340
+ # React to server-pushed status updates
341
+ client.on_notification do |server, method, params|
342
+ puts "Task #{params['taskId']} -> #{params['status']}" if method == 'notifications/tasks/status'
343
+ end
344
+ ```
345
+
203
346
  ### Elicitation (Server-initiated user interactions)
204
347
 
205
348
  ```ruby
@@ -243,6 +386,18 @@ client = MCPClient.create_client(
243
386
  client = MCPClient.create_client(server_definition_file: 'servers.json')
244
387
  ```
245
388
 
389
+ ### Retries
390
+
391
+ The `retries:` option controls automatic retry with exponential backoff. Only
392
+ failures where the request most likely did **not** complete at the server are
393
+ retried: transport/network errors and HTTP **5xx** responses. Application-level
394
+ failures — a JSON-RPC error response or an HTTP **4xx** — are **never** retried,
395
+ because the server already processed or rejected the request and re-sending
396
+ would risk re-executing a non-idempotent `tools/call`. Retryable server failures
397
+ raise `MCPClient::Errors::TransientServerError`, a subclass of
398
+ `MCPClient::Errors::ServerError`, so existing `rescue ServerError` handlers are
399
+ unaffected.
400
+
246
401
  ### Faraday Customization
247
402
 
248
403
  ```ruby
@@ -334,9 +489,65 @@ See `examples/` for complete implementations:
334
489
  - `gemini_ai_mcp.rb` - Google Vertex AI integration
335
490
  - `ruby_llm_mcp.rb` - RubyLLM integration (OpenAI provider)
336
491
 
492
+ ## Running the Examples
493
+
494
+ 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`.
495
+
496
+ ### Prerequisites
497
+
498
+ 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:
499
+
500
+ - `ruby`, `bundle`, `curl`, `lsof` - on `PATH`
501
+ - `python3` (or `$PYTHON`) plus a separate `python` binary - on `PATH`
502
+ - Python packages `flask`, `fastmcp`, `mcp` - importable by `$PYTHON`
503
+ - `npx` (Node) - needed by the `npx`-based example (`json_input`) and by every LLM example, which spawn `npx` filesystem/Playwright servers
504
+
505
+ ### Usage
506
+
507
+ ```bash
508
+ examples/run_all_examples.sh # run everything runnable on this machine
509
+ RUN_AI=0 examples/run_all_examples.sh # skip the paid-LLM examples
510
+ RUN_NPX=0 examples/run_all_examples.sh # skip the npx-based example (json_input)
511
+ LOG_DIR=/path examples/run_all_examples.sh # write logs to a chosen dir
512
+ PYTHON=python3.12 TIMEOUT=180 examples/run_all_examples.sh # override interpreter and per-example timeout
513
+ ```
514
+
515
+ ### Environment Knobs
516
+
517
+ | Variable | Default | Effect |
518
+ |----------|---------|--------|
519
+ | `RUN_AI` | `1` | Set to `0` to skip the LLM integrations, which make **real, paid** API calls. |
520
+ | `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. |
521
+ | `PYTHON` | `python3` | Interpreter used to launch the Python/Flask/FastMCP servers and run the import preflight checks. |
522
+ | `TIMEOUT` | `120` | Per-example wall-clock timeout in seconds; a timeout is reported as a `FAIL`. |
523
+ | `LOG_DIR` | fresh `mktemp` dir | Directory for per-example and per-server logs; the path is printed after preflight and in the summary. |
524
+
525
+ ### Secrets and API Keys
526
+
527
+ 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:
528
+
529
+ ```bash
530
+ cp examples/secrets.env.example examples/secrets.env
531
+ # then set ZAPIER_MCP_TOKEN=... to enable the Zapier streamable-HTTP example
532
+ ```
533
+
534
+ 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:
535
+
536
+ - `ruby_anthropic_mcp.rb` - `ANTHROPIC_API_KEY` (+ `npx`)
537
+ - `openai_ruby_mcp.rb` - `OPENAI_API_KEY` (+ `npx`)
538
+ - `ruby_openai_mcp.rb`, `ruby_llm_mcp.rb` - `OPENAI_API_KEY` (+ `npx`, plus a Playwright MCP server on `:8931`)
539
+ - `gemini_ai_mcp.rb` - a Vertex service-account JSON at `VERTEX_CREDENTIALS_FILE` (default `examples/google-credentials.json`, + `npx`)
540
+
541
+ ### How Pass/Fail Is Judged
542
+
543
+ 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.
544
+
545
+ 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).
546
+
337
547
  ## OAuth 2.1 Authentication
338
548
 
339
549
  ```ruby
550
+ require 'mcp_client'
340
551
  require 'mcp_client/auth/browser_oauth'
341
552
 
342
553
  oauth = MCPClient::Auth::OAuthProvider.new(
@@ -361,6 +572,20 @@ Features: PKCE, server discovery (`.well-known`), dynamic registration, token re
361
572
 
362
573
  See [OAUTH.md](OAUTH.md) for full documentation.
363
574
 
575
+ ### OAuth Extras (2025-11-25)
576
+
577
+ - **Client ID Metadata Documents (SEP-991)** — pass
578
+ `client_id_metadata_url: 'https://myapp.example/oauth-client.json'` (an HTTPS
579
+ URL with a path, which doubles as the `client_id`); when the authorization
580
+ server advertises `client_id_metadata_document_supported`, dynamic client
581
+ registration is skipped entirely.
582
+ - **Scope challenges (SEP-835)** — an HTTP 403 `insufficient_scope` challenge
583
+ raises `MCPClient::Errors::InsufficientScopeError` (a `ConnectionError`
584
+ subclass) exposing `#scope` and `#error_description`; the challenged scopes
585
+ are treated as authoritative for the next authorization flow.
586
+ - **PKCE** — authorization refuses to proceed when the authorization server
587
+ does not advertise `code_challenge_methods_supported` including `S256`.
588
+
364
589
  ## Server Notifications
365
590
 
366
591
  ```ruby
@@ -383,7 +608,9 @@ Both HTTP and Streamable HTTP transports automatically handle session-based serv
383
608
  - **Session capture**: Extracts `Mcp-Session-Id` from initialize response
384
609
  - **Session persistence**: Includes session header in subsequent requests
385
610
  - **Session termination**: Sends DELETE request during cleanup
386
- - **Resumability** (Streamable HTTP): Tracks event IDs for message replay
611
+ - **Resumability** (Streamable HTTP, SEP-1699): tracks SSE event IDs and, when a
612
+ response stream is interrupted, resumes via GET with `Last-Event-ID` so the
613
+ server can replay missed messages — honoring the server's `retry:` directive
387
614
 
388
615
  No configuration required - works automatically.
389
616