ask-mcp 0.6.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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +28 -0
- data/lib/ask/mcp/runtime_executor.rb +208 -0
- data/lib/ask/mcp/tool_discovery.rb +29 -0
- data/lib/ask/mcp/version.rb +1 -1
- data/lib/ask/mcp.rb +2 -0
- metadata +31 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b2b34899f61be9226b77f6e212d5756555c86e52e17ba301fcf3eaa24f1d3a62
|
|
4
|
+
data.tar.gz: 51656c01a274744d51bb328d11f0f9dc84ac6f20fbe7fa5bb0b90125e670a2d3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: baec4cee8a4bdd42174107b545e0c7a144dd7a646f64e37f3ed09d3f57826a417a1a801ec5f06fc8aed7119a9357c41f524549e62b5c0446e756ebbd5df0171c
|
|
7
|
+
data.tar.gz: dfaddecd4733e04fa5fbac5ee68ae95031cf4e3111a9db7237170ac7601f261ddb25f7f82eab41f478a2aa010aa9c31c92ca385b2b41d4e615cf628363e43578
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@
|
|
|
7
7
|
PKCE, and redeems the redirected code — or a refresh token — for tokens.
|
|
8
8
|
HTTP rides a small injected seam, so hosts can bring their own client.
|
|
9
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
|
+
|
|
10
22
|
## [0.5.0] - 2026-09-17
|
|
11
23
|
|
|
12
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,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
|
data/lib/ask/mcp/version.rb
CHANGED
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"
|
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.6.
|
|
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.
|
|
@@ -119,11 +147,13 @@ files:
|
|
|
119
147
|
- lib/ask/mcp/native/messages.rb
|
|
120
148
|
- lib/ask/mcp/prompt.rb
|
|
121
149
|
- lib/ask/mcp/resource.rb
|
|
150
|
+
- lib/ask/mcp/runtime_executor.rb
|
|
122
151
|
- lib/ask/mcp/server.rb
|
|
123
152
|
- lib/ask/mcp/server/core.rb
|
|
124
153
|
- lib/ask/mcp/server/http.rb
|
|
125
154
|
- lib/ask/mcp/server/stdio.rb
|
|
126
155
|
- lib/ask/mcp/tool.rb
|
|
156
|
+
- lib/ask/mcp/tool_discovery.rb
|
|
127
157
|
- lib/ask/mcp/trace_context.rb
|
|
128
158
|
- lib/ask/mcp/transport/sse.rb
|
|
129
159
|
- lib/ask/mcp/transport/stdio.rb
|