ask-mcp 0.1.0 → 0.2.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/CHANGELOG.md +21 -1
- data/README.md +43 -4
- data/lib/ask/mcp/adapters/ask_tool_server.rb +86 -0
- data/lib/ask/mcp/server/stdio.rb +146 -0
- data/lib/ask/mcp/server.rb +14 -0
- data/lib/ask/mcp/version.rb +1 -1
- data/lib/ask/mcp.rb +1 -0
- metadata +6 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 01bf6655c874ece6be6c44d5864e19c844f7edd1fa88c8f90b60b836092bcd90
|
|
4
|
+
data.tar.gz: 2cebafa8dd9d36901b5e78e81350c235ef837f1d4a010792f6c7d23982729969
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 292a26a5c625cd6e1b06d214ce5b3cd9e38a7562f8e388b8d2817ee4fc915d6f743301c23466c03d2f5e5f3399607a02ca07c2c32bc179696cb1c71386609d5c
|
|
7
|
+
data.tar.gz: 9c6395fbae61b1013fb72013dfd3d069f76956ad25efd88c62444b3f6c33823462a01c4cb91b129f1a7001a5fef3d7580b91986391128aea41b33c253221afda
|
data/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
|
-
|
|
1
|
+
## [0.2.0] - 2026-06-26
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- Server Runtime: `Ask::MCP::Server::Stdio` — run as an MCP server over stdio
|
|
5
|
+
with full initialize handshake, tool discovery, and tool call dispatch.
|
|
6
|
+
- `Ask::MCP::Adapters::AskToolServer` — converts `Ask::Tool` instances to MCP
|
|
7
|
+
tool definitions and dispatches calls, enabling any Ask::Tool to be exposed
|
|
8
|
+
via MCP without modification.
|
|
9
|
+
- `Ask::MCP::Server.start_stdio` entry point for easy one-line server setup.
|
|
10
|
+
- `examples/` — reference usage in README "Running as an MCP Server" section.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Server Runtime adds 56 new tests across adapter, server stdio, integration,
|
|
14
|
+
and start_stdio entry point (170 total, up from 114).
|
|
15
|
+
- `ask-mcp` gem description updated to reflect client + server capabilities.
|
|
16
|
+
- Architecture section updated to include server/ and adapters/ask_tool_server.rb.
|
|
17
|
+
|
|
18
|
+
## [0.1.1] - 2026-06-25
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- Major test expansion: Transport tests (SSE/Stdio/StreamableHTTP), MessagesParser, OAuth, Client(17t with cache invalidation), Server(7t), Tool(8t), Messages(17t with serialization), integration tests(7t with mock server). Infrastructure: rubocop, overcommit, CI matrix, gemspec, SimpleCov.
|
|
2
22
|
|
|
3
23
|
## [0.1.0] - 2026-06-10
|
|
4
24
|
|
data/README.md
CHANGED
|
@@ -41,6 +41,43 @@ puts result
|
|
|
41
41
|
client.stop
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
+
|
|
45
|
+
## Running as an MCP Server
|
|
46
|
+
|
|
47
|
+
`ask-mcp` can run as a standalone MCP server over stdio, exposing any
|
|
48
|
+
`Ask::Tool` instances to MCP-compatible clients (Codex Desktop, Claude Code, etc.).
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
require "ask/mcp"
|
|
52
|
+
require "ask-tools-shell"
|
|
53
|
+
require "ask-web-search"
|
|
54
|
+
|
|
55
|
+
tools = Ask::Tools::Shell::TOOLS.map(&:new)
|
|
56
|
+
tools << Ask::Tools::WebSearch.new
|
|
57
|
+
|
|
58
|
+
Ask::MCP::Server.start_stdio(
|
|
59
|
+
name: "my-server",
|
|
60
|
+
tools: tools,
|
|
61
|
+
capabilities: { tools: {} }
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Configure your MCP client:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"mcpServers": {
|
|
70
|
+
"my-server": {
|
|
71
|
+
"command": "ruby",
|
|
72
|
+
"args": ["path/to/your/script.rb"]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
For a production example, see [llm-proxy](https://github.com/ask-rb/llm-proxy)
|
|
79
|
+
which embeds the same server and adds rate limiting, auth, and monitoring.
|
|
80
|
+
|
|
44
81
|
## Transports
|
|
45
82
|
|
|
46
83
|
```ruby
|
|
@@ -146,28 +183,30 @@ wrapped = Ask::MCP::Adapters::AskTool.wrap(client.tools)
|
|
|
146
183
|
wrapped.each { |name, adapter| agent.register_tool(adapter.to_ask_tool) }
|
|
147
184
|
```
|
|
148
185
|
|
|
186
|
+
|
|
149
187
|
## Architecture
|
|
150
188
|
|
|
151
189
|
```
|
|
152
190
|
ask-mcp/
|
|
153
191
|
├── lib/ask/mcp.rb # Entry point, factory methods
|
|
154
192
|
├── lib/ask/mcp/client.rb # MCP client (connect, call_tool, etc.)
|
|
155
|
-
├── lib/ask/mcp/server.rb # MCP server representation
|
|
193
|
+
├── lib/ask/mcp/server.rb # MCP server representation + Server.start_stdio entry point
|
|
194
|
+
├── lib/ask/mcp/server/stdio.rb # MCP server stdio runtime (run as server)
|
|
156
195
|
├── lib/ask/mcp/tool.rb # MCP tool representation
|
|
157
196
|
├── lib/ask/mcp/resource.rb # MCP resource representation
|
|
158
197
|
├── lib/ask/mcp/prompt.rb # MCP prompt representation
|
|
159
198
|
├── lib/ask/mcp/native/messages.rb # JSON-RPC message layer
|
|
160
199
|
├── lib/ask/mcp/transport/
|
|
161
|
-
│ ├── stdio.rb # stdio transport
|
|
200
|
+
│ ├── stdio.rb # stdio transport (client direction)
|
|
162
201
|
│ ├── sse.rb # Server-Sent Events transport
|
|
163
202
|
│ └── streamable_http.rb # Streamable HTTP transport
|
|
164
203
|
├── lib/ask/mcp/auth/
|
|
165
204
|
│ ├── oauth.rb # OAuth 2.1 for MCP
|
|
166
205
|
│ └── token.rb # Token-based auth
|
|
167
206
|
└── lib/ask/mcp/adapters/
|
|
168
|
-
|
|
207
|
+
├── ask_tool.rb # MCP::Tool → Ask::Tool adapter
|
|
208
|
+
└── ask_tool_server.rb # Ask::Tool → MCP server adapter (reverse)
|
|
169
209
|
```
|
|
170
|
-
|
|
171
210
|
## Development
|
|
172
211
|
|
|
173
212
|
```bash
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module MCP
|
|
5
|
+
module Adapters
|
|
6
|
+
# Converts Ask::Tool instances into MCP tool definitions and dispatches
|
|
7
|
+
# calls. This is the server-direction adapter — it takes Ask::Tool objects
|
|
8
|
+
# (from tool packs like ask-tools-shell) and makes them available over MCP.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
# adapter = AskToolServer.new(Ask::Tools::Shell.all)
|
|
12
|
+
# adapter.definitions # => [{ name: "bash", description: "...", inputSchema: {...} }]
|
|
13
|
+
# adapter.call("bash", { command: "echo hi" }) # => { content: [...], isError: ... }
|
|
14
|
+
class AskToolServer
|
|
15
|
+
attr_reader :tools
|
|
16
|
+
|
|
17
|
+
# @param tools [Array<#call, #name, #description, #params_schema>] tool instances to expose.
|
|
18
|
+
# Each tool must respond to name, description, params_schema, and call(args).
|
|
19
|
+
def initialize(tools = [])
|
|
20
|
+
@tools = tools
|
|
21
|
+
@tool_map = tools.each_with_object({}) { |t, h| h[t.name] = t }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# MCP tool definitions for tools/list
|
|
25
|
+
# @return [Array<Hash>]
|
|
26
|
+
def definitions
|
|
27
|
+
@tools.map do |tool|
|
|
28
|
+
schema = tool.params_schema || { type: "object", properties: {}, required: [] }
|
|
29
|
+
{
|
|
30
|
+
name: tool.name,
|
|
31
|
+
description: tool.description || "",
|
|
32
|
+
inputSchema: schema
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Call a tool and wrap the result in MCP format
|
|
38
|
+
# @param name [String] tool name
|
|
39
|
+
# @param arguments [Hash] arguments (may have symbol or string keys)
|
|
40
|
+
# @return [Hash] { content: [...], isError: true/false }
|
|
41
|
+
def call(name, arguments = {})
|
|
42
|
+
tool = @tool_map[name]
|
|
43
|
+
unless tool
|
|
44
|
+
return error_result("Tool not found: #{name}")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Stringify keys — Ask::Tool subclasses expect string keys
|
|
48
|
+
normalized = deep_stringify_keys(arguments)
|
|
49
|
+
result = tool.call(normalized)
|
|
50
|
+
wrap_result(result)
|
|
51
|
+
rescue StandardError => e
|
|
52
|
+
# If the tool raised Ask::Tool::Halt, treat it as a success
|
|
53
|
+
if defined?(Ask::Tool::Halt) && e.is_a?(Ask::Tool::Halt)
|
|
54
|
+
return { content: [{ type: "text", text: e.content.to_s }], isError: false }
|
|
55
|
+
end
|
|
56
|
+
error_result("#{e.class}: #{e.message}")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
private
|
|
60
|
+
|
|
61
|
+
def wrap_result(result)
|
|
62
|
+
if result.respond_to?(:ok?) ? result.ok? : result.ok
|
|
63
|
+
output = result.respond_to?(:output) ? result.output : result.to_s
|
|
64
|
+
text = output.is_a?(Hash) ? (output[:summary] || output.to_s) : output.to_s
|
|
65
|
+
{ content: [{ type: "text", text: text }], isError: false }
|
|
66
|
+
else
|
|
67
|
+
msg = result.respond_to?(:error_message) ? result.error_message : result.to_s
|
|
68
|
+
{ content: [{ type: "text", text: "Error: #{msg}" }], isError: true }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def error_result(message)
|
|
73
|
+
{ content: [{ type: "text", text: message }], isError: true }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def deep_stringify_keys(obj)
|
|
77
|
+
case obj
|
|
78
|
+
when Hash then obj.each_with_object({}) { |(k, v), h| h[k.to_s] = deep_stringify_keys(v) }
|
|
79
|
+
when Array then obj.map { |v| deep_stringify_keys(v) }
|
|
80
|
+
else obj
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module Ask
|
|
6
|
+
module MCP
|
|
7
|
+
class Server
|
|
8
|
+
# MCP server over stdio transport.
|
|
9
|
+
#
|
|
10
|
+
# Reads JSON-RPC 2.0 messages from stdin and writes responses to stdout.
|
|
11
|
+
# Designed to be spawned as a child process by MCP clients (Codex Desktop,
|
|
12
|
+
# Claude Code, etc.) with command/args config.
|
|
13
|
+
#
|
|
14
|
+
# Usage:
|
|
15
|
+
# Server::Stdio.new(name: "my-server", tools: my_tools).start
|
|
16
|
+
#
|
|
17
|
+
class Stdio
|
|
18
|
+
PROTOCOL_VERSION = "0.1.0"
|
|
19
|
+
|
|
20
|
+
attr_reader :name, :tools, :capabilities, :resources, :prompts
|
|
21
|
+
|
|
22
|
+
def initialize(name:, tools: [], capabilities: {}, resources: {}, prompts: {}, debug: false)
|
|
23
|
+
@name = name
|
|
24
|
+
@capabilities = capabilities
|
|
25
|
+
@resources = resources
|
|
26
|
+
@prompts = prompts
|
|
27
|
+
@debug = debug
|
|
28
|
+
|
|
29
|
+
@adapter = Adapters::AskToolServer.new(tools || [])
|
|
30
|
+
@initialized = false
|
|
31
|
+
@running = false
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Start the server (blocking). Processes stdin until EOF.
|
|
35
|
+
def start
|
|
36
|
+
@running = true
|
|
37
|
+
$stdout.sync = true
|
|
38
|
+
|
|
39
|
+
debug_log "Server starting: #{@name} (PID #{Process.pid})"
|
|
40
|
+
debug_log "Tools: #{@adapter.definitions.map { |d| d[:name] }.join(', ')}"
|
|
41
|
+
|
|
42
|
+
while @running && (line = $stdin.gets)
|
|
43
|
+
line = line.strip
|
|
44
|
+
next if line.empty?
|
|
45
|
+
|
|
46
|
+
process_line(line)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
debug_log "stdin closed — exiting"
|
|
50
|
+
rescue Errno::EBADF, IOError
|
|
51
|
+
# stdin closed externally
|
|
52
|
+
ensure
|
|
53
|
+
@running = false
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Stop the server (non-blocking, closes stdin to unblock the read loop).
|
|
57
|
+
def stop
|
|
58
|
+
@running = false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Is the server still running?
|
|
62
|
+
def running?
|
|
63
|
+
@running
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
def process_line(line)
|
|
69
|
+
msg = JSON.parse(line, symbolize_names: true)
|
|
70
|
+
handle_message(msg)
|
|
71
|
+
rescue JSON::ParserError => e
|
|
72
|
+
send_error(nil, -32700, "Parse error: #{e.message}")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def handle_message(msg)
|
|
76
|
+
method = msg[:method]
|
|
77
|
+
id = msg[:id]
|
|
78
|
+
params = msg[:params] || {}
|
|
79
|
+
has_id = msg.key?(:id)
|
|
80
|
+
|
|
81
|
+
case method
|
|
82
|
+
when "initialize"
|
|
83
|
+
handle_initialize(id, params)
|
|
84
|
+
when "notifications/initialized"
|
|
85
|
+
@initialized = true
|
|
86
|
+
debug_log "Client initialized"
|
|
87
|
+
when "tools/list"
|
|
88
|
+
return send_error(id, -32000, "Server not initialized") unless @initialized
|
|
89
|
+
handle_tools_list(id)
|
|
90
|
+
when "tools/call"
|
|
91
|
+
return send_error(id, -32000, "Server not initialized") unless @initialized
|
|
92
|
+
handle_tool_call(id, params)
|
|
93
|
+
else
|
|
94
|
+
debug_log "Unknown method: #{method}"
|
|
95
|
+
send_error(id, -32601, "Method not found: #{method}") if has_id
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def handle_initialize(id, _params)
|
|
100
|
+
@initialized = true
|
|
101
|
+
debug_log "Handling initialize (id=#{id.inspect})"
|
|
102
|
+
send_result(id, {
|
|
103
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
104
|
+
capabilities: @capabilities,
|
|
105
|
+
serverInfo: {
|
|
106
|
+
name: @name,
|
|
107
|
+
version: Ask::MCP::VERSION
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
debug_log "Initialize complete"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def handle_tools_list(id)
|
|
114
|
+
debug_log "Handling tools/list (id=#{id.inspect})"
|
|
115
|
+
defs = @adapter.definitions
|
|
116
|
+
debug_log "tools/list returning #{defs.length} tool definitions"
|
|
117
|
+
send_result(id, { tools: defs })
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def handle_tool_call(id, params)
|
|
121
|
+
tool_name = params[:name].to_s
|
|
122
|
+
arguments = params[:arguments] || {}
|
|
123
|
+
|
|
124
|
+
debug_log "Handling tools/call: #{tool_name} (id=#{id.inspect})"
|
|
125
|
+
|
|
126
|
+
result = @adapter.call(tool_name, arguments)
|
|
127
|
+
send_result(id, result)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def send_result(id, result)
|
|
131
|
+
$stdout.puts({ jsonrpc: "2.0", id: id, result: result }.to_json)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def send_error(id, code, message)
|
|
135
|
+
$stdout.puts({ jsonrpc: "2.0", id: id, error: { code: code, message: message } }.to_json)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def debug_log(msg)
|
|
139
|
+
return unless @debug
|
|
140
|
+
ts = Time.now.strftime("%H:%M:%S.%L")
|
|
141
|
+
$stderr.puts "[#{ts}] [ask-mcp] #{msg}"
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
data/lib/ask/mcp/server.rb
CHANGED
|
@@ -36,6 +36,20 @@ module Ask
|
|
|
36
36
|
prompts: @prompts.values.map(&:to_h)
|
|
37
37
|
}
|
|
38
38
|
end
|
|
39
|
+
|
|
40
|
+
# Start an MCP server over stdio.
|
|
41
|
+
# Blocking — designed to be the last line of an entry-point script.
|
|
42
|
+
#
|
|
43
|
+
# @param name [String] server name
|
|
44
|
+
# @param tools [Array<#call, #name, #description, #params_schema>] tool instances to expose
|
|
45
|
+
# @param capabilities [Hash] MCP capabilities (default: { tools: {} })
|
|
46
|
+
# @param debug [Boolean] enable stderr debug logging
|
|
47
|
+
def self.start_stdio(name:, tools: [], capabilities: { tools: {} }, debug: false)
|
|
48
|
+
Stdio.new(name: name, tools: tools, capabilities: capabilities, debug: debug).start
|
|
49
|
+
end
|
|
39
50
|
end
|
|
40
51
|
end
|
|
41
52
|
end
|
|
53
|
+
|
|
54
|
+
# Load Server subclasses after the Server class is defined
|
|
55
|
+
require_relative "server/stdio"
|
data/lib/ask/mcp/version.rb
CHANGED
data/lib/ask/mcp.rb
CHANGED
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.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Kaka Ruto
|
|
@@ -80,8 +80,8 @@ dependencies:
|
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
81
|
version: '13.0'
|
|
82
82
|
description: Connect to MCP servers via stdio, SSE, and Streamable HTTP transports.
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
Run as an MCP server to expose Ask::Tool instances. Discover tools, resources, and
|
|
84
|
+
prompts. OAuth 2.1 authentication.
|
|
85
85
|
email:
|
|
86
86
|
- kaka@myrrlabs.com
|
|
87
87
|
executables: []
|
|
@@ -95,6 +95,7 @@ files:
|
|
|
95
95
|
- lib/ask-mcp.rb
|
|
96
96
|
- lib/ask/mcp.rb
|
|
97
97
|
- lib/ask/mcp/adapters/ask_tool.rb
|
|
98
|
+
- lib/ask/mcp/adapters/ask_tool_server.rb
|
|
98
99
|
- lib/ask/mcp/auth/oauth.rb
|
|
99
100
|
- lib/ask/mcp/auth/token.rb
|
|
100
101
|
- lib/ask/mcp/client.rb
|
|
@@ -102,6 +103,7 @@ files:
|
|
|
102
103
|
- lib/ask/mcp/prompt.rb
|
|
103
104
|
- lib/ask/mcp/resource.rb
|
|
104
105
|
- lib/ask/mcp/server.rb
|
|
106
|
+
- lib/ask/mcp/server/stdio.rb
|
|
105
107
|
- lib/ask/mcp/tool.rb
|
|
106
108
|
- lib/ask/mcp/transport/sse.rb
|
|
107
109
|
- lib/ask/mcp/transport/stdio.rb
|
|
@@ -131,5 +133,5 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
131
133
|
requirements: []
|
|
132
134
|
rubygems_version: 4.0.3
|
|
133
135
|
specification_version: 4
|
|
134
|
-
summary: Model Context Protocol (MCP) client for Ruby
|
|
136
|
+
summary: Model Context Protocol (MCP) client and server for Ruby
|
|
135
137
|
test_files: []
|