ask-mcp 0.3.0 → 0.4.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 +30 -22
- data/lib/ask/mcp/server/stdio.rb +60 -21
- data/lib/ask/mcp/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ae155e99332cb2a3fcec6b65c6d0ac20aa9b2b81b27f1e3aa3c667dc0c7a42fb
|
|
4
|
+
data.tar.gz: af84c5f5aae82c640e2c12316cde40444fe7733d6ad5aaddd6780242b98e1a1c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 62f473ff0fdc35b25adcdadf3aeb2a65c330767f7b73cc85bed204a32bff7fcc28f0348c2ee3f8e8ae1e3d7eaf3977714af67d67038c1be990d9feca2199d81f
|
|
7
|
+
data.tar.gz: e511364526282fcb429740dbd65bbce3e51d0bde06a06cb672e33a0c30b1b09e1971c663145f1b272d7e01015060349447bb7d53081b8dd9813dea02f19591e9
|
data/CHANGELOG.md
CHANGED
|
@@ -1,37 +1,45 @@
|
|
|
1
|
+
## [0.4.0] - 2026-06-26
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
- Request ID deduplication — retried `tools/call` with the same ID returns
|
|
5
|
+
cached result instead of re-executing the tool.
|
|
6
|
+
- SIGTERM/SIGHUP graceful shutdown — closes stdin to unblock the read loop
|
|
7
|
+
so the current tool call can finish before the process exits.
|
|
8
|
+
- Configurable `tool_timeout` option — prevents hung tool calls from
|
|
9
|
+
blocking the server indefinitely.
|
|
10
|
+
- `ping` handler for MCP keepalive support.
|
|
11
|
+
- 10 new tests: request dedup (3), tool timeout (2), SIGTERM (1),
|
|
12
|
+
multiline JSON safety (3), ping (1) — 180 total.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- `Server::Stdio` now echoes back the client's requested protocol version
|
|
16
|
+
instead of hardcoding a value.
|
|
17
|
+
|
|
1
18
|
## [0.3.0] - 2026-06-26
|
|
2
19
|
|
|
3
20
|
### Changed
|
|
4
|
-
- Renamed `Adapters::AskToolServer` → `Adapters::ToolServer` —
|
|
5
|
-
|
|
6
|
-
- README
|
|
7
|
-
|
|
8
|
-
- Gemspec description updated to reflect general-purpose positioning.
|
|
21
|
+
- Renamed `Adapters::AskToolServer` → `Adapters::ToolServer` — duck-typed,
|
|
22
|
+
works with any Ruby object, not just Ask::Tool instances.
|
|
23
|
+
- README rewritten to lead with general-purpose duck-typed server example.
|
|
24
|
+
- ask-rb integration is now a documented sub-section.
|
|
9
25
|
|
|
10
26
|
## [0.2.0] - 2026-06-26
|
|
11
27
|
|
|
12
28
|
### Added
|
|
13
|
-
- Server Runtime: `Ask::MCP::Server::Stdio`
|
|
14
|
-
|
|
15
|
-
-
|
|
16
|
-
- `Ask::MCP::Server.start_stdio` entry point for easy one-line server setup.
|
|
17
|
-
|
|
18
|
-
### Changed
|
|
19
|
-
- 56 new tests across adapter, server stdio, integration, and start_stdio (170 total).
|
|
29
|
+
- Server Runtime: `Ask::MCP::Server::Stdio` with full initialize handshake.
|
|
30
|
+
- `Server.start_stdio` entry point for one-line server setup.
|
|
31
|
+
- 56 new tests (170 total at time of release).
|
|
20
32
|
|
|
21
33
|
## [0.1.1] - 2026-06-25
|
|
22
34
|
|
|
23
35
|
### Changed
|
|
24
|
-
- Major test expansion
|
|
36
|
+
- Major test expansion across all modules.
|
|
25
37
|
|
|
26
38
|
## [0.1.0] - 2026-06-10
|
|
27
39
|
|
|
28
40
|
### Added
|
|
29
|
-
- Core MCP client with
|
|
30
|
-
- stdio, SSE, and Streamable HTTP transports
|
|
31
|
-
- Tool, Resource,
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
- Ask::Tool adapter for ask-agent integration
|
|
35
|
-
- Thread-safe request/response matching with configurable timeouts
|
|
36
|
-
- Server-side notifications handling
|
|
37
|
-
- Factory methods: `from_stdio`, `from_sse`, `from_http`, `connect`
|
|
41
|
+
- Core MCP client with JSON-RPC 2.0 message layer.
|
|
42
|
+
- stdio, SSE, and Streamable HTTP transports.
|
|
43
|
+
- Tool, Resource, Prompt data models.
|
|
44
|
+
- OAuth 2.1 and token-based authentication.
|
|
45
|
+
- ask-agent integration via AskTool adapter.
|
data/lib/ask/mcp/server/stdio.rb
CHANGED
|
@@ -1,70 +1,80 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
|
+
require "timeout"
|
|
4
5
|
|
|
5
6
|
module Ask
|
|
6
7
|
module MCP
|
|
7
8
|
class Server
|
|
8
9
|
# 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
10
|
class Stdio
|
|
18
|
-
|
|
11
|
+
MAX_RESULT_CACHE = 100
|
|
19
12
|
|
|
20
13
|
attr_reader :name, :tools, :capabilities, :resources, :prompts
|
|
21
14
|
|
|
22
|
-
def initialize(name:, tools: [], capabilities: {}, resources: {}, prompts: {},
|
|
15
|
+
def initialize(name:, tools: [], capabilities: {}, resources: {}, prompts: {},
|
|
16
|
+
debug: false, tool_timeout: nil)
|
|
23
17
|
@name = name
|
|
24
18
|
@capabilities = capabilities
|
|
25
19
|
@resources = resources
|
|
26
20
|
@prompts = prompts
|
|
27
21
|
@debug = debug
|
|
22
|
+
@tool_timeout = tool_timeout
|
|
28
23
|
|
|
29
24
|
@adapter = Adapters::ToolServer.new(tools || [])
|
|
30
25
|
@initialized = false
|
|
31
26
|
@running = false
|
|
27
|
+
@shutdown_requested = false
|
|
28
|
+
@result_cache = {}
|
|
32
29
|
end
|
|
33
30
|
|
|
34
|
-
# Start the server (blocking). Processes stdin until EOF.
|
|
35
31
|
def start
|
|
36
32
|
@running = true
|
|
37
33
|
$stdout.sync = true
|
|
38
34
|
|
|
35
|
+
# Graceful shutdown: on TERM/HUP, set flag and close stdin to
|
|
36
|
+
# unblock the read loop so the current tool call can finish.
|
|
37
|
+
trap("TERM") { graceful_shutdown }
|
|
38
|
+
trap("HUP") { graceful_shutdown }
|
|
39
|
+
|
|
39
40
|
debug_log "Server starting: #{@name} (PID #{Process.pid})"
|
|
40
41
|
debug_log "Tools: #{@adapter.definitions.map { |d| d[:name] }.join(', ')}"
|
|
41
42
|
|
|
42
|
-
while @running && (line = $stdin.gets)
|
|
43
|
+
while @running && !@shutdown_requested && (line = $stdin.gets)
|
|
43
44
|
line = line.strip
|
|
44
45
|
next if line.empty?
|
|
45
|
-
|
|
46
46
|
process_line(line)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
49
|
debug_log "stdin closed — exiting"
|
|
50
50
|
rescue Errno::EBADF, IOError
|
|
51
|
-
# stdin closed externally
|
|
51
|
+
# stdin closed externally (e.g. from trap handler)
|
|
52
|
+
rescue SignalException
|
|
53
|
+
# SIGTERM or SIGHUP during blocked read
|
|
52
54
|
ensure
|
|
53
55
|
@running = false
|
|
56
|
+
@shutdown_requested = false
|
|
57
|
+
trap("TERM", "DEFAULT")
|
|
58
|
+
trap("HUP", "DEFAULT")
|
|
54
59
|
end
|
|
55
60
|
|
|
56
|
-
# Stop the server (non-blocking, closes stdin to unblock the read loop).
|
|
57
61
|
def stop
|
|
58
62
|
@running = false
|
|
59
63
|
end
|
|
60
64
|
|
|
61
|
-
# Is the server still running?
|
|
62
65
|
def running?
|
|
63
66
|
@running
|
|
64
67
|
end
|
|
65
68
|
|
|
66
69
|
private
|
|
67
70
|
|
|
71
|
+
def graceful_shutdown
|
|
72
|
+
@shutdown_requested = true
|
|
73
|
+
# Close stdin to unblock $stdin.gets so the signal handler
|
|
74
|
+
# returns promptly and the process exits cleanly.
|
|
75
|
+
$stdin.close rescue nil
|
|
76
|
+
end
|
|
77
|
+
|
|
68
78
|
def process_line(line)
|
|
69
79
|
msg = JSON.parse(line, symbolize_names: true)
|
|
70
80
|
handle_message(msg)
|
|
@@ -90,17 +100,20 @@ module Ask
|
|
|
90
100
|
when "tools/call"
|
|
91
101
|
return send_error(id, -32000, "Server not initialized") unless @initialized
|
|
92
102
|
handle_tool_call(id, params)
|
|
103
|
+
when "ping"
|
|
104
|
+
send_result(id, {}) if has_id
|
|
93
105
|
else
|
|
94
106
|
debug_log "Unknown method: #{method}"
|
|
95
107
|
send_error(id, -32601, "Method not found: #{method}") if has_id
|
|
96
108
|
end
|
|
97
109
|
end
|
|
98
110
|
|
|
99
|
-
def handle_initialize(id,
|
|
111
|
+
def handle_initialize(id, params)
|
|
100
112
|
@initialized = true
|
|
101
|
-
|
|
113
|
+
client_version = params[:protocolVersion] || PROTOCOL_VERSION
|
|
114
|
+
debug_log "Handling initialize (id=#{id.inspect}, version=#{client_version})"
|
|
102
115
|
send_result(id, {
|
|
103
|
-
protocolVersion:
|
|
116
|
+
protocolVersion: client_version,
|
|
104
117
|
capabilities: @capabilities,
|
|
105
118
|
serverInfo: {
|
|
106
119
|
name: @name,
|
|
@@ -111,20 +124,41 @@ module Ask
|
|
|
111
124
|
end
|
|
112
125
|
|
|
113
126
|
def handle_tools_list(id)
|
|
114
|
-
debug_log "Handling tools/list (id=#{id.inspect})"
|
|
115
127
|
defs = @adapter.definitions
|
|
116
128
|
debug_log "tools/list returning #{defs.length} tool definitions"
|
|
117
129
|
send_result(id, { tools: defs })
|
|
118
130
|
end
|
|
119
131
|
|
|
120
132
|
def handle_tool_call(id, params)
|
|
133
|
+
cache_key = id.to_s
|
|
134
|
+
|
|
135
|
+
# Return cached result for retried requests (same ID, already processed)
|
|
136
|
+
if @result_cache.key?(cache_key)
|
|
137
|
+
debug_log "Returning cached result for id=#{id}"
|
|
138
|
+
return send_result(id, @result_cache[cache_key])
|
|
139
|
+
end
|
|
140
|
+
|
|
121
141
|
tool_name = params[:name].to_s
|
|
122
142
|
arguments = params[:arguments] || {}
|
|
123
143
|
|
|
124
144
|
debug_log "Handling tools/call: #{tool_name} (id=#{id.inspect})"
|
|
125
145
|
|
|
126
|
-
result = @
|
|
146
|
+
result = if @tool_timeout
|
|
147
|
+
Timeout.timeout(@tool_timeout) { @adapter.call(tool_name, arguments) }
|
|
148
|
+
else
|
|
149
|
+
@adapter.call(tool_name, arguments)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
@result_cache[cache_key] = result
|
|
153
|
+
trim_cache
|
|
154
|
+
|
|
127
155
|
send_result(id, result)
|
|
156
|
+
rescue Timeout::Error
|
|
157
|
+
debug_log "Tool call timed out: #{tool_name}"
|
|
158
|
+
send_result(id, {
|
|
159
|
+
content: [{ type: "text", text: "Tool call timed out: #{tool_name}" }],
|
|
160
|
+
isError: true
|
|
161
|
+
})
|
|
128
162
|
end
|
|
129
163
|
|
|
130
164
|
def send_result(id, result)
|
|
@@ -140,6 +174,11 @@ module Ask
|
|
|
140
174
|
ts = Time.now.strftime("%H:%M:%S.%L")
|
|
141
175
|
$stderr.puts "[#{ts}] [ask-mcp] #{msg}"
|
|
142
176
|
end
|
|
177
|
+
|
|
178
|
+
def trim_cache
|
|
179
|
+
return if @result_cache.size <= MAX_RESULT_CACHE
|
|
180
|
+
@result_cache.shift(@result_cache.size - MAX_RESULT_CACHE)
|
|
181
|
+
end
|
|
143
182
|
end
|
|
144
183
|
end
|
|
145
184
|
end
|
data/lib/ask/mcp/version.rb
CHANGED