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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f1427ce9d98f71ef7654b7f8ac59cfd2c58940dbecf5d32525005e1af1e1a440
4
- data.tar.gz: e4efdc1538eb707317ceb9f1fb2d06ba5b2e0f4903ee9a8eff7c094915d28d36
3
+ metadata.gz: 01bf6655c874ece6be6c44d5864e19c844f7edd1fa88c8f90b60b836092bcd90
4
+ data.tar.gz: 2cebafa8dd9d36901b5e78e81350c235ef837f1d4a010792f6c7d23982729969
5
5
  SHA512:
6
- metadata.gz: e18c000c2f9069f8648e1090c0283d33359ae451172b0c831fa60b7d3ccc980285ea963bda1e86cae26ffa35ac5cfea6b89a48da6ad84b34ac1fc492ce9de3bf
7
- data.tar.gz: 1456d670425a9b6cfb1f0609f6af58b4b81ea1d10755b3f6c0c9d0f2e93b7cf146a6aecbcca1426fcb9e3387ab4a94b3320a3f4c21d5486935bacd6e7d27f6d8
6
+ metadata.gz: 292a26a5c625cd6e1b06d214ce5b3cd9e38a7562f8e388b8d2817ee4fc915d6f743301c23466c03d2f5e5f3399607a02ca07c2c32bc179696cb1c71386609d5c
7
+ data.tar.gz: 9c6395fbae61b1013fb72013dfd3d069f76956ad25efd88c62444b3f6c33823462a01c4cb91b129f1a7001a5fef3d7580b91986391128aea41b33c253221afda
data/CHANGELOG.md CHANGED
@@ -1,4 +1,24 @@
1
- # Changelog
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
- └── ask_tool.rb # MCP::Tool → Ask::Tool adapter
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
@@ -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"
@@ -1,5 +1,5 @@
1
1
  module Ask
2
2
  module MCP
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
data/lib/ask/mcp.rb CHANGED
@@ -28,6 +28,7 @@ module Ask
28
28
 
29
29
  module Adapters
30
30
  autoload :AskTool, "ask/mcp/adapters/ask_tool"
31
+ autoload :AskToolServer, "ask/mcp/adapters/ask_tool_server"
31
32
  end
32
33
 
33
34
  class Error < StandardError; 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.1.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
- Discover tools, resources, and prompts. OAuth 2.1 authentication. Convert MCP tools
84
- to Ask::Tool instances. Supports the full MCP spec.
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: []