parse-stack-next 5.6.0 → 5.7.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 +409 -0
- data/README.md +378 -6
- data/docs/caching.md +748 -0
- data/examples/basic_client.rb +3 -3
- data/examples/basic_server.rb +3 -3
- data/examples/live_query_listener.rb +12 -12
- data/examples/rag_chatbot.rb +14 -14
- data/examples/transaction_example.rb +44 -45
- data/examples/webhook_server.rb +3 -3
- data/lib/parse/access.rb +493 -0
- data/lib/parse/acl_scope.rb +125 -52
- data/lib/parse/agent/approval_gate.rb +0 -0
- data/lib/parse/agent/cancellation_token.rb +3 -3
- data/lib/parse/agent/constraint_translator.rb +4 -4
- data/lib/parse/agent/describe.rb +34 -34
- data/lib/parse/agent/errors.rb +9 -9
- data/lib/parse/agent/mcp_client.rb +61 -59
- data/lib/parse/agent/mcp_dispatcher.rb +89 -101
- data/lib/parse/agent/mcp_rack_app.rb +92 -93
- data/lib/parse/agent/mcp_server.rb +1 -1
- data/lib/parse/agent/mcp_subscriptions.rb +22 -22
- data/lib/parse/agent/metadata_audit.rb +1 -2
- data/lib/parse/agent/metadata_dsl.rb +7 -7
- data/lib/parse/agent/metadata_registry.rb +12 -12
- data/lib/parse/agent/prompt_hardening.rb +4 -4
- data/lib/parse/agent/prompts.rb +10 -10
- data/lib/parse/agent/result_formatter.rb +4 -4
- data/lib/parse/agent/tools.rb +493 -400
- data/lib/parse/agent.rb +157 -116
- data/lib/parse/api/hooks.rb +1 -1
- data/lib/parse/api/server.rb +2 -2
- data/lib/parse/api/users.rb +2 -2
- data/lib/parse/atlas_search/index_manager.rb +1 -1
- data/lib/parse/atlas_search/session.rb +40 -218
- data/lib/parse/atlas_search.rb +181 -60
- data/lib/parse/authorization.rb +466 -0
- data/lib/parse/cache/invalidation.rb +167 -0
- data/lib/parse/cache/keyspace.rb +306 -0
- data/lib/parse/cache/moneta_surface.rb +126 -0
- data/lib/parse/cache/pool.rb +48 -5
- data/lib/parse/cache/redis.rb +425 -10
- data/lib/parse/cache/scoped_view.rb +443 -0
- data/lib/parse/cache/sub_cache.rb +264 -0
- data/lib/parse/cache/upstream_roles.rb +230 -0
- data/lib/parse/client/authentication.rb +1 -1
- data/lib/parse/client/body_builder.rb +1 -2
- data/lib/parse/client/caching.rb +84 -5
- data/lib/parse/client.rb +327 -189
- data/lib/parse/clp_scope.rb +225 -28
- data/lib/parse/console.rb +3 -3
- data/lib/parse/embeddings/batch_embedder.rb +3 -3
- data/lib/parse/embeddings/cache.rb +17 -17
- data/lib/parse/embeddings/cohere.rb +21 -22
- data/lib/parse/embeddings/image_fetch.rb +22 -21
- data/lib/parse/embeddings/jina.rb +21 -21
- data/lib/parse/embeddings/local_http.rb +6 -7
- data/lib/parse/embeddings/media_file.rb +5 -4
- data/lib/parse/embeddings/openai.rb +5 -5
- data/lib/parse/embeddings/provider.rb +1 -1
- data/lib/parse/embeddings/qwen.rb +10 -10
- data/lib/parse/embeddings/spend_cap.rb +2 -3
- data/lib/parse/embeddings/streaming_body.rb +11 -11
- data/lib/parse/embeddings/video_source.rb +7 -6
- data/lib/parse/embeddings/voyage.rb +59 -61
- data/lib/parse/embeddings.rb +28 -27
- data/lib/parse/graphql/type_generator.rb +1 -1
- data/lib/parse/graphql.rb +5 -5
- data/lib/parse/live_query/client.rb +1 -1
- data/lib/parse/live_query.rb +1 -1
- data/lib/parse/lock.rb +15 -18
- data/lib/parse/lock_backend.rb +1 -1
- data/lib/parse/lookup_rewriter.rb +6 -6
- data/lib/parse/model/classes/role.rb +227 -28
- data/lib/parse/model/classes/user.rb +60 -1
- data/lib/parse/model/clp.rb +4 -4
- data/lib/parse/model/core/actions.rb +97 -15
- data/lib/parse/model/core/create_lock.rb +0 -2
- data/lib/parse/model/core/describe.rb +57 -57
- data/lib/parse/model/core/embed_managed.rb +21 -23
- data/lib/parse/model/core/fetching.rb +2 -0
- data/lib/parse/model/core/indexing.rb +14 -14
- data/lib/parse/model/core/parse_reference.rb +1 -1
- data/lib/parse/model/core/properties.rb +2 -2
- data/lib/parse/model/core/querying.rb +1 -1
- data/lib/parse/model/core/schema.rb +2 -2
- data/lib/parse/model/core/search_indexing.rb +2 -2
- data/lib/parse/model/core/vector_searchable.rb +4 -5
- data/lib/parse/model/file.rb +21 -15
- data/lib/parse/model/geojson.rb +2 -2
- data/lib/parse/model/geopoint.rb +1 -0
- data/lib/parse/model/object.rb +132 -66
- data/lib/parse/model/pointer.rb +2 -0
- data/lib/parse/model/polygon.rb +3 -6
- data/lib/parse/model/push.rb +2 -2
- data/lib/parse/model/vector.rb +3 -1
- data/lib/parse/mongodb.rb +512 -282
- data/lib/parse/pipeline_security.rb +3 -0
- data/lib/parse/query/constraints.rb +46 -46
- data/lib/parse/query.rb +146 -59
- data/lib/parse/retrieval/agent_tool.rb +34 -20
- data/lib/parse/retrieval/chunk.rb +1 -0
- data/lib/parse/retrieval/reranker/cohere.rb +11 -11
- data/lib/parse/retrieval/reranker.rb +3 -4
- data/lib/parse/retrieval/retriever.rb +4 -6
- data/lib/parse/schema/index_migrator.rb +26 -26
- data/lib/parse/schema/search_index_migrator.rb +19 -19
- data/lib/parse/stack/tasks.rb +6 -6
- data/lib/parse/stack/version.rb +1 -1
- data/lib/parse/stack.rb +10 -10
- data/lib/parse/vector_search/hybrid.rb +66 -41
- data/lib/parse/vector_search.rb +14 -11
- data/lib/parse/webhooks/payload.rb +1 -0
- data/lib/parse/webhooks/registration.rb +4 -4
- data/lib/parse/webhooks/trigger_audit.rb +50 -49
- data/lib/parse/webhooks.rb +22 -4
- data/parse-stack-next.gemspec +6 -6
- metadata +10 -1
|
@@ -68,9 +68,9 @@ module Parse
|
|
|
68
68
|
# they still see the latest registry state on the next
|
|
69
69
|
# `tools/list` / `prompts/list` poll.
|
|
70
70
|
CAPABILITIES = {
|
|
71
|
-
"tools"
|
|
72
|
-
"resources" => { "subscribe"
|
|
73
|
-
"prompts"
|
|
71
|
+
"tools" => { "listChanged" => true },
|
|
72
|
+
"resources" => { "subscribe" => false, "listChanged" => false },
|
|
73
|
+
"prompts" => { "listChanged" => true },
|
|
74
74
|
}.freeze
|
|
75
75
|
|
|
76
76
|
# Parse class-name identifier regex — used to validate resource URIs.
|
|
@@ -150,9 +150,9 @@ module Parse
|
|
|
150
150
|
# dispatches on the same shared agent would race: the second
|
|
151
151
|
# request's ensure would null the first request's still-needed
|
|
152
152
|
# token.
|
|
153
|
-
prev_progress_callback
|
|
154
|
-
prev_cancellation_token
|
|
155
|
-
prev_approval_gate
|
|
153
|
+
prev_progress_callback = agent.progress_callback if agent.respond_to?(:progress_callback)
|
|
154
|
+
prev_cancellation_token = agent.cancellation_token if agent.respond_to?(:cancellation_token)
|
|
155
|
+
prev_approval_gate = agent.approval_gate if agent.respond_to?(:approval_gate)
|
|
156
156
|
|
|
157
157
|
# Install the progress callback and cancellation token on the
|
|
158
158
|
# agent for the duration of the dispatch. Cleared in the ensure
|
|
@@ -164,12 +164,12 @@ module Parse
|
|
|
164
164
|
# two threads concurrently — the snapshot-restore pattern here
|
|
165
165
|
# only handles sequential interleave. MCPRackApp's `agent_factory:`
|
|
166
166
|
# is documented to return a fresh agent per request.
|
|
167
|
-
agent.progress_callback
|
|
168
|
-
agent.cancellation_token
|
|
167
|
+
agent.progress_callback = progress_callback if progress_callback && agent.respond_to?(:progress_callback=)
|
|
168
|
+
agent.cancellation_token = cancellation_token if cancellation_token && agent.respond_to?(:cancellation_token=)
|
|
169
169
|
# Install the per-session approval gate (MCP elicitation) so
|
|
170
170
|
# agent.execute can request human approval for destructive tools.
|
|
171
171
|
# Restored in the ensure block like the other per-request state.
|
|
172
|
-
agent.approval_gate
|
|
172
|
+
agent.approval_gate = approval_gate if approval_gate && agent.respond_to?(:approval_gate=)
|
|
173
173
|
|
|
174
174
|
# Guard: body must be a Hash with a "method" key.
|
|
175
175
|
unless body.is_a?(Hash) && body.key?("method")
|
|
@@ -179,7 +179,7 @@ module Parse
|
|
|
179
179
|
|
|
180
180
|
method = body["method"]
|
|
181
181
|
params = body["params"] || {}
|
|
182
|
-
id
|
|
182
|
+
id = body["id"]
|
|
183
183
|
|
|
184
184
|
# JSON-RPC notifications MUST NOT carry an `id` field. Reject
|
|
185
185
|
# `notifications/*` methods that include one — silently treating
|
|
@@ -191,7 +191,6 @@ module Parse
|
|
|
191
191
|
|
|
192
192
|
result_hash = dispatch(method, params, agent, id, logger, subscription_manager)
|
|
193
193
|
{ status: result_hash[:status], body: result_hash[:body] }
|
|
194
|
-
|
|
195
194
|
rescue Parse::Agent::Unauthorized => e
|
|
196
195
|
{ status: 401, body: jsonrpc_error(body.is_a?(Hash) ? body["id"] : nil, -32001, "Unauthorized") }
|
|
197
196
|
rescue StandardError => e
|
|
@@ -206,7 +205,7 @@ module Parse
|
|
|
206
205
|
# this request's token visible to a sibling dispatch on a
|
|
207
206
|
# shared agent.
|
|
208
207
|
if agent.respond_to?(:progress_callback=)
|
|
209
|
-
agent.progress_callback
|
|
208
|
+
agent.progress_callback = prev_progress_callback
|
|
210
209
|
end
|
|
211
210
|
if agent.respond_to?(:cancellation_token=)
|
|
212
211
|
agent.cancellation_token = prev_cancellation_token
|
|
@@ -296,7 +295,6 @@ module Parse
|
|
|
296
295
|
else
|
|
297
296
|
{ status: 200, body: jsonrpc_envelope(id, result: result) }
|
|
298
297
|
end
|
|
299
|
-
|
|
300
298
|
rescue Parse::Agent::Unauthorized => e
|
|
301
299
|
{ status: 401, body: jsonrpc_error(id, -32001, "Unauthorized") }
|
|
302
300
|
rescue Parse::Agent::AccessDenied
|
|
@@ -340,17 +338,16 @@ module Parse
|
|
|
340
338
|
# @return [Hash] protocol version, capabilities, and server info.
|
|
341
339
|
def self.handle_initialize(params, subscription_manager = nil)
|
|
342
340
|
requested = params.is_a?(Hash) ? params["protocolVersion"] : nil
|
|
343
|
-
negotiated =
|
|
344
|
-
if requested.is_a?(String) && SUPPORTED_PROTOCOL_VERSIONS.include?(requested)
|
|
341
|
+
negotiated = if requested.is_a?(String) && SUPPORTED_PROTOCOL_VERSIONS.include?(requested)
|
|
345
342
|
requested
|
|
346
343
|
else
|
|
347
344
|
PROTOCOL_VERSION
|
|
348
345
|
end
|
|
349
346
|
{
|
|
350
347
|
"protocolVersion" => negotiated,
|
|
351
|
-
"capabilities"
|
|
352
|
-
"serverInfo"
|
|
353
|
-
"name"
|
|
348
|
+
"capabilities" => capabilities_for(subscription_manager),
|
|
349
|
+
"serverInfo" => {
|
|
350
|
+
"name" => "parse-stack-mcp",
|
|
354
351
|
"version" => Parse::Stack::VERSION,
|
|
355
352
|
},
|
|
356
353
|
}
|
|
@@ -415,7 +412,7 @@ module Parse
|
|
|
415
412
|
end
|
|
416
413
|
|
|
417
414
|
sym_args = arguments.transform_keys(&:to_sym)
|
|
418
|
-
result
|
|
415
|
+
result = agent.execute(tool_name.to_sym, **sym_args)
|
|
419
416
|
|
|
420
417
|
# Cancellation short-circuit. Tools cooperate by returning a
|
|
421
418
|
# `success: false, cancelled: true` envelope when `agent.cancelled?`
|
|
@@ -426,12 +423,12 @@ module Parse
|
|
|
426
423
|
# still honor the client's intent by not surfacing the result).
|
|
427
424
|
if result[:cancelled] || (agent.respond_to?(:cancelled?) && agent.cancelled?)
|
|
428
425
|
return {
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
426
|
+
"content" => [
|
|
427
|
+
{ "type" => "text", "text" => (result[:error] || "Cancelled by client").to_s },
|
|
428
|
+
],
|
|
429
|
+
"isError" => true,
|
|
430
|
+
"cancelled" => true,
|
|
431
|
+
}
|
|
435
432
|
end
|
|
436
433
|
|
|
437
434
|
if result[:success]
|
|
@@ -443,8 +440,7 @@ module Parse
|
|
|
443
440
|
# with a _truncated block. Models handle partial success much
|
|
444
441
|
# better than full refusal — they continue the task instead of
|
|
445
442
|
# restarting. Other tools fall through to the structural refusal.
|
|
446
|
-
recovered_text =
|
|
447
|
-
if %w[query_class get_objects aggregate].include?(tool_name)
|
|
443
|
+
recovered_text = if %w[query_class get_objects aggregate].include?(tool_name)
|
|
448
444
|
attempt_truncate_response(result[:data], MAX_TOOL_RESPONSE_BYTES, tool_name)
|
|
449
445
|
end
|
|
450
446
|
|
|
@@ -502,9 +498,9 @@ module Parse
|
|
|
502
498
|
"isError" => true,
|
|
503
499
|
}
|
|
504
500
|
meta = {}
|
|
505
|
-
meta["parse.error_code"]
|
|
506
|
-
meta["parse.retry_after"] = result[:retry_after]
|
|
507
|
-
meta["parse.details"]
|
|
501
|
+
meta["parse.error_code"] = result[:error_code].to_s if result[:error_code]
|
|
502
|
+
meta["parse.retry_after"] = result[:retry_after] if result[:retry_after]
|
|
503
|
+
meta["parse.details"] = result[:details] if result[:details].is_a?(Hash) && result[:details].any?
|
|
508
504
|
envelope["_meta"] = meta unless meta.empty?
|
|
509
505
|
envelope
|
|
510
506
|
end
|
|
@@ -539,14 +535,7 @@ module Parse
|
|
|
539
535
|
# narrowing guidance carry the message.
|
|
540
536
|
return nil if data[:output].is_a?(String) && data[:headers].is_a?(Array)
|
|
541
537
|
|
|
542
|
-
rows =
|
|
543
|
-
if data[:results].is_a?(Array) then data[:results]
|
|
544
|
-
elsif data["results"].is_a?(Array) then data["results"]
|
|
545
|
-
elsif data[:objects].is_a?(Hash) then data[:objects].values
|
|
546
|
-
elsif data["objects"].is_a?(Hash) then data["objects"].values
|
|
547
|
-
elsif data[:object].is_a?(Hash) then [data[:object]]
|
|
548
|
-
elsif data["object"].is_a?(Hash) then [data["object"]]
|
|
549
|
-
end
|
|
538
|
+
rows = if data[:results].is_a?(Array) then data[:results] elsif data["results"].is_a?(Array) then data["results"] elsif data[:objects].is_a?(Hash) then data[:objects].values elsif data["objects"].is_a?(Hash) then data["objects"].values elsif data[:object].is_a?(Hash) then [data[:object]] elsif data["object"].is_a?(Hash) then [data["object"]] end
|
|
550
539
|
|
|
551
540
|
return nil unless rows.is_a?(Array) && rows.any?
|
|
552
541
|
|
|
@@ -566,10 +555,10 @@ module Parse
|
|
|
566
555
|
|
|
567
556
|
return nil if bytes_per_field.empty?
|
|
568
557
|
|
|
569
|
-
sorted
|
|
570
|
-
top
|
|
571
|
-
n
|
|
572
|
-
largest
|
|
558
|
+
sorted = bytes_per_field.sort_by { |_, b| -b }
|
|
559
|
+
top = sorted.first(3)
|
|
560
|
+
n = sample.size.to_f
|
|
561
|
+
largest = sorted.first[0]
|
|
573
562
|
|
|
574
563
|
# Produce a POSITIVE keys: list rather than asking the LLM to
|
|
575
564
|
# subtract. `keys:` is inclusive — models that see "excluding 'X'"
|
|
@@ -581,7 +570,7 @@ module Parse
|
|
|
581
570
|
|
|
582
571
|
formatted = top.map { |k, b| "#{k} (~#{humanize_bytes(b / n)}/record)" }.join(", ")
|
|
583
572
|
"Largest fields by bytes: #{formatted}. " \
|
|
584
|
-
|
|
573
|
+
"Try keys: #{keep_fields.join(",").inspect} (drops the heaviest field)."
|
|
585
574
|
end
|
|
586
575
|
private_class_method :diagnose_oversize
|
|
587
576
|
|
|
@@ -661,7 +650,7 @@ module Parse
|
|
|
661
650
|
rows = data[:results] || data["results"]
|
|
662
651
|
return nil unless rows.is_a?(Array) && rows.any?
|
|
663
652
|
|
|
664
|
-
sample
|
|
653
|
+
sample = rows.first(5).select { |r| r.is_a?(Hash) }
|
|
665
654
|
return nil if sample.empty?
|
|
666
655
|
|
|
667
656
|
heaviest = find_heaviest_field(sample)
|
|
@@ -676,7 +665,7 @@ module Parse
|
|
|
676
665
|
# Read the caller's effective skip so next_skip can resume rather
|
|
677
666
|
# than reset pagination. Only relevant for query_class; aggregate
|
|
678
667
|
# pipelines are deterministic and not paginatable.
|
|
679
|
-
pagination
|
|
668
|
+
pagination = data[:pagination] || data["pagination"] || {}
|
|
680
669
|
original_skip = (pagination[:skip] || pagination["skip"] || 0).to_i
|
|
681
670
|
|
|
682
671
|
# The recovered envelope must strip stale cardinality keys so the
|
|
@@ -708,8 +697,7 @@ module Parse
|
|
|
708
697
|
candidate.delete(:next_call)
|
|
709
698
|
candidate.delete("next_call")
|
|
710
699
|
|
|
711
|
-
initial_hint =
|
|
712
|
-
if tool_name == "aggregate"
|
|
700
|
+
initial_hint = if tool_name == "aggregate"
|
|
713
701
|
"Field '#{heaviest}' was dropped from all rows to fit the #{max_bytes}-byte response cap. " \
|
|
714
702
|
"Narrow the pipeline with a $match or $project stage to reduce result size, " \
|
|
715
703
|
"or call get_object(class_name: <class>, object_id: <id>) for the dropped field."
|
|
@@ -719,15 +707,15 @@ module Parse
|
|
|
719
707
|
end
|
|
720
708
|
|
|
721
709
|
annotation = {
|
|
722
|
-
reason:
|
|
710
|
+
reason: "response_exceeded_max_bytes",
|
|
723
711
|
dropped_fields: [heaviest],
|
|
724
|
-
kept_count:
|
|
712
|
+
kept_count: trimmed_rows.size,
|
|
725
713
|
original_count: rows.size,
|
|
726
|
-
hint:
|
|
714
|
+
hint: initial_hint,
|
|
727
715
|
}
|
|
728
716
|
|
|
729
717
|
# First try: heaviest field dropped, all rows kept.
|
|
730
|
-
candidate[:results]
|
|
718
|
+
candidate[:results] = trimmed_rows
|
|
731
719
|
candidate[:_truncated] = annotation
|
|
732
720
|
text = JSON.pretty_generate(candidate)
|
|
733
721
|
return text if text.bytesize <= max_bytes
|
|
@@ -736,10 +724,10 @@ module Parse
|
|
|
736
724
|
# from the trimmed sample, then verify and back off by one if the
|
|
737
725
|
# estimator overshoots (JSON overhead).
|
|
738
726
|
sample_after = trimmed_rows.first([5, trimmed_rows.size].min)
|
|
739
|
-
sample_text
|
|
740
|
-
per_row
|
|
727
|
+
sample_text = JSON.pretty_generate(sample_after)
|
|
728
|
+
per_row = sample_text.bytesize / sample_after.size.to_f
|
|
741
729
|
|
|
742
|
-
envelope_data
|
|
730
|
+
envelope_data = candidate.merge(results: [])
|
|
743
731
|
envelope_bytes = JSON.pretty_generate(envelope_data).bytesize
|
|
744
732
|
budget = max_bytes - envelope_bytes - 256 # safety margin
|
|
745
733
|
return nil if budget <= 0 || per_row <= 0
|
|
@@ -749,7 +737,7 @@ module Parse
|
|
|
749
737
|
return nil if fit_count < 1
|
|
750
738
|
|
|
751
739
|
loop do
|
|
752
|
-
candidate[:results]
|
|
740
|
+
candidate[:results] = trimmed_rows.first(fit_count)
|
|
753
741
|
candidate[:_truncated][:kept_count] = fit_count
|
|
754
742
|
|
|
755
743
|
if tool_name == "query_class"
|
|
@@ -793,7 +781,7 @@ module Parse
|
|
|
793
781
|
objects = data[:objects] || data["objects"]
|
|
794
782
|
return nil unless objects.is_a?(Hash) && objects.any?
|
|
795
783
|
|
|
796
|
-
sample
|
|
784
|
+
sample = objects.values.first(5).select { |r| r.is_a?(Hash) }
|
|
797
785
|
return nil if sample.empty?
|
|
798
786
|
|
|
799
787
|
heaviest = find_heaviest_field(sample)
|
|
@@ -810,27 +798,27 @@ module Parse
|
|
|
810
798
|
candidate = data.dup
|
|
811
799
|
|
|
812
800
|
annotation = {
|
|
813
|
-
reason:
|
|
814
|
-
dropped_fields:
|
|
815
|
-
kept_count:
|
|
816
|
-
original_count:
|
|
801
|
+
reason: "response_exceeded_max_bytes",
|
|
802
|
+
dropped_fields: [heaviest],
|
|
803
|
+
kept_count: trimmed_objects.size,
|
|
804
|
+
original_count: objects.size,
|
|
817
805
|
dropped_for_size: [],
|
|
818
|
-
hint:
|
|
819
|
-
|
|
806
|
+
hint: "Field '#{heaviest}' was dropped from all records to fit the #{max_bytes}-byte response cap. " \
|
|
807
|
+
"To retrieve it for a specific record, call get_object(class_name: <class>, object_id: <id>).",
|
|
820
808
|
}
|
|
821
809
|
|
|
822
810
|
# First try: heaviest field dropped, all records kept.
|
|
823
|
-
candidate[:objects]
|
|
811
|
+
candidate[:objects] = trimmed_objects
|
|
824
812
|
candidate[:_truncated] = annotation
|
|
825
813
|
text = JSON.pretty_generate(candidate)
|
|
826
814
|
return text if text.bytesize <= max_bytes
|
|
827
815
|
|
|
828
816
|
# Still over budget — drop trailing records by insertion order.
|
|
829
817
|
sample_after = trimmed_objects.values.first([5, trimmed_objects.size].min)
|
|
830
|
-
sample_text
|
|
831
|
-
per_rec
|
|
818
|
+
sample_text = JSON.pretty_generate(sample_after)
|
|
819
|
+
per_rec = sample_text.bytesize / sample_after.size.to_f
|
|
832
820
|
|
|
833
|
-
envelope_data
|
|
821
|
+
envelope_data = candidate.merge(objects: {})
|
|
834
822
|
envelope_bytes = JSON.pretty_generate(envelope_data).bytesize
|
|
835
823
|
budget = max_bytes - envelope_bytes - 256 # safety margin
|
|
836
824
|
return nil if budget <= 0 || per_rec <= 0
|
|
@@ -840,11 +828,11 @@ module Parse
|
|
|
840
828
|
return nil if fit_count < 1
|
|
841
829
|
|
|
842
830
|
loop do
|
|
843
|
-
kept_keys
|
|
831
|
+
kept_keys = trimmed_objects.keys.first(fit_count)
|
|
844
832
|
dropped_keys = trimmed_objects.keys - kept_keys
|
|
845
833
|
|
|
846
|
-
candidate[:objects]
|
|
847
|
-
candidate[:_truncated][:kept_count]
|
|
834
|
+
candidate[:objects] = trimmed_objects.slice(*kept_keys)
|
|
835
|
+
candidate[:_truncated][:kept_count] = fit_count
|
|
848
836
|
candidate[:_truncated][:dropped_for_size] = dropped_keys
|
|
849
837
|
candidate[:_truncated][:hint] =
|
|
850
838
|
"Field '#{heaviest}' was dropped and only #{fit_count} of #{objects.size} records fit " \
|
|
@@ -863,8 +851,8 @@ module Parse
|
|
|
863
851
|
# @api private
|
|
864
852
|
def self.humanize_bytes(n)
|
|
865
853
|
n = n.to_f
|
|
866
|
-
return "#{n.round} B"
|
|
867
|
-
return "#{(n / 1024.0).round(1)} KB"
|
|
854
|
+
return "#{n.round} B" if n < 1024
|
|
855
|
+
return "#{(n / 1024.0).round(1)} KB" if n < 1_048_576
|
|
868
856
|
"#{(n / 1_048_576.0).round(1)} MB"
|
|
869
857
|
end
|
|
870
858
|
private_class_method :humanize_bytes
|
|
@@ -887,30 +875,30 @@ module Parse
|
|
|
887
875
|
# every MCP client. We now concatenate `custom` and `built_in` and
|
|
888
876
|
# fall back to the legacy `classes` key in case a custom agent
|
|
889
877
|
# subclass returns the older shape.
|
|
890
|
-
data
|
|
891
|
-
classes
|
|
892
|
-
classes
|
|
878
|
+
data = result[:data] || {}
|
|
879
|
+
classes = (data[:custom] || []) + (data[:built_in] || [])
|
|
880
|
+
classes = data[:classes] || [] if classes.empty? && data[:classes]
|
|
893
881
|
resources = classes.flat_map do |cls|
|
|
894
|
-
name
|
|
882
|
+
name = cls[:name]
|
|
895
883
|
klass_desc = cls[:description] || "Parse class (#{cls[:type] || "Custom"})"
|
|
896
884
|
[
|
|
897
885
|
{
|
|
898
|
-
"uri"
|
|
899
|
-
"name"
|
|
886
|
+
"uri" => "parse://#{name}/schema",
|
|
887
|
+
"name" => "#{name} schema",
|
|
900
888
|
"description" => "Field definitions and types for #{name}. #{klass_desc}",
|
|
901
|
-
"mimeType"
|
|
889
|
+
"mimeType" => "application/json",
|
|
902
890
|
},
|
|
903
891
|
{
|
|
904
|
-
"uri"
|
|
905
|
-
"name"
|
|
892
|
+
"uri" => "parse://#{name}/count",
|
|
893
|
+
"name" => "#{name} count",
|
|
906
894
|
"description" => "Total number of #{name} objects",
|
|
907
|
-
"mimeType"
|
|
895
|
+
"mimeType" => "application/json",
|
|
908
896
|
},
|
|
909
897
|
{
|
|
910
|
-
"uri"
|
|
911
|
-
"name"
|
|
898
|
+
"uri" => "parse://#{name}/samples",
|
|
899
|
+
"name" => "#{name} samples",
|
|
912
900
|
"description" => "Five most recent #{name} objects",
|
|
913
|
-
"mimeType"
|
|
901
|
+
"mimeType" => "application/json",
|
|
914
902
|
},
|
|
915
903
|
]
|
|
916
904
|
end
|
|
@@ -939,21 +927,21 @@ module Parse
|
|
|
939
927
|
"resourceTemplates" => [
|
|
940
928
|
{
|
|
941
929
|
"uriTemplate" => "parse://{className}/schema",
|
|
942
|
-
"name"
|
|
930
|
+
"name" => "Parse class schema",
|
|
943
931
|
"description" => "Field definitions and types for a Parse class. Expand {className} with any class your agent can list via tools/list or resources/list.",
|
|
944
|
-
"mimeType"
|
|
932
|
+
"mimeType" => "application/json",
|
|
945
933
|
},
|
|
946
934
|
{
|
|
947
935
|
"uriTemplate" => "parse://{className}/count",
|
|
948
|
-
"name"
|
|
936
|
+
"name" => "Parse class object count",
|
|
949
937
|
"description" => "Total number of objects in a Parse class.",
|
|
950
|
-
"mimeType"
|
|
938
|
+
"mimeType" => "application/json",
|
|
951
939
|
},
|
|
952
940
|
{
|
|
953
941
|
"uriTemplate" => "parse://{className}/samples",
|
|
954
|
-
"name"
|
|
942
|
+
"name" => "Parse class sample objects",
|
|
955
943
|
"description" => "Five most recent objects from a Parse class.",
|
|
956
|
-
"mimeType"
|
|
944
|
+
"mimeType" => "application/json",
|
|
957
945
|
},
|
|
958
946
|
],
|
|
959
947
|
}
|
|
@@ -969,12 +957,12 @@ module Parse
|
|
|
969
957
|
# @param agent [Parse::Agent]
|
|
970
958
|
# @return [Hash] MCP contents envelope or an error hash.
|
|
971
959
|
def self.handle_resources_read(params, agent)
|
|
972
|
-
uri
|
|
960
|
+
uri = params["uri"].to_s
|
|
973
961
|
match = uri.match(%r{\Aparse://([A-Za-z_][A-Za-z0-9_]*)(?:/(schema|count|samples))?\z})
|
|
974
962
|
return { error: { "code" => -32602, "message" => "Invalid resource URI: #{uri}" } } unless match
|
|
975
963
|
|
|
976
964
|
class_name = match[1]
|
|
977
|
-
kind
|
|
965
|
+
kind = match[2] || "schema"
|
|
978
966
|
|
|
979
967
|
result = case kind
|
|
980
968
|
when "schema"
|
|
@@ -989,9 +977,9 @@ module Parse
|
|
|
989
977
|
{
|
|
990
978
|
"contents" => [
|
|
991
979
|
{
|
|
992
|
-
"uri"
|
|
980
|
+
"uri" => uri,
|
|
993
981
|
"mimeType" => "application/json",
|
|
994
|
-
"text"
|
|
982
|
+
"text" => JSON.pretty_generate(result[:data]),
|
|
995
983
|
},
|
|
996
984
|
],
|
|
997
985
|
}
|
|
@@ -1021,8 +1009,8 @@ module Parse
|
|
|
1021
1009
|
return subscriptions_unsupported_error unless manager.respond_to?(:supported?) && manager.supported?
|
|
1022
1010
|
ok = manager.subscribe(
|
|
1023
1011
|
session_id: agent_session_id(agent),
|
|
1024
|
-
uri:
|
|
1025
|
-
agent:
|
|
1012
|
+
uri: params["uri"].to_s,
|
|
1013
|
+
agent: agent,
|
|
1026
1014
|
)
|
|
1027
1015
|
return {} if ok
|
|
1028
1016
|
# subscribe returned false: the session's listening stream was torn down
|
|
@@ -1032,8 +1020,8 @@ module Parse
|
|
|
1032
1020
|
# contract an empty result == subscribed) so the client reopens its GET
|
|
1033
1021
|
# stream and retries instead of waiting forever for updates.
|
|
1034
1022
|
{ error: { "code" => -32602,
|
|
1035
|
-
|
|
1036
|
-
|
|
1023
|
+
"message" => "resources/subscribe: the session no longer has an open " \
|
|
1024
|
+
"listening stream; reopen the GET stream and retry" } }
|
|
1037
1025
|
end
|
|
1038
1026
|
private_class_method :handle_resources_subscribe
|
|
1039
1027
|
|
|
@@ -1047,7 +1035,7 @@ module Parse
|
|
|
1047
1035
|
return subscriptions_unsupported_error unless manager.respond_to?(:supported?) && manager.supported?
|
|
1048
1036
|
manager.unsubscribe(
|
|
1049
1037
|
session_id: agent_session_id(agent),
|
|
1050
|
-
uri:
|
|
1038
|
+
uri: params["uri"].to_s,
|
|
1051
1039
|
)
|
|
1052
1040
|
{}
|
|
1053
1041
|
end
|
|
@@ -1100,8 +1088,8 @@ module Parse
|
|
|
1100
1088
|
#
|
|
1101
1089
|
# @return [Hash] MCP messages envelope or an error hash with :error key.
|
|
1102
1090
|
def self.handle_prompts_get(params)
|
|
1103
|
-
name
|
|
1104
|
-
args
|
|
1091
|
+
name = params["name"].to_s
|
|
1092
|
+
args = params["arguments"] || {}
|
|
1105
1093
|
result = Parse::Agent::Prompts.render(name, args)
|
|
1106
1094
|
|
|
1107
1095
|
# Guard against oversized prompt renderers. The renderer is untrusted
|