ruby-mcp-client 2.1.0 → 3.0.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/OAUTH.md +555 -0
- data/README.md +825 -48
- data/lib/mcp_client/audio_content.rb +1 -1
- data/lib/mcp_client/auth/browser_oauth.rb +131 -21
- data/lib/mcp_client/auth/oauth_provider/challenge_handling.rb +532 -0
- data/lib/mcp_client/auth/oauth_provider/client_authentication.rb +121 -0
- data/lib/mcp_client/auth/oauth_provider/pending_requests.rb +51 -0
- data/lib/mcp_client/auth/oauth_provider/registration_store.rb +486 -0
- data/lib/mcp_client/auth/oauth_provider/response_validation.rb +441 -0
- data/lib/mcp_client/auth/oauth_provider/scope_selection.rb +134 -0
- data/lib/mcp_client/auth/oauth_provider/token_store.rb +419 -0
- data/lib/mcp_client/auth/oauth_provider.rb +1354 -386
- data/lib/mcp_client/auth/peer_text.rb +174 -0
- data/lib/mcp_client/auth.rb +298 -32
- data/lib/mcp_client/cached_result.rb +145 -0
- data/lib/mcp_client/called_tool_definition.rb +138 -0
- data/lib/mcp_client/client/cache_slices.rb +195 -0
- data/lib/mcp_client/client/list_aggregation.rb +243 -0
- data/lib/mcp_client/client/notification_routing.rb +155 -0
- data/lib/mcp_client/client/sampling_validation.rb +200 -0
- data/lib/mcp_client/client/task_api.rb +531 -0
- data/lib/mcp_client/client/task_lifetimes.rb +269 -0
- data/lib/mcp_client/client/task_registry.rb +254 -0
- data/lib/mcp_client/client/task_shape.rb +102 -0
- data/lib/mcp_client/client/task_support.rb +1166 -0
- data/lib/mcp_client/client/task_updates.rb +457 -0
- data/lib/mcp_client/client/task_wait_boundaries.rb +198 -0
- data/lib/mcp_client/client/task_workers.rb +63 -0
- data/lib/mcp_client/client.rb +796 -518
- data/lib/mcp_client/deep_copy.rb +49 -0
- data/lib/mcp_client/deprecation_notices.rb +94 -0
- data/lib/mcp_client/deprecations.rb +419 -0
- data/lib/mcp_client/errors.rb +474 -7
- data/lib/mcp_client/header_params.rb +320 -0
- data/lib/mcp_client/http_transport_base/bounded_inflate.rb +41 -0
- data/lib/mcp_client/http_transport_base/cache_support.rb +694 -0
- data/lib/mcp_client/http_transport_base/era_detection.rb +134 -0
- data/lib/mcp_client/http_transport_base/listen_stream.rb +763 -0
- data/lib/mcp_client/http_transport_base/param_headers.rb +35 -0
- data/lib/mcp_client/http_transport_base/request_recovery.rb +156 -0
- data/lib/mcp_client/http_transport_base/session_recovery.rb +113 -0
- data/lib/mcp_client/http_transport_base/sse_event_scanner.rb +145 -0
- data/lib/mcp_client/http_transport_base/stream_capture.rb +160 -0
- data/lib/mcp_client/http_transport_base/stream_recovery.rb +318 -0
- data/lib/mcp_client/http_transport_base/tool_listing.rb +277 -0
- data/lib/mcp_client/http_transport_base.rb +666 -120
- data/lib/mcp_client/input_round_trips.rb +128 -0
- data/lib/mcp_client/json_rpc_common/envelopes.rb +32 -0
- data/lib/mcp_client/json_rpc_common/error_bodies.rb +105 -0
- data/lib/mcp_client/json_rpc_common/input_waits.rb +167 -0
- data/lib/mcp_client/json_rpc_common.rb +900 -13
- data/lib/mcp_client/oauth_client.rb +14 -5
- data/lib/mcp_client/prompt.rb +4 -0
- data/lib/mcp_client/request_authorization.rb +128 -0
- data/lib/mcp_client/request_meta_scope.rb +77 -0
- data/lib/mcp_client/request_metadata.rb +287 -0
- data/lib/mcp_client/resource.rb +4 -0
- data/lib/mcp_client/resource_content.rb +20 -0
- data/lib/mcp_client/resource_template.rb +4 -0
- data/lib/mcp_client/result_caching.rb +999 -0
- data/lib/mcp_client/result_completeness.rb +34 -0
- data/lib/mcp_client/root.rb +6 -0
- data/lib/mcp_client/round_trip_marker.rb +28 -0
- data/lib/mcp_client/schema_validator/annotations.rb +82 -0
- data/lib/mcp_client/schema_validator/composition.rb +86 -0
- data/lib/mcp_client/schema_validator/dialects.rb +66 -0
- data/lib/mcp_client/schema_validator/ecma_patterns.rb +567 -0
- data/lib/mcp_client/schema_validator/evaluation.rb +517 -0
- data/lib/mcp_client/schema_validator/input_requirements.rb +84 -0
- data/lib/mcp_client/schema_validator/instances.rb +449 -0
- data/lib/mcp_client/schema_validator/keyword_scan.rb +121 -0
- data/lib/mcp_client/schema_validator/normalization.rb +104 -0
- data/lib/mcp_client/schema_validator/references.rb +610 -0
- data/lib/mcp_client/schema_validator/scalars.rb +126 -0
- data/lib/mcp_client/schema_validator/shapes.rb +319 -0
- data/lib/mcp_client/schema_validator/uri_references.rb +153 -0
- data/lib/mcp_client/schema_validator.rb +882 -208
- data/lib/mcp_client/server_base.rb +233 -5
- data/lib/mcp_client/server_factory.rb +9 -3
- data/lib/mcp_client/server_http/json_rpc_transport.rb +219 -4
- data/lib/mcp_client/server_http.rb +307 -90
- data/lib/mcp_client/server_sse/json_rpc_transport.rb +113 -25
- data/lib/mcp_client/server_sse/sse_parser.rb +39 -6
- data/lib/mcp_client/server_sse.rb +227 -62
- data/lib/mcp_client/server_stdio/child_session.rb +98 -0
- data/lib/mcp_client/server_stdio/json_rpc_transport.rb +1003 -28
- data/lib/mcp_client/server_stdio.rb +772 -183
- data/lib/mcp_client/server_streamable_http/json_rpc_transport.rb +189 -25
- data/lib/mcp_client/server_streamable_http.rb +302 -115
- data/lib/mcp_client/session_pin.rb +119 -0
- data/lib/mcp_client/subscription/notification_dispatcher.rb +354 -0
- data/lib/mcp_client/subscription.rb +852 -0
- data/lib/mcp_client/subscription_support.rb +715 -0
- data/lib/mcp_client/task.rb +286 -14
- data/lib/mcp_client/tool.rb +31 -3
- data/lib/mcp_client/version.rb +21 -6
- data/lib/mcp_client.rb +108 -19
- metadata +68 -2
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCPClient
|
|
4
|
+
# The guard an operation runs before it projects a payload out of a result.
|
|
5
|
+
#
|
|
6
|
+
# An unfinished answer (MCP 2026-07-28 InputRequiredResult) that reaches
|
|
7
|
+
# such an operation would lose the server's inputRequests and its opaque
|
|
8
|
+
# requestState, and would be presented as an empty successful answer. The
|
|
9
|
+
# resolver that wraps every request drives the round trips a modern server
|
|
10
|
+
# asks for, so what reaches here is either finished or something no
|
|
11
|
+
# resolver claims; this is the backstop for the latter.
|
|
12
|
+
module ResultCompleteness
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# @param result [Object] the JSON-RPC result
|
|
16
|
+
# @param method [String] the request method, for the message
|
|
17
|
+
# @return [Object] the result, when it is complete
|
|
18
|
+
# @raise [MCPClient::Errors::InputRequiredError] when it is unfinished --
|
|
19
|
+
# the whole result rides on the error's `data`, so a host can drive the
|
|
20
|
+
# round trip itself
|
|
21
|
+
# @raise [MCPClient::Errors::InvalidResultError] for any other
|
|
22
|
+
# discriminator this client cannot carry through
|
|
23
|
+
def require_complete_result!(result, method)
|
|
24
|
+
type = MCPClient::JsonRpcCommon.result_type(result)
|
|
25
|
+
return result if type == 'complete'
|
|
26
|
+
|
|
27
|
+
message = "#{method} answered with resultType #{type.to_s[0, 64].inspect}, which this " \
|
|
28
|
+
'client cannot carry through'
|
|
29
|
+
raise MCPClient::Errors::InputRequiredError.new(message, data: result) if type == 'input_required'
|
|
30
|
+
|
|
31
|
+
raise MCPClient::Errors::InvalidResultError.new("Invalid result: #{message}", data: result)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/mcp_client/root.rb
CHANGED
|
@@ -5,6 +5,12 @@ require 'uri'
|
|
|
5
5
|
module MCPClient
|
|
6
6
|
# Represents an MCP Root - a URI that defines a boundary where servers can operate
|
|
7
7
|
# Roots are declared by clients to inform servers about relevant resources and their locations
|
|
8
|
+
#
|
|
9
|
+
# @deprecated Roots is deprecated since MCP 2026-07-28 (SEP-2577); earliest
|
|
10
|
+
# removal is the first revision released on or after 2027-07-28. The class
|
|
11
|
+
# keeps working for as long as the feature does, and building one raises no
|
|
12
|
+
# notice — using the list does. Pass directories or files through tool
|
|
13
|
+
# parameters, resource URIs or server configuration instead.
|
|
8
14
|
class Root
|
|
9
15
|
attr_reader :uri, :name, :meta
|
|
10
16
|
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCPClient
|
|
4
|
+
# Whether the request a thread last resolved went through a multi
|
|
5
|
+
# round-trip retry (MCP 2026-07-28 InputRequiredResult). A result that
|
|
6
|
+
# depended on input responses MUST NOT be cached, and the marker is
|
|
7
|
+
# thread-local because a transport serves concurrent requests: another
|
|
8
|
+
# request completing meanwhile says nothing about this one.
|
|
9
|
+
module RoundTripMarker
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
# @return [Boolean]
|
|
13
|
+
def last_result_from_round_trip?
|
|
14
|
+
Thread.current[round_trip_marker_key] == true
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param flag [Boolean]
|
|
18
|
+
# @return [void]
|
|
19
|
+
def mark_round_trip_result(flag)
|
|
20
|
+
Thread.current[round_trip_marker_key] = flag
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Symbol] the thread-local key of this transport's round-trip marker
|
|
24
|
+
def round_trip_marker_key
|
|
25
|
+
:"mcp_client_round_trip_#{object_id}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCPClient
|
|
4
|
+
module SchemaValidator
|
|
5
|
+
# What the schemas applied to one instance value evaluated of it: the
|
|
6
|
+
# annotation results `unevaluatedProperties` and `unevaluatedItems` read
|
|
7
|
+
# (JSON Schema 2020-12 Core Sections 11.2 and 11.3). One is kept per
|
|
8
|
+
# application of a schema to an object or an array, filled in by the
|
|
9
|
+
# keywords the node evaluates itself (`properties`, `patternProperties`,
|
|
10
|
+
# `additionalProperties`, `prefixItems`, `items`, `contains` and the two
|
|
11
|
+
# keywords themselves) and merged from every in-place applicator whose
|
|
12
|
+
# subschema passed — a failed subschema annotates nothing, and a cousin
|
|
13
|
+
# (a sibling branch of the same composition) never sees it.
|
|
14
|
+
class Evaluated
|
|
15
|
+
def initialize
|
|
16
|
+
@all = false
|
|
17
|
+
@names = {}
|
|
18
|
+
@prefix = 0
|
|
19
|
+
@indices = {}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @return [Boolean] whether every member or item was evaluated
|
|
23
|
+
def all?
|
|
24
|
+
@all
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Every member or item is evaluated (an `items` schema, an
|
|
28
|
+
# `additionalProperties` schema, or the unevaluated keyword itself).
|
|
29
|
+
# @return [void]
|
|
30
|
+
def all!
|
|
31
|
+
@all = true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @param name [Object] a property name (either key form)
|
|
35
|
+
# @return [void]
|
|
36
|
+
def name!(name)
|
|
37
|
+
@names[name.to_s] = true unless @all
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# @param count [Integer] how many leading items a tuple evaluated
|
|
41
|
+
# @return [void]
|
|
42
|
+
def prefix!(count)
|
|
43
|
+
@prefix = count if count > @prefix
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @param index [Integer] an item `contains` matched
|
|
47
|
+
# @return [void]
|
|
48
|
+
def index!(index)
|
|
49
|
+
@indices[index] = true unless @all
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# @param name [Object] a property name (either key form)
|
|
53
|
+
# @return [Boolean] whether the member was evaluated
|
|
54
|
+
def property?(name)
|
|
55
|
+
@all || @names.key?(name.to_s)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @param index [Integer]
|
|
59
|
+
# @return [Boolean] whether the item was evaluated
|
|
60
|
+
def item?(index)
|
|
61
|
+
@all || index < @prefix || @indices.key?(index)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Take over what a passed subschema evaluated of the same value.
|
|
65
|
+
# @param other [Evaluated, nil]
|
|
66
|
+
# @return [Evaluated] self
|
|
67
|
+
def merge!(other)
|
|
68
|
+
return self if other.nil?
|
|
69
|
+
return all! || self if other.all?
|
|
70
|
+
|
|
71
|
+
@names.merge!(other.names)
|
|
72
|
+
@prefix = other.prefix if other.prefix > @prefix
|
|
73
|
+
@indices.merge!(other.indices)
|
|
74
|
+
self
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
protected
|
|
78
|
+
|
|
79
|
+
attr_reader :names, :prefix, :indices
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCPClient
|
|
4
|
+
module SchemaValidator
|
|
5
|
+
# How complete a verdict on one value is. Extended into SchemaValidator,
|
|
6
|
+
# so the methods are its own; {Evaluation} applies the composition
|
|
7
|
+
# keywords themselves and reads the answers here.
|
|
8
|
+
#
|
|
9
|
+
# A composition branch is evaluated speculatively (its errors are a
|
|
10
|
+
# verdict, not output). anyOf and allOf are monotonic, so a branch that
|
|
11
|
+
# passes as far as the validator can evaluate it is accepted; not, oneOf
|
|
12
|
+
# and if are not, so a branch that still holds an unevaluated assertion
|
|
13
|
+
# applying to the instance is :undecided and never a match, while one
|
|
14
|
+
# decided by its evaluated keywords (or carrying only annotations) is a
|
|
15
|
+
# full verdict.
|
|
16
|
+
#
|
|
17
|
+
# Every standard assertion whose verdict this validator can reach is
|
|
18
|
+
# evaluated ({Instances#validate_object},
|
|
19
|
+
# {Instances#validate_array}, {Scalars#validate_number},
|
|
20
|
+
# and the unevaluated keywords from the annotations {Evaluation}
|
|
21
|
+
# collects), so what is left here is only what genuinely cannot be
|
|
22
|
+
# decided: a dynamic reference whose target the dynamic scope could
|
|
23
|
+
# re-bind, and `format` where it asserts.
|
|
24
|
+
module Composition
|
|
25
|
+
# Whether a schema object carries an assertion the validator does not
|
|
26
|
+
# evaluate (in the dialect in force) that applies to this instance, so
|
|
27
|
+
# its verdict is only partial. Annotations (`format` in 2019-09 and
|
|
28
|
+
# 2020-12, `contentSchema`) decide nothing and leave the verdict whole;
|
|
29
|
+
# draft-07 `format` asserts (Validation Section 7.2), and the validator
|
|
30
|
+
# does not evaluate formats, so a string branch carrying one is
|
|
31
|
+
# undecided there. Dynamic references are evaluated, against the
|
|
32
|
+
# dynamic scope, and leave no verdict partial.
|
|
33
|
+
# @param schema [Hash] the schema object
|
|
34
|
+
# @param dialect [String, nil] the dialect in force at it
|
|
35
|
+
# @param data [Object] the instance
|
|
36
|
+
# @param _ctx [Context] the validation context
|
|
37
|
+
# @return [Boolean]
|
|
38
|
+
def partial_keywords?(schema, dialect, data, _ctx)
|
|
39
|
+
dialect == DRAFT_07 && data.is_a?(String) && schema.key?('format')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# The number of matching items `contains` requires: its companion
|
|
43
|
+
# where the dialect defines one and gives it a number, else the
|
|
44
|
+
# default of 1 (JSON Schema 2020-12 Validation Section 6.4.4).
|
|
45
|
+
# @return [Numeric]
|
|
46
|
+
def contains_min(schema, dialect)
|
|
47
|
+
min = schema['minContains'] if keyword_known?('minContains', dialect)
|
|
48
|
+
min.is_a?(Numeric) ? min : 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @return [Numeric, nil] the number of matching items `contains`
|
|
52
|
+
# allows, when the dialect defines the companion and it is a number
|
|
53
|
+
def contains_max(schema, dialect)
|
|
54
|
+
max = schema['maxContains'] if keyword_known?('maxContains', dialect)
|
|
55
|
+
max if max.is_a?(Numeric)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# @param data [Hash] the instance
|
|
59
|
+
# @param name [Object] the property name a schema keyword names
|
|
60
|
+
# @return [Boolean] whether the instance carries the property in either
|
|
61
|
+
# key form
|
|
62
|
+
def property_present?(data, name)
|
|
63
|
+
name = name.to_s
|
|
64
|
+
data.key?(name) || data.key?(name.to_sym)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Match a server-supplied pattern against a property name. Both come
|
|
68
|
+
# from the peer, so — exactly like {.validate_pattern} — the match runs
|
|
69
|
+
# under the validation-wide deadline: a backtracking expression here
|
|
70
|
+
# must not be able to hold the calling thread.
|
|
71
|
+
# @param deadline [Float, nil] monotonic deadline for the whole validation
|
|
72
|
+
# @return [Boolean]
|
|
73
|
+
# @raise [Aborted] when the budget is exhausted
|
|
74
|
+
def pattern_matches?(pattern, name, deadline = nil)
|
|
75
|
+
remaining = pattern_budget_remaining(deadline)
|
|
76
|
+
raise Aborted, "validation time budget exhausted before pattern #{clip(pattern.inspect)}" if remaining.zero?
|
|
77
|
+
|
|
78
|
+
ecma_regexp(pattern, remaining, deadline).match?(name)
|
|
79
|
+
rescue Regexp::TimeoutError
|
|
80
|
+
raise Aborted, "pattern #{clip(pattern.inspect)} exceeded the #{PATTERN_MATCH_TIMEOUT}s matching budget"
|
|
81
|
+
rescue RegexpError, TypeError
|
|
82
|
+
false
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCPClient
|
|
4
|
+
module SchemaValidator
|
|
5
|
+
# Dialect selection: the `$schema` a document (or an embedded resource
|
|
6
|
+
# root) declares, its canonical supported form, and the problems an
|
|
7
|
+
# unusable declaration reports. Extended into SchemaValidator, so the
|
|
8
|
+
# methods are its own.
|
|
9
|
+
module Dialects
|
|
10
|
+
# The dialect a schema declares, or the default.
|
|
11
|
+
# @param schema [Object] the schema
|
|
12
|
+
# @return [String] the `$schema` value (without a trailing `#`), or DEFAULT_DIALECT
|
|
13
|
+
def dialect(schema)
|
|
14
|
+
return DEFAULT_DIALECT unless schema.is_a?(Hash)
|
|
15
|
+
return DEFAULT_DIALECT unless schema.key?('$schema') || schema.key?(:$schema)
|
|
16
|
+
|
|
17
|
+
declared = schema.key?('$schema') ? schema['$schema'] : schema[:$schema]
|
|
18
|
+
# A declaration that is present but unusable is not the default.
|
|
19
|
+
return nil unless declared.is_a?(String) && !declared.empty?
|
|
20
|
+
|
|
21
|
+
declared.sub(/#\z/, '')
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The supported dialect a declared URI stands for (scheme-insensitive),
|
|
25
|
+
# or nil.
|
|
26
|
+
# @param uri [String]
|
|
27
|
+
# @return [String, nil]
|
|
28
|
+
def canonical_dialect(uri)
|
|
29
|
+
canonical = uri.sub(/#\z/, '').sub(%r{\Ahttps?://}, '')
|
|
30
|
+
SUPPORTED_DIALECTS.find { |d| d.sub(%r{\Ahttps?://}, '') == canonical }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @param uri [String]
|
|
34
|
+
# @return [Boolean]
|
|
35
|
+
def supported_dialect?(uri)
|
|
36
|
+
!canonical_dialect(uri).nil?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# The dialect in force at a schema object: an embedded resource root (a
|
|
40
|
+
# subschema whose `$id` is a URI) may declare its own `$schema`, which
|
|
41
|
+
# is that resource's dialect (JSON Schema 2020-12 Core Section 8.1.1);
|
|
42
|
+
# anywhere else the inherited dialect applies (a `$schema` that is not
|
|
43
|
+
# at a resource root is ignored).
|
|
44
|
+
# @param schema [Hash]
|
|
45
|
+
# @param inherited [String, nil] the enclosing resource's dialect
|
|
46
|
+
# @return [String, nil] the canonical dialect, or nil when the embedded
|
|
47
|
+
# declaration is malformed or unsupported
|
|
48
|
+
def embedded_dialect(schema, inherited)
|
|
49
|
+
return inherited unless resource_root?(schema, inherited) && schema.key?('$schema')
|
|
50
|
+
|
|
51
|
+
declared = dialect(schema)
|
|
52
|
+
declared && canonical_dialect(declared)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# @return [String, nil] why an embedded resource's `$schema` is unusable
|
|
56
|
+
def embedded_dialect_problem(schema)
|
|
57
|
+
declared = dialect(schema)
|
|
58
|
+
return 'embedded resource $schema must be a non-empty string naming the dialect' if declared.nil?
|
|
59
|
+
return nil if supported_dialect?(declared)
|
|
60
|
+
|
|
61
|
+
"embedded resource dialect #{clip(declared.inspect)} is not supported " \
|
|
62
|
+
"(supported: #{SUPPORTED_DIALECTS.join(', ')})"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|