mcp 1.9.0 → 1.10.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/README.md +3 -0
- data/lib/json_rpc_handler.rb +1 -1
- data/lib/mcp/client/http.rb +29 -5
- data/lib/mcp/client/oauth/client_credentials_provider.rb +16 -0
- data/lib/mcp/client/oauth/cross_app_access_provider.rb +19 -2
- data/lib/mcp/client/oauth/flow.rb +11 -3
- data/lib/mcp/client/oauth/jwt_client_assertion.rb +3 -2
- data/lib/mcp/client/oauth/pkce.rb +3 -2
- data/lib/mcp/client/stdio.rb +15 -3
- data/lib/mcp/client.rb +5 -1
- data/lib/mcp/instrumentation.rb +12 -7
- data/lib/mcp/mcp_param_headers.rb +332 -0
- data/lib/mcp/server/oauth/jwt_verifier.rb +34 -5
- data/lib/mcp/server/transports/stdio_transport.rb +12 -0
- data/lib/mcp/server/transports/streamable_http_transport.rb +168 -19
- data/lib/mcp/server.rb +60 -35
- data/lib/mcp/server_session.rb +23 -1
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +2 -0
- metadata +3 -3
- data/lib/mcp/client/mcp_param_headers.rb +0 -242
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 87d807c4e295950341aa7123459ac862a601c3bb840a881ac2a8c48ae2b8b2f0
|
|
4
|
+
data.tar.gz: b09a469593f943812b51f7d1f3db3eb1fb6d1c63c4d321c1cf6d6936158f842a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 124b4d117e0b4c0079a4af053d2de713bce66847f1c0d95404cd99d54a929ef0f250669b7207c8d6658ced4a70e63f1708376dcbe379a848f65cf8e4c5880cc0
|
|
7
|
+
data.tar.gz: 4e77573617cbafc37ac1b1fb59aee3fe6f96825c474fc912ca24f766c6690fd3d5df4b2e0c24831210b4d8c7700340f1daf4043ceaa3977b8dfaf589b5e75a01
|
data/README.md
CHANGED
|
@@ -122,6 +122,9 @@ response = client.call_tool(
|
|
|
122
122
|
stdio_transport.close
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
+
Stdio servers inherit only a small base environment by default, including `GEM_HOME` and `GEM_PATH`.
|
|
126
|
+
Pass extra variables through `env:`, or set `inherit_env: true` to restore full inheritance.
|
|
127
|
+
|
|
125
128
|
The same client can connect to Streamable HTTP servers with `MCP::Client::HTTP`;
|
|
126
129
|
see [Client Transports](https://ruby.sdk.modelcontextprotocol.io/client/transports/).
|
|
127
130
|
|
data/lib/json_rpc_handler.rb
CHANGED
data/lib/mcp/client/http.rb
CHANGED
|
@@ -6,7 +6,7 @@ require_relative "../configuration"
|
|
|
6
6
|
require_relative "../methods"
|
|
7
7
|
require_relative "../protocol_deprecations"
|
|
8
8
|
require_relative "../version"
|
|
9
|
-
require_relative "mcp_param_headers"
|
|
9
|
+
require_relative "../mcp_param_headers"
|
|
10
10
|
require_relative "modern_envelope"
|
|
11
11
|
|
|
12
12
|
module MCP
|
|
@@ -545,8 +545,12 @@ module MCP
|
|
|
545
545
|
def send_notification(notification:)
|
|
546
546
|
method = notification[:method] || notification["method"]
|
|
547
547
|
|
|
548
|
-
redacting_authorization_header
|
|
548
|
+
redacting_authorization_header do
|
|
549
|
+
client.post("", notification, session_headers) { |req| req.options.on_data = bounded_on_data }
|
|
550
|
+
end
|
|
549
551
|
nil
|
|
552
|
+
rescue MessageTooLargeError => e
|
|
553
|
+
raise RequestHandlerError.new(e.message, { method: method }, error_type: :internal_error)
|
|
550
554
|
rescue Faraday::Error => e
|
|
551
555
|
raise RequestHandlerError.new(
|
|
552
556
|
"Failed to send #{method} notification",
|
|
@@ -573,7 +577,11 @@ module MCP
|
|
|
573
577
|
end
|
|
574
578
|
|
|
575
579
|
begin
|
|
576
|
-
redacting_authorization_header
|
|
580
|
+
redacting_authorization_header do
|
|
581
|
+
client.delete("", nil, session_headers) { |req| req.options.on_data = bounded_on_data }
|
|
582
|
+
end
|
|
583
|
+
rescue MessageTooLargeError => e
|
|
584
|
+
raise RequestHandlerError.new(e.message, {}, error_type: :internal_error)
|
|
577
585
|
rescue Faraday::ClientError => e
|
|
578
586
|
raise unless [404, 405].include?(e.response&.dig(:status))
|
|
579
587
|
ensure
|
|
@@ -1196,12 +1204,13 @@ module MCP
|
|
|
1196
1204
|
error: { code: e.code, message: e.message },
|
|
1197
1205
|
}
|
|
1198
1206
|
rescue StandardError => e
|
|
1207
|
+
warn("MCP::Client::HTTP: error handling #{message["method"]} request: #{e.class}: #{e.message.dump}")
|
|
1199
1208
|
{
|
|
1200
1209
|
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
1201
1210
|
id: message["id"],
|
|
1202
1211
|
error: {
|
|
1203
1212
|
code: JsonRpcHandler::ErrorCode::INTERNAL_ERROR,
|
|
1204
|
-
message: "Internal error
|
|
1213
|
+
message: "Internal error",
|
|
1205
1214
|
},
|
|
1206
1215
|
}
|
|
1207
1216
|
end
|
|
@@ -1233,7 +1242,22 @@ module MCP
|
|
|
1233
1242
|
end
|
|
1234
1243
|
|
|
1235
1244
|
def send_client_response(response)
|
|
1236
|
-
redacting_authorization_header
|
|
1245
|
+
redacting_authorization_header do
|
|
1246
|
+
client.post("", response, session_headers) { |req| req.options.on_data = bounded_on_data }
|
|
1247
|
+
end
|
|
1248
|
+
end
|
|
1249
|
+
|
|
1250
|
+
# Counts discarded response bytes without parsing the body, whatever its content type.
|
|
1251
|
+
def bounded_on_data
|
|
1252
|
+
received_bytes = 0
|
|
1253
|
+
|
|
1254
|
+
proc do |chunk, _received_bytes, _env|
|
|
1255
|
+
received_bytes += chunk.bytesize
|
|
1256
|
+
|
|
1257
|
+
if received_bytes > @max_message_bytes
|
|
1258
|
+
raise MessageTooLargeError, "Server response body exceeds #{@max_message_bytes} bytes"
|
|
1259
|
+
end
|
|
1260
|
+
end
|
|
1237
1261
|
end
|
|
1238
1262
|
|
|
1239
1263
|
def parse_json_buffer(buffer, method, params)
|
|
@@ -35,6 +35,9 @@ module MCP
|
|
|
35
35
|
# the server's `token_endpoint_auth_signing_alg_values_supported` fails
|
|
36
36
|
# loudly at construction instead of as a 401 (the TypeScript and Python SDKs
|
|
37
37
|
# also take the algorithm as an explicit option).
|
|
38
|
+
# - `issuer` - String issuer identifier of the authorization server the credentials belong to.
|
|
39
|
+
# A different authorization server raises `Flow::AuthorizationError`. Omitting this is deprecated
|
|
40
|
+
# and emits a warning; without it, credentials go to whichever authorization server the MCP server names.
|
|
38
41
|
# - `scope` - String of space-separated scopes to request when the server's
|
|
39
42
|
# `WWW-Authenticate` and the Protected Resource Metadata do not specify one.
|
|
40
43
|
# - `storage` - Object responding to `tokens`, `save_tokens(tokens)`, `client_information`,
|
|
@@ -59,12 +62,14 @@ module MCP
|
|
|
59
62
|
|
|
60
63
|
attr_reader :scope, :storage
|
|
61
64
|
|
|
65
|
+
# @note Omitting `issuer:` is deprecated. Pass the authorization server's issuer URL to limit the credentials to that server.
|
|
62
66
|
def initialize(
|
|
63
67
|
client_id:,
|
|
64
68
|
client_secret: nil,
|
|
65
69
|
token_endpoint_auth_method: "client_secret_basic",
|
|
66
70
|
private_key: nil,
|
|
67
71
|
signing_algorithm: nil,
|
|
72
|
+
issuer: nil,
|
|
68
73
|
scope: nil,
|
|
69
74
|
storage: nil,
|
|
70
75
|
authorization_request_validator: nil,
|
|
@@ -82,7 +87,18 @@ module MCP
|
|
|
82
87
|
"client_credentials request is not allowed."
|
|
83
88
|
end
|
|
84
89
|
|
|
90
|
+
if issuer.nil?
|
|
91
|
+
warn(
|
|
92
|
+
"Omitting `issuer:` is deprecated. Without it, the credentials go to whichever authorization server " \
|
|
93
|
+
"the MCP server names. Pass the authorization server's issuer URL as `issuer:` so they are only sent there.",
|
|
94
|
+
uplevel: 1,
|
|
95
|
+
)
|
|
96
|
+
elsif !issuer.is_a?(String) || blank?(issuer)
|
|
97
|
+
raise InvalidCredentialsError, "issuer must be a non-empty String: the issuer identifier of the authorization server the credentials belong to."
|
|
98
|
+
end
|
|
99
|
+
|
|
85
100
|
client_information = { "client_id" => client_id, "token_endpoint_auth_method" => token_endpoint_auth_method }
|
|
101
|
+
client_information["issuer"] = issuer if issuer
|
|
86
102
|
|
|
87
103
|
if token_endpoint_auth_method == "private_key_jwt"
|
|
88
104
|
validate_private_key_jwt_arguments!(
|
|
@@ -21,6 +21,9 @@ module MCP
|
|
|
21
21
|
#
|
|
22
22
|
# Optional keyword arguments:
|
|
23
23
|
#
|
|
24
|
+
# - `issuer` - String issuer identifier of the MCP authorization server the credentials belong to.
|
|
25
|
+
# A mismatch raises `Flow::AuthorizationError` before `assertion_provider` is called. Omitting this is
|
|
26
|
+
# deprecated and emits a warning; without it, credentials go to whichever authorization server the MCP server names.
|
|
24
27
|
# - `scope` - String of space-separated scopes to request when the server's `WWW-Authenticate` and
|
|
25
28
|
# the Protected Resource Metadata do not specify one.
|
|
26
29
|
# - `storage` - Object responding to `tokens`, `save_tokens(tokens)`, `client_information`, and `save_client_information(info)`.
|
|
@@ -41,10 +44,12 @@ module MCP
|
|
|
41
44
|
|
|
42
45
|
attr_reader :scope, :storage
|
|
43
46
|
|
|
47
|
+
# @note Omitting `issuer:` is deprecated. Pass the authorization server's issuer URL to limit the credentials to that server.
|
|
44
48
|
def initialize(
|
|
45
49
|
client_id:,
|
|
46
50
|
client_secret:,
|
|
47
51
|
assertion_provider:,
|
|
52
|
+
issuer: nil,
|
|
48
53
|
scope: nil,
|
|
49
54
|
storage: nil,
|
|
50
55
|
authorization_request_validator: nil,
|
|
@@ -63,6 +68,16 @@ module MCP
|
|
|
63
68
|
raise InvalidConfigurationError, "assertion_provider must be callable as `call(audience:, resource:)` and return the ID-JAG assertion."
|
|
64
69
|
end
|
|
65
70
|
|
|
71
|
+
if issuer.nil?
|
|
72
|
+
warn(
|
|
73
|
+
"Omitting `issuer:` is deprecated. Without it, the credentials go to whichever authorization server " \
|
|
74
|
+
"the MCP server names. Pass the authorization server's issuer URL as `issuer:` so they are only sent there.",
|
|
75
|
+
uplevel: 1,
|
|
76
|
+
)
|
|
77
|
+
elsif !issuer.is_a?(String) || blank?(issuer)
|
|
78
|
+
raise InvalidConfigurationError, "issuer must be a non-empty String: the issuer identifier of the authorization server the credentials belong to."
|
|
79
|
+
end
|
|
80
|
+
|
|
66
81
|
http_client_customizer = validated_http_client_customizer(http_client_customizer)
|
|
67
82
|
|
|
68
83
|
@assertion_provider = assertion_provider
|
|
@@ -71,11 +86,13 @@ module MCP
|
|
|
71
86
|
@authorization_request_validator = authorization_request_validator
|
|
72
87
|
@token_request_params = frozen_token_request_params(token_request_params)
|
|
73
88
|
@http_client_customizer = http_client_customizer
|
|
74
|
-
|
|
89
|
+
client_information = {
|
|
75
90
|
"client_id" => client_id,
|
|
76
91
|
"client_secret" => client_secret,
|
|
77
92
|
"token_endpoint_auth_method" => "client_secret_basic",
|
|
78
|
-
|
|
93
|
+
}
|
|
94
|
+
client_information["issuer"] = issuer if issuer
|
|
95
|
+
@storage.save_client_information(client_information)
|
|
79
96
|
end
|
|
80
97
|
|
|
81
98
|
# See `Provider#authorization_flow`.
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "base64"
|
|
4
3
|
require "json"
|
|
5
4
|
require "openssl"
|
|
6
5
|
require "securerandom"
|
|
@@ -380,7 +379,10 @@ module MCP
|
|
|
380
379
|
def ensure_client_credentials_issuer!(client_info, as_metadata:)
|
|
381
380
|
stored_issuer = client_info_required_value(client_info, "issuer")
|
|
382
381
|
return if stored_issuer.nil?
|
|
383
|
-
|
|
382
|
+
|
|
383
|
+
current_issuer = as_metadata["issuer"]
|
|
384
|
+
return if stored_issuer == current_issuer
|
|
385
|
+
return if stored_issuer == "#{current_issuer}/" || "#{stored_issuer}/" == current_issuer
|
|
384
386
|
|
|
385
387
|
raise AuthorizationError,
|
|
386
388
|
"Stored client credentials are bound to a different authorization server " \
|
|
@@ -399,6 +401,7 @@ module MCP
|
|
|
399
401
|
unless client_info.is_a?(Hash) && client_info_required_value(client_info, "client_id")
|
|
400
402
|
raise AuthorizationError, "Cannot run the jwt-bearer grant: the provider has no stored `client_id`."
|
|
401
403
|
end
|
|
404
|
+
ensure_client_credentials_issuer!(client_info, as_metadata: as_metadata)
|
|
402
405
|
|
|
403
406
|
# Asked before the assertion is minted, for the same reason registration waits on the authorization-code grant:
|
|
404
407
|
# obtaining an ID-JAG sends the identity provider an audience of this authorization server, which must not happen once
|
|
@@ -1106,6 +1109,11 @@ module MCP
|
|
|
1106
1109
|
end
|
|
1107
1110
|
|
|
1108
1111
|
def ensure_refreshable_client_information!(client_info, as_metadata:)
|
|
1112
|
+
case provider_authorization_flow
|
|
1113
|
+
when :client_credentials, :jwt_bearer
|
|
1114
|
+
return ensure_client_credentials_issuer!(client_info, as_metadata: as_metadata)
|
|
1115
|
+
end
|
|
1116
|
+
|
|
1109
1117
|
stored_issuer = client_info_required_value(client_info, "issuer")
|
|
1110
1118
|
return if stored_issuer.nil?
|
|
1111
1119
|
return if stored_issuer == as_metadata["issuer"]
|
|
@@ -1644,7 +1652,7 @@ module MCP
|
|
|
1644
1652
|
def basic_auth_credentials(client_id, client_secret)
|
|
1645
1653
|
encoded_id = URI.encode_www_form_component(client_id)
|
|
1646
1654
|
encoded_secret = URI.encode_www_form_component(client_secret)
|
|
1647
|
-
|
|
1655
|
+
["#{encoded_id}:#{encoded_secret}"].pack("m0")
|
|
1648
1656
|
end
|
|
1649
1657
|
|
|
1650
1658
|
def http_get(url)
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "base64"
|
|
4
3
|
require "json"
|
|
5
4
|
require "openssl"
|
|
6
5
|
require "securerandom"
|
|
@@ -118,8 +117,10 @@ module MCP
|
|
|
118
117
|
end.join.then { |hex| [hex].pack("H*") }
|
|
119
118
|
end
|
|
120
119
|
|
|
120
|
+
# base64url without padding (RFC 7515 Section 2), spelled with `pack` because `base64` is no longer
|
|
121
|
+
# a default gem as of Ruby 3.4 and this gem does not depend on it.
|
|
121
122
|
def base64url(data)
|
|
122
|
-
|
|
123
|
+
[data].pack("m0").tr("+/", "-_").delete("=")
|
|
123
124
|
end
|
|
124
125
|
end
|
|
125
126
|
end
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "base64"
|
|
4
3
|
require "digest"
|
|
5
4
|
require "securerandom"
|
|
6
5
|
|
|
@@ -27,7 +26,9 @@ module MCP
|
|
|
27
26
|
# Generates a PKCE pair (code_verifier and S256 code_challenge).
|
|
28
27
|
def generate
|
|
29
28
|
verifier = SecureRandom.urlsafe_base64(64)
|
|
30
|
-
|
|
29
|
+
# base64url without padding (RFC 7636 Appendix A), spelled with `pack` because `base64` is no longer
|
|
30
|
+
# a default gem as of Ruby 3.4 and this gem does not depend on it.
|
|
31
|
+
challenge = [Digest::SHA256.digest(verifier)].pack("m0").tr("+/", "-_").delete("=")
|
|
31
32
|
|
|
32
33
|
{ code_verifier: verifier, code_challenge: challenge, code_challenge_method: "S256" }
|
|
33
34
|
end
|
data/lib/mcp/client/stdio.rb
CHANGED
|
@@ -34,9 +34,20 @@ module MCP
|
|
|
34
34
|
# Matches the C# SDK's `DiscoverProbeTimeout` default.
|
|
35
35
|
DEFAULT_DISCOVER_PROBE_TIMEOUT = 5
|
|
36
36
|
|
|
37
|
+
# Inherited Windows environment, including RubyGems paths.
|
|
38
|
+
WINDOWS_INHERITED_ENV_VARS = %w[
|
|
39
|
+
APPDATA HOMEDRIVE HOMEPATH LOCALAPPDATA PATH PATHEXT PROCESSOR_ARCHITECTURE
|
|
40
|
+
SYSTEMDRIVE SYSTEMROOT TEMP USERNAME USERPROFILE GEM_HOME GEM_PATH
|
|
41
|
+
].freeze
|
|
42
|
+
|
|
43
|
+
# Inherited Unix environment, including RubyGems paths.
|
|
44
|
+
UNIX_INHERITED_ENV_VARS = %w[HOME LOGNAME PATH SHELL TERM USER GEM_HOME GEM_PATH].freeze
|
|
45
|
+
|
|
46
|
+
DEFAULT_INHERITED_ENV_VARS = Gem.win_platform? ? WINDOWS_INHERITED_ENV_VARS : UNIX_INHERITED_ENV_VARS
|
|
47
|
+
|
|
37
48
|
attr_reader :command, :args, :env, :server_info
|
|
38
49
|
|
|
39
|
-
def initialize(command:, args: [], env: nil, read_timeout: nil, max_line_bytes: MAX_LINE_BYTES)
|
|
50
|
+
def initialize(command:, args: [], env: nil, inherit_env: false, read_timeout: nil, max_line_bytes: MAX_LINE_BYTES)
|
|
40
51
|
# Reject `nil` or non-positive values: `IO#gets("\n", nil)` and a negative
|
|
41
52
|
# limit read without an upper bound, which would silently disable the
|
|
42
53
|
# protection this option exists to provide.
|
|
@@ -47,6 +58,7 @@ module MCP
|
|
|
47
58
|
@command = command
|
|
48
59
|
@args = args
|
|
49
60
|
@env = env
|
|
61
|
+
@inherit_env = inherit_env
|
|
50
62
|
@read_timeout = read_timeout
|
|
51
63
|
@max_line_bytes = max_line_bytes
|
|
52
64
|
@stdin = nil
|
|
@@ -166,8 +178,8 @@ module MCP
|
|
|
166
178
|
def start
|
|
167
179
|
raise "MCP::Client::Stdio already started" if @started
|
|
168
180
|
|
|
169
|
-
spawn_env = @env || {}
|
|
170
|
-
@stdin, @stdout, @stderr, @wait_thread = Open3.popen3(spawn_env, @command, *@args)
|
|
181
|
+
spawn_env = DEFAULT_INHERITED_ENV_VARS.to_h { |key| [key, ENV[key]] }.compact.merge(@env || {})
|
|
182
|
+
@stdin, @stdout, @stderr, @wait_thread = Open3.popen3(spawn_env, @command, *@args, unsetenv_others: !@inherit_env)
|
|
171
183
|
@stdout.set_encoding("UTF-8")
|
|
172
184
|
@stdin.set_encoding("UTF-8")
|
|
173
185
|
|
data/lib/mcp/client.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "client/elicitation"
|
|
4
|
-
require_relative "client/mcp_param_headers"
|
|
5
4
|
require_relative "client/modern_envelope"
|
|
6
5
|
require_relative "client/oauth"
|
|
7
6
|
require_relative "client/stdio"
|
|
@@ -20,6 +19,11 @@ module MCP
|
|
|
20
19
|
# every response would otherwise be followed indefinitely, growing the retained pages with it.
|
|
21
20
|
MAX_PAGES = 1_000
|
|
22
21
|
|
|
22
|
+
# The custom-header codec lived under this name until the server transport came to share it;
|
|
23
|
+
# the name stays for code that referenced it.
|
|
24
|
+
# @api private
|
|
25
|
+
McpParamHeaders = MCP::McpParamHeaders
|
|
26
|
+
|
|
23
27
|
class ServerError < StandardError
|
|
24
28
|
attr_reader :code, :data
|
|
25
29
|
|
data/lib/mcp/instrumentation.rb
CHANGED
|
@@ -5,16 +5,17 @@ module MCP
|
|
|
5
5
|
def instrument_call(method, server_context: {}, exception_already_reported: nil, &block)
|
|
6
6
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
7
7
|
begin
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
instrumentation_data = {}
|
|
9
|
+
@instrumentation_data = instrumentation_data
|
|
10
|
+
instrumentation_data.merge!(method: method)
|
|
10
11
|
# `self.` is required: the `server_context:` keyword above shadows the
|
|
11
12
|
# reader, and the value we want here is the user-defined hash passed to
|
|
12
13
|
# `Server.new`, not the per-call reporter context.
|
|
13
14
|
if configuration.instrument_server_context? && respond_to?(:server_context)
|
|
14
|
-
|
|
15
|
+
instrumentation_data.merge!(server_context: self.server_context)
|
|
15
16
|
end
|
|
16
17
|
|
|
17
|
-
result = configuration.around_request.call(
|
|
18
|
+
result = configuration.around_request.call(instrumentation_data) { block.call(instrumentation_data) }
|
|
18
19
|
|
|
19
20
|
result
|
|
20
21
|
rescue => e
|
|
@@ -28,22 +29,26 @@ module MCP
|
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
unless already_reported
|
|
31
|
-
|
|
32
|
+
instrumentation_data.merge!(error: :internal_error) unless instrumentation_data.key?(:error)
|
|
32
33
|
configuration.exception_reporter.call(e, server_context)
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
raise
|
|
36
37
|
ensure
|
|
37
38
|
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
38
|
-
|
|
39
|
+
instrumentation_data.merge!(duration: end_time - start_time)
|
|
39
40
|
|
|
40
41
|
# Backward compatibility: `instrumentation_callback` is soft-deprecated
|
|
41
42
|
# in favor of `around_request`, but existing callers still expect it
|
|
42
43
|
# to fire after every request until it is removed in a future version.
|
|
43
|
-
configuration.instrumentation_callback.call(
|
|
44
|
+
configuration.instrumentation_callback.call(instrumentation_data)
|
|
44
45
|
end
|
|
45
46
|
end
|
|
46
47
|
|
|
48
|
+
# @deprecated Use the data hash passed to the {#instrument_call} block or
|
|
49
|
+
# `around_request` instead. `add_instrumentation_data` uses a shared record
|
|
50
|
+
# and is not safe for concurrent calls.
|
|
51
|
+
# @see #instrument_call
|
|
47
52
|
def add_instrumentation_data(**kwargs)
|
|
48
53
|
@instrumentation_data.merge!(kwargs)
|
|
49
54
|
end
|