ask-mcp 0.1.1 → 0.3.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 +27 -14
- data/README.md +100 -11
- data/lib/ask/mcp/adapters/tool_server.rb +91 -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: 90e04370e4d5b1136abb513220b68beb46a5ca2c74d8f7ca694fa59297f8f32e
|
|
4
|
+
data.tar.gz: 17f2accea77cefc0ea51fedcc3879e219fff47760999b0ba39e0e168d30dbfe1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d8a481b9467e8f4d6d4d305da61346855f4d27ac94de77f109ac528e704599c18aee5a9c5deb64f7edb71a71b4e446cc1801149df21891d15eefdcb441eb1d09
|
|
7
|
+
data.tar.gz: b78ccc9a910fbfbeaf2e273ce6169279e4195624871f89356d6564ab6cd46380dd876951544e90b2f3b3b57328ae00180c6d218e1d66afa5416090c5703e1560
|
data/CHANGELOG.md
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
|
+
## [0.3.0] - 2026-06-26
|
|
2
|
+
|
|
3
|
+
### 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.
|
|
9
|
+
|
|
10
|
+
## [0.2.0] - 2026-06-26
|
|
11
|
+
|
|
12
|
+
### 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).
|
|
20
|
+
|
|
1
21
|
## [0.1.1] - 2026-06-25
|
|
2
22
|
|
|
3
23
|
### Changed
|
|
4
|
-
- Major test expansion: Transport tests
|
|
5
|
-
# Changelog
|
|
24
|
+
- Major test expansion: Transport tests, MessagesParser, OAuth, Client, Server, Tool, Messages, integration. Rubocop, overcommit, CI matrix, SimpleCov.
|
|
6
25
|
|
|
7
26
|
## [0.1.0] - 2026-06-10
|
|
8
27
|
|
|
9
28
|
### Added
|
|
10
29
|
- Core MCP client with full JSON-RPC 2.0 message layer
|
|
11
|
-
- stdio
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
- Tool calling, resource reading, and prompt retrieval
|
|
17
|
-
- Token-based authentication (Bearer/Basic)
|
|
18
|
-
- OAuth 2.1 authentication (client credentials + authorization code flows)
|
|
19
|
-
- Ask::Tool adapter for integration with ask-agent
|
|
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
|
|
20
35
|
- Thread-safe request/response matching with configurable timeouts
|
|
21
|
-
- Server-side notifications handling
|
|
22
|
-
- Comprehensive test suite with mock MCP server
|
|
36
|
+
- Server-side notifications handling
|
|
23
37
|
- Factory methods: `from_stdio`, `from_sse`, `from_http`, `connect`
|
|
24
|
-
- Ability to cache or bypass caching for tools/resources/prompts
|
data/README.md
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/ask-mcp)
|
|
4
4
|
|
|
5
|
-
**Model Context Protocol (MCP) client for Ruby.** Connect to MCP
|
|
6
|
-
stdio, SSE, or Streamable HTTP transports.
|
|
7
|
-
|
|
5
|
+
**Model Context Protocol (MCP) client and server for Ruby.** Connect to MCP
|
|
6
|
+
servers via stdio, SSE, or Streamable HTTP transports. Run as an MCP server
|
|
7
|
+
to expose your own tools to any MCP client. No framework lock-in — just
|
|
8
|
+
implement a couple of duck-typed methods and you're done.
|
|
8
9
|
|
|
9
10
|
MCP is the industry standard for LLM tool discovery — the same protocol used by
|
|
10
11
|
Claude Code, Codex, Cursor, and GitHub Copilot.
|
|
@@ -21,26 +22,112 @@ Or add to your Gemfile:
|
|
|
21
22
|
gem "ask-mcp", "~> 0.1.0"
|
|
22
23
|
```
|
|
23
24
|
|
|
24
|
-
## Quick Start
|
|
25
|
+
## Quick Start — Client
|
|
26
|
+
|
|
27
|
+
Connect to any MCP server and call its tools:
|
|
25
28
|
|
|
26
29
|
```ruby
|
|
27
30
|
require "ask/mcp"
|
|
28
31
|
|
|
29
|
-
# Connect to a local MCP server via stdio
|
|
30
32
|
client = Ask::MCP.from_stdio("npx", ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"])
|
|
31
33
|
client.start
|
|
32
34
|
|
|
33
|
-
# List available tools
|
|
34
35
|
client.tools.each { |name, tool| puts "#{name}: #{tool.description}" }
|
|
35
36
|
|
|
36
|
-
# Call a tool
|
|
37
37
|
result = client.call_tool("read_file", path: "/tmp/test.txt")
|
|
38
38
|
puts result
|
|
39
39
|
|
|
40
|
-
# Clean up
|
|
41
40
|
client.stop
|
|
42
41
|
```
|
|
43
42
|
|
|
43
|
+
## Quick Start — Server
|
|
44
|
+
|
|
45
|
+
Run as a standalone MCP server exposing your own tools to any MCP client
|
|
46
|
+
(Codex, Claude Code, Cursor, etc.). Any object that responds to `name`,
|
|
47
|
+
`description`, `params_schema`, and `call(args)` will work:
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
require "ask/mcp"
|
|
51
|
+
|
|
52
|
+
# Define your tools — no base class needed, just duck typing
|
|
53
|
+
class Greeter
|
|
54
|
+
def name; "greet" end
|
|
55
|
+
def description; "Greets someone by name" end
|
|
56
|
+
def params_schema
|
|
57
|
+
{ type: "object", properties: { "name" => { "type" => "string" } }, required: ["name"] }
|
|
58
|
+
end
|
|
59
|
+
def call(args = {})
|
|
60
|
+
"Hello, #{args['name']}!"
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Start the server (blocking — runs until stdin closes)
|
|
65
|
+
Ask::MCP::Server.start_stdio(
|
|
66
|
+
name: "my-server",
|
|
67
|
+
tools: [Greeter.new]
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Configure your MCP client:
|
|
72
|
+
|
|
73
|
+
```json
|
|
74
|
+
{
|
|
75
|
+
"mcpServers": {
|
|
76
|
+
"my-server": {
|
|
77
|
+
"command": "ruby",
|
|
78
|
+
"args": ["/path/to/your/server.rb"]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Tools can return any value the client can use. The server automatically wraps
|
|
85
|
+
the result into MCP's `content` array format.
|
|
86
|
+
|
|
87
|
+
### What about complex tools?
|
|
88
|
+
|
|
89
|
+
The result object from `call(args)` should respond to `ok?` (or `ok`) and
|
|
90
|
+
`output` / `error_message`. Use `OpenStruct` for simple cases:
|
|
91
|
+
|
|
92
|
+
```ruby
|
|
93
|
+
require "ostruct"
|
|
94
|
+
|
|
95
|
+
class BashTool
|
|
96
|
+
def name; "bash" end
|
|
97
|
+
def description; "Run a shell command" end
|
|
98
|
+
def params_schema
|
|
99
|
+
{ type: "object", properties: { "command" => { "type" => "string" } }, required: ["command"] }
|
|
100
|
+
end
|
|
101
|
+
def call(args = {})
|
|
102
|
+
output = `#{args['command']} 2>&1`
|
|
103
|
+
OpenStruct.new(ok?: true, output: output)
|
|
104
|
+
rescue => e
|
|
105
|
+
OpenStruct.new(ok?: false, error_message: e.message)
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
For a production example with shell tools, file ops, and web search,
|
|
111
|
+
see [llm-proxy](https://github.com/ask-rb/llm-proxy).
|
|
112
|
+
|
|
113
|
+
### Using with ask-tools
|
|
114
|
+
|
|
115
|
+
If you use the ask-rb ecosystem, you can expose Ask::Tool subclasses directly:
|
|
116
|
+
|
|
117
|
+
```ruby
|
|
118
|
+
require "ask/mcp"
|
|
119
|
+
require "ask-tools-shell"
|
|
120
|
+
require "ask-web-search"
|
|
121
|
+
|
|
122
|
+
tools = Ask::Tools::Shell::TOOLS.map(&:new) + [Ask::Tools::WebSearch.new]
|
|
123
|
+
|
|
124
|
+
Ask::MCP::Server.start_stdio(
|
|
125
|
+
name: "my-server",
|
|
126
|
+
tools: tools,
|
|
127
|
+
capabilities: { tools: {} }
|
|
128
|
+
)
|
|
129
|
+
```
|
|
130
|
+
|
|
44
131
|
## Transports
|
|
45
132
|
|
|
46
133
|
```ruby
|
|
@@ -152,20 +239,22 @@ wrapped.each { |name, adapter| agent.register_tool(adapter.to_ask_tool) }
|
|
|
152
239
|
ask-mcp/
|
|
153
240
|
├── lib/ask/mcp.rb # Entry point, factory methods
|
|
154
241
|
├── lib/ask/mcp/client.rb # MCP client (connect, call_tool, etc.)
|
|
155
|
-
├── lib/ask/mcp/server.rb # MCP server representation
|
|
242
|
+
├── lib/ask/mcp/server.rb # MCP server representation + Server.start_stdio entry point
|
|
243
|
+
├── lib/ask/mcp/server/stdio.rb # MCP server stdio runtime (run as server)
|
|
156
244
|
├── lib/ask/mcp/tool.rb # MCP tool representation
|
|
157
245
|
├── lib/ask/mcp/resource.rb # MCP resource representation
|
|
158
246
|
├── lib/ask/mcp/prompt.rb # MCP prompt representation
|
|
159
247
|
├── lib/ask/mcp/native/messages.rb # JSON-RPC message layer
|
|
160
248
|
├── lib/ask/mcp/transport/
|
|
161
|
-
│ ├── stdio.rb # stdio transport
|
|
249
|
+
│ ├── stdio.rb # stdio transport (client direction)
|
|
162
250
|
│ ├── sse.rb # Server-Sent Events transport
|
|
163
251
|
│ └── streamable_http.rb # Streamable HTTP transport
|
|
164
252
|
├── lib/ask/mcp/auth/
|
|
165
253
|
│ ├── oauth.rb # OAuth 2.1 for MCP
|
|
166
254
|
│ └── token.rb # Token-based auth
|
|
167
255
|
└── lib/ask/mcp/adapters/
|
|
168
|
-
|
|
256
|
+
├── ask_tool.rb # MCP::Tool → Ask::Tool adapter
|
|
257
|
+
└── tool_server.rb # Duck-typed tools → MCP server adapter
|
|
169
258
|
```
|
|
170
259
|
|
|
171
260
|
## Development
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Ask
|
|
4
|
+
module MCP
|
|
5
|
+
module Adapters
|
|
6
|
+
# Converts duck-typed tool objects into MCP tool definitions and dispatches
|
|
7
|
+
# calls. This is the server-direction adapter — it takes any objects that
|
|
8
|
+
# respond to +name+, +description+, +params_schema+, and +call(args)+ and
|
|
9
|
+
# exposes them over MCP.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# class MyTool
|
|
13
|
+
# def name; "hello" end
|
|
14
|
+
# def description; "Says hello" end
|
|
15
|
+
# def params_schema; nil end
|
|
16
|
+
# def call(args = {}); OpenStruct.new(ok?: true, output: "Hello!") end
|
|
17
|
+
# end
|
|
18
|
+
#
|
|
19
|
+
# adapter = ToolServer.new([MyTool.new])
|
|
20
|
+
# adapter.definitions # => [{ name: "hello", ... }]
|
|
21
|
+
# adapter.call("hello", {}) # => { content: [...], isError: false }
|
|
22
|
+
class ToolServer
|
|
23
|
+
attr_reader :tools
|
|
24
|
+
|
|
25
|
+
# @param tools [Array<#call, #name, #description, #params_schema>] tool instances to expose
|
|
26
|
+
def initialize(tools = [])
|
|
27
|
+
@tools = tools
|
|
28
|
+
@tool_map = tools.each_with_object({}) { |t, h| h[t.name] = t }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# MCP tool definitions for tools/list
|
|
32
|
+
# @return [Array<Hash>]
|
|
33
|
+
def definitions
|
|
34
|
+
@tools.map do |tool|
|
|
35
|
+
schema = tool.params_schema || { type: "object", properties: {}, required: [] }
|
|
36
|
+
{
|
|
37
|
+
name: tool.name,
|
|
38
|
+
description: tool.description || "",
|
|
39
|
+
inputSchema: schema
|
|
40
|
+
}
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Call a tool and wrap the result in MCP format
|
|
45
|
+
# @param name [String] tool name
|
|
46
|
+
# @param arguments [Hash] arguments (may have symbol or string keys)
|
|
47
|
+
# @return [Hash] { content: [...], isError: true/false }
|
|
48
|
+
def call(name, arguments = {})
|
|
49
|
+
tool = @tool_map[name]
|
|
50
|
+
unless tool
|
|
51
|
+
return error_result("Tool not found: #{name}")
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
normalized = deep_stringify_keys(arguments)
|
|
55
|
+
result = tool.call(normalized)
|
|
56
|
+
wrap_result(result)
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
if defined?(Ask::Tool::Halt) && e.is_a?(Ask::Tool::Halt)
|
|
59
|
+
return { content: [{ type: "text", text: e.content.to_s }], isError: false }
|
|
60
|
+
end
|
|
61
|
+
error_result("#{e.class}: #{e.message}")
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
private
|
|
65
|
+
|
|
66
|
+
def wrap_result(result)
|
|
67
|
+
if result.respond_to?(:ok?) ? result.ok? : result.ok
|
|
68
|
+
output = result.respond_to?(:output) ? result.output : result.to_s
|
|
69
|
+
text = output.is_a?(Hash) ? (output[:summary] || output.to_s) : output.to_s
|
|
70
|
+
{ content: [{ type: "text", text: text }], isError: false }
|
|
71
|
+
else
|
|
72
|
+
msg = result.respond_to?(:error_message) ? result.error_message : result.to_s
|
|
73
|
+
{ content: [{ type: "text", text: "Error: #{msg}" }], isError: true }
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def error_result(message)
|
|
78
|
+
{ content: [{ type: "text", text: message }], isError: true }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def deep_stringify_keys(obj)
|
|
82
|
+
case obj
|
|
83
|
+
when Hash then obj.each_with_object({}) { |(k, v), h| h[k.to_s] = deep_stringify_keys(v) }
|
|
84
|
+
when Array then obj.map { |v| deep_stringify_keys(v) }
|
|
85
|
+
else obj
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
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::ToolServer.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.3.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 exposing any Ruby object as a tool. Discover tools, resources,
|
|
84
|
+
and 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/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: []
|