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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90e04370e4d5b1136abb513220b68beb46a5ca2c74d8f7ca694fa59297f8f32e
4
- data.tar.gz: 17f2accea77cefc0ea51fedcc3879e219fff47760999b0ba39e0e168d30dbfe1
3
+ metadata.gz: ae155e99332cb2a3fcec6b65c6d0ac20aa9b2b81b27f1e3aa3c667dc0c7a42fb
4
+ data.tar.gz: af84c5f5aae82c640e2c12316cde40444fe7733d6ad5aaddd6780242b98e1a1c
5
5
  SHA512:
6
- metadata.gz: d8a481b9467e8f4d6d4d305da61346855f4d27ac94de77f109ac528e704599c18aee5a9c5deb64f7edb71a71b4e446cc1801149df21891d15eefdcb441eb1d09
7
- data.tar.gz: b78ccc9a910fbfbeaf2e273ce6169279e4195624871f89356d6564ab6cd46380dd876951544e90b2f3b3b57328ae00180c6d218e1d66afa5416090c5703e1560
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` — the adapter is
5
- now duck-typed and works with any Ruby object, not just Ask::Tool instances.
6
- - README fully rewritten: leads with a general-purpose duck-typed server example
7
- instead of ask-tools-specific code. ask-rb integration is now a sub-section.
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` run as an MCP server over stdio
14
- with full initialize handshake, tool discovery, and tool call dispatch.
15
- - `Adapters::AskToolServer` converts tools to MCP definitions and dispatches calls.
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: Transport tests, MessagesParser, OAuth, Client, Server, Tool, Messages, integration. Rubocop, overcommit, CI matrix, SimpleCov.
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 full JSON-RPC 2.0 message layer
30
- - stdio, SSE, and Streamable HTTP transports
31
- - Tool, Resource, and Prompt data models
32
- - Client lifecycle, tool calling, resource reading, prompt retrieval
33
- - Token-based and OAuth 2.1 authentication
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.
@@ -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
- PROTOCOL_VERSION = "0.1.0"
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: {}, debug: false)
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, _params)
111
+ def handle_initialize(id, params)
100
112
  @initialized = true
101
- debug_log "Handling initialize (id=#{id.inspect})"
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: PROTOCOL_VERSION,
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 = @adapter.call(tool_name, arguments)
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
@@ -1,5 +1,5 @@
1
1
  module Ask
2
2
  module MCP
3
- VERSION = "0.3.0"
3
+ VERSION = "0.4.0"
4
4
  end
5
5
  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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto