From 0cc5d9c3f97e5b9608c7888e052288bef9294110 Mon Sep 17 00:00:00 2001 From: Eric Hare Date: Thu, 5 Nov 2026 10:46:58 -0800 Subject: [PATCH] fix: describe every MCP tool argument and the JSON it takes --- .../api/v1/mcp/CollectionCommandTools.java | 193 ++++++++++++------ .../api/v1/mcp/GeneralCommandTools.java | 10 +- .../api/v1/mcp/KeyspaceCommandTools.java | 92 ++++++--- .../v1/mcp/McpClauseSchemaContractTest.java | 83 ++++++-- 4 files changed, 262 insertions(+), 116 deletions(-) diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/v1/mcp/CollectionCommandTools.java b/src/main/java/io/stargate/sgv2/jsonapi/api/v1/mcp/CollectionCommandTools.java index efb89bf227..a55c028a00 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/v1/mcp/CollectionCommandTools.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/v1/mcp/CollectionCommandTools.java @@ -22,6 +22,9 @@ * MCP tool provider for collection-level (and table-level) commands {@link * io.stargate.sgv2.jsonapi.api.model.command.CollectionCommand}. These correspond to the {@link * io.stargate.sgv2.jsonapi.api.v1.CollectionResource} REST endpoint. + * + *

The filter and sort arguments have no description here. A {@code @ToolArg} description would + * replace the one on their classes, which Swagger also uses. */ public class CollectionCommandTools { @@ -31,7 +34,10 @@ public class CollectionCommandTools { public Uni alterTable( @ToolArg(description = "Name of the keyspace") String keyspace, @ToolArg(description = "Name of the table") String table, - @ToolArg(description = "The alter table operation") AlterTableOperation operation) { + @ToolArg( + description = + "One change to the table, keyed by add, drop, addVectorize, or dropVectorize, for example {\"add\": {\"columns\": {\"rating\": \"int\"}}}") + AlterTableOperation operation) { var command = new AlterTableCommand(operation); return mcpResource.processCollectionCommand(keyspace, table, command); @@ -41,7 +47,7 @@ public Uni alterTable( public Uni countDocuments( @ToolArg(description = "Name of the keyspace") String keyspace, @ToolArg(description = "Name of the collection") String collection, - @ToolArg(description = "filter", required = false) FilterDefinition filter) { + @ToolArg(required = false) FilterDefinition filter) { var command = new CountDocumentsCommand(filter); return mcpResource.processCollectionCommand(keyspace, collection, command); @@ -51,17 +57,22 @@ public Uni countDocuments( public Uni createIndex( @ToolArg(description = "Name of the keyspace") String keyspace, @ToolArg(description = "Name of the table") String table, - @ToolArg(description = "Name of the new Index") String indexName, - @ToolArg(description = "Definition of the index to create") + @ToolArg(description = "Name of the new index") String indexName, + @ToolArg( + description = + "Column to index and its index options, for example {\"column\": \"status\"}") RegularIndexDefinitionDesc definition, @ToolArg( description = - "Optional type of the index to create. The only supported value is '" + "Type of the index to create. The only supported value is '" + ApiIndexType.Constants.REGULAR + "'.", required = false) String indexType, - @ToolArg(description = "Options for the command.", required = false) + @ToolArg( + description = + "Set ifNotExists to true to succeed when an index with this name already exists", + required = false) CreateIndexCommand.CreateIndexCommandOptions options) { var command = new CreateIndexCommand(indexName, definition, indexType, options); @@ -74,17 +85,22 @@ public Uni createIndex( public Uni createTextIndex( @ToolArg(description = "Name of the keyspace") String keyspace, @ToolArg(description = "Name of the table") String table, - @ToolArg(description = "Name of the new Index") String indexName, - @ToolArg(description = "Definition of the index to create") + @ToolArg(description = "Name of the new index") String indexName, + @ToolArg( + description = + "Column to index and its index options, for example {\"column\": \"title\"}") TextIndexDefinitionDesc definition, @ToolArg( description = - "Optional type of the index to create. The only supported value is '" + "Type of the index to create. The only supported value is '" + ApiIndexType.Constants.TEXT + "'.", required = false) String indexType, - @ToolArg(description = "Options for the command.", required = false) + @ToolArg( + description = + "Set ifNotExists to true to succeed when an index with this name already exists", + required = false) CreateTextIndexCommand.CommandOptions options) { var command = new CreateTextIndexCommand(indexName, definition, indexType, options); @@ -95,17 +111,22 @@ public Uni createTextIndex( public Uni createVectorIndex( @ToolArg(description = "Name of the keyspace") String keyspace, @ToolArg(description = "Name of the table") String table, - @ToolArg(description = "Name of the new Index") String indexName, - @ToolArg(description = "Definition of the index to create") + @ToolArg(description = "Name of the new index") String indexName, + @ToolArg( + description = + "Column to index and its index options, for example {\"column\": \"embedding\", \"options\": {\"metric\": \"cosine\"}}") VectorIndexDefinitionDesc definition, @ToolArg( description = - "Optional type of the index to create. The only supported value is '" + "Type of the index to create. The only supported value is '" + ApiIndexType.Constants.VECTOR + "'.", required = false) String indexType, - @ToolArg(description = "Options for the command.", required = false) + @ToolArg( + description = + "Set ifNotExists to true to succeed when an index with this name already exists", + required = false) CreateVectorIndexCommand.CreateVectorIndexCommandOptions options) { var command = new CreateVectorIndexCommand(indexName, definition, indexType, options); @@ -117,11 +138,8 @@ public Uni createVectorIndex( "Command that finds documents based on the filter and deletes them from a collection") public Uni deleteMany( @ToolArg(description = "Name of the keyspace") String keyspace, - @ToolArg(description = "Name of the collection/table") String collection, - @ToolArg( - description = "Filter clause based on which documents are identified", - required = false) - FilterDefinition filter) { + @ToolArg(description = "Name of the collection or table") String collection, + @ToolArg(required = false) FilterDefinition filter) { var command = new DeleteManyCommand(filter); return mcpResource.processCollectionCommand(keyspace, collection, command); @@ -130,10 +148,9 @@ public Uni deleteMany( @Tool(description = "Command that finds a single document and deletes it from a collection") public Uni deleteOne( @ToolArg(description = "Name of the keyspace") String keyspace, - @ToolArg(description = "Name of the collection/table") String collection, - @ToolArg(description = "Filter clause based on which documents are identified") - FilterDefinition filter, - @ToolArg(description = "Sort clause", required = false) SortDefinition sort) { + @ToolArg(description = "Name of the collection or table") String collection, + @ToolArg FilterDefinition filter, + @ToolArg(required = false) SortDefinition sort) { var command = new DeleteOneCommand(filter, sort); return mcpResource.processCollectionCommand(keyspace, collection, command); @@ -152,8 +169,6 @@ public Uni estimatedDocumentCount( public Uni findAndRerank( @ToolArg(description = "Name of the keyspace") String keyspace, @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 = @@ -173,11 +188,18 @@ public Uni findAndRerank( @Tool(description = "Command that finds JSON documents from a collection.") public Uni find( @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) SortDefinition sort, - @ToolArg(description = "options", required = false) FindCommand.Options options) { + @ToolArg(description = "Name of the collection or table") String collection, + @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) SortDefinition sort, + @ToolArg( + description = "Options for result limits, paging, and what the response includes", + required = false) + FindCommand.Options options) { var command = new FindCommand(filter, projection, sort, options); return mcpResource.processCollectionCommand(keyspace, collection, command); @@ -188,10 +210,14 @@ public Uni find( "Command that finds a single JSON document from a collection and deletes it. The deleted document is returned") public Uni findOneAndDelete( @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 = "sort", required = false) SortDefinition sort, - @ToolArg(description = "projection", required = false) JsonNode projection) { + @ToolArg(description = "Name of the collection") String collection, + @ToolArg(required = false) FilterDefinition filter, + @ToolArg(required = false) SortDefinition sort, + @ToolArg( + description = + "Fields to include or exclude in the returned document, for example {\"title\": 1}", + required = false) + JsonNode projection) { var command = new FindOneAndDeleteCommand(filter, sort, projection); return mcpResource.processCollectionCommand(keyspace, collection, command); @@ -202,12 +228,22 @@ public Uni findOneAndDelete( "Command that finds a single JSON document from a collection and replaces it with the replacement document.") public Uni findOneAndReplace( @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 = "sort", required = false) SortDefinition sort, - @ToolArg(description = "projection", required = false) JsonNode projection, - @ToolArg(description = "replacement") ObjectNode replacement, - @ToolArg(description = "options", required = false) + @ToolArg(description = "Name of the collection") String collection, + @ToolArg(required = false) FilterDefinition filter, + @ToolArg(required = false) SortDefinition sort, + @ToolArg( + description = + "Fields to include or exclude in the returned document, for example {\"title\": 1}", + required = false) + JsonNode projection, + @ToolArg( + description = + "Document that replaces the matching document, for example {\"title\": \"Dune\", \"status\": \"active\"}") + ObjectNode replacement, + @ToolArg( + description = + "Options for upsert and whether to return the document before or after the replacement", + required = false) FindOneAndReplaceCommand.Options options) { var command = new FindOneAndReplaceCommand(filter, sort, projection, replacement, options); @@ -219,12 +255,20 @@ public Uni findOneAndReplace( "Command that finds a single JSON document from a collection and updates the values provided in the update clause.") public Uni findOneAndUpdate( @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) SortDefinition sort, - @ToolArg(description = "update") UpdateClause update, - @ToolArg(description = "options", required = false) FindOneAndUpdateCommand.Options options) { + @ToolArg(description = "Name of the collection") String collection, + @ToolArg(required = false) FilterDefinition filter, + @ToolArg( + description = + "Fields to include or exclude in the returned document, for example {\"title\": 1}", + required = false) + JsonNode projection, + @ToolArg(required = false) SortDefinition sort, + @ToolArg UpdateClause update, + @ToolArg( + description = + "Options for upsert and whether to return the document before or after the update", + required = false) + FindOneAndUpdateCommand.Options options) { var command = new FindOneAndUpdateCommand(filter, projection, sort, update, options); return mcpResource.processCollectionCommand(keyspace, collection, command); @@ -233,11 +277,16 @@ public Uni findOneAndUpdate( @Tool(description = "Command that finds a single JSON document from a collection.") public Uni findOne( @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) SortDefinition sort, - @ToolArg(description = "options", required = false) FindOneCommand.Options options) { + @ToolArg(description = "Name of the collection or table") String collection, + @ToolArg(required = false) FilterDefinition filter, + @ToolArg( + description = + "Fields to include or exclude in the returned document, for example {\"title\": 1}", + required = false) + JsonNode projection, + @ToolArg(required = false) SortDefinition sort, + @ToolArg(description = "Options for what the response includes", required = false) + FindOneCommand.Options options) { var command = new FindOneCommand(filter, projection, sort, options); return mcpResource.processCollectionCommand(keyspace, collection, command); @@ -247,7 +296,9 @@ public Uni findOne( public Uni listIndexes( @ToolArg(description = "Name of the keyspace") String keyspace, @ToolArg(description = "Name of the table") String table, - @ToolArg(description = "Options for the `listIndexes` command.", required = false) + @ToolArg( + description = "Set explain to true to include the properties of each index", + required = false) ListIndexesCommand.Options options) { var command = new ListIndexesCommand(options); @@ -257,9 +308,12 @@ public Uni listIndexes( @Tool(description = "Command that inserts multiple JSON documents to a collection.") public Uni insertMany( @ToolArg(description = "Name of the keyspace") String keyspace, - @ToolArg(description = "Name of the collection/table") String collection, - @ToolArg(description = "JSON documents to insert") List documents, - @ToolArg(description = "Options for inserting many documents", required = false) + @ToolArg(description = "Name of the collection or table") String collection, + @ToolArg( + description = + "Documents or rows to insert, for example [{\"title\": \"Dune\"}, {\"title\": \"Emma\"}]") + List documents, + @ToolArg(description = "Options for insert order and per-document results", required = false) InsertManyCommand.Options options) { var command = new InsertManyCommand(documents, options); @@ -269,8 +323,11 @@ public Uni insertMany( @Tool(description = "Command that inserts a single JSON document to a collection.") public Uni insertOne( @ToolArg(description = "Name of the keyspace") String keyspace, - @ToolArg(description = "Name of the collection/table") String collection, - @ToolArg(description = "JSON document to insert") JsonNode document) { + @ToolArg(description = "Name of the collection or table") String collection, + @ToolArg( + description = + "Document or row to insert, for example {\"title\": \"Dune\", \"status\": \"active\"}") + JsonNode document) { var command = new InsertOneCommand(document); return mcpResource.processCollectionCommand(keyspace, collection, command); @@ -281,10 +338,11 @@ public Uni insertOne( "Command that finds documents from a collection and updates it with the values provided in the update clause.") public Uni updateMany( @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 = "update") UpdateClause update, - @ToolArg(description = "options", required = false) UpdateManyCommand.Options options) { + @ToolArg(description = "Name of the collection") String collection, + @ToolArg(required = false) FilterDefinition filter, + @ToolArg UpdateClause update, + @ToolArg(description = "Options for upsert and paging", required = false) + UpdateManyCommand.Options options) { var command = new UpdateManyCommand(filter, update, options); return mcpResource.processCollectionCommand(keyspace, collection, command); @@ -295,11 +353,14 @@ public Uni updateMany( "Command that finds a single JSON document from a table or collection and updates the values provided in the update clause.") public Uni updateOne( @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 = "update") UpdateClause update, - @ToolArg(description = "sort", required = false) SortDefinition sort, - @ToolArg(description = "options", required = false) UpdateOneCommand.Options options) { + @ToolArg(description = "Name of the collection or table") String collection, + @ToolArg(required = false) FilterDefinition filter, + @ToolArg UpdateClause update, + @ToolArg(required = false) SortDefinition sort, + @ToolArg( + description = "Set upsert to true to insert a document when none matches the filter", + required = false) + UpdateOneCommand.Options options) { var command = new UpdateOneCommand(filter, update, sort, options); return mcpResource.processCollectionCommand(keyspace, collection, command); diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/v1/mcp/GeneralCommandTools.java b/src/main/java/io/stargate/sgv2/jsonapi/api/v1/mcp/GeneralCommandTools.java index aaf01203ea..c50fb114a6 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/v1/mcp/GeneralCommandTools.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/v1/mcp/GeneralCommandTools.java @@ -22,8 +22,8 @@ public class GeneralCommandTools { @Tool(description = "Command that creates a keyspace.") public Uni createKeyspace( - @ToolArg(description = "Required name of the new Keyspace") String name, - @ToolArg(description = "Options for creating a new keyspace.", required = false) + @ToolArg(description = "Name of the new keyspace") String name, + @ToolArg(description = "Replication settings for the new keyspace", required = false) CreateKeyspaceCommand.Options options) { var command = new CreateKeyspaceCommand(name, options); @@ -32,7 +32,7 @@ public Uni createKeyspace( @Tool(description = "Command that deletes a Keyspace.") public Uni dropKeyspace( - @ToolArg(description = "Required name of the Keyspace to remove") @NotEmpty String name) { + @ToolArg(description = "Name of the keyspace to drop") @NotEmpty String name) { var command = new DropKeyspaceCommand(name); return mcpResource.processCommand(mcpResource.buildDatabaseContext(command), command); @@ -49,7 +49,7 @@ public Uni findKeyspaces() { public Uni findEmbeddingProviders( @ToolArg( description = - "Optional model status filter: SUPPORTED, DEPRECATED, or END_OF_LIFE. If omitted, only SUPPORTED models are returned.", + "Set filterModelStatus to SUPPORTED, DEPRECATED, or END_OF_LIFE to list models with that status, or to an empty string to list all models. Without options, only SUPPORTED models are listed.", required = false) FindEmbeddingProvidersCommand.Options options) { @@ -61,7 +61,7 @@ public Uni findEmbeddingProviders( public Uni findRerankingProviders( @ToolArg( description = - "Optional model status filter: SUPPORTED, DEPRECATED, or END_OF_LIFE. If omitted, only SUPPORTED models are returned.", + "Set filterModelStatus to SUPPORTED, DEPRECATED, or END_OF_LIFE to list models with that status, or to an empty string to list all models. Without options, only SUPPORTED models are listed.", required = false) FindRerankingProvidersCommand.Options options) { diff --git a/src/main/java/io/stargate/sgv2/jsonapi/api/v1/mcp/KeyspaceCommandTools.java b/src/main/java/io/stargate/sgv2/jsonapi/api/v1/mcp/KeyspaceCommandTools.java index faec415caa..b637e54eb0 100644 --- a/src/main/java/io/stargate/sgv2/jsonapi/api/v1/mcp/KeyspaceCommandTools.java +++ b/src/main/java/io/stargate/sgv2/jsonapi/api/v1/mcp/KeyspaceCommandTools.java @@ -24,9 +24,12 @@ public class KeyspaceCommandTools { */ @Tool(description = "Command that creates a collection.") public Uni createCollection( - @ToolArg(description = "Name of the existing keyspace") String keyspace, + @ToolArg(description = "Name of the keyspace") String keyspace, @ToolArg(description = "Name of the new collection") String collection, - @ToolArg(description = "Configuration options for the collection", required = false) + @ToolArg( + description = + "Collection settings for document IDs, vector search, indexing, lexical search, and reranking", + required = false) CreateCollectionCommand.Options options) { var command = new CreateCollectionCommand(collection, options); @@ -35,8 +38,8 @@ public Uni createCollection( @Tool(description = "Command that deletes a collection if one exists.") public Uni deleteCollection( - @ToolArg(description = "Name of the existing Keyspace") String keyspace, - @ToolArg(description = "Name of the existing Collection") String collection) { + @ToolArg(description = "Name of the keyspace") String keyspace, + @ToolArg(description = "Name of the collection to delete") String collection) { var command = new DeleteCollectionCommand(collection); return mcpResource.processCommand(mcpResource.buildKeyspaceContext(keyspace, command), command); @@ -44,8 +47,10 @@ public Uni deleteCollection( @Tool(description = "Command that lists all available collections in a keyspace.") public Uni findCollections( - @ToolArg(description = "Name of the existing Keyspace") String keyspace, - @ToolArg(description = "include collection properties.", required = false) + @ToolArg(description = "Name of the keyspace") String keyspace, + @ToolArg( + description = "Set explain to true to include the properties of each collection", + required = false) FindCollectionsCommand.Options options) { var command = new FindCollectionsCommand(options); @@ -57,11 +62,18 @@ public Uni findCollections( */ @Tool(description = "Command that alters a user defined type.") public Uni alterType( - @ToolArg(description = "Keyspace of the type to alter") String keyspace, + @ToolArg(description = "Name of the keyspace") String keyspace, @ToolArg(description = "Name of the type to alter") String name, - @ToolArg(description = "Operation to rename fields", required = false) + @ToolArg( + description = + "Fields to rename, mapping old names to new names, for example {\"fields\": {\"zip\": \"postcode\"}}", + required = false) RenameTypeFieldsDesc rename, - @ToolArg(description = "Operation to add fields", required = false) TypeDefinitionDesc add) { + @ToolArg( + description = + "Fields to add to the type, for example {\"fields\": {\"country\": \"text\"}}", + required = false) + TypeDefinitionDesc add) { var command = new AlterTypeCommand(name, rename, add); return mcpResource.processCommand(mcpResource.buildKeyspaceContext(keyspace, command), command); @@ -69,10 +81,15 @@ public Uni alterType( @Tool(description = "Command that creates an API Table.") public Uni createTable( - @ToolArg(description = "Name of the existing keyspace") String keyspace, - @ToolArg(description = "Name of the new Table") String table, - @ToolArg(description = "Table definition") TableDefinitionDesc definition, - @ToolArg(description = "Configuration options for the table", required = false) + @ToolArg(description = "Name of the keyspace") String keyspace, + @ToolArg(description = "Name of the new table") String table, + @ToolArg( + description = + "Columns and primary key of the new table, for example {\"columns\": {\"id\": \"text\", \"title\": \"text\"}, \"primaryKey\": \"id\"}") + TableDefinitionDesc definition, + @ToolArg( + description = "Set ifNotExists to true to succeed when the table already exists", + required = false) CreateTableCommand.Options options) { var command = new CreateTableCommand(table, definition, options); @@ -81,10 +98,15 @@ public Uni createTable( @Tool(description = "Command that creates a user defined type.") public Uni createType( - @ToolArg(description = "Name of the existing keyspace") String keyspace, - @ToolArg(description = "Name of the type to create") String name, - @ToolArg(description = "Type definition") TypeDefinitionDesc definition, - @ToolArg(description = "Configuration options for the command", required = false) + @ToolArg(description = "Name of the keyspace") String keyspace, + @ToolArg(description = "Name of the new type") String name, + @ToolArg( + description = + "Fields of the new type, for example {\"fields\": {\"city\": \"text\", \"postcode\": \"int\"}}") + TypeDefinitionDesc definition, + @ToolArg( + description = "Set ifNotExists to true to succeed when the type already exists", + required = false) CreateTypeCommand.Options options) { var command = new CreateTypeCommand(name, definition, options); @@ -93,9 +115,11 @@ public Uni createType( @Tool(description = "Command that drops an index for a column.") public Uni dropIndex( - @ToolArg(description = "Name of the existing keyspace") String keyspace, - @ToolArg(description = "Name of the Index to remove") String name, - @ToolArg(description = "Dropping index command option.", required = false) + @ToolArg(description = "Name of the keyspace") String keyspace, + @ToolArg(description = "Name of the index to drop") String name, + @ToolArg( + description = "Set ifExists to true to succeed when the index does not exist", + required = false) DropIndexCommand.Options options) { var command = new DropIndexCommand(name, options); @@ -104,9 +128,11 @@ public Uni dropIndex( @Tool(description = "Command that drops a table if one exists.") public Uni dropTable( - @ToolArg(description = "Name of the existing keyspace") String keyspace, - @ToolArg(description = "Name of the Table to remove") String table, - @ToolArg(description = "Optional options for drop command", required = false) + @ToolArg(description = "Name of the keyspace") String keyspace, + @ToolArg(description = "Name of the table to drop") String table, + @ToolArg( + description = "Set ifExists to true to succeed when the table does not exist", + required = false) DropTableCommand.Options options) { var command = new DropTableCommand(table, options); @@ -115,9 +141,11 @@ public Uni dropTable( @Tool(description = "Command that drops a user defined type if one exists.") public Uni dropType( - @ToolArg(description = "Name of the existing keyspace") String keyspace, - @ToolArg(description = "Name of the Type to remove") String name, - @ToolArg(description = "Options for drop command", required = false) + @ToolArg(description = "Name of the keyspace") String keyspace, + @ToolArg(description = "Name of the type to drop") String name, + @ToolArg( + description = "Set ifExists to true to succeed when the type does not exist", + required = false) DropTypeCommand.Options options) { var command = new DropTypeCommand(name, options); @@ -126,8 +154,10 @@ public Uni dropType( @Tool(description = "Command that lists all available tables in a keyspace") public Uni listTables( - @ToolArg(description = "Name of the existing keyspace") String keyspace, - @ToolArg(description = "Options for the `listTables` command.", required = false) + @ToolArg(description = "Name of the keyspace") String keyspace, + @ToolArg( + description = "Set explain to true to include the properties of each table", + required = false) ListTablesCommand.Options options) { var command = new ListTablesCommand(options); @@ -136,8 +166,10 @@ public Uni listTables( @Tool(description = "Command that lists all available types in a keyspace") public Uni listTypes( - @ToolArg(description = "Name of the existing keyspace") String keyspace, - @ToolArg(description = "Options for the `listTypes` command.", required = false) + @ToolArg(description = "Name of the keyspace") String keyspace, + @ToolArg( + description = "Set explain to true to include the fields of each type", + required = false) ListTypesCommand.Options options) { var command = new ListTypesCommand(options); diff --git a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/mcp/McpClauseSchemaContractTest.java b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/mcp/McpClauseSchemaContractTest.java index 99f5e33de0..372bb3eea0 100644 --- a/src/test/java/io/stargate/sgv2/jsonapi/api/v1/mcp/McpClauseSchemaContractTest.java +++ b/src/test/java/io/stargate/sgv2/jsonapi/api/v1/mcp/McpClauseSchemaContractTest.java @@ -5,6 +5,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; @@ -331,26 +332,78 @@ void rerankToolUsesSortClassDescription() throws Exception { schemaMetadata(document, propertySchema(document, "sort"), "description").asText()); } + /** Clause arguments take their descriptions from their classes, which Swagger also uses. */ @Test - void rerankToolUsesFilterClassDescription() throws Exception { - var description = FilterDefinition.class.getAnnotation(Schema.class).description(); - assertFalse(description.isBlank()); - var document = inputSchema("findAndRerank"); - assertEquals( - description, - schemaMetadata(document, propertySchema(document, "filter"), "description").asText()); + void clauseArgumentsUseClassDescriptions() throws Exception { + var filter = FilterDefinition.class.getAnnotation(Schema.class).description(); + var sort = SortDefinition.class.getAnnotation(Schema.class).description(); + var checked = 0; + for (var tool : toolManager) { + var document = inputSchema(tool.name()); + var properties = document.path("properties"); + if (properties.has("filter")) { + assertEquals( + filter, + schemaMetadata(document, properties.path("filter"), "description").asText(), + tool.name()); + checked++; + } + // findAndRerank uses FindAndRerankSort, checked in rerankToolUsesSortClassDescription. + if (properties.has("sort") && !tool.name().equals("findAndRerank")) { + assertEquals( + sort, + schemaMetadata(document, properties.path("sort"), "description").asText(), + tool.name()); + checked++; + } + } + assertTrue(checked > 0, "No tool has a filter or sort argument"); } - /** A description that only repeats the argument name tells a client nothing. */ + /** + * A description that only repeats the argument name tells a client nothing. The schema already + * lists required arguments, so descriptions do not start with "Optional" or "Required". + */ @Test - void rerankToolDescribesEveryArgument() throws Exception { - var document = inputSchema("findAndRerank"); - for (var argument : document.path("properties").properties()) { - var name = argument.getKey(); - var description = schemaMetadata(document, argument.getValue(), "description").asText(); - assertFalse(description.isBlank(), name); - assertNotEquals(name, description.toLowerCase(), name); + void everyToolDescribesEveryArgument() throws Exception { + for (var tool : toolManager) { + var document = inputSchema(tool.name()); + for (var argument : document.path("properties").properties()) { + var label = tool.name() + "." + argument.getKey(); + var description = schemaMetadata(document, argument.getValue(), "description").asText(); + assertFalse(description.isBlank(), label); + assertNotEquals(argument.getKey(), description.toLowerCase(), label); + assertFalse(description.matches("(?i)(optional|required)\\b.*"), label); + } + } + } + + /** + * Clients copy the example at the end of an argument description, so the server must parse it as + * that argument. Several argument schemas still show Java fields rather than the JSON, which + * makes the example the only reliable guide. + */ + @Test + void argumentDescriptionExamplesDeserialize() throws Exception { + var marker = "for example "; + // Trailing text after the example would mean the example is not the whole JSON value. + var reader = objectMapper.reader().with(DeserializationFeature.FAIL_ON_TRAILING_TOKENS); + var checked = 0; + for (var tool : toolManager) { + for (var argument : tool.arguments()) { + var start = argument.description().indexOf(marker); + if (start < 0) { + continue; + } + var label = tool.name() + "." + argument.name(); + var example = reader.readTree(argument.description().substring(start + marker.length())); + var type = objectMapper.constructType(argument.type()); + assertNotNull( + assertDoesNotThrow(() -> objectMapper.treeToValue(example, type), label), label); + checked++; + } } + assertTrue(checked > 0, "No argument description has an example"); } @Test