model-context-protocol-rb 0.3.3 → 0.4.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/CHANGELOG.md +23 -1
- data/README.md +745 -198
- data/lib/model_context_protocol/server/configuration.rb +79 -2
- data/lib/model_context_protocol/server/content.rb +321 -0
- data/lib/model_context_protocol/server/content_helpers.rb +84 -0
- data/lib/model_context_protocol/server/pagination.rb +71 -0
- data/lib/model_context_protocol/server/prompt.rb +123 -31
- data/lib/model_context_protocol/server/registry.rb +94 -18
- data/lib/model_context_protocol/server/resource.rb +95 -25
- data/lib/model_context_protocol/server/resource_template.rb +26 -13
- data/lib/model_context_protocol/server/streamable_http_transport.rb +211 -54
- data/lib/model_context_protocol/server/tool.rb +83 -61
- data/lib/model_context_protocol/server.rb +115 -18
- data/lib/model_context_protocol/version.rb +1 -3
- data/tasks/mcp.rake +28 -2
- data/tasks/templates/dev-http.erb +244 -0
- data/tasks/templates/dev.erb +7 -1
- metadata +6 -2
@@ -8,6 +8,12 @@ module ModelContextProtocol
|
|
8
8
|
# Raised when configured with invalid version.
|
9
9
|
class InvalidServerVersionError < StandardError; end
|
10
10
|
|
11
|
+
# Raised when configured with invalid title.
|
12
|
+
class InvalidServerTitleError < StandardError; end
|
13
|
+
|
14
|
+
# Raised when configured with invalid instructions.
|
15
|
+
class InvalidServerInstructionsError < StandardError; end
|
16
|
+
|
11
17
|
# Raised when configured with invalid registry.
|
12
18
|
class InvalidRegistryError < StandardError; end
|
13
19
|
|
@@ -20,14 +26,16 @@ module ModelContextProtocol
|
|
20
26
|
# Raised when an invalid log level is provided
|
21
27
|
class InvalidLogLevelError < StandardError; end
|
22
28
|
|
29
|
+
# Raised when pagination configuration is invalid
|
30
|
+
class InvalidPaginationError < StandardError; end
|
31
|
+
|
23
32
|
# Valid MCP log levels per the specification
|
24
33
|
VALID_LOG_LEVELS = %w[debug info notice warning error critical alert emergency].freeze
|
25
34
|
|
26
|
-
attr_accessor :name, :registry, :version, :transport
|
35
|
+
attr_accessor :name, :registry, :version, :transport, :pagination, :title, :instructions
|
27
36
|
attr_reader :logger
|
28
37
|
|
29
38
|
def initialize
|
30
|
-
# Always create a logger - enabled by default
|
31
39
|
@logging_enabled = true
|
32
40
|
@default_log_level = "info"
|
33
41
|
@logger = ModelContextProtocol::Server::MCPLogger.new(
|
@@ -77,11 +85,48 @@ module ModelContextProtocol
|
|
77
85
|
end
|
78
86
|
end
|
79
87
|
|
88
|
+
def pagination_enabled?
|
89
|
+
return true if pagination.nil?
|
90
|
+
|
91
|
+
case pagination
|
92
|
+
when Hash
|
93
|
+
pagination[:enabled] != false
|
94
|
+
when false
|
95
|
+
false
|
96
|
+
else
|
97
|
+
true
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def pagination_options
|
102
|
+
case pagination
|
103
|
+
when Hash
|
104
|
+
{
|
105
|
+
enabled: pagination[:enabled] != false,
|
106
|
+
default_page_size: pagination[:default_page_size] || 100,
|
107
|
+
max_page_size: pagination[:max_page_size] || 1000,
|
108
|
+
cursor_ttl: pagination[:cursor_ttl] || 3600
|
109
|
+
}
|
110
|
+
when false
|
111
|
+
{enabled: false}
|
112
|
+
else
|
113
|
+
{
|
114
|
+
enabled: true,
|
115
|
+
default_page_size: 100,
|
116
|
+
max_page_size: 1000,
|
117
|
+
cursor_ttl: 3600
|
118
|
+
}
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
80
122
|
def validate!
|
81
123
|
raise InvalidServerNameError unless valid_name?
|
82
124
|
raise InvalidRegistryError unless valid_registry?
|
83
125
|
raise InvalidServerVersionError unless valid_version?
|
84
126
|
validate_transport!
|
127
|
+
validate_pagination!
|
128
|
+
validate_title!
|
129
|
+
validate_instructions!
|
85
130
|
|
86
131
|
validate_environment_variables!
|
87
132
|
end
|
@@ -166,5 +211,37 @@ module ModelContextProtocol
|
|
166
211
|
raise InvalidTransportError, "redis_client must be a Redis-compatible client"
|
167
212
|
end
|
168
213
|
end
|
214
|
+
|
215
|
+
def validate_pagination!
|
216
|
+
return unless pagination_enabled?
|
217
|
+
|
218
|
+
opts = pagination_options
|
219
|
+
|
220
|
+
if opts[:max_page_size] <= 0
|
221
|
+
raise InvalidPaginationError, "Invalid pagination max_page_size: must be positive"
|
222
|
+
end
|
223
|
+
|
224
|
+
if opts[:default_page_size] <= 0 || opts[:default_page_size] > opts[:max_page_size]
|
225
|
+
raise InvalidPaginationError, "Invalid pagination default_page_size: must be between 1 and #{opts[:max_page_size]}"
|
226
|
+
end
|
227
|
+
|
228
|
+
if opts[:cursor_ttl] && opts[:cursor_ttl] <= 0
|
229
|
+
raise InvalidPaginationError, "Invalid pagination cursor_ttl: must be positive or nil"
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def validate_title!
|
234
|
+
return if title.nil?
|
235
|
+
return if title.is_a?(String)
|
236
|
+
|
237
|
+
raise InvalidServerTitleError, "Server title must be a string"
|
238
|
+
end
|
239
|
+
|
240
|
+
def validate_instructions!
|
241
|
+
return if instructions.nil?
|
242
|
+
return if instructions.is_a?(String)
|
243
|
+
|
244
|
+
raise InvalidServerInstructionsError, "Server instructions must be a string"
|
245
|
+
end
|
169
246
|
end
|
170
247
|
end
|
@@ -0,0 +1,321 @@
|
|
1
|
+
require "json-schema"
|
2
|
+
|
3
|
+
module ModelContextProtocol
|
4
|
+
module Server::Content
|
5
|
+
class ContentValidationError < StandardError; end
|
6
|
+
|
7
|
+
Text = Data.define(:meta, :annotations, :text) do
|
8
|
+
def serialized
|
9
|
+
serialized_data = {
|
10
|
+
_meta: meta,
|
11
|
+
annotations:,
|
12
|
+
text:,
|
13
|
+
type: "text"
|
14
|
+
}.compact
|
15
|
+
|
16
|
+
validation_errors = JSON::Validator.fully_validate(schema, serialized_data)
|
17
|
+
if validation_errors.empty?
|
18
|
+
serialized_data
|
19
|
+
else
|
20
|
+
raise ContentValidationError, validation_errors.join(", ")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def schema
|
27
|
+
{
|
28
|
+
type: "object",
|
29
|
+
required: ["type", "text"],
|
30
|
+
properties: {
|
31
|
+
_meta: {
|
32
|
+
type: "object",
|
33
|
+
description: "Contains metadata about the content block"
|
34
|
+
},
|
35
|
+
annotations: {
|
36
|
+
type: "object",
|
37
|
+
description: "Optional metadata about the purpose and use of the content block"
|
38
|
+
},
|
39
|
+
text: {
|
40
|
+
type: "string",
|
41
|
+
description: "The text content of the content block"
|
42
|
+
},
|
43
|
+
type: {
|
44
|
+
type: "string",
|
45
|
+
description: "The type of content block",
|
46
|
+
pattern: "^text$"
|
47
|
+
}
|
48
|
+
}
|
49
|
+
}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
Image = Data.define(:meta, :annotations, :data, :mime_type) do
|
54
|
+
def serialized
|
55
|
+
serialized_data = {
|
56
|
+
_meta: meta,
|
57
|
+
annotations:,
|
58
|
+
data:,
|
59
|
+
mimeType: mime_type,
|
60
|
+
type: "image"
|
61
|
+
}.compact
|
62
|
+
|
63
|
+
validation_errors = JSON::Validator.fully_validate(schema, serialized_data)
|
64
|
+
if validation_errors.empty?
|
65
|
+
serialized_data
|
66
|
+
else
|
67
|
+
raise ContentValidationError, validation_errors.join(", ")
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def schema
|
74
|
+
{
|
75
|
+
type: "object",
|
76
|
+
required: ["data", "mimeType", "type"],
|
77
|
+
properties: {
|
78
|
+
_meta: {
|
79
|
+
type: "object",
|
80
|
+
description: "Contains metadata about the content block"
|
81
|
+
},
|
82
|
+
annotations: {
|
83
|
+
type: "object",
|
84
|
+
description: "Optional metadata about the purpose and use of the content block"
|
85
|
+
},
|
86
|
+
data: {
|
87
|
+
type: "string",
|
88
|
+
description: "The base64 encoded image data"
|
89
|
+
},
|
90
|
+
mimeType: {
|
91
|
+
type: "string",
|
92
|
+
description: "The mime type associated with the image data"
|
93
|
+
},
|
94
|
+
type: {
|
95
|
+
type: "string",
|
96
|
+
description: "The type of content block",
|
97
|
+
pattern: "^image$"
|
98
|
+
}
|
99
|
+
}
|
100
|
+
}
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
Audio = Data.define(:meta, :annotations, :data, :mime_type) do
|
105
|
+
def serialized
|
106
|
+
serialized_data = {
|
107
|
+
_meta: meta,
|
108
|
+
annotations:,
|
109
|
+
data:,
|
110
|
+
mimeType: mime_type,
|
111
|
+
type: "audio"
|
112
|
+
}.compact
|
113
|
+
|
114
|
+
validation_errors = JSON::Validator.fully_validate(schema, serialized_data)
|
115
|
+
if validation_errors.empty?
|
116
|
+
serialized_data
|
117
|
+
else
|
118
|
+
raise ContentValidationError, validation_errors.join(", ")
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
def schema
|
125
|
+
{
|
126
|
+
type: "object",
|
127
|
+
required: ["data", "mimeType", "type"],
|
128
|
+
properties: {
|
129
|
+
_meta: {
|
130
|
+
type: "object",
|
131
|
+
description: "Contains metadata about the content block"
|
132
|
+
},
|
133
|
+
annotations: {
|
134
|
+
type: "object",
|
135
|
+
description: "Optional metadata about the purpose and use of the content block"
|
136
|
+
},
|
137
|
+
data: {
|
138
|
+
type: "string",
|
139
|
+
description: "The base64 encoded image data"
|
140
|
+
},
|
141
|
+
mimeType: {
|
142
|
+
type: "string",
|
143
|
+
description: "The mime type associated with the image data"
|
144
|
+
},
|
145
|
+
type: {
|
146
|
+
type: "string",
|
147
|
+
description: "The type of content block",
|
148
|
+
pattern: "^audio$"
|
149
|
+
}
|
150
|
+
}
|
151
|
+
}
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
EmbeddedResource = Data.define(:meta, :resource) do
|
156
|
+
def serialized
|
157
|
+
serialized_data = {
|
158
|
+
_meta: meta,
|
159
|
+
resource:,
|
160
|
+
type: "resource"
|
161
|
+
}.compact
|
162
|
+
|
163
|
+
validation_errors = JSON::Validator.fully_validate(schema, serialized_data)
|
164
|
+
if validation_errors.empty?
|
165
|
+
serialized_data
|
166
|
+
else
|
167
|
+
raise ContentValidationError, validation_errors.join(", ")
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
def schema
|
174
|
+
{
|
175
|
+
type: "object",
|
176
|
+
required: ["type", "resource"],
|
177
|
+
properties: {
|
178
|
+
_meta: {
|
179
|
+
type: "object",
|
180
|
+
description: "Contains metadata about the content block"
|
181
|
+
},
|
182
|
+
resource: {
|
183
|
+
type: "object",
|
184
|
+
description: "The resource embedded in the content block"
|
185
|
+
},
|
186
|
+
type: {
|
187
|
+
type: "string",
|
188
|
+
description: "The type of content block",
|
189
|
+
pattern: "^resource$"
|
190
|
+
}
|
191
|
+
}
|
192
|
+
}
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
ResourceLink = Data.define(:meta, :annotations, :description, :mime_type, :name, :size, :title, :uri) do
|
197
|
+
def serialized
|
198
|
+
serialized_data = {
|
199
|
+
_meta: meta,
|
200
|
+
annotations:,
|
201
|
+
description:,
|
202
|
+
mimeType: mime_type,
|
203
|
+
name:,
|
204
|
+
size:,
|
205
|
+
title:,
|
206
|
+
type: "resource_link",
|
207
|
+
uri:
|
208
|
+
}.compact
|
209
|
+
|
210
|
+
validation_errors = JSON::Validator.fully_validate(schema, serialized_data)
|
211
|
+
if validation_errors.empty?
|
212
|
+
serialized_data
|
213
|
+
else
|
214
|
+
raise ContentValidationError, validation_errors.join(", ")
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
private
|
219
|
+
|
220
|
+
def schema
|
221
|
+
{
|
222
|
+
type: "object",
|
223
|
+
required: ["name", "type", "uri"],
|
224
|
+
properties: {
|
225
|
+
_meta: {
|
226
|
+
type: "object",
|
227
|
+
description: "Contains metadata about the content block"
|
228
|
+
},
|
229
|
+
annotations: {
|
230
|
+
type: "object",
|
231
|
+
description: "Optional metadata about the purpose and use of the content block"
|
232
|
+
},
|
233
|
+
description: {
|
234
|
+
type: "string",
|
235
|
+
description: "A description of what this resource represents"
|
236
|
+
},
|
237
|
+
mimeType: {
|
238
|
+
type: "string",
|
239
|
+
description: "The mime type of the resource in bytes (if known)"
|
240
|
+
},
|
241
|
+
name: {
|
242
|
+
type: "string",
|
243
|
+
description: "Name of the resource link, intended for programmatic use"
|
244
|
+
},
|
245
|
+
size: {
|
246
|
+
type: "number",
|
247
|
+
description: "The size of the raw resource content in bytes (if known)"
|
248
|
+
},
|
249
|
+
title: {
|
250
|
+
type: "string",
|
251
|
+
description: "Name of the resource link, intended for display purposes"
|
252
|
+
},
|
253
|
+
type: {
|
254
|
+
type: "string",
|
255
|
+
description: "The type of content block",
|
256
|
+
pattern: "^resource_link$"
|
257
|
+
},
|
258
|
+
uri: {
|
259
|
+
type: "string",
|
260
|
+
description: "The URI of this resource"
|
261
|
+
}
|
262
|
+
}
|
263
|
+
}
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
Annotations = Data.define(:audience, :last_modified, :priority) do
|
268
|
+
def serialized
|
269
|
+
serialized_data = {audience:, lastModified: last_modified, priority:}.compact
|
270
|
+
|
271
|
+
validation_errors = JSON::Validator.fully_validate(schema, serialized_data)
|
272
|
+
unless validation_errors.empty?
|
273
|
+
raise ContentValidationError, validation_errors.join(", ")
|
274
|
+
end
|
275
|
+
|
276
|
+
serialized_data.empty? ? nil : serialized_data
|
277
|
+
end
|
278
|
+
|
279
|
+
private
|
280
|
+
|
281
|
+
def schema
|
282
|
+
{
|
283
|
+
type: "object",
|
284
|
+
description: "Optional metadata about the purpose and use of the content block",
|
285
|
+
properties: {
|
286
|
+
audience: {
|
287
|
+
oneOf: [
|
288
|
+
{
|
289
|
+
type: "string",
|
290
|
+
enum: ["user", "assistant"]
|
291
|
+
},
|
292
|
+
{
|
293
|
+
type: "array",
|
294
|
+
items: {
|
295
|
+
type: "string",
|
296
|
+
enum: ["user", "assistant"]
|
297
|
+
},
|
298
|
+
minItems: 1,
|
299
|
+
maxItems: 2,
|
300
|
+
uniqueItems: true
|
301
|
+
}
|
302
|
+
],
|
303
|
+
description: "The intended audience for the content block"
|
304
|
+
},
|
305
|
+
lastModified: {
|
306
|
+
type: "string",
|
307
|
+
pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d{3})?Z$",
|
308
|
+
description: "The date the content was last modified (in ISO 8601 format)"
|
309
|
+
},
|
310
|
+
priority: {
|
311
|
+
type: "number",
|
312
|
+
description: "The weight of importance the audience should place on the contents of the content block",
|
313
|
+
minimum: 0.0,
|
314
|
+
maximum: 1.0
|
315
|
+
}
|
316
|
+
}
|
317
|
+
}
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module ModelContextProtocol
|
2
|
+
module Server::ContentHelpers
|
3
|
+
def text_content(text:, meta: nil, annotations: {})
|
4
|
+
serialized_annotations = ModelContextProtocol::Server::Content::Annotations[
|
5
|
+
audience: annotations[:audience],
|
6
|
+
last_modified: annotations[:last_modified],
|
7
|
+
priority: annotations[:priority]
|
8
|
+
].serialized
|
9
|
+
|
10
|
+
ModelContextProtocol::Server::Content::Text[
|
11
|
+
meta:,
|
12
|
+
annotations: serialized_annotations,
|
13
|
+
text:
|
14
|
+
]
|
15
|
+
end
|
16
|
+
|
17
|
+
def image_content(data:, mime_type:, meta: nil, annotations: {})
|
18
|
+
serialized_annotations = ModelContextProtocol::Server::Content::Annotations[
|
19
|
+
audience: annotations[:audience],
|
20
|
+
last_modified: annotations[:last_modified],
|
21
|
+
priority: annotations[:priority]
|
22
|
+
].serialized
|
23
|
+
|
24
|
+
ModelContextProtocol::Server::Content::Image[
|
25
|
+
meta:,
|
26
|
+
annotations: serialized_annotations,
|
27
|
+
data:,
|
28
|
+
mime_type:
|
29
|
+
]
|
30
|
+
end
|
31
|
+
|
32
|
+
def audio_content(data:, mime_type:, meta: nil, annotations: {})
|
33
|
+
serialized_annotations = ModelContextProtocol::Server::Content::Annotations[
|
34
|
+
audience: annotations[:audience],
|
35
|
+
last_modified: annotations[:last_modified],
|
36
|
+
priority: annotations[:priority]
|
37
|
+
].serialized
|
38
|
+
|
39
|
+
ModelContextProtocol::Server::Content::Audio[
|
40
|
+
meta:,
|
41
|
+
annotations: serialized_annotations,
|
42
|
+
data:,
|
43
|
+
mime_type:
|
44
|
+
]
|
45
|
+
end
|
46
|
+
|
47
|
+
def embedded_resource_content(resource:)
|
48
|
+
extracted_resource = resource.serialized[:contents].first
|
49
|
+
annotations = extracted_resource.key?(:annotations) ? extracted_resource.delete(:annotations) : {}
|
50
|
+
|
51
|
+
serialized_annotations = ModelContextProtocol::Server::Content::Annotations[
|
52
|
+
audience: annotations[:audience],
|
53
|
+
last_modified: annotations[:lastModified],
|
54
|
+
priority: annotations[:priority]
|
55
|
+
].serialized
|
56
|
+
|
57
|
+
extracted_resource[:annotations] = serialized_annotations if serialized_annotations
|
58
|
+
|
59
|
+
ModelContextProtocol::Server::Content::EmbeddedResource[
|
60
|
+
meta: nil,
|
61
|
+
resource: extracted_resource
|
62
|
+
]
|
63
|
+
end
|
64
|
+
|
65
|
+
def resource_link(name:, uri:, meta: nil, annotations: {}, description: nil, mime_type: nil, size: nil, title: nil)
|
66
|
+
serialized_annotations = ModelContextProtocol::Server::Content::Annotations[
|
67
|
+
audience: annotations[:audience],
|
68
|
+
last_modified: annotations[:last_modified],
|
69
|
+
priority: annotations[:priority]
|
70
|
+
].serialized
|
71
|
+
|
72
|
+
ModelContextProtocol::Server::Content::ResourceLink[
|
73
|
+
meta:,
|
74
|
+
annotations: serialized_annotations,
|
75
|
+
description:,
|
76
|
+
mime_type:,
|
77
|
+
name:,
|
78
|
+
size:,
|
79
|
+
title:,
|
80
|
+
uri:
|
81
|
+
].serialized
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "json"
|
2
|
+
require "base64"
|
3
|
+
|
4
|
+
module ModelContextProtocol
|
5
|
+
class Server::Pagination
|
6
|
+
# Raised when an invalid cursor is provided
|
7
|
+
class InvalidCursorError < StandardError; end
|
8
|
+
|
9
|
+
DEFAULT_PAGE_SIZE = 100
|
10
|
+
MAX_PAGE_SIZE = 1000
|
11
|
+
|
12
|
+
PaginatedResponse = Data.define(:items, :next_cursor) do
|
13
|
+
def serialized(key)
|
14
|
+
result = {key => items}
|
15
|
+
result[:nextCursor] = next_cursor if next_cursor
|
16
|
+
result
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
def paginate(items, cursor: nil, page_size: DEFAULT_PAGE_SIZE, cursor_ttl: nil)
|
22
|
+
page_size = [page_size, MAX_PAGE_SIZE].min
|
23
|
+
offset = cursor ? decode_cursor(cursor) : 0
|
24
|
+
page_items = items[offset, page_size] || []
|
25
|
+
next_offset = offset + page_items.length
|
26
|
+
next_cursor = if next_offset < items.length
|
27
|
+
encode_cursor(next_offset, items.length, ttl: cursor_ttl)
|
28
|
+
end
|
29
|
+
|
30
|
+
PaginatedResponse[items: page_items, next_cursor: next_cursor]
|
31
|
+
end
|
32
|
+
|
33
|
+
def encode_cursor(offset, total, ttl: nil)
|
34
|
+
data = {
|
35
|
+
offset: offset,
|
36
|
+
total: total,
|
37
|
+
timestamp: Time.now.to_i
|
38
|
+
}
|
39
|
+
data[:expires_at] = Time.now.to_i + ttl if ttl
|
40
|
+
|
41
|
+
Base64.urlsafe_encode64(JSON.generate(data), padding: false)
|
42
|
+
end
|
43
|
+
|
44
|
+
def decode_cursor(cursor, validate_ttl: true)
|
45
|
+
data = JSON.parse(Base64.urlsafe_decode64(cursor))
|
46
|
+
|
47
|
+
if validate_ttl && data["expires_at"] && Time.now.to_i > data["expires_at"]
|
48
|
+
raise InvalidCursorError, "Cursor has expired"
|
49
|
+
end
|
50
|
+
|
51
|
+
data["offset"]
|
52
|
+
rescue JSON::ParserError, ArgumentError => e
|
53
|
+
raise InvalidCursorError, "Invalid cursor format: #{e.message}"
|
54
|
+
end
|
55
|
+
|
56
|
+
def pagination_requested?(params)
|
57
|
+
params.key?("cursor") || params.key?("pageSize")
|
58
|
+
end
|
59
|
+
|
60
|
+
def extract_pagination_params(params, default_page_size: DEFAULT_PAGE_SIZE, max_page_size: MAX_PAGE_SIZE)
|
61
|
+
page_size = if params["pageSize"]
|
62
|
+
[params["pageSize"].to_i, max_page_size].min
|
63
|
+
else
|
64
|
+
default_page_size
|
65
|
+
end
|
66
|
+
|
67
|
+
{cursor: params["cursor"], page_size:}
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|