ask-mcp 0.4.0 → 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.
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ask
4
+ module MCP
5
+ # Validation of `x-mcp-header` annotations in tool inputSchemas
6
+ # (2026-07-28, SEP-2243).
7
+ #
8
+ # Servers MAY designate tool parameters to be mirrored into HTTP headers
9
+ # via an `x-mcp-header` extension property. Clients using the Streamable
10
+ # HTTP transport MUST reject a tool definition whose annotations violate
11
+ # the constraints below (rejection = exclude the tool from tools/list);
12
+ # clients on other transports (stdio, SSE) MAY ignore the annotations.
13
+ module XMcpHeader
14
+ # RFC 9110 field-name token characters (1*tchar).
15
+ TOKEN_RE = /\A[!#$%&'*+\-.^_`|~0-9A-Za-z]+\z/
16
+ # Primitive types that may carry an x-mcp-header annotation.
17
+ PRIMITIVE_TYPES = %w[string integer boolean].freeze
18
+ # JavaScript safe integer range.
19
+ JS_SAFE_MIN = -(2**53) + 1
20
+ JS_SAFE_MAX = 2**53 - 1
21
+
22
+ module_function
23
+
24
+ # Returns the reason the tool definition's x-mcp-header annotations are
25
+ # invalid, or nil if the definition is valid (or has no annotations).
26
+ def invalid_reason(schema)
27
+ seen = {}
28
+ find_invalid(schema, seen, tainted: false)
29
+ end
30
+
31
+ # Whether an integer value is representable as a safe header value.
32
+ def safe_integer?(value)
33
+ value.is_a?(Integer) && value.between?(JS_SAFE_MIN, JS_SAFE_MAX)
34
+ end
35
+
36
+ # Depth-first walk. An annotation is valid only if the chain from the
37
+ # schema root to it passes solely through `properties` keys; every other
38
+ # container keyword (items, oneOf/anyOf/allOf/not, if/then/else, $ref,
39
+ # $defs, ...) taints the path, which is exactly the spec's "statically
40
+ # reachable" rule.
41
+ def find_invalid(node, seen, tainted:)
42
+ return nil unless node.is_a?(Hash)
43
+
44
+ annotation = node[:'x-mcp-header'] || node["x-mcp-header"]
45
+ if annotation
46
+ return "x-mcp-header must not appear under non-properties keywords" if tainted
47
+
48
+ reason = annotation_error(annotation, node)
49
+ return reason if reason
50
+
51
+ key = annotation.to_s.downcase
52
+ return "duplicate x-mcp-header #{annotation.inspect} (case-insensitive)" if seen[key]
53
+
54
+ seen[key] = true
55
+ end
56
+
57
+ node.each do |key, value|
58
+ if key.to_s == "properties" && value.is_a?(Hash)
59
+ # The allowed step: descending through a `properties` key does not
60
+ # taint the chain.
61
+ value.each_value do |sub|
62
+ reason = find_invalid(sub, seen, tainted: tainted)
63
+ return reason if reason
64
+ end
65
+ elsif value.is_a?(Hash)
66
+ reason = find_invalid(value, seen, tainted: true)
67
+ return reason if reason
68
+ elsif value.is_a?(Array)
69
+ value.each do |item|
70
+ next unless item.is_a?(Hash)
71
+ reason = find_invalid(item, seen, tainted: true)
72
+ return reason if reason
73
+ end
74
+ end
75
+ end
76
+
77
+ nil
78
+ end
79
+
80
+ def annotation_error(header_name, prop)
81
+ name = header_name.to_s
82
+ return "x-mcp-header must not be empty" if name.empty?
83
+ return "x-mcp-header #{name.inspect} contains invalid characters" unless name.match?(TOKEN_RE)
84
+
85
+ type = prop[:type] || prop["type"]
86
+ unless PRIMITIVE_TYPES.include?(type.to_s)
87
+ return "x-mcp-header #{name.inspect} must be on a primitive type (string/integer/boolean), got #{type.inspect}"
88
+ end
89
+
90
+ nil
91
+ end
92
+ private_class_method :find_invalid, :annotation_error
93
+ end
94
+ end
95
+ end
data/lib/ask/mcp.rb CHANGED
@@ -4,12 +4,26 @@ require_relative "mcp/version"
4
4
 
5
5
  module Ask
6
6
  module MCP
7
+ # The MCP protocol revision this gem implements and advertises.
8
+ # Single source of truth — Client and Server::Stdio both reference this
9
+ # so the advertised version can never drift between the two sides.
10
+ #
11
+ # `PROTOCOL_VERSION` is the legacy default (stateful `initialize`
12
+ # handshake). `SUPPORTED_PROTOCOL_VERSIONS` is what the server advertises
13
+ # via `server/discover`; `LATEST_PROTOCOL_VERSION` is the stateless
14
+ # 2026-07-28 revision that clients and servers negotiate to.
15
+ PROTOCOL_VERSION = "2025-06-18"
16
+ LATEST_PROTOCOL_VERSION = "2026-07-28"
17
+ SUPPORTED_PROTOCOL_VERSIONS = [PROTOCOL_VERSION, "2025-11-25", LATEST_PROTOCOL_VERSION].freeze
18
+
7
19
  autoload :Client, "ask/mcp/client"
8
20
  autoload :Server, "ask/mcp/server"
9
21
  autoload :Tool, "ask/mcp/tool"
10
22
  autoload :Resource, "ask/mcp/resource"
11
23
  autoload :Prompt, "ask/mcp/prompt"
12
24
  autoload :Validator, "ask/mcp/validator"
25
+ autoload :XMcpHeader, "ask/mcp/x_mcp_header"
26
+ autoload :TraceContext, "ask/mcp/trace_context"
13
27
 
14
28
  module Native
15
29
  autoload :Messages, "ask/mcp/native/messages"
@@ -24,6 +38,7 @@ module Ask
24
38
  module Auth
25
39
  autoload :OAuth, "ask/mcp/auth/oauth"
26
40
  autoload :Token, "ask/mcp/auth/token"
41
+ autoload :ClientIdMetadataDocument, "ask/mcp/auth/client_id_metadata_document"
27
42
  end
28
43
 
29
44
  module Adapters
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.0
4
+ version: 0.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -37,6 +37,20 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '5.0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: base64
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
40
54
  - !ruby/object:Gem::Dependency
41
55
  name: minitest
42
56
  requirement: !ruby/object:Gem::Requirement
@@ -91,11 +105,13 @@ files:
91
105
  - CHANGELOG.md
92
106
  - LICENSE
93
107
  - README.md
108
+ - docs/SPEC_COMPLIANCE.md
94
109
  - docs/auth-setup.md
95
110
  - lib/ask-mcp.rb
96
111
  - lib/ask/mcp.rb
97
112
  - lib/ask/mcp/adapters/ask_tool.rb
98
113
  - lib/ask/mcp/adapters/tool_server.rb
114
+ - lib/ask/mcp/auth/client_id_metadata_document.rb
99
115
  - lib/ask/mcp/auth/oauth.rb
100
116
  - lib/ask/mcp/auth/token.rb
101
117
  - lib/ask/mcp/client.rb
@@ -105,11 +121,13 @@ files:
105
121
  - lib/ask/mcp/server.rb
106
122
  - lib/ask/mcp/server/stdio.rb
107
123
  - lib/ask/mcp/tool.rb
124
+ - lib/ask/mcp/trace_context.rb
108
125
  - lib/ask/mcp/transport/sse.rb
109
126
  - lib/ask/mcp/transport/stdio.rb
110
127
  - lib/ask/mcp/transport/streamable_http.rb
111
128
  - lib/ask/mcp/validator.rb
112
129
  - lib/ask/mcp/version.rb
130
+ - lib/ask/mcp/x_mcp_header.rb
113
131
  homepage: https://github.com/ask-rb/ask-mcp
114
132
  licenses:
115
133
  - MIT