ask-mcp 0.4.1 → 0.4.2
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 +105 -0
- data/README.md +104 -175
- data/docs/SPEC_COMPLIANCE.md +184 -0
- data/lib/ask/mcp/adapters/tool_server.rb +8 -1
- data/lib/ask/mcp/auth/client_id_metadata_document.rb +74 -0
- data/lib/ask/mcp/auth/oauth.rb +52 -4
- data/lib/ask/mcp/client.rb +237 -23
- data/lib/ask/mcp/native/messages.rb +23 -2
- data/lib/ask/mcp/prompt.rb +9 -3
- data/lib/ask/mcp/resource.rb +9 -3
- data/lib/ask/mcp/server/stdio.rb +194 -5
- data/lib/ask/mcp/server.rb +7 -2
- data/lib/ask/mcp/tool.rb +11 -4
- data/lib/ask/mcp/trace_context.rb +39 -0
- data/lib/ask/mcp/transport/sse.rb +5 -1
- data/lib/ask/mcp/transport/stdio.rb +1 -1
- data/lib/ask/mcp/transport/streamable_http.rb +108 -26
- data/lib/ask/mcp/validator.rb +19 -1
- data/lib/ask/mcp/version.rb +1 -1
- data/lib/ask/mcp/x_mcp_header.rb +95 -0
- data/lib/ask/mcp.rb +15 -0
- metadata +19 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d73da1e33a97a8bd6825f2eb3e0f9c770ffbbc8b1a9502db612b49e71fc87193
|
|
4
|
+
data.tar.gz: 5f7cf6b4cb501db18cf5a8367215292c923cf97c078acb226e7546d86e3218a8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c932454bfccb278fa718290ea84cb3e721296db46ab449c941005f363f404e5342a3836354660d525604c441088b556e938403f99c1d6c80b9066a68906ed3b0
|
|
7
|
+
data.tar.gz: c8a46f82f4f4907aed57bb1394856f9894a8d9d4bf727fc011604f0b8f68fd621ba2884d72cd194f7e30b9b4545804aba99e412ab55d0a3cbb2498f6708587a1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,108 @@
|
|
|
1
|
+
## [0.4.2] - 2026-08-04
|
|
2
|
+
|
|
3
|
+
### Added
|
|
4
|
+
|
|
5
|
+
- **2026-07-28 stateless protocol core (dual-mode)** — new
|
|
6
|
+
`Ask::MCP::LATEST_PROTOCOL_VERSION` (`"2026-07-28"`) and
|
|
7
|
+
`SUPPORTED_PROTOCOL_VERSIONS` constants. The client probes
|
|
8
|
+
`server/discover` and, for a 2026-07-28 peer, skips the `initialize`
|
|
9
|
+
handshake and sends every request with `_meta`
|
|
10
|
+
(`io.modelcontextprotocol/protocolVersion`, `clientCapabilities`,
|
|
11
|
+
`clientInfo`). The server answers `server/discover`, detects stateless
|
|
12
|
+
requests from `_meta`, and keeps the legacy handshake for older peers. A
|
|
13
|
+
server that supports only ≤ 2025-11-25 is detected and still handshakes
|
|
14
|
+
with the negotiated version.
|
|
15
|
+
- **`resultType` on server results** — `"complete"` for stateless peers
|
|
16
|
+
(`"input_required"` via MRTR).
|
|
17
|
+
- **MRTR (Multi Round-Trip Requests)** — the client resolves
|
|
18
|
+
`inputRequests` through the `on_elicitation`/`on_sampling`/`on_request`
|
|
19
|
+
handlers, retries the original request with `inputResponses`, echoes
|
|
20
|
+
`requestState` verbatim, uses a fresh JSON-RPC id per retry, and caps round
|
|
21
|
+
trips (`MAX_MRTR_ROUND_TRIPS`).
|
|
22
|
+
- **Server-initiated request framework** — `Client#on_request` plus
|
|
23
|
+
`on_elicitation` and `on_sampling` conveniences; the client declares
|
|
24
|
+
support via `client_capabilities`.
|
|
25
|
+
- **Streamable HTTP rework (2026-07-28 shape)** — one POST per message with
|
|
26
|
+
`MCP-Protocol-Version`, `Mcp-Method`, `Mcp-Name` headers (Base64 sentinel
|
|
27
|
+
value encoding); per-response Content-Type decides single JSON vs SSE
|
|
28
|
+
stream; SSE keep-alive comments ignored; `subscriptions/listen` long-lived
|
|
29
|
+
streams via `Transport::StreamableHTTP#listen` / `Client#listen`; x-mcp-
|
|
30
|
+
header tool parameters mirrored into `Mcp-Param-{Name}` headers. Sessions,
|
|
31
|
+
the GET endpoint, and `Last-Event-ID` were removed upstream and are not
|
|
32
|
+
implemented. `base64` added as a runtime dependency (Ruby 3.4+).
|
|
33
|
+
- **x-mcp-header validation (SEP-2243)** — `Ask::MCP::XMcpHeader` validates
|
|
34
|
+
annotations (empty names, invalid characters, non-primitive types,
|
|
35
|
+
case-insensitive duplicates, non-`properties`-reachable placement);
|
|
36
|
+
Streamable HTTP clients exclude invalid tool definitions from `tools/list`
|
|
37
|
+
with a warning; out-of-range integers are omitted from headers.
|
|
38
|
+
- **OTel trace context (SEP-414)** — `Ask::MCP::TraceContext` extracts
|
|
39
|
+
`traceparent`/`tracestate`/`baggage` from headers (incl. Rack `HTTP_*`) and
|
|
40
|
+
`_meta`; clients propagate configured `_meta` fields via `meta:` option.
|
|
41
|
+
- **Server list_changed notifications** — `Server::Stdio#notify_tools_
|
|
42
|
+
list_changed`, `#notify_resources_list_changed`, `#notify_prompts_
|
|
43
|
+
list_changed` emit the change notifications (deterministically ordered
|
|
44
|
+
before the triggering response); the client invalidates its caches on
|
|
45
|
+
receipt. Verified end-to-end with a dedicated notify test server.
|
|
46
|
+
- **Client ID Metadata Documents (SEP-991)** — `Auth::ClientIdMetadataDocument`
|
|
47
|
+
builds and validates the self-hosted client metadata document (required
|
|
48
|
+
fields, https URL client_id with path, URL/client_id match,
|
|
49
|
+
`application_type` native/web) — the 2026-07-28 recommended replacement
|
|
50
|
+
for the deprecated Dynamic Client Registration.
|
|
51
|
+
- **RFC 9207 `iss` validation** — `Auth::OAuth#validate_iss!` rejects an
|
|
52
|
+
`iss` that does not match the recorded issuer.
|
|
53
|
+
- **`CacheableResult`** — `ttlMs` + `cacheScope` (configurable via
|
|
54
|
+
`cache_ttl_ms`/`cache_scope`) emitted on list/read results for stateless
|
|
55
|
+
peers; deterministic `tools/list` ordering locked by test.
|
|
56
|
+
- **`ping` removed for 2026-07-28 peers** (Method-not-found); legacy clients
|
|
57
|
+
still get `{}`.
|
|
58
|
+
- **Error-code allocation policy** — `HEADER_MISMATCH` `-32020`,
|
|
59
|
+
`MISSING_REQUIRED_CLIENT_CAPABILITY` `-32021`,
|
|
60
|
+
`UNSUPPORTED_PROTOCOL_VERSION` `-32022`; resource-not-found renumbered to
|
|
61
|
+
`-32602` for stateless peers.
|
|
62
|
+
- **OIDC Discovery 1.0 (2025-11-25)** — `Auth::OAuth#discover!` resolves
|
|
63
|
+
endpoints from `/.well-known/openid-configuration` (issuer-derived or
|
|
64
|
+
explicit URL); `token_url` is now optional at construction.
|
|
65
|
+
- **JSON Schema 2020-12 dialect** — `Validator` accepts schemas declaring the
|
|
66
|
+
2020-12 metaschema (strips it and retries on the json-schema gem, which
|
|
67
|
+
lacks 2020-12); `const` and other shared keywords enforced.
|
|
68
|
+
- **2025-11-25 display metadata (SEP-973)** — `Tool`, `Resource`, and
|
|
69
|
+
`Prompt` now parse (`from_h`) and emit (`to_h`) the optional `title` and
|
|
70
|
+
`icons` (array of `{src, mimeType, sizes}`) fields, and
|
|
71
|
+
`Adapters::ToolServer` passes them through in `tools/list` definitions
|
|
72
|
+
when the tool object provides them. Clients connecting to 2025-11-25
|
|
73
|
+
servers no longer drop these fields.
|
|
74
|
+
- **Server now serves resources & prompts** — `Server::Stdio` implements
|
|
75
|
+
`resources/list`, `resources/read`, `resources/templates/list`,
|
|
76
|
+
`prompts/list`, and `prompts/get` (previously only tools were served).
|
|
77
|
+
Duck-typed resource/prompt/template objects are serialized from their
|
|
78
|
+
accessors, and `Server.start_stdio` accepts `resources`, `prompts`, and
|
|
79
|
+
`resource_templates` keyword args.
|
|
80
|
+
- `RecordingTransport` test helper (`test/support/recording_transport.rb`) —
|
|
81
|
+
in-memory transport for asserting what `Client` puts on the wire without
|
|
82
|
+
spawning a subprocess.
|
|
83
|
+
- `docs/SPEC_COMPLIANCE.md` — full compliance status across all three
|
|
84
|
+
revisions (2025-06-18 / 2025-11-25 / 2026-07-28).
|
|
85
|
+
|
|
86
|
+
### Changed
|
|
87
|
+
|
|
88
|
+
- HTTP+SSE transport marked deprecated (2026-07-28 feature lifecycle);
|
|
89
|
+
still functional for legacy servers. Prefer Streamable HTTP.
|
|
90
|
+
- `send` on transports now accepts an optional extra-headers argument
|
|
91
|
+
(used for `Mcp-Param-*` mirroring; ignored by stdio/SSE).
|
|
92
|
+
|
|
93
|
+
### Fixed
|
|
94
|
+
|
|
95
|
+
- **Client advertised a bogus protocol version** — `Ask::MCP::Client` sent
|
|
96
|
+
`protocolVersion: "0.1.0"` in `initialize` while `Server::Stdio` pinned
|
|
97
|
+
`2025-06-18`. The advertised MCP revision is now a single source of truth,
|
|
98
|
+
`Ask::MCP::PROTOCOL_VERSION` (`"2025-06-18"`), referenced by both client and
|
|
99
|
+
server; the old per-class constants remain as deprecated aliases.
|
|
100
|
+
- **Flaky subprocess timing tests** — `test_transport_send_and_receive` and
|
|
101
|
+
`test_sigterm_triggers_shutdown` failed intermittently under CI load.
|
|
102
|
+
Replaced fixed `sleep`s with a shared `wait_until` poll-until-deadline
|
|
103
|
+
helper (`test/test_helper.rb`); `test_spawn_and_communicate` now does a
|
|
104
|
+
real round-trip via `cat` instead of an assertion that could never fail.
|
|
105
|
+
|
|
1
106
|
## [0.4.1] - 2026-07-31
|
|
2
107
|
|
|
3
108
|
### Changed
|
data/README.md
CHANGED
|
@@ -2,13 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://badge.fury.io/rb/ask-mcp)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
servers
|
|
7
|
-
to expose your own tools to any MCP client
|
|
8
|
-
implement a couple of duck-typed
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
Model Context Protocol (MCP) client and server for Ruby. Connect to MCP
|
|
6
|
+
servers over stdio, SSE, or Streamable HTTP transports, or run as an MCP
|
|
7
|
+
server to expose your own tools to any MCP client (Claude Code, Codex, Cursor,
|
|
8
|
+
GitHub Copilot). No framework lock-in: implement a couple of duck-typed
|
|
9
|
+
methods and you are done.
|
|
10
|
+
|
|
11
|
+
Speaks the protocol **dual-mode**: legacy `initialize`-handshake revisions
|
|
12
|
+
(`2025-06-18`, `2025-11-25`) and the stateless `2026-07-28` revision
|
|
13
|
+
(`server/discover`, per-request `_meta`, MRTR). Clients negotiate
|
|
14
|
+
automatically; servers answer both eras on one connection. See
|
|
15
|
+
[docs/SPEC_COMPLIANCE.md](docs/SPEC_COMPLIANCE.md) for the full status across
|
|
16
|
+
all three revisions.
|
|
12
17
|
|
|
13
18
|
## Installation
|
|
14
19
|
|
|
@@ -16,13 +21,7 @@ Claude Code, Codex, Cursor, and GitHub Copilot.
|
|
|
16
21
|
gem "ask-mcp"
|
|
17
22
|
```
|
|
18
23
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
```ruby
|
|
22
|
-
gem "ask-mcp", "~> 0.1.0"
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
## Quick Start — Client
|
|
24
|
+
## Quick Start: Client
|
|
26
25
|
|
|
27
26
|
Connect to any MCP server and call its tools:
|
|
28
27
|
|
|
@@ -40,16 +39,37 @@ puts result
|
|
|
40
39
|
client.stop
|
|
41
40
|
```
|
|
42
41
|
|
|
43
|
-
|
|
42
|
+
`start` probes `server/discover` first: a `2026-07-28` server is used
|
|
43
|
+
statelessly (no handshake; every request carries `_meta`), and older servers
|
|
44
|
+
fall back to the `initialize` handshake automatically.
|
|
45
|
+
|
|
46
|
+
### Server-initiated requests (elicitation, sampling)
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
(
|
|
47
|
-
|
|
48
|
+
Servers can ask the client for input — user answers (elicitation) or LLM
|
|
49
|
+
completions (sampling). Register handlers; the client answers the server and,
|
|
50
|
+
in the stateless revision, completes the request via
|
|
51
|
+
[MRTR](https://modelcontextprotocol.io/specification/2026-07-28/basic/patterns/mrtr):
|
|
52
|
+
|
|
53
|
+
```ruby
|
|
54
|
+
client.on_elicitation { |params| { message: "42" } }
|
|
55
|
+
client.on_sampling do |params|
|
|
56
|
+
# params includes `tools` / `toolChoice` when the server offers tool calling
|
|
57
|
+
{ role: "assistant", content: { type: "text", text: "Paris" } }
|
|
58
|
+
end
|
|
59
|
+
# any on_request("method") { |params| ... } works for custom server requests
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Declare support so servers know they can ask:
|
|
63
|
+
`Ask::MCP::Client.new(transport, client_capabilities: { elicitation: {}, sampling: {} })`.
|
|
64
|
+
|
|
65
|
+
## Quick Start: Server
|
|
66
|
+
|
|
67
|
+
Run as a standalone MCP server. Any object that responds to `name`,
|
|
68
|
+
`description`, `params_schema`, and `call(args)` works:
|
|
48
69
|
|
|
49
70
|
```ruby
|
|
50
71
|
require "ask/mcp"
|
|
51
72
|
|
|
52
|
-
# Define your tools — no base class needed, just duck typing
|
|
53
73
|
class Greeter
|
|
54
74
|
def name; "greet" end
|
|
55
75
|
def description; "Greets someone by name" end
|
|
@@ -61,14 +81,17 @@ class Greeter
|
|
|
61
81
|
end
|
|
62
82
|
end
|
|
63
83
|
|
|
64
|
-
#
|
|
84
|
+
# Blocking: runs until stdin closes
|
|
65
85
|
Ask::MCP::Server.start_stdio(
|
|
66
86
|
name: "my-server",
|
|
67
|
-
tools: [Greeter.new]
|
|
87
|
+
tools: [Greeter.new],
|
|
88
|
+
resources: { "greeting://world" => GreetingResource.new }, # uri → object
|
|
89
|
+
prompts: { "greet" => GreetPrompt.new }, # name → object
|
|
90
|
+
resource_templates: { "file:///{path}" => FileTemplate.new }
|
|
68
91
|
)
|
|
69
92
|
```
|
|
70
93
|
|
|
71
|
-
|
|
94
|
+
Point any MCP client at it:
|
|
72
95
|
|
|
73
96
|
```json
|
|
74
97
|
{
|
|
@@ -81,198 +104,104 @@ Configure your MCP client:
|
|
|
81
104
|
}
|
|
82
105
|
```
|
|
83
106
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
107
|
+
The result of `call(args)` may be a plain value, or an object responding to
|
|
108
|
+
`ok?` and `output` / `error_message` (for example an `OpenStruct`). The server
|
|
109
|
+
wraps results into MCP's `content` array format automatically. Resources and
|
|
110
|
+
prompts are duck-typed: objects with `to_h` are serialized directly, otherwise
|
|
111
|
+
accessors (`title`, `description`, `mime_type`, `icons`, `arguments`) are
|
|
112
|
+
collected; `resources/read` and `prompts/get` pull contents from `content` /
|
|
113
|
+
`read` / `messages`.
|
|
88
114
|
|
|
89
|
-
|
|
90
|
-
`output` / `error_message`. Use `OpenStruct` for simple cases:
|
|
115
|
+
When your tool/resource/prompt sets change at runtime, tell clients:
|
|
91
116
|
|
|
92
117
|
```ruby
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
)
|
|
118
|
+
server.notify_tools_list_changed
|
|
119
|
+
server.notify_resources_list_changed
|
|
120
|
+
server.notify_prompts_list_changed
|
|
129
121
|
```
|
|
130
122
|
|
|
131
123
|
## Transports
|
|
132
124
|
|
|
133
125
|
```ruby
|
|
134
|
-
# stdio
|
|
126
|
+
# stdio: local processes
|
|
135
127
|
Ask::MCP.from_stdio("npx", ["-y", "@modelcontextprotocol/server-github"])
|
|
136
128
|
|
|
137
|
-
# SSE
|
|
129
|
+
# SSE: remote servers with Server-Sent Events (deprecated upstream — prefer Streamable HTTP)
|
|
138
130
|
Ask::MCP.from_sse("https://mcp.example.com/sse")
|
|
139
131
|
|
|
140
|
-
# Streamable HTTP
|
|
132
|
+
# Streamable HTTP: remote servers
|
|
141
133
|
Ask::MCP.from_http("https://mcp.example.com/mcp")
|
|
142
134
|
```
|
|
143
135
|
|
|
144
|
-
|
|
136
|
+
Each factory returns a client backed by `Ask::MCP::Transport::Stdio`,
|
|
137
|
+
`Ask::MCP::Transport::SSE`, or `Ask::MCP::Transport::StreamableHTTP`.
|
|
145
138
|
|
|
146
|
-
|
|
139
|
+
The Streamable HTTP transport implements the `2026-07-28` shape: one POST per
|
|
140
|
+
message with `MCP-Protocol-Version` / `Mcp-Method` / `Mcp-Name` headers
|
|
141
|
+
(Base64-sentinel value encoding), per-response JSON-or-SSE handling, and
|
|
142
|
+
`subscriptions/listen` long-lived notification streams:
|
|
147
143
|
|
|
148
144
|
```ruby
|
|
149
|
-
|
|
150
|
-
transport = Ask::MCP::Transport::Stdio.new("ruby", ["server.rb"])
|
|
151
|
-
client = Ask::MCP::Client.new(transport, timeout: 30)
|
|
152
|
-
|
|
153
|
-
# Start the session (sends initialize + receives capabilities)
|
|
154
|
-
client.start
|
|
155
|
-
|
|
156
|
-
# Use the client
|
|
157
|
-
client.tools # => { "tool_name" => #<Ask::MCP::Tool> }
|
|
158
|
-
client.resources # => { "resource_uri" => #<Ask::MCP::Resource> }
|
|
159
|
-
client.prompts # => { "prompt_name" => #<Ask::MCP::Prompt> }
|
|
160
|
-
client.call_tool("tool_name", arg1: "value")
|
|
161
|
-
client.read_resource("file:///path")
|
|
162
|
-
client.get_prompt("prompt_name", arg1: "value")
|
|
163
|
-
|
|
164
|
-
# Stop the session
|
|
165
|
-
client.stop
|
|
145
|
+
client.listen(toolsListChanged: true, resourceSubscriptions: ["file:///x"])
|
|
166
146
|
```
|
|
167
147
|
|
|
168
|
-
|
|
148
|
+
Tool parameters annotated with `x-mcp-header` in the server's `inputSchema`
|
|
149
|
+
are mirrored into `Mcp-Param-{Name}` headers; tool definitions with invalid
|
|
150
|
+
annotations are excluded from `tools/list` on HTTP transports.
|
|
151
|
+
|
|
152
|
+
## Essential API
|
|
153
|
+
|
|
154
|
+
| Entry point | Purpose |
|
|
155
|
+
|---|---|
|
|
156
|
+
| `client.start` / `client.stop` | Negotiate the protocol (discover or handshake) and shut down |
|
|
157
|
+
| `client.tools` / `client.resources` / `client.prompts` | Indexed lists exposed by the server (title/icons preserved) |
|
|
158
|
+
| `client.call_tool(name, args)` | Invoke a tool; also `read_resource(uri)` and `get_prompt(name, args)` |
|
|
159
|
+
| `client.on_request(method)` / `on_elicitation` / `on_sampling` | Answer server-initiated requests (MRTR) |
|
|
160
|
+
| `client.listen(notifications)` | Open a `subscriptions/listen` notification stream |
|
|
161
|
+
| `server.notify_*_list_changed` | Emit change notifications to clients |
|
|
162
|
+
| `Ask::MCP::Adapters::AskTool.wrap(tools_hash)` | Adapter from MCP tools to `Ask::Tool` instances for ask-agent |
|
|
163
|
+
| `Ask::MCP::Adapters::ToolServer` | Adapter from duck-typed tools to MCP server tools |
|
|
164
|
+
| `Ask::MCP::Auth::Token.new(token)` | Token-based auth (`apply(headers)`) |
|
|
165
|
+
| `Ask::MCP::Auth::OAuth.new(client_id:, ...)` | OAuth for MCP; `discover!` (OIDC), `authenticate!`, `validate_iss!`, `apply(headers)` |
|
|
166
|
+
| `Ask::MCP::Auth::ClientIdMetadataDocument` | Build/validate Client ID Metadata Documents (2026-07-28 client registration) |
|
|
167
|
+
| `Ask::MCP::TraceContext` | OpenTelemetry `traceparent`/`tracestate`/`baggage` extraction for `_meta` |
|
|
168
|
+
| `Ask::MCP::XMcpHeader` | Validation of `x-mcp-header` tool annotations |
|
|
169
|
+
|
|
170
|
+
OAuth endpoints can be discovered instead of configured:
|
|
169
171
|
|
|
170
172
|
```ruby
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
input_schema: {
|
|
176
|
-
type: "object",
|
|
177
|
-
properties: { path: { type: "string" } },
|
|
178
|
-
required: ["path"]
|
|
179
|
-
}
|
|
180
|
-
)
|
|
181
|
-
tool.name # => "read_file"
|
|
182
|
-
tool.description # => "Read a file from disk"
|
|
183
|
-
tool.input_schema # => { type: "object", ... }
|
|
184
|
-
|
|
185
|
-
# Resource
|
|
186
|
-
resource = Ask::MCP::Resource.new(
|
|
187
|
-
uri: "file:///tmp/test.txt",
|
|
188
|
-
name: "Test File",
|
|
189
|
-
mime_type: "text/plain"
|
|
190
|
-
)
|
|
191
|
-
|
|
192
|
-
# Prompt
|
|
193
|
-
prompt = Ask::MCP::Prompt.new(
|
|
194
|
-
name: "greet",
|
|
195
|
-
description: "Generate a greeting",
|
|
196
|
-
arguments: [{ name: "name", description: "Name to greet", required: true }]
|
|
197
|
-
)
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
### Authentication
|
|
201
|
-
|
|
202
|
-
```ruby
|
|
203
|
-
# Token-based auth
|
|
204
|
-
token = Ask::MCP::Auth::Token.new("my-api-token")
|
|
205
|
-
headers = token.apply({}) # => { "Authorization" => "Bearer my-api-token" }
|
|
206
|
-
|
|
207
|
-
# OAuth 2.1
|
|
208
|
-
oauth = Ask::MCP::Auth::OAuth.new(
|
|
209
|
-
client_id: "my-client",
|
|
210
|
-
client_secret: "my-secret",
|
|
211
|
-
token_url: "https://auth.example.com/token",
|
|
212
|
-
scopes: ["mcp"]
|
|
213
|
-
)
|
|
214
|
-
oauth.authenticate!
|
|
215
|
-
headers = oauth.apply({})
|
|
173
|
+
oauth = Ask::MCP::Auth::OAuth.new(client_id: "my-client", issuer: "https://auth.example.com")
|
|
174
|
+
oauth.discover! # fetches /.well-known/openid-configuration
|
|
175
|
+
oauth.authenticate! # client-credentials or authorization-code flow
|
|
176
|
+
client = Ask::MCP::Client.new(transport, auth: oauth)
|
|
216
177
|
```
|
|
217
178
|
|
|
218
179
|
### With ask-agent
|
|
219
180
|
|
|
220
181
|
```ruby
|
|
221
|
-
require "ask/mcp"
|
|
222
|
-
|
|
223
182
|
client = Ask::MCP.from_stdio("npx", ["-y", "@modelcontextprotocol/server-github"])
|
|
224
183
|
client.start
|
|
225
184
|
|
|
226
|
-
# Convert MCP tools to Ask::Tool instances for use with Ask::Agent
|
|
227
|
-
client.tools.each do |name, mcp_tool|
|
|
228
|
-
agent.register_tool(mcp_tool.to_ask_tool)
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
# Or use the adapter directly
|
|
232
185
|
wrapped = Ask::MCP::Adapters::AskTool.wrap(client.tools)
|
|
233
186
|
wrapped.each { |name, adapter| agent.register_tool(adapter.to_ask_tool) }
|
|
234
187
|
```
|
|
235
188
|
|
|
236
|
-
|
|
189
|
+
Expose `Ask::Tool` subclasses as an MCP server with `Ask::MCP::Server.start_stdio(name:, tools:, capabilities: { tools: {} })`; the `ToolServer` adapter handles them.
|
|
237
190
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
├── lib/ask/mcp/prompt.rb # MCP prompt representation
|
|
247
|
-
├── lib/ask/mcp/native/messages.rb # JSON-RPC message layer
|
|
248
|
-
├── lib/ask/mcp/transport/
|
|
249
|
-
│ ├── stdio.rb # stdio transport (client direction)
|
|
250
|
-
│ ├── sse.rb # Server-Sent Events transport
|
|
251
|
-
│ └── streamable_http.rb # Streamable HTTP transport
|
|
252
|
-
├── lib/ask/mcp/auth/
|
|
253
|
-
│ ├── oauth.rb # OAuth 2.1 for MCP
|
|
254
|
-
│ └── token.rb # Token-based auth
|
|
255
|
-
└── lib/ask/mcp/adapters/
|
|
256
|
-
├── ask_tool.rb # MCP::Tool → Ask::Tool adapter
|
|
257
|
-
└── tool_server.rb # Duck-typed tools → MCP server adapter
|
|
258
|
-
```
|
|
191
|
+
## Full documentation
|
|
192
|
+
|
|
193
|
+
The full ask-rb documentation lives at https://ask-rb.github.io/ask-docs.
|
|
194
|
+
https://ask-rb.github.io/ask-docs/core/mcp covers ask-mcp in depth, including
|
|
195
|
+
tool, resource, and prompt objects, protocol details, and auth. See also the
|
|
196
|
+
[Auth Setup Guide](docs/auth-setup.md) for token and OAuth 2.1 setup with
|
|
197
|
+
ask-auth, and [docs/SPEC_COMPLIANCE.md](docs/SPEC_COMPLIANCE.md) for the
|
|
198
|
+
protocol compliance status. API reference: https://ask-rb.github.io/ask-docs/reference/api.
|
|
259
199
|
|
|
260
200
|
## Development
|
|
261
201
|
|
|
262
|
-
|
|
263
|
-
#
|
|
264
|
-
bundle exec rake test
|
|
265
|
-
|
|
266
|
-
# Run specific tests
|
|
267
|
-
bundle exec ruby -Itest test/messages_test.rb
|
|
268
|
-
bundle exec ruby -Itest test/stdio_integration_test.rb
|
|
269
|
-
```
|
|
202
|
+
bundle install
|
|
203
|
+
bundle exec rake test # full suite + self-contained conformance (both protocol eras)
|
|
270
204
|
|
|
271
205
|
## License
|
|
272
206
|
|
|
273
207
|
MIT
|
|
274
|
-
|
|
275
|
-
## Authentication
|
|
276
|
-
|
|
277
|
-
See the [Auth Setup Guide](docs/auth-setup.md) for detailed documentation on
|
|
278
|
-
token-based and OAuth 2.1 authentication, including ask-auth integration.
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# ask-mcp — MCP Specification Compliance
|
|
2
|
+
|
|
3
|
+
Status of `ask-mcp` against the Model Context Protocol specification.
|
|
4
|
+
|
|
5
|
+
**Advertised versions:** `Ask::MCP::PROTOCOL_VERSION` = `2025-06-18` (legacy
|
|
6
|
+
default), `Ask::MCP::LATEST_PROTOCOL_VERSION` = `2026-07-28` (stateless),
|
|
7
|
+
`Ask::MCP::SUPPORTED_PROTOCOL_VERSIONS` = `[2025-06-18, 2025-11-25,
|
|
8
|
+
2026-07-28]`. Clients negotiate via `server/discover` (falling back to the
|
|
9
|
+
legacy `initialize` handshake); servers speak both eras on one connection.
|
|
10
|
+
|
|
11
|
+
## Strategy (applies to every item below)
|
|
12
|
+
|
|
13
|
+
1. **Single source of truth.** Protocol constants live on `Ask::MCP`; client
|
|
14
|
+
and server reference them. This is what the `0.1.0` bug was about — never
|
|
15
|
+
reintroduce per-class duplicates.
|
|
16
|
+
2. **Dual-mode, not rewrite.** `2026-07-28` removed the `initialize`
|
|
17
|
+
handshake, but the spec *requires* backward compatibility with older peers
|
|
18
|
+
(missing `resultType` → treat as `"complete"`; `server/discover` as a
|
|
19
|
+
backward-compat probe on stdio; `UnsupportedProtocolVersionError`). The
|
|
20
|
+
gem speaks the old and new protocols at once, negotiated per peer.
|
|
21
|
+
3. **Tests first, wire-level.** Where a change alters what goes over the
|
|
22
|
+
wire, assert on the wire: `RecordingTransport` (unit, no subprocess) for
|
|
23
|
+
the client, `MCPServerHarness` for the server.
|
|
24
|
+
4. **Don't build on deprecated features.** Roots, Sampling, Logging, the
|
|
25
|
+
HTTP+SSE transport, and OAuth Dynamic Client Registration are deprecated
|
|
26
|
+
as of `2026-07-28` — keep existing support working, don't add new surface.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 1. Baseline: `2025-06-18` (done)
|
|
31
|
+
|
|
32
|
+
- [x] `initialize` / `notifications/initialized` handshake (client + server)
|
|
33
|
+
- [x] stdio transport
|
|
34
|
+
- [x] Streamable HTTP transport
|
|
35
|
+
- [x] HTTP+SSE transport (legacy, deprecated)
|
|
36
|
+
- [x] `tools/list`, `tools/call` (+ retry dedup, timeouts, graceful shutdown)
|
|
37
|
+
- [x] `resources/list`, `resources/read`, `resources/templates/list`
|
|
38
|
+
- [x] `prompts/list`, `prompts/get`
|
|
39
|
+
- [x] `ping` (legacy only — removed for 2026-07-28 peers)
|
|
40
|
+
- [x] OAuth resource-server authorization (`auth/oauth.rb`)
|
|
41
|
+
- [x] JSON-RPC 2.0 message types, parser, error codes (`native/messages.rb`)
|
|
42
|
+
- [x] Server serves tools, resources, prompts, and resource templates
|
|
43
|
+
(duck-typed serialization; `Server.start_stdio` accepts `resources`,
|
|
44
|
+
`prompts`, `resource_templates`)
|
|
45
|
+
|
|
46
|
+
## 2. `2025-11-25` gap (done)
|
|
47
|
+
|
|
48
|
+
- [x] **Icons + title display metadata (SEP-973)** — `title` (optional) and
|
|
49
|
+
`icons` (array of `{src, mimeType, sizes}`) on tools, resources, prompts;
|
|
50
|
+
parsed/emitted by the value objects, passed through by
|
|
51
|
+
`Adapters::ToolServer`.
|
|
52
|
+
- [x] **Server serves resources & prompts** — `resources/list`,
|
|
53
|
+
`resources/read`, `resources/templates/list`, `prompts/list`, `prompts/get`
|
|
54
|
+
handlers in `Server::Stdio`; `resources/read`/`prompts/get` pull contents
|
|
55
|
+
from `content`/`read`/`messages` or return the spec error codes.
|
|
56
|
+
- [x] **OIDC discovery (1.0)** — `Auth::OAuth#discover!` fetches
|
|
57
|
+
`/.well-known/openid-configuration` (derived from `issuer` per RFC 8414 or
|
|
58
|
+
an explicit `discovery_url`) and populates `token_url`, `auth_url`,
|
|
59
|
+
`issuer`; `token_url` is now optional at construction.
|
|
60
|
+
- [x] **Elicitation** — client handles `elicitation/create` via
|
|
61
|
+
`on_elicitation` (schema/params passed through verbatim, so titled/untitled,
|
|
62
|
+
single/multi-select enums and defaults are supported by construction).
|
|
63
|
+
- [x] **Sampling with tool calling (SEP-1577)** — client handles
|
|
64
|
+
`sampling/createMessage` via `on_sampling`, passing `tools`/`toolChoice`
|
|
65
|
+
through untouched.
|
|
66
|
+
- [x] **JSON Schema 2020-12 as default dialect** — `Validator` accepts
|
|
67
|
+
schemas declaring the 2020-12 dialect (the json-schema gem lacks 2020-12,
|
|
68
|
+
so the metaschema is stripped and validation retried; shared keywords
|
|
69
|
+
including `const` are enforced).
|
|
70
|
+
- [x] **Tool-name guidance** — spec is SHOULD-level; ask-mcp does not reject
|
|
71
|
+
tool names.
|
|
72
|
+
- [~] **Experimental Tasks** — explicitly not implemented: deprecated in
|
|
73
|
+
2026-07-28 in favor of the `io.modelcontextprotocol/tasks` extension.
|
|
74
|
+
|
|
75
|
+
## 3. `2026-07-28` (stateless revision) — done, dual-mode
|
|
76
|
+
|
|
77
|
+
### 3.1 Stateless core
|
|
78
|
+
|
|
79
|
+
- [x] **`server/discover`** — server advertises `protocolVersions`,
|
|
80
|
+
`capabilities`, `serverInfo`; client probes it first and falls back to the
|
|
81
|
+
legacy handshake when the method is unknown (`UnsupportedProtocolVersion`
|
|
82
|
+
semantics via negotiated version lists).
|
|
83
|
+
- [x] **Remove `initialize` handshake in stateless mode** — every request
|
|
84
|
+
carries `_meta` (`io.modelcontextprotocol/protocolVersion`,
|
|
85
|
+
`clientCapabilities`, `clientInfo`); server identifies itself in results
|
|
86
|
+
via `serverInfo` (in discover) and `resultType`. Legacy handshake kept for
|
|
87
|
+
older peers; a server that supports ≤ 2025-11-25 is detected and the client
|
|
88
|
+
still handshakes with the negotiated version.
|
|
89
|
+
- [x] **`resultType` on all results** — server emits `"complete"` for
|
|
90
|
+
stateless peers (and `"input_required"` on MRTR); clients treat missing as
|
|
91
|
+
`"complete"` per spec.
|
|
92
|
+
- [x] **MRTR (Multi Round-Trip Requests, SEP-2322)** — client resolves
|
|
93
|
+
`inputRequests` (elicitation/sampling/roots) through the registered
|
|
94
|
+
handlers, retries with `inputResponses`, echoes `requestState` verbatim,
|
|
95
|
+
uses a fresh JSON-RPC id per retry, and caps round trips.
|
|
96
|
+
- [x] **Remove `ping`, `logging/setLevel`, `notifications/roots/list_changed`**
|
|
97
|
+
— `ping` returns Method-not-found for stateless peers (`logging/setLevel`
|
|
98
|
+
and roots notifications were never implemented; nothing to remove).
|
|
99
|
+
- [x] **Error-code allocation policy** — `-32020..-32099` reserved:
|
|
100
|
+
`HeaderMismatch` `-32020`, `MissingRequiredClientCapability` `-32021`,
|
|
101
|
+
`UnsupportedProtocolVersion` `-32022`; resource-not-found renumbered to
|
|
102
|
+
`-32602` for stateless peers.
|
|
103
|
+
|
|
104
|
+
### 3.2 Transports
|
|
105
|
+
|
|
106
|
+
- [x] **Streamable HTTP: stateless shape** — no `Mcp-Session-Id`, no GET
|
|
107
|
+
stream endpoint, no `Last-Event-ID`; one POST per message; per-response
|
|
108
|
+
Content-Type decides single JSON vs SSE stream; SSE comment (keep-alive)
|
|
109
|
+
lines ignored.
|
|
110
|
+
- [x] **Required request headers** — `MCP-Protocol-Version` (matching body
|
|
111
|
+
`_meta`), `Mcp-Method`, `Mcp-Name` (`params.name`/`params.uri` for
|
|
112
|
+
`tools/call`, `resources/read`, `prompts/get`), with Base64 sentinel value
|
|
113
|
+
encoding for non-ASCII/unsafe values.
|
|
114
|
+
- [x] **x-mcp-header param mirroring (SEP-2243)** — client mirrors annotated
|
|
115
|
+
tool parameters into `Mcp-Param-{Name}` headers with value encoding, and
|
|
116
|
+
validates the annotations (`Ask::MCP::XMcpHeader`): empty names, invalid
|
|
117
|
+
field-name characters, non-primitive types, case-insensitive duplicates,
|
|
118
|
+
and annotations not statically reachable via a `properties`-only chain
|
|
119
|
+
(items/composition/conditionals/$ref/$defs) all make a tool definition
|
|
120
|
+
invalid, and Streamable HTTP clients exclude such tools from `tools/list`
|
|
121
|
+
with a warning. Integer values outside the JavaScript safe range are
|
|
122
|
+
omitted from headers at call time.
|
|
123
|
+
- [x] **`subscriptions/listen`** — transport opens the long-lived POST
|
|
124
|
+
response stream; `Client#listen(notifications)` convenience; delivered
|
|
125
|
+
notifications flow into the client's cache-invalidation handling.
|
|
126
|
+
Server-side emission: `Server::Stdio#notify_tools_list_changed` /
|
|
127
|
+
`notify_resources_list_changed` / `notify_prompts_list_changed` write the
|
|
128
|
+
notifications (deterministically ordered before the triggering response);
|
|
129
|
+
clients invalidate their caches on receipt (verified end-to-end).
|
|
130
|
+
- [x] **HTTP+SSE transport deprecated (SEP-2596)** — marked in code/docs;
|
|
131
|
+
still functional for legacy servers.
|
|
132
|
+
|
|
133
|
+
### 3.3 Results, caching, errors
|
|
134
|
+
|
|
135
|
+
- [x] **`CacheableResult`** — `ttlMs` + `cacheScope` (`"private"` default,
|
|
136
|
+
configurable) emitted on `tools/list`, `resources/list`,
|
|
137
|
+
`resources/templates/list`, `resources/read`, `prompts/list` for stateless
|
|
138
|
+
peers.
|
|
139
|
+
- [x] **Deterministic tool ordering** — `tools/list` order is stable (locked
|
|
140
|
+
by test) for client caching and prompt-cache hit rates.
|
|
141
|
+
- [x] **`extensions` capability field** — capabilities are caller-provided
|
|
142
|
+
pass-through, so extension capabilities are supported by construction.
|
|
143
|
+
|
|
144
|
+
### 3.4 Authorization
|
|
145
|
+
|
|
146
|
+
- [x] **Issuer tracking** — `discover!` records the discovered `issuer`.
|
|
147
|
+
- [x] **`iss` validation (RFC 9207)** — `Auth::OAuth#validate_iss!` rejects
|
|
148
|
+
an `iss` that does not match the recorded issuer.
|
|
149
|
+
- [x] **Client ID Metadata Documents (SEP-991)** —
|
|
150
|
+
`Auth::ClientIdMetadataDocument` builds and validates the self-hosted JSON
|
|
151
|
+
document (required `client_id`/`client_name`/`redirect_uris`, https URL
|
|
152
|
+
client_id with a path, `client_id` matching the document URL,
|
|
153
|
+
`application_type` `native`/`web` with `native` default for CLI clients).
|
|
154
|
+
URL-based client IDs are portable across authorization servers, so no
|
|
155
|
+
re-registration is needed when the server changes.
|
|
156
|
+
- [x] **Credential binding by issuer** — satisfied by construction: each
|
|
157
|
+
`OAuth` instance holds credentials for the single authorization server it
|
|
158
|
+
is configured/discovered against (no shared credential store exists).
|
|
159
|
+
- [~] **`application_type` in Dynamic Client Registration** — ask-mcp has no
|
|
160
|
+
DCR implementation (it is deprecated in 2026-07-28); `application_type` is
|
|
161
|
+
supported via the Client ID Metadata Document builder.
|
|
162
|
+
|
|
163
|
+
### 3.5 Deprecations (keep working, don't extend)
|
|
164
|
+
|
|
165
|
+
- [x] Roots, Sampling, Logging marked deprecated (no new build-out; the
|
|
166
|
+
client still exposes `on_sampling`/elicitation handlers for MRTR).
|
|
167
|
+
- [x] **OTel trace context conventions (SEP-414)** — `Ask::MCP::TraceContext`
|
|
168
|
+
extracts `traceparent`/`tracestate`/`baggage` from HTTP headers (incl.
|
|
169
|
+
Rack's `HTTP_*` convention) and from `_meta`; clients propagate configured
|
|
170
|
+
`_meta` fields (e.g. `meta: TraceContext.from_headers(rack_env)`).
|
|
171
|
+
|
|
172
|
+
## 4. Conformance / verification
|
|
173
|
+
|
|
174
|
+
- Unit (no subprocess): `bundle exec ruby -Itest test/client_test.rb`
|
|
175
|
+
- Server (harness + subprocess): `bundle exec ruby -Itest test/server/stdio_test.rb`
|
|
176
|
+
- Transports: `bundle exec ruby -Itest test/transport/`
|
|
177
|
+
- Full suite: `bundle exec rake test`
|
|
178
|
+
- Status: 299 runs, 686 assertions, 0 failures (green across repeated runs).
|
|
179
|
+
|
|
180
|
+
Conformance: `test/conformance_test.rb` is the self-contained stand-in for the
|
|
181
|
+
official MCP test suite — it runs the full lifecycle against the real server
|
|
182
|
+
in both the `2025-06-18` handshake era and the `2026-07-28` stateless era, plus
|
|
183
|
+
a stateless client end-to-end. Run the official MCP fixtures here too once the
|
|
184
|
+
2026-07-28 fixtures are published.
|