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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 909c171336e0497c2377c2f4106e1ec0314adbbbf15791819caf6be0c9d04035
4
- data.tar.gz: 5a5e95508499f38e310cce0052fcd4c9dfd2c95f8a4e965621c37d35c392293d
3
+ metadata.gz: d73da1e33a97a8bd6825f2eb3e0f9c770ffbbc8b1a9502db612b49e71fc87193
4
+ data.tar.gz: 5f7cf6b4cb501db18cf5a8367215292c923cf97c078acb226e7546d86e3218a8
5
5
  SHA512:
6
- metadata.gz: 52526b46e9574fb4289ddde3b7ff340945eead680763ad7ece3903a8bea89a5a535cb3b9951b0236f6749808b22254c9f4b691133eff56e24a7438eb68ae8743
7
- data.tar.gz: be8c62053892b8aa52e5216fcb58c7857ecfdff12b025173d9cd1be1a384cced8dd41ef4f9e17ca531fe6d869743dbc63caad275fbccb82014ddd4d94d49c3a2
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
  [![Gem Version](https://badge.fury.io/rb/ask-mcp.svg)](https://badge.fury.io/rb/ask-mcp)
4
4
 
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.
9
-
10
- MCP is the industry standard for LLM tool discovery — the same protocol used by
11
- Claude Code, Codex, Cursor, and GitHub Copilot.
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
- Or add to your Gemfile:
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
- ## Quick Start Server
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
- 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
+ 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
- # Start the server (blocking — runs until stdin closes)
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
- Configure your MCP client:
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
- 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?
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
- The result object from `call(args)` should respond to `ok?` (or `ok`) and
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
- 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
- )
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 local processes
126
+ # stdio: local processes
135
127
  Ask::MCP.from_stdio("npx", ["-y", "@modelcontextprotocol/server-github"])
136
128
 
137
- # SSE remote servers with Server-Sent Events
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 remote servers
132
+ # Streamable HTTP: remote servers
141
133
  Ask::MCP.from_http("https://mcp.example.com/mcp")
142
134
  ```
143
135
 
144
- ## API
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
- ### Client Lifecycle
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
- # Create a client with any transport
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
- ### Tool, Resource, Prompt Objects
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
- # Tool
172
- tool = Ask::MCP::Tool.new(
173
- name: "read_file",
174
- description: "Read a file from disk",
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
- ## Architecture
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
- ask-mcp/
240
- ├── lib/ask/mcp.rb # Entry point, factory methods
241
- ├── lib/ask/mcp/client.rb # MCP client (connect, call_tool, etc.)
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)
244
- ├── lib/ask/mcp/tool.rb # MCP tool representation
245
- ├── lib/ask/mcp/resource.rb # MCP resource representation
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
- ```bash
263
- # Run tests
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.