actionmcp 0.102.0 → 0.103.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 +11 -3
- data/lib/action_mcp/test_helper/session_store_assertions.rb +0 -70
- data/lib/action_mcp/version.rb +1 -1
- data/lib/action_mcp.rb +0 -1
- metadata +2 -27
- data/lib/action_mcp/client/active_record_session_store.rb +0 -57
- data/lib/action_mcp/client/base.rb +0 -225
- data/lib/action_mcp/client/blueprint.rb +0 -163
- data/lib/action_mcp/client/catalog.rb +0 -164
- data/lib/action_mcp/client/collection.rb +0 -168
- data/lib/action_mcp/client/elicitation.rb +0 -34
- data/lib/action_mcp/client/json_rpc_handler.rb +0 -202
- data/lib/action_mcp/client/logging.rb +0 -19
- data/lib/action_mcp/client/messaging.rb +0 -28
- data/lib/action_mcp/client/prompt_book.rb +0 -117
- data/lib/action_mcp/client/prompts.rb +0 -47
- data/lib/action_mcp/client/request_timeouts.rb +0 -74
- data/lib/action_mcp/client/resources.rb +0 -100
- data/lib/action_mcp/client/roots.rb +0 -13
- data/lib/action_mcp/client/server.rb +0 -60
- data/lib/action_mcp/client/session_store.rb +0 -39
- data/lib/action_mcp/client/session_store_factory.rb +0 -27
- data/lib/action_mcp/client/streamable_client.rb +0 -264
- data/lib/action_mcp/client/streamable_http_transport.rb +0 -306
- data/lib/action_mcp/client/test_session_store.rb +0 -84
- data/lib/action_mcp/client/toolbox.rb +0 -199
- data/lib/action_mcp/client/tools.rb +0 -47
- data/lib/action_mcp/client/transport.rb +0 -137
- data/lib/action_mcp/client/volatile_session_store.rb +0 -38
- data/lib/action_mcp/client.rb +0 -71
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0b98e5c438f85bfe710dbe07d1b9cc2a12d7347b4fc200c66a0492f380615599
|
|
4
|
+
data.tar.gz: 921adb81f040f14533248ce6c52b142a85d8445299e19a83501171b344601c11
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9aee10304fc9e926a34805c760545adb565b5a19dd7fe59e445ed938d82b2e92e9e7d8e5762db2a5dfc12a844fdbbfb9b84ed7a15f2879703fe7c83b6eb29ec2
|
|
7
|
+
data.tar.gz: 4b35888139f56c100415e64f495e61efcd43eac2652bf99b1dfbf1ae269fb317d19fad9f0a1ee99b8a19d1e2a36dd3d1818d4ff76a67456f7e9c7edc21457fb7
|
data/README.md
CHANGED
|
@@ -46,6 +46,14 @@ In short, ActionMCP helps you build an MCP server (the component that exposes ca
|
|
|
46
46
|
|
|
47
47
|
> **Client connections:** The client part of ActionMCP is meant to connect to remote MCP servers only. Connecting to local processes (such as via STDIO) is not supported.
|
|
48
48
|
|
|
49
|
+
## Requirements
|
|
50
|
+
|
|
51
|
+
- **Ruby**: 3.4.8+ or 4.0.0+
|
|
52
|
+
- **Rails**: 8.1.1+
|
|
53
|
+
- **Database**: PostgreSQL, MySQL, or SQLite3
|
|
54
|
+
|
|
55
|
+
ActionMCP is tested against Ruby 3.4.8 and 4.0.0 with Rails 8.1.1+.
|
|
56
|
+
|
|
49
57
|
## Installation
|
|
50
58
|
|
|
51
59
|
To start using ActionMCP, add it to your project:
|
|
@@ -179,7 +187,7 @@ For tools that perform sensitive operations (file system access, database modifi
|
|
|
179
187
|
class FileSystemTool < ApplicationMCPTool
|
|
180
188
|
tool_name "read_file"
|
|
181
189
|
description "Read contents of a file"
|
|
182
|
-
|
|
190
|
+
|
|
183
191
|
# Require explicit consent before execution
|
|
184
192
|
requires_consent!
|
|
185
193
|
|
|
@@ -392,7 +400,7 @@ ActionMCP provides comprehensive documentation across multiple specialized guide
|
|
|
392
400
|
- **[Installation & Configuration](README.md#installation)** - Initial setup, database migrations, and basic configuration
|
|
393
401
|
- **[Authentication with Gateway](README.md#authentication-with-gateway)** - User authentication and authorization patterns
|
|
394
402
|
|
|
395
|
-
### Component Development
|
|
403
|
+
### Component Development
|
|
396
404
|
- **[📋 TOOLS.MD](TOOLS.MD)** - Complete guide to developing MCP tools
|
|
397
405
|
- Generator usage and best practices
|
|
398
406
|
- Property definitions, validation, and consent management
|
|
@@ -1042,7 +1050,7 @@ class MyTool < ApplicationMCPTool
|
|
|
1042
1050
|
report_error("Clear error message for the LLM")
|
|
1043
1051
|
return
|
|
1044
1052
|
end
|
|
1045
|
-
|
|
1053
|
+
|
|
1046
1054
|
# Normal processing
|
|
1047
1055
|
render(text: "Success message")
|
|
1048
1056
|
end
|
|
@@ -51,64 +51,6 @@ module ActionMCP
|
|
|
51
51
|
message || "Expected #{expected} session operations#{type_desc}, got #{actual}"
|
|
52
52
|
end
|
|
53
53
|
|
|
54
|
-
# Client session store assertions
|
|
55
|
-
def assert_client_session_saved(session_id, message = nil)
|
|
56
|
-
assert client_session_store.session_saved?(session_id),
|
|
57
|
-
message || "Expected client session #{session_id} to have been saved"
|
|
58
|
-
end
|
|
59
|
-
|
|
60
|
-
def assert_client_session_not_saved(session_id, message = nil)
|
|
61
|
-
assert_not client_session_store.session_saved?(session_id),
|
|
62
|
-
message || "Expected client session #{session_id} not to have been saved"
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def assert_client_session_loaded(session_id, message = nil)
|
|
66
|
-
assert client_session_store.session_loaded?(session_id),
|
|
67
|
-
message || "Expected client session #{session_id} to have been loaded"
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def assert_client_session_not_loaded(session_id, message = nil)
|
|
71
|
-
assert_not client_session_store.session_loaded?(session_id),
|
|
72
|
-
message || "Expected client session #{session_id} not to have been loaded"
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def assert_client_session_updated(session_id, message = nil)
|
|
76
|
-
assert client_session_store.session_updated?(session_id),
|
|
77
|
-
message || "Expected client session #{session_id} to have been updated"
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
def assert_client_session_not_updated(session_id, message = nil)
|
|
81
|
-
assert_not client_session_store.session_updated?(session_id),
|
|
82
|
-
message || "Expected client session #{session_id} not to have been updated"
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def assert_client_session_deleted(session_id, message = nil)
|
|
86
|
-
assert client_session_store.session_deleted?(session_id),
|
|
87
|
-
message || "Expected client session #{session_id} to have been deleted"
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def assert_client_session_not_deleted(session_id, message = nil)
|
|
91
|
-
assert_not client_session_store.session_deleted?(session_id),
|
|
92
|
-
message || "Expected client session #{session_id} not to have been deleted"
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
def assert_client_session_operation_count(expected, type = nil, message = nil)
|
|
96
|
-
actual = client_session_store.operation_count(type)
|
|
97
|
-
type_desc = type ? " of type #{type}" : ""
|
|
98
|
-
assert_equal expected, actual,
|
|
99
|
-
message || "Expected #{expected} client session operations#{type_desc}, got #{actual}"
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def assert_client_session_data_includes(session_id, expected_data, message = nil)
|
|
103
|
-
saved_data = client_session_store.last_saved_data(session_id)
|
|
104
|
-
assert saved_data, "No saved data found for session #{session_id}"
|
|
105
|
-
|
|
106
|
-
expected_data.each do |key, value|
|
|
107
|
-
assert_equal value, saved_data[key],
|
|
108
|
-
message || "Expected session #{session_id} data to include #{key}: #{value}"
|
|
109
|
-
end
|
|
110
|
-
end
|
|
111
|
-
|
|
112
54
|
private
|
|
113
55
|
|
|
114
56
|
def server_session_store
|
|
@@ -117,18 +59,6 @@ module ActionMCP
|
|
|
117
59
|
|
|
118
60
|
store
|
|
119
61
|
end
|
|
120
|
-
|
|
121
|
-
def client_session_store
|
|
122
|
-
# This would need to be set by the test or could use a thread-local variable
|
|
123
|
-
# For now, we'll assume it's available as an instance variable
|
|
124
|
-
store = @client_session_store || Thread.current[:test_client_session_store]
|
|
125
|
-
unless store
|
|
126
|
-
raise "Client session store not set. Set @client_session_store or Thread.current[:test_client_session_store]"
|
|
127
|
-
end
|
|
128
|
-
raise "Client session store is not a TestSessionStore" unless store.is_a?(ActionMCP::Client::TestSessionStore)
|
|
129
|
-
|
|
130
|
-
store
|
|
131
|
-
end
|
|
132
62
|
end
|
|
133
63
|
end
|
|
134
64
|
end
|
data/lib/action_mcp/version.rb
CHANGED
data/lib/action_mcp.rb
CHANGED
|
@@ -29,7 +29,6 @@ end.setup
|
|
|
29
29
|
|
|
30
30
|
module ActionMCP
|
|
31
31
|
require_relative "action_mcp/version"
|
|
32
|
-
require_relative "action_mcp/client"
|
|
33
32
|
|
|
34
33
|
# Error raised when structured content doesn't match the declared output_schema
|
|
35
34
|
class StructuredContentValidationError < StandardError; end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: actionmcp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.103.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
@@ -173,31 +173,6 @@ files:
|
|
|
173
173
|
- lib/action_mcp/base_response.rb
|
|
174
174
|
- lib/action_mcp/callbacks.rb
|
|
175
175
|
- lib/action_mcp/capability.rb
|
|
176
|
-
- lib/action_mcp/client.rb
|
|
177
|
-
- lib/action_mcp/client/active_record_session_store.rb
|
|
178
|
-
- lib/action_mcp/client/base.rb
|
|
179
|
-
- lib/action_mcp/client/blueprint.rb
|
|
180
|
-
- lib/action_mcp/client/catalog.rb
|
|
181
|
-
- lib/action_mcp/client/collection.rb
|
|
182
|
-
- lib/action_mcp/client/elicitation.rb
|
|
183
|
-
- lib/action_mcp/client/json_rpc_handler.rb
|
|
184
|
-
- lib/action_mcp/client/logging.rb
|
|
185
|
-
- lib/action_mcp/client/messaging.rb
|
|
186
|
-
- lib/action_mcp/client/prompt_book.rb
|
|
187
|
-
- lib/action_mcp/client/prompts.rb
|
|
188
|
-
- lib/action_mcp/client/request_timeouts.rb
|
|
189
|
-
- lib/action_mcp/client/resources.rb
|
|
190
|
-
- lib/action_mcp/client/roots.rb
|
|
191
|
-
- lib/action_mcp/client/server.rb
|
|
192
|
-
- lib/action_mcp/client/session_store.rb
|
|
193
|
-
- lib/action_mcp/client/session_store_factory.rb
|
|
194
|
-
- lib/action_mcp/client/streamable_client.rb
|
|
195
|
-
- lib/action_mcp/client/streamable_http_transport.rb
|
|
196
|
-
- lib/action_mcp/client/test_session_store.rb
|
|
197
|
-
- lib/action_mcp/client/toolbox.rb
|
|
198
|
-
- lib/action_mcp/client/tools.rb
|
|
199
|
-
- lib/action_mcp/client/transport.rb
|
|
200
|
-
- lib/action_mcp/client/volatile_session_store.rb
|
|
201
176
|
- lib/action_mcp/configuration.rb
|
|
202
177
|
- lib/action_mcp/console_detector.rb
|
|
203
178
|
- lib/action_mcp/content.rb
|
|
@@ -327,7 +302,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
327
302
|
- !ruby/object:Gem::Version
|
|
328
303
|
version: '0'
|
|
329
304
|
requirements: []
|
|
330
|
-
rubygems_version: 4.0.
|
|
305
|
+
rubygems_version: 4.0.3
|
|
331
306
|
specification_version: 4
|
|
332
307
|
summary: Lightweight Model Context Protocol (MCP) server toolkit for Ruby/Rails
|
|
333
308
|
test_files: []
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActionMCP
|
|
4
|
-
module Client
|
|
5
|
-
# ActiveRecord-backed session store for production
|
|
6
|
-
class ActiveRecordSessionStore
|
|
7
|
-
include SessionStore
|
|
8
|
-
|
|
9
|
-
def load_session(session_id)
|
|
10
|
-
session = ActionMCP::Session.find_by(id: session_id)
|
|
11
|
-
return nil unless session
|
|
12
|
-
|
|
13
|
-
{
|
|
14
|
-
id: session.id,
|
|
15
|
-
protocol_version: session.protocol_version,
|
|
16
|
-
client_info: session.client_info,
|
|
17
|
-
client_capabilities: session.client_capabilities,
|
|
18
|
-
server_info: session.server_info,
|
|
19
|
-
server_capabilities: session.server_capabilities,
|
|
20
|
-
created_at: session.created_at,
|
|
21
|
-
updated_at: session.updated_at
|
|
22
|
-
}
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def save_session(session_id, session_data)
|
|
26
|
-
session = ActionMCP::Session.find_or_initialize_by(id: session_id)
|
|
27
|
-
|
|
28
|
-
# Only assign attributes that exist in the database
|
|
29
|
-
attributes = {}
|
|
30
|
-
attributes[:protocol_version] = session_data[:protocol_version] if session_data.key?(:protocol_version)
|
|
31
|
-
attributes[:client_info] = session_data[:client_info] if session_data.key?(:client_info)
|
|
32
|
-
attributes[:client_capabilities] = session_data[:client_capabilities] if session_data.key?(:client_capabilities)
|
|
33
|
-
attributes[:server_info] = session_data[:server_info] if session_data.key?(:server_info)
|
|
34
|
-
attributes[:server_capabilities] = session_data[:server_capabilities] if session_data.key?(:server_capabilities)
|
|
35
|
-
|
|
36
|
-
# Store any extra data in a jsonb column if available
|
|
37
|
-
# For now, we'll skip last_event_id and session_data as they don't exist in the DB
|
|
38
|
-
|
|
39
|
-
session.assign_attributes(attributes)
|
|
40
|
-
session.save!
|
|
41
|
-
session_data
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def delete_session(session_id)
|
|
45
|
-
ActionMCP::Session.find_by(id: session_id)&.destroy
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
def session_exists?(session_id)
|
|
49
|
-
ActionMCP::Session.exists?(id: session_id)
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def cleanup_expired_sessions(older_than: 24.hours.ago)
|
|
53
|
-
ActionMCP::Session.where("updated_at < ?", older_than).delete_all
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
end
|
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "transport"
|
|
4
|
-
|
|
5
|
-
module ActionMCP
|
|
6
|
-
module Client
|
|
7
|
-
# Base client class containing common MCP functionality
|
|
8
|
-
class Base
|
|
9
|
-
# Include all transport protocol modules
|
|
10
|
-
include Messaging
|
|
11
|
-
include Tools
|
|
12
|
-
include Prompts
|
|
13
|
-
include Resources
|
|
14
|
-
include Roots
|
|
15
|
-
include Elicitation
|
|
16
|
-
|
|
17
|
-
attr_reader :logger, :transport,
|
|
18
|
-
:connection_error, :server,
|
|
19
|
-
:server_capabilities, :session,
|
|
20
|
-
:catalog, :blueprint,
|
|
21
|
-
:prompt_book, :toolbox
|
|
22
|
-
|
|
23
|
-
delegate :connected?, :ready?, to: :transport
|
|
24
|
-
|
|
25
|
-
def initialize(transport:, logger: ActionMCP.logger, protocol_version: nil, **options)
|
|
26
|
-
@logger = logger
|
|
27
|
-
@transport = transport
|
|
28
|
-
@session = nil # Session will be created/loaded based on server response
|
|
29
|
-
@session_id = options[:session_id] # Optional session ID for resumption
|
|
30
|
-
@protocol_version = protocol_version || ActionMCP::DEFAULT_PROTOCOL_VERSION
|
|
31
|
-
@server_capabilities = nil
|
|
32
|
-
@connection_error = nil
|
|
33
|
-
@initialized = false
|
|
34
|
-
|
|
35
|
-
# Resource objects
|
|
36
|
-
@catalog = Catalog.new([], self)
|
|
37
|
-
# Resource template objects
|
|
38
|
-
@blueprint = Blueprint.new([], self)
|
|
39
|
-
# Prompt objects
|
|
40
|
-
@prompt_book = PromptBook.new([], self)
|
|
41
|
-
# Tool objects
|
|
42
|
-
@toolbox = Toolbox.new([], self)
|
|
43
|
-
|
|
44
|
-
setup_transport_callbacks
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Connect to the MCP server
|
|
48
|
-
def connect
|
|
49
|
-
return true if connected?
|
|
50
|
-
|
|
51
|
-
begin
|
|
52
|
-
log_debug("Connecting to MCP server via #{transport.class.name}...")
|
|
53
|
-
@connection_error = nil
|
|
54
|
-
|
|
55
|
-
success = @transport.connect
|
|
56
|
-
unless success
|
|
57
|
-
log_error("Failed to establish transport connection")
|
|
58
|
-
return false
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
log_debug("Connected to MCP server")
|
|
62
|
-
true
|
|
63
|
-
rescue StandardError => e
|
|
64
|
-
@connection_error = e.message
|
|
65
|
-
log_error("Failed to connect to MCP server: #{e.message}")
|
|
66
|
-
false
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Disconnect from the MCP server
|
|
71
|
-
def disconnect
|
|
72
|
-
return true unless connected?
|
|
73
|
-
|
|
74
|
-
begin
|
|
75
|
-
@transport.disconnect
|
|
76
|
-
log_debug("Disconnected from MCP server")
|
|
77
|
-
true
|
|
78
|
-
rescue StandardError => e
|
|
79
|
-
log_error("Error disconnecting from MCP server: #{e.message}")
|
|
80
|
-
false
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
# Send a request to the MCP server
|
|
85
|
-
def write_message(payload)
|
|
86
|
-
unless ready?
|
|
87
|
-
log_error("Cannot send request - transport not ready")
|
|
88
|
-
return false
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
begin
|
|
92
|
-
# Only write to session if it exists (after initialization)
|
|
93
|
-
session&.write(payload)
|
|
94
|
-
data = payload.to_json unless payload.is_a?(String)
|
|
95
|
-
@transport.send_message(data)
|
|
96
|
-
true
|
|
97
|
-
rescue StandardError => e
|
|
98
|
-
log_error("Failed to send request: #{e.message}")
|
|
99
|
-
false
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
|
|
103
|
-
def server=(server)
|
|
104
|
-
@server = if server.is_a?(Client::Server)
|
|
105
|
-
server
|
|
106
|
-
else
|
|
107
|
-
Client::Server.new(server)
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
# Only update session if it exists
|
|
111
|
-
return unless @session
|
|
112
|
-
|
|
113
|
-
@session.server_capabilities = server.capabilities
|
|
114
|
-
@session.server_info = server.server_info
|
|
115
|
-
@session.save
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def initialized?
|
|
119
|
-
@initialized && @session&.initialized?
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
def inspect
|
|
123
|
-
session_info = @session ? "session: #{@session.id}" : "session: none"
|
|
124
|
-
"#<#{self.class.name} transport: #{transport.class.name}, server: #{server}, client_name: #{client_info[:name]}, client_version: #{client_info[:version]}, capabilities: #{client_capabilities}, connected: #{connected?}, initialized: #{initialized?}, #{session_info}>"
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
protected
|
|
128
|
-
|
|
129
|
-
def setup_transport_callbacks
|
|
130
|
-
# Create JSON-RPC handler
|
|
131
|
-
@json_rpc_handler = JsonRpcHandler.new(session, self)
|
|
132
|
-
|
|
133
|
-
# Set up transport callbacks
|
|
134
|
-
@transport.on_message do |message|
|
|
135
|
-
handle_raw_message(message)
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
@transport.on_error do |error|
|
|
139
|
-
handle_transport_error(error)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
@transport.on_connect do
|
|
143
|
-
handle_transport_connect
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
@transport.on_disconnect do
|
|
147
|
-
handle_transport_disconnect
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
def handle_raw_message(raw)
|
|
152
|
-
@json_rpc_handler.call(raw)
|
|
153
|
-
rescue MultiJson::ParseError => e
|
|
154
|
-
log_error("JSON parse error: #{e} (raw: #{raw})")
|
|
155
|
-
rescue StandardError => e
|
|
156
|
-
log_error("Error handling message: #{e} (raw: #{raw})")
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
def handle_transport_error(error)
|
|
160
|
-
@connection_error = error.message
|
|
161
|
-
log_error("Transport error: #{error.message}")
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def handle_transport_connect
|
|
165
|
-
log_debug("Transport connected")
|
|
166
|
-
# Send initial capabilities after connection
|
|
167
|
-
send_initial_capabilities
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def handle_transport_disconnect
|
|
171
|
-
log_debug("Transport disconnected")
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def send_initial_capabilities
|
|
175
|
-
log_debug("Sending client capabilities")
|
|
176
|
-
|
|
177
|
-
# If we have a session_id, we're trying to resume
|
|
178
|
-
log_debug("Attempting to resume session: #{@session_id}") if @session_id
|
|
179
|
-
|
|
180
|
-
params = {
|
|
181
|
-
protocolVersion: @protocol_version,
|
|
182
|
-
capabilities: client_capabilities,
|
|
183
|
-
clientInfo: client_info
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
# Include session_id if we're trying to resume
|
|
187
|
-
params[:sessionId] = @session_id if @session_id
|
|
188
|
-
|
|
189
|
-
# Use a unique request ID (not session ID since we don't have one yet)
|
|
190
|
-
request_id = SecureRandom.uuid_v7
|
|
191
|
-
send_jsonrpc_request("initialize", params: params, id: request_id)
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def client_capabilities
|
|
195
|
-
{
|
|
196
|
-
# Base client capabilities can be defined here
|
|
197
|
-
# TODO
|
|
198
|
-
}
|
|
199
|
-
end
|
|
200
|
-
|
|
201
|
-
def user_agent
|
|
202
|
-
"ActionMCP-Client"
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
def client_info
|
|
206
|
-
{
|
|
207
|
-
name: user_agent,
|
|
208
|
-
version: ActionMCP.gem_version.to_s
|
|
209
|
-
}
|
|
210
|
-
end
|
|
211
|
-
|
|
212
|
-
def log_debug(message)
|
|
213
|
-
logger.debug("[ActionMCP::#{self.class.name.split('::').last}] #{message}")
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def log_info(message)
|
|
217
|
-
logger.info("[ActionMCP::#{self.class.name.split('::').last}] #{message}")
|
|
218
|
-
end
|
|
219
|
-
|
|
220
|
-
def log_error(message)
|
|
221
|
-
logger.error("[ActionMCP::#{self.class.name.split('::').last}] #{message}")
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
end
|
|
225
|
-
end
|
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module ActionMCP
|
|
4
|
-
module Client
|
|
5
|
-
# Blueprints
|
|
6
|
-
#
|
|
7
|
-
# A collection that manages and provides access to URI templates (blueprints) for Model Context Protocol (MCP)
|
|
8
|
-
# resource discovery. These blueprints allow dynamic construction of resource URIs by filling in
|
|
9
|
-
# variable placeholders with specific values. The class supports lazy loading of templates when
|
|
10
|
-
# initialized with a client.
|
|
11
|
-
#
|
|
12
|
-
# Example usage:
|
|
13
|
-
# # Eager loading
|
|
14
|
-
# template_data = client.list_resource_templates # Returns array of URI template definitions
|
|
15
|
-
# blueprints = Blueprint.new(template_data)
|
|
16
|
-
#
|
|
17
|
-
# # Lazy loading
|
|
18
|
-
# blueprints = Blueprint.new([], client)
|
|
19
|
-
# templates = blueprints.all # Templates are loaded here
|
|
20
|
-
#
|
|
21
|
-
# # Access a specific blueprint by pattern
|
|
22
|
-
# file_blueprint = Blueprint.find_by_pattern("file://{path}")
|
|
23
|
-
#
|
|
24
|
-
# # Generate a concrete URI from a blueprint with parameters
|
|
25
|
-
# uri = Blueprint.construct("file://{path}", { path: "/logs/app.log" })
|
|
26
|
-
#
|
|
27
|
-
class Blueprint < Collection
|
|
28
|
-
# Initialize a new Blueprints collection with URI template definitions
|
|
29
|
-
#
|
|
30
|
-
# @param templates [Array<Hash>] Array of URI template definition hashes, each containing
|
|
31
|
-
# uriTemplate, name, description, and optionally mimeType keys
|
|
32
|
-
# @param client [Object, nil] Optional client for lazy loading of templates
|
|
33
|
-
def initialize(templates, client)
|
|
34
|
-
super(templates, client)
|
|
35
|
-
self.templates = @collection_data
|
|
36
|
-
@load_method = :list_resource_templates
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Find a blueprint by its URI pattern
|
|
40
|
-
#
|
|
41
|
-
# @param pattern [String] URI template pattern to find
|
|
42
|
-
# @return [Blueprint, nil] The blueprint with the given pattern, or nil if not found
|
|
43
|
-
def find_by_pattern(pattern)
|
|
44
|
-
all.find { |blueprint| blueprint.pattern == pattern }
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Find blueprints by name
|
|
48
|
-
#
|
|
49
|
-
# @param name [String] Name of the blueprints to find
|
|
50
|
-
# @return [Array<Blueprint>] Blueprints with the given name
|
|
51
|
-
def find_by_name(name)
|
|
52
|
-
all.select { |blueprint| blueprint.name == name }
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Construct a concrete URI by applying parameters to a blueprint
|
|
56
|
-
#
|
|
57
|
-
# @param pattern [String] URI template pattern to use
|
|
58
|
-
# @param params [Hash] Parameters to substitute into the pattern
|
|
59
|
-
# @return [String] The constructed URI with parameters applied
|
|
60
|
-
# @raise [KeyError] If a required parameter is missing
|
|
61
|
-
def construct(pattern, params)
|
|
62
|
-
blueprint = find_by_pattern(pattern)
|
|
63
|
-
raise ArgumentError, "Unknown blueprint pattern: #{pattern}" unless blueprint
|
|
64
|
-
|
|
65
|
-
blueprint.construct(params)
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Check if the collection contains a blueprint with the given pattern
|
|
69
|
-
#
|
|
70
|
-
# @param pattern [String] The blueprint pattern to check for
|
|
71
|
-
# @return [Boolean] true if a blueprint with the pattern exists
|
|
72
|
-
def contains?(pattern)
|
|
73
|
-
all.any? { |blueprint| blueprint.pattern == pattern }
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
# Group blueprints by their base protocol
|
|
77
|
-
#
|
|
78
|
-
# @return [Hash<String, Array<Blueprint>>] Hash mapping protocols to arrays of blueprints
|
|
79
|
-
def group_by_protocol
|
|
80
|
-
all.group_by(&:protocol)
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
# Convert raw template data into ResourceTemplate objects
|
|
84
|
-
#
|
|
85
|
-
# @param templates [Array<Hash>] Array of template definition hashes
|
|
86
|
-
def templates=(templates)
|
|
87
|
-
@collection_data = templates.map { |template_data| ResourceTemplate.new(template_data) }
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
# Internal Blueprint class to represent individual URI templates
|
|
91
|
-
class ResourceTemplate
|
|
92
|
-
attr_reader :pattern, :name, :description, :mime_type, :annotations
|
|
93
|
-
|
|
94
|
-
# Initialize a new ResourceTemplate instance
|
|
95
|
-
#
|
|
96
|
-
# @param data [Hash] ResourceTemplate definition hash containing uriTemplate, name, description,
|
|
97
|
-
# and optionally mimeType, and annotations
|
|
98
|
-
def initialize(data)
|
|
99
|
-
@pattern = data["uriTemplate"]
|
|
100
|
-
@name = data["name"]
|
|
101
|
-
@description = data["description"]
|
|
102
|
-
@mime_type = data["mimeType"]
|
|
103
|
-
@variable_pattern = /{([^}]+)}/
|
|
104
|
-
@annotations = data["annotations"] || {}
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
# Extract variable names from the template pattern
|
|
108
|
-
#
|
|
109
|
-
# @return [Array<String>] List of variable names in the pattern
|
|
110
|
-
def variables
|
|
111
|
-
@pattern.scan(@variable_pattern).flatten
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Get the protocol part of the URI template
|
|
115
|
-
#
|
|
116
|
-
# @return [String] The protocol (scheme) of the URI template
|
|
117
|
-
def protocol
|
|
118
|
-
@pattern.split("://").first
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# Construct a concrete URI by substituting parameters into the template pattern
|
|
122
|
-
#
|
|
123
|
-
# @param params [Hash] Parameters to substitute into the pattern
|
|
124
|
-
# @return [String] The constructed URI with parameters applied
|
|
125
|
-
# @raise [KeyError] If a required parameter is missing
|
|
126
|
-
def construct(params)
|
|
127
|
-
result = @pattern.dup
|
|
128
|
-
|
|
129
|
-
variables.each do |var|
|
|
130
|
-
raise KeyError, "Missing required parameter: #{var}" unless params.key?(var.to_sym) || params.key?(var)
|
|
131
|
-
|
|
132
|
-
value = params[var.to_sym] || params[var]
|
|
133
|
-
result.gsub!("{#{var}}", value.to_s)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
result
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
# Check if this template is compatible with a set of parameters
|
|
140
|
-
#
|
|
141
|
-
# @param params [Hash] Parameters to check
|
|
142
|
-
# @return [Boolean] true if all required variables have corresponding parameters
|
|
143
|
-
def compatible_with?(params)
|
|
144
|
-
symbolized_params = params.transform_keys(&:to_sym)
|
|
145
|
-
variables.all? { |var| symbolized_params.key?(var.to_sym) }
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
# Generate a hash representation of the blueprint
|
|
149
|
-
#
|
|
150
|
-
# @return [Hash] Hash containing blueprint details
|
|
151
|
-
def to_h
|
|
152
|
-
{
|
|
153
|
-
"uriTemplate" => @pattern,
|
|
154
|
-
"name" => @name,
|
|
155
|
-
"description" => @description,
|
|
156
|
-
"mimeType" => @mime_type,
|
|
157
|
-
"annotations" => @annotations
|
|
158
|
-
}
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
end
|
|
162
|
-
end
|
|
163
|
-
end
|