ruby-mcp-client 0.8.0 → 0.9.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 +495 -9
- data/lib/mcp_client/auth/browser_oauth.rb +424 -0
- data/lib/mcp_client/auth/oauth_provider.rb +131 -19
- data/lib/mcp_client/auth.rb +1 -1
- data/lib/mcp_client/client.rb +159 -45
- data/lib/mcp_client/json_rpc_common.rb +3 -1
- data/lib/mcp_client/resource_content.rb +80 -0
- data/lib/mcp_client/resource_template.rb +57 -0
- data/lib/mcp_client/server_base.rb +31 -3
- data/lib/mcp_client/server_factory.rb +4 -2
- data/lib/mcp_client/server_http.rb +150 -0
- data/lib/mcp_client/server_sse/sse_parser.rb +11 -0
- data/lib/mcp_client/server_sse.rb +198 -12
- data/lib/mcp_client/server_stdio/json_rpc_transport.rb +5 -0
- data/lib/mcp_client/server_stdio.rb +197 -7
- data/lib/mcp_client/server_streamable_http/json_rpc_transport.rb +8 -1
- data/lib/mcp_client/server_streamable_http.rb +198 -16
- data/lib/mcp_client/tool.rb +40 -4
- data/lib/mcp_client/version.rb +2 -2
- data/lib/mcp_client.rb +2 -0
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a5e35c00292938e3c6cd17885d6578e465d12853ed1844f67dc4633096bb2fa3
|
|
4
|
+
data.tar.gz: e9419a3969689a0768ea8290f2a45f5214fcc4b74521ac38aac3bfd842b7ec49
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 063bc6c91cf5d6e82a05b17dd90519e07d3168baf5efb36969e98d0893d40f4d630db3181dfd28929fc255f579780c041b407dfd8bfec17f3461022f2cb84471
|
|
7
|
+
data.tar.gz: e92a6c8d29474c08966008f9e2e5f530520258f546b40694fc0245eaf3da2ad6064ce7bb312596a07d5e4a94f4516603256c2bd7375cfc67065180f636f64eb1
|
data/README.md
CHANGED
|
@@ -40,17 +40,22 @@ with popular AI services with built-in conversions:
|
|
|
40
40
|
- `to_anthropic_tools()` - Formats tools for Anthropic Claude API
|
|
41
41
|
- `to_google_tools()` - Formats tools for Google Vertex AI API (automatically removes "$schema" keys not accepted by Vertex AI)
|
|
42
42
|
|
|
43
|
-
## MCP
|
|
43
|
+
## MCP Protocol Support
|
|
44
44
|
|
|
45
|
-
This Ruby MCP Client implements
|
|
45
|
+
This Ruby MCP Client implements the **MCP 2025-06-18** specification with full backward compatibility.
|
|
46
46
|
|
|
47
|
-
###
|
|
47
|
+
### Key Features
|
|
48
|
+
|
|
49
|
+
**MCP 2025-06-18 (Latest):**
|
|
50
|
+
- **Structured Tool Outputs** - Tools can declare output schemas and return type-safe, validated structured data
|
|
51
|
+
- **Elicitation (Server-initiated User Interactions)** - Servers can request user input during tool execution via bidirectional JSON-RPC (stdio, SSE, and Streamable HTTP transports)
|
|
52
|
+
- **Tool Annotations** - Support for tool behavior annotations (readOnly, destructive, requiresConfirmation) for safer tool execution
|
|
48
53
|
- **OAuth 2.1 Authorization Framework** - Complete authentication with PKCE, dynamic client registration, server discovery, and runtime configuration
|
|
49
54
|
- **Streamable HTTP Transport** - Enhanced transport with Server-Sent Event formatted responses and session management
|
|
50
55
|
- **HTTP Redirect Support** - Automatic redirect handling for both SSE and HTTP transports with configurable limits
|
|
51
56
|
- **FastMCP Compatibility** - Full compatibility with FastMCP servers including proper line ending handling
|
|
52
57
|
- **Prompts Support** - Full implementation of MCP prompts for dynamic content generation
|
|
53
|
-
- **Resources Support** -
|
|
58
|
+
- **Resources Support** - Full MCP resources specification compliance including templates, subscriptions, pagination, and annotations
|
|
54
59
|
|
|
55
60
|
## Usage
|
|
56
61
|
|
|
@@ -165,11 +170,30 @@ result = client.get_prompt('greeting', { name: 'Ruby Developer' })
|
|
|
165
170
|
result = client.get_prompt('greeting', { name: 'Ruby Developer' }, server: 'filesystem')
|
|
166
171
|
|
|
167
172
|
# === Working with Resources ===
|
|
168
|
-
# List available resources from all servers
|
|
169
|
-
|
|
173
|
+
# List available resources from all servers (returns hash with 'resources' array and optional 'nextCursor')
|
|
174
|
+
result = client.list_resources
|
|
175
|
+
resources = result['resources'] # Array of Resource objects from all servers
|
|
176
|
+
next_cursor = result['nextCursor'] # nil when aggregating multiple servers
|
|
177
|
+
|
|
178
|
+
# Get resources from a specific server with pagination support
|
|
179
|
+
result = client.servers.first.list_resources
|
|
180
|
+
resources = result['resources'] # Array of Resource objects
|
|
181
|
+
next_cursor = result['nextCursor'] # For pagination
|
|
170
182
|
|
|
171
|
-
#
|
|
183
|
+
# List resources with pagination (only works with single server or client.list_resources with cursor)
|
|
184
|
+
result = client.list_resources(cursor: next_cursor) # Uses first server when cursor provided
|
|
185
|
+
|
|
186
|
+
# Read a specific resource by URI (returns array of ResourceContent objects)
|
|
172
187
|
contents = client.read_resource('file:///example.txt')
|
|
188
|
+
contents.each do |content|
|
|
189
|
+
if content.text?
|
|
190
|
+
puts content.text # Text content
|
|
191
|
+
elsif content.binary?
|
|
192
|
+
data = Base64.decode64(content.blob) # Binary content
|
|
193
|
+
end
|
|
194
|
+
puts content.mime_type if content.mime_type
|
|
195
|
+
puts content.annotations if content.annotations # Optional metadata
|
|
196
|
+
end
|
|
173
197
|
|
|
174
198
|
# Read a resource from a specific server
|
|
175
199
|
contents = client.read_resource('file:///example.txt', server: 'filesystem')
|
|
@@ -413,11 +437,16 @@ prompts.each { |prompt| puts "- #{prompt.name}: #{prompt.description}" }
|
|
|
413
437
|
greeting = client.get_prompt('greeting', { name: 'Ruby Developer' })
|
|
414
438
|
|
|
415
439
|
# List and read resources
|
|
416
|
-
|
|
440
|
+
result = client.list_resources
|
|
441
|
+
resources = result['resources']
|
|
417
442
|
puts "Found #{resources.length} resources:"
|
|
418
443
|
resources.each { |resource| puts "- #{resource.name} (#{resource.uri})" }
|
|
419
444
|
|
|
420
|
-
|
|
445
|
+
# Read resource (returns array of ResourceContent objects)
|
|
446
|
+
contents = client.read_resource('file:///sample/README.md')
|
|
447
|
+
contents.each do |content|
|
|
448
|
+
puts content.text if content.text?
|
|
449
|
+
end
|
|
421
450
|
```
|
|
422
451
|
|
|
423
452
|
#### Streamable HTTP Example
|
|
@@ -557,14 +586,22 @@ client = Anthropic::Client.new(access_token: ENV['ANTHROPIC_API_KEY'])
|
|
|
557
586
|
```
|
|
558
587
|
|
|
559
588
|
Complete examples can be found in the `examples/` directory:
|
|
589
|
+
|
|
590
|
+
**AI Integration Examples:**
|
|
560
591
|
- `ruby_openai_mcp.rb` - Integration with alexrudall/ruby-openai gem
|
|
561
592
|
- `openai_ruby_mcp.rb` - Integration with official openai/openai-ruby gem
|
|
562
593
|
- `ruby_anthropic_mcp.rb` - Integration with alexrudall/ruby-anthropic gem
|
|
563
594
|
- `gemini_ai_mcp.rb` - Integration with Google Vertex AI and Gemini models
|
|
595
|
+
|
|
596
|
+
**Transport Examples:**
|
|
564
597
|
- `streamable_http_example.rb` - Streamable HTTP transport with Playwright MCP
|
|
565
598
|
- `echo_server.py` & `echo_server_client.rb` - FastMCP server example with full setup
|
|
566
599
|
- `echo_server_streamable.py` & `echo_server_streamable_client.rb` - Enhanced streamable HTTP server example
|
|
567
600
|
|
|
601
|
+
**MCP 2025 Protocol Features:**
|
|
602
|
+
- `structured_output_server.py` & `test_structured_outputs.rb` - Structured tool outputs (MCP 2025-06-18)
|
|
603
|
+
- `echo_server_with_annotations.py` & `test_tool_annotations.rb` - Tool annotations (MCP 2025-03-26)
|
|
604
|
+
|
|
568
605
|
## MCP Server Compatibility
|
|
569
606
|
|
|
570
607
|
This client works with any MCP-compatible server, including:
|
|
@@ -753,6 +790,83 @@ auth_url = oauth_provider.start_authorization_flow
|
|
|
753
790
|
token = oauth_provider.complete_authorization_flow(code, state)
|
|
754
791
|
```
|
|
755
792
|
|
|
793
|
+
### Browser-Based OAuth Authentication
|
|
794
|
+
|
|
795
|
+
For the easiest authentication experience, use the browser-based OAuth flow that automatically handles the entire process:
|
|
796
|
+
|
|
797
|
+
```ruby
|
|
798
|
+
require 'mcp_client/auth/browser_oauth'
|
|
799
|
+
|
|
800
|
+
# Create OAuth provider
|
|
801
|
+
oauth_provider = MCPClient::Auth::OAuthProvider.new(
|
|
802
|
+
server_url: 'https://api.example.com/mcp',
|
|
803
|
+
redirect_uri: 'http://localhost:8080/callback',
|
|
804
|
+
scope: 'read:tools write:tools' # Optional
|
|
805
|
+
)
|
|
806
|
+
|
|
807
|
+
# Create browser OAuth helper
|
|
808
|
+
browser_oauth = MCPClient::Auth::BrowserOAuth.new(
|
|
809
|
+
oauth_provider,
|
|
810
|
+
callback_port: 8080, # Optional: Port for local server (default: 8080)
|
|
811
|
+
callback_path: '/callback' # Optional: Callback path (default: '/callback')
|
|
812
|
+
)
|
|
813
|
+
|
|
814
|
+
# Authenticate (opens browser automatically and handles callback)
|
|
815
|
+
token = browser_oauth.authenticate(
|
|
816
|
+
timeout: 300, # Optional: 5 minutes (default: 300)
|
|
817
|
+
auto_open_browser: true # Optional: Auto-open browser (default: true)
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
# Create authenticated client (use streamable_http for modern MCP servers)
|
|
821
|
+
server_config = {
|
|
822
|
+
type: 'streamable_http', # or 'http' for simple HTTP-only servers
|
|
823
|
+
base_url: 'https://api.example.com/mcp',
|
|
824
|
+
oauth_provider: oauth_provider
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
client = MCPClient::Client.new(mcp_server_configs: [server_config])
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
**How it works:**
|
|
831
|
+
1. **Automatically starts a local HTTP server** (using pure Ruby `TCPServer`) to handle the OAuth callback
|
|
832
|
+
2. **Opens your default browser** to the authorization page (macOS: `open`, Linux: `xdg-open`, Windows: `start`)
|
|
833
|
+
3. **Captures the authorization code** automatically from the callback
|
|
834
|
+
4. **Completes the OAuth flow** and stores the access token
|
|
835
|
+
5. **Handles token refresh** automatically when tokens expire
|
|
836
|
+
|
|
837
|
+
**Features:**
|
|
838
|
+
- **Zero external dependencies** - Uses pure Ruby stdlib (`TCPServer`)
|
|
839
|
+
- **User-friendly HTML pages** - Shows success/error pages in the browser
|
|
840
|
+
- **Automatic token management** - Handles token refresh transparently
|
|
841
|
+
- **Configurable timeout** - Raises `Timeout::Error` if user doesn't authorize in time
|
|
842
|
+
- **Token storage** - Supports custom storage backends for persistent tokens
|
|
843
|
+
- **Port conflict detection** - Clear error messages if port is already in use
|
|
844
|
+
- **Security hardened** - PKCE, CSRF protection, request validation, header limits
|
|
845
|
+
|
|
846
|
+
**Error Handling:**
|
|
847
|
+
```ruby
|
|
848
|
+
begin
|
|
849
|
+
token = browser_oauth.authenticate
|
|
850
|
+
rescue Timeout::Error
|
|
851
|
+
puts "User took too long to authorize"
|
|
852
|
+
rescue MCPClient::Errors::ConnectionError => e
|
|
853
|
+
puts "OAuth flow failed: #{e.message}"
|
|
854
|
+
# Port already in use, server doesn't support OAuth, etc.
|
|
855
|
+
rescue ArgumentError => e
|
|
856
|
+
puts "Invalid state parameter (CSRF protection)"
|
|
857
|
+
end
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
**Server Requirements:**
|
|
861
|
+
- OAuth 2.1 Protocol with PKCE
|
|
862
|
+
- Authorization Server Discovery via:
|
|
863
|
+
- `/.well-known/oauth-authorization-server` (primary, for self-contained servers)
|
|
864
|
+
- `/.well-known/oauth-protected-resource` (fallback, for delegated auth)
|
|
865
|
+
- Dynamic Client Registration (recommended)
|
|
866
|
+
- Authorization Code Grant Flow
|
|
867
|
+
|
|
868
|
+
For a complete working example, see [`examples/oauth_browser_auth.rb`](examples/oauth_browser_auth.rb).
|
|
869
|
+
|
|
756
870
|
### OAuth Features
|
|
757
871
|
|
|
758
872
|
- **OAuth 2.1 compliance** with PKCE for security
|
|
@@ -761,9 +875,378 @@ token = oauth_provider.complete_authorization_flow(code, state)
|
|
|
761
875
|
- **Token refresh** and automatic token management
|
|
762
876
|
- **Pluggable storage** for tokens and client credentials
|
|
763
877
|
- **Runtime configuration** via getter/setter methods
|
|
878
|
+
- **Browser-based authentication** with automatic callback handling
|
|
764
879
|
|
|
765
880
|
For complete OAuth documentation, see [OAUTH.md](OAUTH.md).
|
|
766
881
|
|
|
882
|
+
## Resources
|
|
883
|
+
|
|
884
|
+
The Ruby MCP Client provides full support for the MCP resources specification, enabling access to files, data, and other content with URI-based identification.
|
|
885
|
+
|
|
886
|
+
### Resource Features
|
|
887
|
+
|
|
888
|
+
- **Resource listing** with cursor-based pagination
|
|
889
|
+
- **Resource templates** with URI patterns (RFC 6570)
|
|
890
|
+
- **Resource subscriptions** for real-time updates
|
|
891
|
+
- **Binary content support** with base64 encoding
|
|
892
|
+
- **Resource annotations** for metadata (audience, priority, lastModified)
|
|
893
|
+
- **ResourceContent objects** for structured content access
|
|
894
|
+
|
|
895
|
+
### Resource API
|
|
896
|
+
|
|
897
|
+
```ruby
|
|
898
|
+
# Get a server instance
|
|
899
|
+
server = client.servers.first # or client.find_server('name')
|
|
900
|
+
|
|
901
|
+
# List resources with pagination
|
|
902
|
+
result = server.list_resources
|
|
903
|
+
resources = result['resources'] # Array of Resource objects
|
|
904
|
+
next_cursor = result['nextCursor'] # String cursor for next page (if any)
|
|
905
|
+
|
|
906
|
+
# Get next page of resources
|
|
907
|
+
if next_cursor
|
|
908
|
+
next_result = server.list_resources(cursor: next_cursor)
|
|
909
|
+
end
|
|
910
|
+
|
|
911
|
+
# Access Resource properties
|
|
912
|
+
resource = resources.first
|
|
913
|
+
resource.uri # "file:///example.txt"
|
|
914
|
+
resource.name # "example.txt"
|
|
915
|
+
resource.title # Optional human-readable title
|
|
916
|
+
resource.description # Optional description
|
|
917
|
+
resource.mime_type # "text/plain"
|
|
918
|
+
resource.size # Optional file size in bytes
|
|
919
|
+
resource.annotations # Optional metadata hash
|
|
920
|
+
|
|
921
|
+
# Read resource contents (returns array of ResourceContent objects)
|
|
922
|
+
contents = server.read_resource(resource.uri)
|
|
923
|
+
|
|
924
|
+
contents.each do |content|
|
|
925
|
+
# Check content type
|
|
926
|
+
if content.text?
|
|
927
|
+
# Text content
|
|
928
|
+
text = content.text
|
|
929
|
+
mime = content.mime_type # e.g., "text/plain"
|
|
930
|
+
elsif content.binary?
|
|
931
|
+
# Binary content (base64 encoded)
|
|
932
|
+
blob = content.blob # Base64 string
|
|
933
|
+
data = Base64.decode64(blob) # Decoded binary data
|
|
934
|
+
end
|
|
935
|
+
|
|
936
|
+
# Access optional annotations
|
|
937
|
+
if content.annotations
|
|
938
|
+
audience = content.annotations['audience'] # e.g., ["user", "assistant"]
|
|
939
|
+
priority = content.annotations['priority'] # e.g., 0.5
|
|
940
|
+
modified = content.annotations['lastModified'] # ISO 8601 timestamp
|
|
941
|
+
end
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
# List resource templates
|
|
945
|
+
templates_result = server.list_resource_templates
|
|
946
|
+
templates = templates_result['resourceTemplates'] # Array of ResourceTemplate objects
|
|
947
|
+
|
|
948
|
+
template = templates.first
|
|
949
|
+
template.uri_template # "file:///{path}" (RFC 6570 URI template)
|
|
950
|
+
template.name # Template name
|
|
951
|
+
template.title # Optional title
|
|
952
|
+
template.description # Optional description
|
|
953
|
+
template.mime_type # Optional MIME type hint
|
|
954
|
+
|
|
955
|
+
# Subscribe to resource updates
|
|
956
|
+
server.subscribe_resource('file:///watched.txt') # Returns true on success
|
|
957
|
+
|
|
958
|
+
# Unsubscribe from resource updates
|
|
959
|
+
server.unsubscribe_resource('file:///watched.txt') # Returns true on success
|
|
960
|
+
|
|
961
|
+
# Check server capabilities for resources
|
|
962
|
+
capabilities = server.capabilities
|
|
963
|
+
if capabilities['resources']
|
|
964
|
+
can_subscribe = capabilities['resources']['subscribe'] # true/false
|
|
965
|
+
list_changed = capabilities['resources']['listChanged'] # true/false
|
|
966
|
+
end
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
### Working with ResourceContent
|
|
970
|
+
|
|
971
|
+
The `ResourceContent` class provides a structured way to handle both text and binary content:
|
|
972
|
+
|
|
973
|
+
```ruby
|
|
974
|
+
# ResourceContent for text
|
|
975
|
+
content = MCPClient::ResourceContent.new(
|
|
976
|
+
uri: 'file:///example.txt',
|
|
977
|
+
name: 'example.txt',
|
|
978
|
+
mime_type: 'text/plain',
|
|
979
|
+
text: 'File contents here',
|
|
980
|
+
annotations: {
|
|
981
|
+
'audience' => ['user'],
|
|
982
|
+
'priority' => 1.0
|
|
983
|
+
}
|
|
984
|
+
)
|
|
985
|
+
|
|
986
|
+
# ResourceContent for binary data
|
|
987
|
+
binary_content = MCPClient::ResourceContent.new(
|
|
988
|
+
uri: 'file:///image.png',
|
|
989
|
+
name: 'image.png',
|
|
990
|
+
mime_type: 'image/png',
|
|
991
|
+
blob: Base64.strict_encode64(binary_data)
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
# Access content
|
|
995
|
+
if content.text?
|
|
996
|
+
puts content.text
|
|
997
|
+
elsif content.binary?
|
|
998
|
+
data = Base64.decode64(content.blob)
|
|
999
|
+
end
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
## Tool Annotations
|
|
1003
|
+
|
|
1004
|
+
MCP 2025-06-18 supports tool annotations that describe tool behavior, enabling safer and more informed tool execution. The Ruby MCP Client provides full support for reading and interpreting these annotations.
|
|
1005
|
+
|
|
1006
|
+
### Annotation Types
|
|
1007
|
+
|
|
1008
|
+
Tools can include the following annotations:
|
|
1009
|
+
|
|
1010
|
+
- **`readOnly`** - Indicates the tool only reads data and doesn't modify state
|
|
1011
|
+
- **`destructive`** - Indicates the tool performs potentially dangerous operations (e.g., deleting data)
|
|
1012
|
+
- **`requiresConfirmation`** - Indicates the tool should require explicit user confirmation before execution
|
|
1013
|
+
|
|
1014
|
+
### Using Tool Annotations
|
|
1015
|
+
|
|
1016
|
+
```ruby
|
|
1017
|
+
# List tools and check their annotations
|
|
1018
|
+
tools = client.list_tools
|
|
1019
|
+
|
|
1020
|
+
tools.each do |tool|
|
|
1021
|
+
puts "Tool: #{tool.name}"
|
|
1022
|
+
|
|
1023
|
+
# Check annotations using helper methods
|
|
1024
|
+
if tool.read_only?
|
|
1025
|
+
puts " → Safe to execute (read-only)"
|
|
1026
|
+
end
|
|
1027
|
+
|
|
1028
|
+
if tool.destructive?
|
|
1029
|
+
puts " ⚠️ Warning: This tool is destructive"
|
|
1030
|
+
end
|
|
1031
|
+
|
|
1032
|
+
if tool.requires_confirmation?
|
|
1033
|
+
puts " 🛡️ Requires user confirmation before execution"
|
|
1034
|
+
end
|
|
1035
|
+
|
|
1036
|
+
# Access raw annotations hash
|
|
1037
|
+
if tool.annotations
|
|
1038
|
+
puts " Annotations: #{tool.annotations.inspect}"
|
|
1039
|
+
end
|
|
1040
|
+
end
|
|
1041
|
+
|
|
1042
|
+
# Make informed decisions based on annotations
|
|
1043
|
+
tool = client.find_tool('delete_user')
|
|
1044
|
+
if tool.destructive? && tool.requires_confirmation?
|
|
1045
|
+
# Prompt user for confirmation before executing
|
|
1046
|
+
puts "This will delete user data. Are you sure? (y/n)"
|
|
1047
|
+
response = gets.chomp
|
|
1048
|
+
if response.downcase == 'y'
|
|
1049
|
+
result = client.call_tool('delete_user', { user_id: 123 })
|
|
1050
|
+
else
|
|
1051
|
+
puts "Operation cancelled"
|
|
1052
|
+
end
|
|
1053
|
+
else
|
|
1054
|
+
# Safe to execute without confirmation
|
|
1055
|
+
result = client.call_tool('get_user', { user_id: 123 })
|
|
1056
|
+
end
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
### Tool Annotation Helper Methods
|
|
1060
|
+
|
|
1061
|
+
The `Tool` class provides convenient helper methods:
|
|
1062
|
+
|
|
1063
|
+
- `tool.read_only?` - Returns true if the tool is marked as read-only
|
|
1064
|
+
- `tool.destructive?` - Returns true if the tool is marked as destructive
|
|
1065
|
+
- `tool.requires_confirmation?` - Returns true if the tool requires confirmation
|
|
1066
|
+
- `tool.annotations` - Returns the raw annotations hash
|
|
1067
|
+
|
|
1068
|
+
### Example
|
|
1069
|
+
|
|
1070
|
+
See `examples/test_tool_annotations.rb` for a complete working example demonstrating:
|
|
1071
|
+
- Listing tools with their annotations
|
|
1072
|
+
- Using annotation helper methods
|
|
1073
|
+
- Making annotation-aware decisions about tool execution
|
|
1074
|
+
- Handling destructive operations safely
|
|
1075
|
+
|
|
1076
|
+
## Structured Tool Outputs
|
|
1077
|
+
|
|
1078
|
+
MCP 2025-06-18 introduces structured tool outputs, allowing tools to declare output schemas and return type-safe, validated data structures instead of just text.
|
|
1079
|
+
|
|
1080
|
+
### Overview
|
|
1081
|
+
|
|
1082
|
+
Tools can now specify an `outputSchema` (JSON Schema) that defines the expected structure of their results. When called, they return data in a `structuredContent` field that validates against this schema, providing:
|
|
1083
|
+
|
|
1084
|
+
- **Type safety** - Predictable, validated data structures
|
|
1085
|
+
- **Better IDE support** - Code completion and type checking
|
|
1086
|
+
- **Easier parsing** - No need to parse text or guess formats
|
|
1087
|
+
- **Backward compatibility** - Text content is still provided alongside structured data
|
|
1088
|
+
|
|
1089
|
+
### Using Structured Outputs
|
|
1090
|
+
|
|
1091
|
+
```ruby
|
|
1092
|
+
# List tools and check for structured output support
|
|
1093
|
+
tools = client.list_tools
|
|
1094
|
+
|
|
1095
|
+
tools.each do |tool|
|
|
1096
|
+
if tool.structured_output?
|
|
1097
|
+
puts "#{tool.name} supports structured output"
|
|
1098
|
+
puts "Output schema: #{tool.output_schema}"
|
|
1099
|
+
end
|
|
1100
|
+
end
|
|
1101
|
+
|
|
1102
|
+
# Call a tool that returns structured output
|
|
1103
|
+
result = client.call_tool('get_weather', { location: 'San Francisco' })
|
|
1104
|
+
|
|
1105
|
+
# Access structured data (type-safe, validated)
|
|
1106
|
+
if result['structuredContent']
|
|
1107
|
+
data = result['structuredContent']
|
|
1108
|
+
puts "Temperature: #{data['temperature']}°C"
|
|
1109
|
+
puts "Conditions: #{data['conditions']}"
|
|
1110
|
+
puts "Humidity: #{data['humidity']}%"
|
|
1111
|
+
end
|
|
1112
|
+
|
|
1113
|
+
# Backward compatibility: text content is still available
|
|
1114
|
+
text_content = result['content']&.first&.dig('text')
|
|
1115
|
+
parsed = JSON.parse(text_content) if text_content
|
|
1116
|
+
```
|
|
1117
|
+
|
|
1118
|
+
### Helper Methods
|
|
1119
|
+
|
|
1120
|
+
The `Tool` class provides a convenient helper method:
|
|
1121
|
+
|
|
1122
|
+
- `tool.structured_output?` - Returns true if the tool has an output schema defined
|
|
1123
|
+
- `tool.output_schema` - Returns the JSON Schema for the tool's output
|
|
1124
|
+
|
|
1125
|
+
### Example
|
|
1126
|
+
|
|
1127
|
+
See `examples/test_structured_outputs.rb` for a complete working example demonstrating:
|
|
1128
|
+
- Detecting tools with structured output support
|
|
1129
|
+
- Accessing output schemas
|
|
1130
|
+
- Calling tools and receiving structured data
|
|
1131
|
+
- Backward compatibility with text content
|
|
1132
|
+
|
|
1133
|
+
The example includes a Python MCP server (`examples/structured_output_server.py`) that provides three tools with structured outputs:
|
|
1134
|
+
- `get_weather` - Weather data with temperature, conditions, humidity
|
|
1135
|
+
- `analyze_text` - Text analysis with word count, character count, statistics
|
|
1136
|
+
- `calculate_stats` - Statistical calculations (mean, median, min, max, etc.)
|
|
1137
|
+
|
|
1138
|
+
## Elicitation (Server-initiated User Interactions) - MCP 2025-06-18
|
|
1139
|
+
|
|
1140
|
+
Elicitation enables MCP servers to request user input during tool execution via bidirectional JSON-RPC.
|
|
1141
|
+
This allows servers to gather additional information, confirm sensitive operations, or interact with users
|
|
1142
|
+
dynamically during the execution of tools.
|
|
1143
|
+
|
|
1144
|
+
### Transport Support
|
|
1145
|
+
|
|
1146
|
+
**Fully Supported:**
|
|
1147
|
+
- ✅ **stdio** - Full bidirectional JSON-RPC over stdin/stdout
|
|
1148
|
+
- ✅ **SSE** - Server sends requests via SSE stream, client responds via HTTP POST
|
|
1149
|
+
- ✅ **Streamable HTTP** - Server sends requests via SSE-formatted responses, client responds via HTTP POST
|
|
1150
|
+
|
|
1151
|
+
**Not Supported:**
|
|
1152
|
+
- ❌ **HTTP** - Pure request-response architecture prevents server-initiated requests
|
|
1153
|
+
|
|
1154
|
+
### Using Elicitation
|
|
1155
|
+
|
|
1156
|
+
To use elicitation, register a handler when creating the client:
|
|
1157
|
+
|
|
1158
|
+
```ruby
|
|
1159
|
+
# Define an elicitation handler
|
|
1160
|
+
elicitation_handler = lambda do |message, requested_schema|
|
|
1161
|
+
puts "Server requests: #{message}"
|
|
1162
|
+
|
|
1163
|
+
# Show expected input format from schema
|
|
1164
|
+
if requested_schema && requested_schema['properties']
|
|
1165
|
+
requested_schema['properties'].each do |field, schema|
|
|
1166
|
+
puts " #{field}: #{schema['type']}"
|
|
1167
|
+
end
|
|
1168
|
+
end
|
|
1169
|
+
|
|
1170
|
+
# Prompt user and return one of three response types:
|
|
1171
|
+
|
|
1172
|
+
# 1. Accept and provide data to the server
|
|
1173
|
+
{
|
|
1174
|
+
'action' => 'accept',
|
|
1175
|
+
'content' => {
|
|
1176
|
+
'field_name' => 'user_input_value'
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
# 2. Decline to provide input
|
|
1181
|
+
# { 'action' => 'decline' }
|
|
1182
|
+
|
|
1183
|
+
# 3. Cancel the operation
|
|
1184
|
+
# { 'action' => 'cancel' }
|
|
1185
|
+
end
|
|
1186
|
+
|
|
1187
|
+
# Create client with elicitation handler
|
|
1188
|
+
# Works with stdio, SSE, and Streamable HTTP transports
|
|
1189
|
+
client = MCPClient::Client.new(
|
|
1190
|
+
mcp_server_configs: [
|
|
1191
|
+
# Stdio transport
|
|
1192
|
+
MCPClient.stdio_config(
|
|
1193
|
+
command: 'python my_server.py',
|
|
1194
|
+
name: 'my-server'
|
|
1195
|
+
),
|
|
1196
|
+
# Or SSE transport
|
|
1197
|
+
MCPClient.sse_config(
|
|
1198
|
+
base_url: 'https://api.example.com/sse',
|
|
1199
|
+
name: 'remote-server'
|
|
1200
|
+
),
|
|
1201
|
+
# Or Streamable HTTP transport
|
|
1202
|
+
MCPClient.streamable_http_config(
|
|
1203
|
+
base_url: 'https://api.example.com',
|
|
1204
|
+
endpoint: '/mcp',
|
|
1205
|
+
name: 'streamable-server'
|
|
1206
|
+
)
|
|
1207
|
+
],
|
|
1208
|
+
elicitation_handler: elicitation_handler
|
|
1209
|
+
)
|
|
1210
|
+
|
|
1211
|
+
# Call tools - connection happens automatically
|
|
1212
|
+
# Server may send elicitation requests during tool execution
|
|
1213
|
+
result = client.call_tool('create_document', { format: 'markdown' }, server: 'my-server')
|
|
1214
|
+
```
|
|
1215
|
+
|
|
1216
|
+
### Response Actions
|
|
1217
|
+
|
|
1218
|
+
The elicitation handler should return a hash with an `action` field:
|
|
1219
|
+
|
|
1220
|
+
1. **Accept** - Provide the requested input:
|
|
1221
|
+
```ruby
|
|
1222
|
+
{
|
|
1223
|
+
'action' => 'accept',
|
|
1224
|
+
'content' => { 'field' => 'value' } # Must match requested schema
|
|
1225
|
+
}
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
2. **Decline** - Refuse to provide input (server should handle gracefully):
|
|
1229
|
+
```ruby
|
|
1230
|
+
{ 'action' => 'decline' }
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
3. **Cancel** - Cancel the entire operation:
|
|
1234
|
+
```ruby
|
|
1235
|
+
{ 'action' => 'cancel' }
|
|
1236
|
+
```
|
|
1237
|
+
|
|
1238
|
+
### Example
|
|
1239
|
+
|
|
1240
|
+
See `examples/elicitation/test_elicitation.rb` for a complete working example demonstrating:
|
|
1241
|
+
- Registering an elicitation handler
|
|
1242
|
+
- Handling different message types and schemas
|
|
1243
|
+
- Responding with accept/decline/cancel actions
|
|
1244
|
+
- Interactive user prompts during tool execution
|
|
1245
|
+
|
|
1246
|
+
The example includes a Python MCP server (`examples/elicitation/elicitation_server.py`) that provides tools using elicitation:
|
|
1247
|
+
- `create_document` - Requests title and content via elicitation
|
|
1248
|
+
- `send_notification` - Confirms before sending a notification
|
|
1249
|
+
|
|
767
1250
|
## Key Features
|
|
768
1251
|
|
|
769
1252
|
### Client Features
|
|
@@ -774,6 +1257,9 @@ For complete OAuth documentation, see [OAUTH.md](OAUTH.md).
|
|
|
774
1257
|
- **Server lookup** - Find servers by name using `find_server`
|
|
775
1258
|
- **Tool association** - Each tool knows which server it belongs to
|
|
776
1259
|
- **Tool discovery** - Find tools by name or pattern
|
|
1260
|
+
- **Structured outputs** - Support for MCP 2025-06-18 structured tool outputs with output schemas and type-safe responses
|
|
1261
|
+
- **Tool annotations** - Support for readOnly, destructive, and requiresConfirmation annotations with helper methods
|
|
1262
|
+
- **Elicitation support** - Server-initiated user interactions during tool execution (stdio, SSE, and Streamable HTTP transports)
|
|
777
1263
|
- **Server disambiguation** - Specify which server to use when tools with same name exist in multiple servers
|
|
778
1264
|
- **Atomic tool calls** - Simple API for invoking tools with parameters
|
|
779
1265
|
- **Batch support** - Call multiple tools in a single operation
|