Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
767afa4
test: reproduce MCP clause schema mismatches
erichare Oct 6, 2026
d827066
fix: advertise Data API clause wire formats in MCP schemas
erichare Oct 6, 2026
acfbe5d
test: validate request rerank override schema examples
erichare Oct 6, 2026
027c386
test: reproduce rejection of stateless MCP 2026-07-28 requests
erichare Oct 12, 2026
b2088a9
fix: upgrade MCP runtime for stateless protocol requests
erichare Oct 12, 2026
bdc563c
test: expect framework server metadata in MCP responses
erichare Oct 13, 2026
bbacbda
Use text blocks for rerank sort examples
erichare Oct 19, 2026
2f2720e
Use a text block for the hybrid limits example
erichare Oct 19, 2026
773477d
Restore findAndRerank descriptions and keep typo fixes
erichare Oct 19, 2026
fdb5f1a
Read the MCP rerank sort description from its class
erichare Oct 19, 2026
de4573f
Remove the duplicate reranking option list
erichare Oct 19, 2026
c61a536
Clarify the clause schema customizer documentation
erichare Oct 19, 2026
f665be8
Share typed clause schemas between OpenAPI and MCP
erichare Oct 19, 2026
f96ac86
Check rerank schema requirements against override validation
erichare Oct 19, 2026
ca50f43
Honor JsonIgnore when generating MCP schemas
erichare Oct 19, 2026
c0944a2
Merge branch 'main' into fix/mcp-clause-wire-schemas
erichare Oct 19, 2026
2d4c204
Document schema contracts and test Jackson property names
erichare Oct 19, 2026
e1c457e
Discover clause schemas and validate tool payloads
erichare Oct 19, 2026
41015d3
Combine MCP schema customizers and focus example tests
erichare Oct 19, 2026
53ce63e
Cache MCP schema definitions with independent copies
erichare Oct 19, 2026
04bf945
Reuse annotation-based OpenAPI schemas for MCP inputs
erichare Oct 20, 2026
ff0f145
Simplify MCP schema annotation handling
erichare Oct 20, 2026
d3e4ea7
Test packaged MCP schemas and fix argument-local references
erichare Oct 21, 2026
15875b7
Remove redundant additionalProperties schema annotations
erichare Oct 21, 2026
5fb06da
Share MCP HTTP test support and run CI for stacked PRs
erichare Oct 22, 2026
e4c6b92
Merge branch 'fix/mcp-simplicity-protocol' into fix/mcp-simplicity-sc…
erichare Oct 22, 2026
0900d87
Preserve accepted empty rerank configuration maps
erichare Oct 22, 2026
ab4d996
Keep rerank authentication validation provider specific
erichare Oct 22, 2026
55d3d72
Apply review wording and cleanup for MCP clause schemas
erichare Oct 23, 2026
0b89e71
Explain clause schema classes and use JsonIgnore for internal fields
erichare Oct 23, 2026
6e2cf87
refactor: load MCP OpenAPI schemas at startup and document the adapter
erichare Oct 23, 2026
d3c2429
ci: restore the original pull request branch filter
erichare Oct 23, 2026
dc4979e
Merge branch 'fix/mcp-protocol-foundation' into fix/mcp-clause-wire-s…
erichare Oct 23, 2026
9692939
fix: describe every findAndRerank MCP tool argument
erichare Oct 23, 2026
94d14c7
fix: describe every MCP tool argument to one standard
erichare Oct 23, 2026
14cabf2
Revert "fix: describe every MCP tool argument to one standard"
erichare Oct 23, 2026
941548a
docs: name filter and sort as clause arguments in the MCP schema adapter
erichare Oct 23, 2026
a7534de
Test every advertised MCP clause schema
erichare Oct 23, 2026
2de2a2f
Merge remote-tracking branch 'origin/main' into fix/mcp-clause-wire-s…
erichare Oct 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.stargate.sgv2.jsonapi.api.model.command;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.JsonNode;
import com.google.common.annotations.VisibleForTesting;
import java.util.Objects;
Expand All @@ -10,13 +11,15 @@
* intermediate form ({@link JsonNode}) from which actual deserialization and validation can be
* deferred until we have context we need
*
* <p>Fields are {@code @JsonIgnore} so OpenAPI and MCP describe only the clause JSON.
*
* @see io.stargate.sgv2.jsonapi.api.model.command.clause.filter.FilterDefinition
* @see io.stargate.sgv2.jsonapi.api.model.command.clause.filter.SortDefinition
* @param <T> Type of the value object to be deserialized from the JSON value
*/
public abstract class JsonDefinition<T> {
/** The wrapped JSON value */
private final JsonNode json;
@JsonIgnore private final JsonNode json;

protected JsonDefinition(JsonNode json) {
this.json = Objects.requireNonNull(json);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.stargate.sgv2.jsonapi.api.model.command.clause.filter;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.JsonNode;
import io.stargate.sgv2.jsonapi.api.model.command.CommandContext;
import io.stargate.sgv2.jsonapi.api.model.command.JsonDefinition;
Expand All @@ -14,18 +15,26 @@
* allows for lazy deserialization into a {@link FilterClause}.
*/
@Schema(
name = "FilterDefinition",
type = SchemaType.OBJECT,
implementation = FilterClause.class,
examples =
"""
{"name": "Aaron", "country": {"$eq": "NZ"}, "age": {"$gt": 40}}
""")
// FilterClause describes parsed state and would incorrectly reserve fields such as empty.
implementation = Object.class,
nullable = true,
description = "A filter to select documents, see examples.",
examples = {
"""
{"age": {"$gt": 40}}
""",
"""
Comment thread
erichare marked this conversation as resolved.
Resolved
{"name": "Aaron", "country": {"$eq": "NZ"}, "age": {"$gt": 40}}
"""
})
public class FilterDefinition extends JsonDefinition<FilterClause> {
/**
* Lazily deserialized {@link FilterClause} from the JSON value. We need this due to existing
* reliance on specific stateful instances of {@link FilterClause}.
*/
private FilterClause filterClause;
@JsonIgnore private FilterClause filterClause;

/**
* To deserialize the whole JSON value, need to ensure DELEGATING mode (instead of PROPERTIES).
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.stargate.sgv2.jsonapi.api.model.command.clause.filter;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import io.stargate.sgv2.jsonapi.api.model.command.CommandContext;
Expand All @@ -17,18 +18,29 @@
* for lazy deserialization into a {@link SortClause}.
*/
@Schema(
name = "SortDefinition",
type = SchemaType.OBJECT,
implementation = SortClause.class,
examples =
"""
{"user.age" : -1, "user.name" : 1}
""")
implementation = Object.class,
nullable = true,
description =
"A sort to order selected documents. Supports sorting document fields by ascending or descending, or sorting by $vector, $vectorize, or $lexical. See examples.",
examples = {
"""
{"user.age" : -1, "user.name" : 1}
""",
"""
{"$vector": [0.1, 0.2, 0.3]}
""",
"""
{"$vectorize": "search text"}
"""
})
public class SortDefinition extends JsonDefinition<SortClause> {
/**
* Lazily deserialized {@link SortClause} from the JSON value. We need this due to existing
* reliance on specific stateful instances of {@link SortClause}.
*/
private SortClause sortClause;
@JsonIgnore private SortClause sortClause;

/**
* To deserialize the whole JSON value, need to ensure DELEGATING mode (instead of PROPERTIES).
Expand Down Expand Up @@ -84,6 +96,7 @@ public boolean hasVsearchClause() {
* Helper method needed for logging purposes, where caller does not have access to the {@link
* CommandContext}.
*/
@JsonIgnore
public List<String> getSortExpressionPaths() {
// We will either be wrapping the clause or have a json value:
if (sortClause != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package io.stargate.sgv2.jsonapi.api.model.command.clause.sort;

import static io.stargate.sgv2.jsonapi.config.constants.DocumentConstants.Fields.*;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import io.stargate.sgv2.jsonapi.metrics.CommandFeatures;
import io.stargate.sgv2.jsonapi.util.recordable.Recordable;
import java.util.*;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.media.SchemaProperty;

/**
* @param vectorizeSort If <code>null</code> then the $vectorize field was not present in the sort
Expand All @@ -19,18 +23,84 @@
*/
@JsonDeserialize(using = FindAndRerankSortClauseDeserializer.class)
@Schema(
name = "FindAndRerankSort",
type = SchemaType.OBJECT,
implementation = Map.class,
examples =
"""
{"$sort" : {"$hybrid" : "Same query for vectorize and bm25 sorting"}}}
{"$sort" : {"$hybrid" : {"$vectorize" : "vectorize sort query" , "$lexical": "lexical sort" }}}
{"$sort" : {"$hybrid" : {"$vector" : [1,2,3] , "$lexical": "lexical sort" }}}
""")
nullable = true,
additionalProperties = Schema.False.class,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do this ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parser only accepts $hybrid and rejects any other key. This makes the schema reject the same payloads, so a client sees the problem before it calls the server.

properties =
@SchemaProperty(
name = HYBRID_FIELD,
anyOf = {String.class, FindAndRerankSort.HybridSortSchema.class}),
description =
"Use $hybrid with a query string or an object containing $vectorize or $vector, and optionally $lexical."
+ " $vector and $vectorize cannot be used together.",
examples = {
Comment thread
erichare marked this conversation as resolved.
Resolved
"""
{"$hybrid": "Same query for vectorize and BM25 sorting"}
""",
"""
{"$hybrid": {"$vectorize": "vectorize sort query", "$lexical": "lexical sort"}}
""",
"""
{"$hybrid": {"$vector": [1, 2, 3], "$lexical": "lexical sort"}}
""",
"""
{"$hybrid": {"$vector": {"$binary": "P4AAAEAAAABAQAAA"}, "$lexical": "lexical query"}}
"""
})
public record FindAndRerankSort(
String vectorizeSort, String lexicalSort, float[] vectorSort, CommandFeatures commandFeatures)
@JsonIgnore String vectorizeSort,
@JsonIgnore String lexicalSort,
@JsonIgnore float[] vectorSort,
@JsonIgnore CommandFeatures commandFeatures)
implements Recordable {

// Schema-only classes. @SchemaProperty can point at a nested schema only through a class, so
// these empty classes carry the @Schema for each nested part of the sort. Nothing creates them.

/** The object form of $hybrid, with $vector or $vectorize and an optional $lexical. */
@Schema(
name = "HybridSort",
type = SchemaType.OBJECT,
implementation = Object.class,
additionalProperties = Schema.False.class,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how does it look when we call with some {"foo": 1} with and without this additionalProperties?

properties = {
@SchemaProperty(
name = VECTOR_EMBEDDING_TEXT_FIELD,
type = SchemaType.STRING,
nullable = true),
@SchemaProperty(name = LEXICAL_CONTENT_FIELD, type = SchemaType.STRING, nullable = true),
@SchemaProperty(
name = VECTOR_EMBEDDING_FIELD,
nullable = true,
anyOf = {float[].class, BinaryVectorSchema.class})
},
not = ConflictingVectorSortSchema.class)
public static class HybridSortSchema {}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WHY does this class exists ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It describes the object form of $hybrid for OpenAPI and MCP, since the record's own fields don't match the JSON clients send. I added a doc comment on it and on the two classes below it.


/** A binary encoded vector such as {"$binary": "P4AAAEAAAABAQAAA"}. */
@Schema(
name = "BinaryVector",
type = SchemaType.OBJECT,
implementation = Object.class,
nullable = true,
additionalProperties = Schema.False.class,
requiredProperties = BINARY_VECTOR_TEXT_FIELD,
properties = @SchemaProperty(name = BINARY_VECTOR_TEXT_FIELD, type = SchemaType.STRING))
public static class BinaryVectorSchema {}

/**
* Matches an object that has both $vector and $vectorize. HybridSort uses it as its {@code not}
* schema, so a sort that sets both fails validation.
*/
@Schema(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this really needs comment to explain what is happening

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a doc comment. HybridSort uses it as its not schema, so a sort with both $vector and $vectorize fails validation.

name = "ConflictingVectorSort",
type = SchemaType.OBJECT,
implementation = Object.class,
requiredProperties = {VECTOR_EMBEDDING_FIELD, VECTOR_EMBEDDING_TEXT_FIELD})
public static class ConflictingVectorSortSchema {}

public static final FindAndRerankSort NO_ARG_SORT =
new FindAndRerankSort(null, null, null, CommandFeatures.EMPTY);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package io.stargate.sgv2.jsonapi.api.model.command.impl;

import static io.stargate.sgv2.jsonapi.config.constants.DocumentConstants.Fields.*;
import static io.stargate.sgv2.jsonapi.config.constants.OpenApiConstants.Components.SCHEMA_REF_PREFIX;
import static io.stargate.sgv2.jsonapi.config.constants.ServiceDescConstants.AUTHENTICATION;
import static io.stargate.sgv2.jsonapi.config.constants.ServiceDescConstants.MODEL_NAME;
import static io.stargate.sgv2.jsonapi.config.constants.ServiceDescConstants.PARAMETERS;
import static io.stargate.sgv2.jsonapi.config.constants.ServiceDescConstants.PROVIDER;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeName;
import com.fasterxml.jackson.core.JsonParser;
Expand All @@ -28,10 +34,9 @@
import javax.annotation.Nullable;
import org.eclipse.microprofile.openapi.annotations.enums.SchemaType;
import org.eclipse.microprofile.openapi.annotations.media.Schema;
import org.eclipse.microprofile.openapi.annotations.media.SchemaProperty;

@Schema(
description =
"Finds documents using using vector and lexical sorting, then reranks the results.")
@Schema(description = "Finds documents using vector and lexical sorting, then reranks the results.")
@JsonTypeName(CommandName.Names.FIND_AND_RERANK)
public record FindAndRerankCommand(
@Valid @JsonProperty("filter") FilterDefinition filterDefinition,
Expand Down Expand Up @@ -70,15 +75,7 @@ public record Options(
implementation = Integer.class)
Integer limit,
/** ---- */
@Schema(
description =
"The maximum number of documents to read for the vector and lexical queries that feed into the reranking. May be a number or an object with $vector and $lexical fields. The accepted range is determined by server configuration.",
examples =
"""
{"hybridLimits" : 100}
{"hybridLimits" : {"$vector" : 100, "$lexical" : 10}}
""")
HybridLimits hybridLimits,
@Schema(ref = HybridLimits.REF) HybridLimits hybridLimits,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: put the function on a new line

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried it, but google-java-format puts it back on one line and the CI format check would fail.

/** ---- */
@Schema(
description =
Expand Down Expand Up @@ -117,18 +114,67 @@ public record Options(
"Optional override for the reranking service. Completely replaces the"
+ " collection-level reranking configuration when provided. Both 'provider' and"
+ " 'modelName' are required.",
implementation = CreateCollectionCommand.Options.RerankServiceDesc.class)
implementation = RerankOverrideSchema.class,
ref = RerankOverrideSchema.REF)
@JsonProperty("rerank")
CreateCollectionCommand.Options.RerankServiceDesc rerankServiceOverride) {}

// Matches the override validation in CollectionRerankDef. Update both together.
@Schema(
name = RerankOverrideSchema.NAME,
type = SchemaType.OBJECT,
implementation = Object.class,
nullable = true,
additionalProperties = Schema.False.class,
requiredProperties = {PROVIDER, MODEL_NAME},
properties = {
@SchemaProperty(name = PROVIDER, type = SchemaType.STRING),
@SchemaProperty(name = MODEL_NAME, type = SchemaType.STRING),
@SchemaProperty(
name = AUTHENTICATION,
type = SchemaType.OBJECT,
nullable = true,
additionalProperties = String.class),
@SchemaProperty(
name = PARAMETERS,
type = SchemaType.OBJECT,
nullable = true,
additionalProperties = Schema.False.class)
},
examples =
"""
{"provider": "nvidia", "modelName": "nvidia/llama-3.2-nv-rerankqa-1b-v2"}
""")
public static class RerankOverrideSchema {
public static final String NAME = "RerankServiceOverride";
public static final String REF = SCHEMA_REF_PREFIX + NAME;
}

@JsonDeserialize(using = HybridLimitsDeserializer.class)
@Schema(
name = HybridLimits.NAME,
description =
"Candidate read limits as one integer for both legs, or an object containing both $vector and $lexical integer limits.",
// Void is the annotation default, but the OpenAPI scanner only sees values set in source.
// Setting it stops the scanner adding the record fields as an object type next to anyOf.
implementation = Void.class,
anyOf = {Integer.class, HybridLimits.ValuesSchema.class},
examples = {
"50",
"""
{"$vector": 50, "$lexical": 10}
"""
})
public record HybridLimits(
@JsonProperty(DocumentConstants.Fields.VECTOR_EMBEDDING_FIELD) int vectorLimit,
/** ---- */
@JsonProperty(DocumentConstants.Fields.LEXICAL_CONTENT_FIELD) int lexicalLimit,
CommandFeatures commandFeatures)
@JsonIgnore CommandFeatures commandFeatures)
implements Recordable {

public static final String NAME = "HybridLimits";
public static final String REF = SCHEMA_REF_PREFIX + NAME;

public static final HybridLimits DEFAULT =
new HybridLimits(
RerankingConstants.HybridSearchLimits.DEFAULT,
Expand All @@ -142,6 +188,19 @@ public DataRecorder recordTo(DataRecorder dataRecorder) {
.append("lexicalLimit", lexicalLimit)
.append("commandFeatures", commandFeatures);
}

/** Schema for the object form of the limits, the second branch of the anyOf above. */
@Schema(
name = "HybridLimitValues",
type = SchemaType.OBJECT,
nullable = true,
additionalProperties = Schema.False.class,
requiredProperties = {VECTOR_EMBEDDING_FIELD, LEXICAL_CONTENT_FIELD},
properties = {
@SchemaProperty(name = VECTOR_EMBEDDING_FIELD, type = SchemaType.INTEGER),
@SchemaProperty(name = LEXICAL_CONTENT_FIELD, type = SchemaType.INTEGER)
})
public static class ValuesSchema {}
}

/** Deserializer for the `hybridLimits` option, the limits for the inner reads. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,16 +148,23 @@ public Uni<ToolResponse> estimatedDocumentCount(
return mcpResource.processCollectionCommand(keyspace, collection, command);
}

@Tool(
description =
"Finds documents using using vector and lexical sorting, then reranks the results.")
@Tool(description = "Finds documents using vector and lexical sorting, then reranks the results.")
public Uni<ToolResponse> findAndRerank(
@ToolArg(description = "Name of the keyspace") String keyspace,
@ToolArg(description = "Name of the collection/table") String collection,
@ToolArg(description = "filter", required = false) FilterDefinition filter,
@ToolArg(description = "projection", required = false) JsonNode projection,
@ToolArg(description = "sort", required = false) FindAndRerankSort sort,
@ToolArg(description = "options", required = false) FindAndRerankCommand.Options options) {
@ToolArg(description = "Name of the collection") String collection,
// filter and sort have no description here because it would replace the one on their
// classes, which Swagger also uses.
@ToolArg(required = false) FilterDefinition filter,
@ToolArg(
description =
"Fields to include or exclude in the returned documents, for example {\"title\": 1}",
required = false)
JsonNode projection,
@ToolArg(required = false) FindAndRerankSort sort,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bring all the parameters up to the same standard for use of description etc

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do. Do you mean every tool in this class, or only findAndRerank?

@erichare erichare Oct 23, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filter now uses the FilterDefinition description like sort does, and projection and options have real descriptions. The other tools stay as they were in this PR, and I can bring them to the same standard in a follow-up if you want.

@ToolArg(
description = "Options for result limits, reranking, and what the response includes",
required = false)
FindAndRerankCommand.Options options) {

var command = new FindAndRerankCommand(filter, projection, sort, options);
return mcpResource.processCollectionCommand(keyspace, collection, command);
Expand Down
Loading
Loading