ask-mcp 0.5.0 → 0.6.1

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: 207c163681c8ee59bc9c869627cb6c053ca5e3824bb2e7eafd4669152c0a86cf
4
- data.tar.gz: '029f07e479189995bb8b9aa7b4e09eb7e70308851789a4a9e05ab09a80e56568'
3
+ metadata.gz: b2b34899f61be9226b77f6e212d5756555c86e52e17ba301fcf3eaa24f1d3a62
4
+ data.tar.gz: 51656c01a274744d51bb328d11f0f9dc84ac6f20fbe7fa5bb0b90125e670a2d3
5
5
  SHA512:
6
- metadata.gz: 823681d47e782fa73574987dfdb65f7a0ef53ff68d8dfb46d3537995d1bfa04a231bac6ec53b6b4fa03f7c34276ee6181d339644b821a391dffbcc9642a42303
7
- data.tar.gz: 4118870bcb52e14efccb0d1fec411c01af5d68e5b62111a5c6818d952004f63a8198c35f7264997f7922a41a7418fb6851a31bd3a20e182ead6fbd685011ef8f
6
+ metadata.gz: baec4cee8a4bdd42174107b545e0c7a144dd7a646f64e37f3ed09d3f57826a417a1a801ec5f06fc8aed7119a9357c41f524549e62b5c0446e756ebbd5df0171c
7
+ data.tar.gz: dfaddecd4733e04fa5fbac5ee68ae95031cf4e3111a9db7237170ac7601f261ddb25f7f82eab41f478a2aa010aa9c31c92ca385b2b41d4e615cf628363e43578
data/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ ## 0.6.0
2
+
3
+ - `Auth::Connect`: the browser OAuth flow for MCP servers, as a reusable
4
+ class. Discovers the authorization server behind a protected resource
5
+ (RFC 9728) and its endpoints (RFC 8414 / OIDC), registers the client on
6
+ the spot when the server allows it, builds the authorization URL with
7
+ PKCE, and redeems the redirected code — or a refresh token — for tokens.
8
+ HTTP rides a small injected seam, so hosts can bring their own client.
9
+
10
+ - **`Ask::MCP::RuntimeExecutor`** — bridges MCP clients into the
11
+ `Ask::Runtime::ToolExecutor` interface. Wraps an `Ask::MCP::Client`,
12
+ implements `#execute(tool_call, context:)`, normalizes MCP content/result
13
+ shapes (array, hash, string, nil) into `ToolResult.success` or `failure`,
14
+ honors context cancellation before calling the client, and emits
15
+ `ToolStarted` / `ToolCompleted` / `ToolFailed` / `ToolCancelled` lifecycle
16
+ events via `context.event_sink` with terminal snapshots.
17
+
18
+ - **`Ask::MCP::ToolDiscovery`** module — helper for discovering MCP tools
19
+ from a client and wrapping them as `Adapters::AskTool` or
20
+ `Ask::Tools::Tool` instances.
21
+
1
22
  ## [0.5.0] - 2026-09-17
2
23
 
3
24
  ### Added
data/README.md CHANGED
@@ -161,6 +161,8 @@ annotations are excluded from `tools/list` on HTTP transports.
161
161
  | `server.notify_*_list_changed` | Emit change notifications to clients |
162
162
  | `Ask::MCP::Adapters::AskTool.wrap(tools_hash)` | Adapter from MCP tools to `Ask::Tool` instances for ask-agent |
163
163
  | `Ask::MCP::Adapters::ToolServer` | Adapter from duck-typed tools to MCP server tools |
164
+ | `Ask::MCP::RuntimeExecutor.new(client)` | Bridge MCP client into `Ask::Runtime::ToolExecutor` |
165
+ | `Ask::MCP::ToolDiscovery` | Discover MCP tools and wrap them as `AskTool` adapters |
164
166
  | `Ask::MCP::Auth::Token.new(token)` | Token-based auth (`apply(headers)`) |
165
167
  | `Ask::MCP::Auth::OAuth.new(client_id:, ...)` | OAuth for MCP; `discover!` (OIDC), `authenticate!`, `validate_iss!`, `apply(headers)` |
166
168
  | `Ask::MCP::Auth::ClientIdMetadataDocument` | Build/validate Client ID Metadata Documents (2026-07-28 client registration) |
@@ -188,6 +190,32 @@ wrapped.each { |name, adapter| agent.register_tool(adapter.to_ask_tool) }
188
190
 
189
191
  Expose `Ask::Tool` subclasses as an MCP server with `Ask::MCP::Server.start_stdio(name:, tools:, capabilities: { tools: {} })`; the `ToolServer` adapter handles them.
190
192
 
193
+ ### With ask-runtime
194
+
195
+ Bridge an MCP client into `Ask::Runtime` tool-call pipelines:
196
+
197
+ ```ruby
198
+ require "ask/mcp"
199
+ require "ask/runtime"
200
+
201
+ client = Ask::MCP.from_stdio("npx", ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"])
202
+ client.start
203
+
204
+ executor = Ask::MCP::RuntimeExecutor.new(client)
205
+
206
+ # Build a tool call and context
207
+ call = Ask::Runtime::ToolCall.new(tool_name: "read_file", input: { path: "/tmp/test.txt" })
208
+ ctx = Ask::Runtime::ExecutionContext.new(session_id: "s1", turn: 1)
209
+
210
+ result = executor.execute(call, context: ctx)
211
+ result.success? # => true
212
+ result.output # => file contents
213
+
214
+ # Discover and wrap tools for integration
215
+ include Ask::MCP::ToolDiscovery
216
+ tools = mcp_tools(client) # => Hash{String => Adapters::AskTool}
217
+ ```
218
+
191
219
  ## Full documentation
192
220
 
193
221
  The full ask-rb documentation lives at https://ask-rb.github.io/ask-docs.
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require "digest"
5
+ require "uri"
6
+
7
+ module Ask
8
+ module MCP
9
+ module Auth
10
+ # The browser dance that gives one person a credential for an MCP
11
+ # server: discover the authorization server behind the protected
12
+ # resource, register this client when the server allows it, hand the
13
+ # host a URL to send the browser to, and redeem the redirected code
14
+ # for tokens.
15
+ #
16
+ # Built for hosts that drive a real browser — a web action redirects
17
+ # to #authorization_url and its callback redeems the code — so the
18
+ # class keeps no browser state of its own. The caller carries the
19
+ # state and the PKCE verifier between the two steps, and persists
20
+ # #registered_client when dynamic registration produced one.
21
+ class Connect
22
+ class Error < StandardError; end
23
+
24
+ attr_reader :endpoint, :registered_client
25
+
26
+ def initialize(endpoint:, client_id: nil, client_secret: nil, http: Http)
27
+ @endpoint = endpoint
28
+ @client_id = client_id
29
+ @client_secret = client_secret
30
+ @http = http
31
+ @metadata = nil
32
+ @registered_client = nil
33
+ end
34
+
35
+ # The URL to send the browser to, with the PKCE verifier the caller
36
+ # must keep until the callback. The scope comes from the server's
37
+ # own advertised scopes unless the caller names one.
38
+ def authorization_url(redirect_uri:, state:, scope: nil, code_verifier: nil)
39
+ metadata = discover!
40
+ client = ensure_client!(redirect_uri)
41
+ verifier = code_verifier || self.class.generate_verifier
42
+
43
+ params = {
44
+ response_type: "code",
45
+ client_id: client[:client_id],
46
+ redirect_uri: redirect_uri,
47
+ state: state,
48
+ code_challenge: pkce_challenge(verifier),
49
+ code_challenge_method: "S256"
50
+ }
51
+ params[:scope] = scope || metadata[:scopes_supported]&.join(" ") || "mcp"
52
+
53
+ {url: "#{metadata[:authorization_endpoint]}?#{URI.encode_www_form(params)}", code_verifier: verifier}
54
+ end
55
+
56
+ # Redeem the redirected code. The same verifier the authorization
57
+ # step handed back comes along; the answer is the caller's token to
58
+ # store against whoever consented.
59
+ def exchange(code:, verifier:, redirect_uri:)
60
+ metadata = discover!
61
+ client = ensure_client!(redirect_uri)
62
+
63
+ params = {
64
+ grant_type: "authorization_code",
65
+ code: code,
66
+ redirect_uri: redirect_uri,
67
+ client_id: client[:client_id],
68
+ code_verifier: verifier
69
+ }
70
+ params[:client_secret] = client[:client_secret] if client[:client_secret]
71
+
72
+ token_response(@http.post_json(metadata[:token_endpoint], params))
73
+ end
74
+
75
+ # A fresh access token from the refresh token the server handed out.
76
+ def refresh(refresh_token:)
77
+ metadata = discover!
78
+ client = ensure_client!
79
+
80
+ params = {
81
+ grant_type: "refresh_token",
82
+ refresh_token: refresh_token,
83
+ client_id: client[:client_id]
84
+ }
85
+ params[:client_secret] = client[:client_secret] if client[:client_secret]
86
+
87
+ token_response(@http.post_json(metadata[:token_endpoint], params))
88
+ end
89
+
90
+ # Walk the chain MCP defines: the resource server names its
91
+ # authorization servers, and one of them names its endpoints. A
92
+ # server that publishes nothing is its own authorization server.
93
+ def discover!
94
+ return @metadata if @metadata
95
+
96
+ issuer = authorization_server_for(endpoint)
97
+ document = authorization_server_metadata(issuer)
98
+ unless document && document[:authorization_endpoint] && document[:token_endpoint]
99
+ raise Error, "no authorization server metadata at #{issuer}"
100
+ end
101
+
102
+ @metadata = {
103
+ issuer: issuer,
104
+ authorization_endpoint: document[:authorization_endpoint],
105
+ token_endpoint: document[:token_endpoint],
106
+ registration_endpoint: document[:registration_endpoint],
107
+ scopes_supported: document[:scopes_supported]
108
+ }
109
+ end
110
+
111
+ # The client this server knows us by: pre-registered credentials
112
+ # when the host has them, otherwise a registration the server
113
+ # created on the spot. The registered client is exposed for the
114
+ # host to persist — a registration that is remembered is one that
115
+ # is never asked for twice.
116
+ def ensure_client!(redirect_uri = nil)
117
+ return {client_id: @client_id, client_secret: @client_secret}.compact if @client_id
118
+
119
+ registration = @metadata && @metadata[:registration_endpoint]
120
+ raise Error, "no client credentials and #{endpoint} does not allow registration" unless registration
121
+ raise Error, "dynamic registration needs a redirect_uri" unless redirect_uri
122
+
123
+ @registered_client ||= begin
124
+ document = @http.post_json(registration, registration_params(redirect_uri))
125
+ {client_id: document[:client_id], client_secret: document[:client_secret]}.compact
126
+ end
127
+ end
128
+
129
+ def self.generate_verifier
130
+ Base64.urlsafe_encode64(SecureRandom.random_bytes(64)).delete("=")
131
+ end
132
+
133
+ def self.pkce_challenge(verifier)
134
+ Base64.urlsafe_encode64(Digest::SHA256.digest(verifier)).delete("=")
135
+ end
136
+
137
+ private
138
+
139
+ attr_reader :http
140
+
141
+ def registration_params(redirect_uri)
142
+ {
143
+ client_name: "Anychat",
144
+ redirect_uris: [redirect_uri],
145
+ grant_types: %w[authorization_code refresh_token],
146
+ response_types: %w[code],
147
+ token_endpoint_auth_method: "none",
148
+ application_type: "web"
149
+ }
150
+ end
151
+
152
+ def pkce_challenge(verifier)
153
+ self.class.pkce_challenge(verifier)
154
+ end
155
+
156
+ # RFC 9728: the resource server publishes its metadata under its own
157
+ # well-known path. The RFC allows the root form and a path-suffixed
158
+ # form; a server that publishes neither is treated as its own
159
+ # authorization server, which is how single-server setups behave.
160
+ def authorization_server_for(url)
161
+ uri = URI.parse(url.to_s)
162
+ candidates = [
163
+ well_known_uri(uri, "oauth-protected-resource"),
164
+ well_known_uri(uri, "oauth-protected-resource", suffix_path: uri.path)
165
+ ].compact
166
+
167
+ document = candidates.filter_map { |candidate| http.get_json(candidate) rescue nil }.first
168
+ servers = document && (document[:authorization_servers] || document["authorization_servers"])
169
+ Array(servers).first || "#{uri.scheme}://#{uri.host}#{uri.port == uri.default_port ? "" : ":#{uri.port}"}"
170
+ rescue URI::InvalidURIError
171
+ raise Error, "not a usable endpoint: #{endpoint.inspect}"
172
+ end
173
+
174
+ # RFC 8414 and OIDC discovery, same bargain: try the standard
175
+ # placements and take the first that answers.
176
+ def authorization_server_metadata(issuer)
177
+ issuer_uri = URI.parse(issuer.to_s)
178
+ [
179
+ well_known_uri(issuer_uri, "oauth-authorization-server"),
180
+ well_known_uri(issuer_uri, "oauth-authorization-server", suffix_path: issuer_uri.path),
181
+ well_known_uri(issuer_uri, "openid-configuration")
182
+ ].compact.filter_map { |candidate| http.get_json(candidate) rescue nil }.first
183
+ end
184
+
185
+ def well_known_uri(uri, name, suffix_path: nil)
186
+ path = suffix_path.to_s.sub(%r{/\z}, "")
187
+ base = "#{uri.scheme}://#{uri.host}#{uri.port == uri.default_port ? "" : ":#{uri.port}"}"
188
+ if path.empty?
189
+ "#{base}/.well-known/#{name}"
190
+ else
191
+ "#{base}/.well-known/#{name}#{path}"
192
+ end
193
+ end
194
+
195
+ def token_response(response)
196
+ unless response[:access_token]
197
+ raise Error, "token request did not return an access_token"
198
+ end
199
+
200
+ {
201
+ access_token: response[:access_token],
202
+ refresh_token: response[:refresh_token],
203
+ expires_at: response[:expires_in] ? Time.now + response[:expires_in].to_i : nil,
204
+ scope: response[:scope]
205
+ }
206
+ end
207
+
208
+ # A thin HTTP seam, so a host can bring its own client and tests can
209
+ # stay off the network. Form-encoded bodies, because that is what
210
+ # authorization servers speak at these endpoints.
211
+ module Http
212
+ module_function
213
+
214
+ def get_json(url)
215
+ require "httpx"
216
+
217
+ response = HTTPX.get(url)
218
+ return nil unless response.status == 200
219
+
220
+ JSON.parse(response.body.to_s, symbolize_names: true)
221
+ rescue JSON::ParserError
222
+ nil
223
+ end
224
+
225
+ def post_json(url, params)
226
+ require "httpx"
227
+
228
+ response = HTTPX.post(url, form: params)
229
+ unless response.status == 200 || response.status == 201
230
+ raise Error, "request to #{url} failed: #{response.status} #{response.body.to_s[0, 200]}"
231
+ end
232
+
233
+ JSON.parse(response.body.to_s, symbolize_names: true)
234
+ rescue JSON::ParserError
235
+ raise Error, "invalid JSON from #{url}"
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ask/runtime"
4
+
5
+ module Ask
6
+ module MCP
7
+ # Bridges an MCP Client into the Ask::Runtime::ToolExecutor interface.
8
+ #
9
+ # Wraps an Ask::MCP::Client and implements the execute() contract so that
10
+ # MCP tools can participate in Ask::Runtime tool-call pipelines.
11
+ #
12
+ # executor = Ask::MCP::RuntimeExecutor.new(client)
13
+ # result = executor.execute(tool_call, context: ctx)
14
+ #
15
+ # Responsibilities:
16
+ # - Delegates to client.call_tool(tool_call.tool_name, tool_call.input)
17
+ # - Normalizes MCP content/result shapes into ToolResult.success or failure
18
+ # - Honors context cancellation before calling the client
19
+ # - Emits ToolStarted / ToolCompleted / ToolFailed / ToolCancelled events
20
+ # via context.event_sink with terminal snapshots
21
+ #
22
+ # Error handling:
23
+ # - MCP isError responses → ToolResult.failure
24
+ # - Client exceptions (ProtocolError, ConnectionError) → ToolResult.failure
25
+ # - Malformed/empty results → ToolResult.success with nil output
26
+ #
27
+ class RuntimeExecutor
28
+ include Ask::Runtime::ToolExecutor
29
+
30
+ # @param client [Ask::MCP::Client] an initialized MCP client
31
+ def initialize(client)
32
+ @client = client
33
+ end
34
+
35
+ # @return [Ask::MCP::Client] the underlying MCP client
36
+ attr_reader :client
37
+
38
+ # Create a RuntimeExecutor from a client.
39
+ #
40
+ # @param client [Ask::MCP::Client] an initialized MCP client
41
+ # @return [RuntimeExecutor]
42
+ def self.from_client(client)
43
+ new(client)
44
+ end
45
+
46
+ # Execute a tool call via the MCP client.
47
+ #
48
+ # @param tool_call [Ask::Runtime::ToolCall] the tool-call request
49
+ # @param context [Ask::Runtime::ExecutionContext, nil] execution context
50
+ # @return [Ask::Runtime::ToolResult] normalized result
51
+ def execute(tool_call, context: nil)
52
+ context ||= Ask::Runtime::ExecutionContext.new
53
+
54
+ started_call = transition(tool_call, :running, started_at: Time.now)
55
+ emit_event(Ask::Runtime::Events::ToolStarted,
56
+ tool_call: started_call, execution_context: context)
57
+
58
+ return cancel(tool_call, context, "Cancelled before execution") if context.cancelled?
59
+
60
+ result = call_tool(tool_call)
61
+ return cancel(tool_call, context, "Cancelled during execution") if context.cancelled?
62
+
63
+ finished_at = Time.now
64
+ duration = finished_at - started_call.started_at
65
+
66
+ terminal_call = started_call.with(
67
+ state: result.success? ? :completed : :failed,
68
+ tool_result: result,
69
+ finished_at: finished_at
70
+ )
71
+
72
+ event_class = result.success? ? Ask::Runtime::Events::ToolCompleted : Ask::Runtime::Events::ToolFailed
73
+ emit_event(event_class,
74
+ tool_call: terminal_call, tool_result: result,
75
+ execution_context: context, duration: duration)
76
+
77
+ result_with_duration(result, duration)
78
+ end
79
+
80
+ private
81
+
82
+ def call_tool(tool_call)
83
+ response = @client.call_tool(tool_call.tool_name, tool_call.input)
84
+ normalize_result(response)
85
+ rescue Ask::MCP::Error, Ask::MCP::ConnectionError => e
86
+ Ask::Runtime::ToolResult.failure(e.message)
87
+ rescue => e
88
+ Ask::Runtime::ToolResult.failure("#{e.class}: #{e.message}")
89
+ end
90
+
91
+ # Normalize MCP tools/call response shapes into a ToolResult.
92
+ #
93
+ # MCP responses are one of:
94
+ # Array — [{ type: "text", text: "..." }, ...]
95
+ # Hash — { content: [...], isError: bool, ... }
96
+ # String — plain text (some servers)
97
+ # nil — empty response
98
+ def normalize_result(response)
99
+ error = error?(response)
100
+ output = extract_output(response)
101
+
102
+ if error
103
+ msg = extract_error_message(response) ||
104
+ (output.is_a?(String) ? output : nil) ||
105
+ "MCP tool error"
106
+ Ask::Runtime::ToolResult.failure(msg)
107
+ else
108
+ Ask::Runtime::ToolResult.success(data: output)
109
+ end
110
+ end
111
+
112
+ def extract_output(response)
113
+ case response
114
+ when Array
115
+ extract_from_content_array(response)
116
+ when Hash
117
+ if response[:content] || response["content"]
118
+ extract_from_content_array(response[:content] || response["content"])
119
+ else
120
+ response
121
+ end
122
+ when String
123
+ response
124
+ when nil
125
+ nil
126
+ else
127
+ response.to_s
128
+ end
129
+ end
130
+
131
+ def extract_from_content_array(array)
132
+ return nil if array.nil? || array.empty?
133
+
134
+ if array.length == 1
135
+ item = array.first
136
+ if item.is_a?(Hash) && (item[:text] || item["text"])
137
+ return item[:text] || item["text"]
138
+ end
139
+ return array
140
+ end
141
+
142
+ texts = array.filter_map do |item|
143
+ item.is_a?(Hash) ? (item[:text] || item["text"]) : item&.to_s
144
+ end
145
+ texts.length == array.length ? texts.join("\n") : array
146
+ end
147
+
148
+ def error?(response)
149
+ return false unless response.is_a?(Hash)
150
+
151
+ response[:isError] || response["isError"]
152
+ end
153
+
154
+ def extract_error_message(response)
155
+ return unless response.is_a?(Hash)
156
+
157
+ content = response[:content] || response["content"]
158
+ return unless content.is_a?(Array) && content.any?
159
+
160
+ first = content.first
161
+ first[:text] || first["text"] if first.is_a?(Hash)
162
+ end
163
+
164
+ def cancel(tool_call, context, reason)
165
+ result = Ask::Runtime::ToolResult.cancelled(reason)
166
+ terminal_call = tool_call.with(
167
+ state: :cancelled,
168
+ tool_result: result,
169
+ finished_at: Time.now
170
+ )
171
+ duration = terminal_call.finished_at - terminal_call.created_at
172
+
173
+ emit_event(Ask::Runtime::Events::ToolCancelled,
174
+ tool_call: terminal_call, tool_result: result,
175
+ execution_context: context, duration: duration)
176
+
177
+ result
178
+ end
179
+
180
+ def transition(tool_call, state, **attrs)
181
+ tool_call.with(state: state, **attrs)
182
+ end
183
+
184
+ def result_with_duration(result, duration)
185
+ return result if result.duration == duration
186
+
187
+ Ask::Runtime::ToolResult.new(
188
+ result: result.result,
189
+ outcome: result.outcome,
190
+ duration: duration
191
+ )
192
+ end
193
+
194
+ def emit_event(event_class, **args)
195
+ timestamp = Time.now
196
+ event = event_class.new(**args, timestamp: timestamp)
197
+ sink = args[:execution_context].event_sink
198
+ event_type = event_class.name.split("::").last
199
+ .gsub(/([a-z])([A-Z])/, '\1_\2')
200
+ .downcase
201
+ .to_sym
202
+ sink.emit(event_type, event: event)
203
+ rescue => e
204
+ warn "[ask-mcp][runtime_executor] event emission failed: #{e.message}"
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module MCP
5
+ # Helpers for discovering MCP tools and exposing them as Ask::Tool wrappers.
6
+ #
7
+ # include Ask::MCP::ToolDiscovery
8
+ # tools = mcp_tools(client) # => Hash{String => Adapters::AskTool}
9
+ #
10
+ module ToolDiscovery
11
+ # Discover tools from an MCP client and wrap them as AskTool adapters.
12
+ #
13
+ # @param client [Ask::MCP::Client] an initialized MCP client
14
+ # @return [Hash{String => Adapters::AskTool}] tools keyed by name
15
+ def mcp_tools(client)
16
+ tools = client.tools
17
+ Adapters::AskTool.wrap(tools)
18
+ end
19
+
20
+ # Discover tools and convert them to Ask::Tools::Tool instances.
21
+ #
22
+ # @param client [Ask::MCP::Client] an initialized MCP client
23
+ # @return [Hash{String => Ask::Tools::Tool}] tools keyed by name
24
+ def mcp_ask_tools(client)
25
+ mcp_tools(client).transform_values(&:to_ask_tool)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Ask
2
4
  module MCP
3
- VERSION = "0.5.0"
5
+ VERSION = "0.6.1"
4
6
  end
5
7
  end
data/lib/ask/mcp.rb CHANGED
@@ -24,6 +24,8 @@ module Ask
24
24
  autoload :Validator, "ask/mcp/validator"
25
25
  autoload :XMcpHeader, "ask/mcp/x_mcp_header"
26
26
  autoload :TraceContext, "ask/mcp/trace_context"
27
+ autoload :RuntimeExecutor, "ask/mcp/runtime_executor"
28
+ autoload :ToolDiscovery, "ask/mcp/tool_discovery"
27
29
 
28
30
  module Native
29
31
  autoload :Messages, "ask/mcp/native/messages"
@@ -37,6 +39,7 @@ module Ask
37
39
 
38
40
  module Auth
39
41
  autoload :OAuth, "ask/mcp/auth/oauth"
42
+ autoload :Connect, "ask/mcp/auth/connect"
40
43
  autoload :Token, "ask/mcp/auth/token"
41
44
  autoload :ClientIdMetadataDocument, "ask/mcp/auth/client_id_metadata_document"
42
45
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -93,6 +93,34 @@ dependencies:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
95
  version: '13.0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: ask-core
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 0.12.0
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 0.12.0
110
+ - !ruby/object:Gem::Dependency
111
+ name: ask-runtime
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 0.1.0
117
+ type: :development
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: 0.1.0
96
124
  description: Connect to MCP servers via stdio, SSE, and Streamable HTTP transports.
97
125
  Run as an MCP server exposing any Ruby object as a tool. Discover tools, resources,
98
126
  and prompts. OAuth 2.1 authentication.
@@ -112,17 +140,20 @@ files:
112
140
  - lib/ask/mcp/adapters/ask_tool.rb
113
141
  - lib/ask/mcp/adapters/tool_server.rb
114
142
  - lib/ask/mcp/auth/client_id_metadata_document.rb
143
+ - lib/ask/mcp/auth/connect.rb
115
144
  - lib/ask/mcp/auth/oauth.rb
116
145
  - lib/ask/mcp/auth/token.rb
117
146
  - lib/ask/mcp/client.rb
118
147
  - lib/ask/mcp/native/messages.rb
119
148
  - lib/ask/mcp/prompt.rb
120
149
  - lib/ask/mcp/resource.rb
150
+ - lib/ask/mcp/runtime_executor.rb
121
151
  - lib/ask/mcp/server.rb
122
152
  - lib/ask/mcp/server/core.rb
123
153
  - lib/ask/mcp/server/http.rb
124
154
  - lib/ask/mcp/server/stdio.rb
125
155
  - lib/ask/mcp/tool.rb
156
+ - lib/ask/mcp/tool_discovery.rb
126
157
  - lib/ask/mcp/trace_context.rb
127
158
  - lib/ask/mcp/transport/sse.rb
128
159
  - lib/ask/mcp/transport/stdio.rb