Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4cbb082
docs: add design spec and implementation plan for schema modification
adityamparikh May 17, 2026
2049a1c
feat(metadata): add SchemaUpdateResult record for schema modification…
adityamparikh May 17, 2026
c6629d2
chore(metadata): align SchemaUpdateResult Jackson annotations with pr…
adityamparikh May 17, 2026
5eadc89
refactor: rename metadata package to schema
adityamparikh May 17, 2026
592ede0
feat(schema): add add-fields MCP tool for additive schema modification
adityamparikh May 17, 2026
e0bd778
chore(schema): cast field name directly instead of String.valueOf
adityamparikh May 17, 2026
bf71734
feat(schema): add add-field-types MCP tool with FieldTypeDefinition h…
adityamparikh May 17, 2026
2c57bc3
test(schema): add whitespace-only collection assertion to addFieldTyp…
adityamparikh May 17, 2026
3504dd5
feat(config): register SchemaUpdateResult for GraalVM native image re…
adityamparikh May 17, 2026
4f367a9
test(schema): integration tests for add-fields and add-field-types
adityamparikh May 17, 2026
2034c58
test: extend MCP client integration tests for schema modification tools
adityamparikh May 17, 2026
87ca85d
docs: document add-fields and add-field-types MCP tools in README
adityamparikh May 17, 2026
188d760
docs: update CLAUDE.md SchemaService entry for new schema-modificatio…
adityamparikh May 17, 2026
dc285e7
fix(native): register AnalyzerDefinition and FieldTypeDefinition for …
adityamparikh May 17, 2026
d4dbb32
fix(native): register SchemaRepresentation for reflection
adityamparikh May 18, 2026
f3fbb42
test: end-to-end shows workflow with 61-doc realistic dataset
adityamparikh May 18, 2026
524b2b6
fix(schema): correct atomicity wording and preserve unknown analyzer …
adityamparikh May 18, 2026
7e176b6
refactor(schema): drop noise fields from SchemaUpdateResult
adityamparikh May 18, 2026
9245191
feat(mcp): add @McpPrompt endpoints for the four canonical Solr workf…
adityamparikh May 19, 2026
e27e8f3
refactor(mcp): split, add view-schema, and tighten prompt code
adityamparikh May 19, 2026
12dafe4
refactor(mcp): inline prompt names, text blocks for JSON in tests, dr…
adityamparikh May 19, 2026
6eee805
feat(completion): filter collection completions by user-typed prefix
claude May 20, 2026
1136670
feat(security): require auth on every @McpPrompt method
adityamparikh May 22, 2026
a6cc51f
Merge PR #86 (add-mcp-prompts) into combine-86-87
adityamparikh May 25, 2026
c65b393
Merge PR #87 (add-mcp-completions) into combine-86-87
adityamparikh May 25, 2026
9a2ca1a
feat(mcp): annotate tools with behavior hints (readOnly/destructive/i…
adityamparikh May 22, 2026
be056bb
feat(completion): handle prompt-arg completion for collection-taking …
adityamparikh May 25, 2026
31feb4e
style(mcp): one-arg-per-line for multi-arg annotations; fix schema-wr…
adityamparikh May 25, 2026
8aa7ab7
fix(native): let native-http boot when no OAuth2 issuer URL is config…
adityamparikh May 27, 2026
0c5cae3
Merge remote-tracking branch 'upstream/main' into combine-86-87
adityamparikh May 27, 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
5 changes: 3 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,9 @@ spotless {
target("src/**/*.java")
// Use Eclipse JDT formatter to avoid google-java-format's incompatibility
// with cutting-edge JDKs (e.g., 25) which can trigger NoSuchMethodError
// against internal javac classes.
eclipse()
// against internal javac classes. Override only the annotation-argument
// alignment so multi-arg @Mcp* annotations render one-arg-per-line.
eclipse().configFile("config/spotless/eclipse-java-formatter.properties")
removeUnusedImports()
trimTrailingWhitespace()
endWithNewline()
Expand Down
16 changes: 16 additions & 0 deletions config/spotless/eclipse-java-formatter.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Eclipse JDT formatter overrides layered on top of the built-in defaults that
# Spotless's eclipse() formatter ships with. Only keys listed here override the
# defaults; everything else stays at Eclipse's stock values.
#
# Why: we want every multi-argument annotation (especially @McpTool, @McpPrompt,
# @McpResource, @McpArg, @McpToolParam) to render with one argument per line so
# the behavior hints (readOnlyHint / destructiveHint / idempotentHint) and the
# descriptive copy stay readable at a glance. The default value here is
# M_COMPACT_SPLIT (16), which only wraps when the line gets too long — leaving
# many annotations collapsed onto a single 200-column line.
#
# Value 48 = M_ONE_PER_LINE_SPLIT (without M_FORCE). The formatter wraps to one
# argument per line only when the line is too long, so single-arg annotations
# like @McpToolParam(description = "...") stay on one line. See:
# org.eclipse.jdt.core.formatter.DefaultCodeFormatterConstants
org.eclipse.jdt.core.formatter.alignment_for_arguments_in_annotation=48
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import io.micrometer.observation.annotation.Observed;
import io.modelcontextprotocol.spec.McpSchema.CompleteRequest;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrServerException;
Expand All @@ -41,7 +43,10 @@
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.mcp.server.config.SolrConfigurationProperties;
import org.apache.solr.mcp.server.util.PromptNames;
import org.springaicommunity.mcp.annotation.McpArg;
import org.springaicommunity.mcp.annotation.McpComplete;
import org.springaicommunity.mcp.annotation.McpPrompt;
import org.springaicommunity.mcp.annotation.McpResource;
import org.springaicommunity.mcp.annotation.McpTool;
import org.springaicommunity.mcp.annotation.McpToolParam;
Expand Down Expand Up @@ -282,24 +287,95 @@ public CollectionService(SolrClient solrClient, ObjectMapper objectMapper) {
*
* @return JSON string containing the list of collections
*/
@McpResource(uri = "solr://collections", name = "solr-collections", description = "List of all Solr collections available in the cluster", mimeType = "application/json")
@PreAuthorize("isAuthenticated()")
@McpResource(
uri = "solr://collections",
name = "solr-collections",
description = "List of all Solr collections available in the cluster",
mimeType = "application/json")
public String getCollectionsResource() throws SolrServerException, IOException {
return toJson(objectMapper, listCollections());
}

/** Maximum number of completion suggestions returned per request. */
static final int MAX_COMPLETION_RESULTS = 100;

/**
* MCP Completion endpoint for collection name autocompletion.
* MCP Completion endpoint for the {@code {collection}} segment of the
* {@code solr://{collection}/schema} resource template.
*
* <p>
* Provides autocompletion support for the collection parameter in the schema
* resource URI template. Returns all available collection names that MCP
* clients can use to complete the {collection} placeholder.
*
* @return list of available collection names for autocompletion
* Returns collection names that start with the user-supplied prefix
* (case-insensitive). When the prefix is empty all collections are returned,
* subject to {@link #MAX_COMPLETION_RESULTS}. The results are sorted so that
* client UIs see a stable ordering.
*
* @param argument
* the partial value the client is completing; its
* {@link CompleteRequest.CompleteArgument#name() name} must be
* {@code collection}
* @return matching collection names, capped at {@link #MAX_COMPLETION_RESULTS}
*/
@PreAuthorize("isAuthenticated()")
@McpComplete(uri = "solr://{collection}/schema")
public List<String> completeCollectionForSchema() throws SolrServerException, IOException {
return listCollections();
public List<String> completeCollection(CompleteRequest.CompleteArgument argument) {
if (argument == null || !"collection".equals(argument.name())) {
return List.of();
}
String prefix = argument.value() == null ? "" : argument.value().toLowerCase(Locale.ROOT);
try {
return listCollections().stream().filter(c -> c != null && c.toLowerCase(Locale.ROOT).startsWith(prefix))
.sorted().limit(MAX_COMPLETION_RESULTS).toList();
} catch (SolrServerException | IOException _) {
return List.of();
}
}

/**
* Completion for the {@code collection} argument of the
* {@code search-collection} prompt (defined in {@code SearchService}).
*
* <p>
* {@code @McpComplete} registers a handler per {@code (ref/prompt, name)} pair,
* so a prompt that takes a collection argument needs its own handler — the
* resource-template handler on {@link #completeCollection} only matches
* {@code ref/resource}. Each wrapper delegates so all collection-name
* completion shares one implementation and one cap.
*/
@PreAuthorize("isAuthenticated()")
@McpComplete(prompt = PromptNames.SEARCH_COLLECTION)
public List<String> completeSearchCollectionPromptArg(CompleteRequest.CompleteArgument argument) {
return completeCollection(argument);
}

/**
* Completion for the {@code collection} argument of the {@code index-data}
* prompt.
*/
@PreAuthorize("isAuthenticated()")
@McpComplete(prompt = PromptNames.INDEX_DATA)
public List<String> completeIndexDataPromptArg(CompleteRequest.CompleteArgument argument) {
return completeCollection(argument);
}

/**
* Completion for the {@code collection} argument of the {@code view-schema}
* prompt.
*/
@PreAuthorize("isAuthenticated()")
@McpComplete(prompt = PromptNames.VIEW_SCHEMA)
public List<String> completeViewSchemaPromptArg(CompleteRequest.CompleteArgument argument) {
return completeCollection(argument);
}

/**
* Completion for the {@code collection} argument of the {@code design-schema}
* prompt.
*/
@PreAuthorize("isAuthenticated()")
@McpComplete(prompt = PromptNames.DESIGN_SCHEMA)
public List<String> completeDesignSchemaPromptArg(CompleteRequest.CompleteArgument argument) {
return completeCollection(argument);
}

/**
Expand Down Expand Up @@ -331,7 +407,10 @@ public List<String> completeCollectionForSchema() throws SolrServerException, IO
* @see CollectionAdminRequest.List
*/
@PreAuthorize("isAuthenticated()")
@McpTool(name = "list-collections", annotations = @McpTool.McpAnnotations(readOnlyHint = true), description = "List solr collections")
@McpTool(
name = "list-collections",
annotations = @McpTool.McpAnnotations(readOnlyHint = true),
description = "List solr collections")
public List<String> listCollections() throws SolrServerException, IOException {
CollectionAdminRequest.List request = new CollectionAdminRequest.List();
CollectionAdminResponse response = request.process(solrClient);
Expand Down Expand Up @@ -402,7 +481,10 @@ public List<String> listCollections() throws SolrServerException, IOException {
* @see #extractCollectionName(String)
*/
@PreAuthorize("isAuthenticated()")
@McpTool(name = "get-collection-stats", annotations = @McpTool.McpAnnotations(readOnlyHint = true), description = "Get stats/metrics on a Solr collection")
@McpTool(
name = "get-collection-stats",
annotations = @McpTool.McpAnnotations(readOnlyHint = true),
description = "Get stats/metrics on a Solr collection")
public SolrMetrics getCollectionStats(
@McpToolParam(description = "Solr collection to get stats/metrics for") String collection)
throws SolrServerException, IOException {
Expand Down Expand Up @@ -944,7 +1026,10 @@ private boolean validateCollectionExists(String collection) throws SolrServerExc
* @see SolrPingResponse
*/
@PreAuthorize("isAuthenticated()")
@McpTool(name = "check-health", annotations = @McpTool.McpAnnotations(readOnlyHint = true), description = "Check health of a Solr collection")
@McpTool(
name = "check-health",
annotations = @McpTool.McpAnnotations(readOnlyHint = true),
description = "Check health of a Solr collection")
public SolrHealthStatus checkHealth(@McpToolParam(description = "Solr collection") String collection) {
String actualCollection = extractCollectionName(collection);
try {
Expand Down Expand Up @@ -996,13 +1081,20 @@ public SolrHealthStatus checkHealth(@McpToolParam(description = "Solr collection
* if there are I/O errors during communication
*/
@PreAuthorize("isAuthenticated()")
@McpTool(name = "create-collection", annotations = @McpTool.McpAnnotations(destructiveHint = false), description = "Create a new Solr collection. "
+ "configSet defaults to _default, numShards and replicationFactor default to 1.")
@McpTool(
name = "create-collection",
annotations = @McpTool.McpAnnotations(destructiveHint = false),
description = "Create a new Solr collection. "
+ "configSet defaults to _default, numShards and replicationFactor default to 1.")
public CollectionCreationResult createCollection(
@McpToolParam(description = "Name of the collection to create") String name,
@McpToolParam(description = "Configset name. Defaults to _default.", required = false) String configSet,
@McpToolParam(description = "Number of shards (SolrCloud only). Defaults to 1.", required = false) Integer numShards,
@McpToolParam(description = "Replication factor (SolrCloud only). Defaults to 1.", required = false) Integer replicationFactor)
@McpToolParam(
description = "Number of shards (SolrCloud only). Defaults to 1.",
required = false) Integer numShards,
@McpToolParam(
description = "Replication factor (SolrCloud only). Defaults to 1.",
required = false) Integer replicationFactor)
throws SolrServerException, IOException {

if (name == null || name.isBlank()) {
Expand All @@ -1018,4 +1110,78 @@ public CollectionCreationResult createCollection(

return new CollectionCreationResult(name, true, "Collection created successfully", new Date());
}

@PreAuthorize("isAuthenticated()")

@McpPrompt(
name = PromptNames.EXPLORE_COLLECTIONS,
title = "Explore Solr collections",
description = "Read-only walkthrough: list collections and characterise each by stats and health.")
public String exploreCollectionsPrompt() {
return """
You are exploring an Apache Solr cluster through MCP tools. Goal: produce a concise,
accurate picture of what already exists. This prompt is read-only; do not create or
modify anything.

1. List collections.
- Call `list-collections` to get the full set of collection names.
- If the user mentioned a target collection by name, note whether it appears in the
list.

2. Characterize each interesting collection.
- For each collection the user cares about (or a small representative sample if they
did not name one), call `get-collection-stats` to read numDocs, segment counts, and
cache/handler metrics, and `check-health` to confirm the collection responds to a
ping and the doc count is non-zero where expected.

3. Summarize.
- Tell the user which collections exist, which look healthy, and which look empty or
stale.
- If the user's intent does not match any existing collection, suggest the
`setup-collection` prompt to create one.
""";
}

@PreAuthorize("isAuthenticated()")

@McpPrompt(
name = PromptNames.SETUP_COLLECTION,
title = "Set up a new Solr collection",
description = "Guided workflow: validate a name, pick configset / shards / replication factor, create the collection, and verify it.")
public String setupCollectionPrompt(@McpArg(
name = "name",
description = "Desired collection name. Lowercase letters, digits, underscores, hyphens — no spaces.",
required = true) String name,
@McpArg(
name = "purpose",
description = "Optional one-line description of what the collection is for (used only to ground the conversation).",
required = false) String purpose) {
String purposeLine = (purpose == null || purpose.isBlank()) ? "" : "\nPurpose: %s\n".formatted(purpose.strip());
return """
You are setting up a new Solr collection named `%s` through MCP tools.%s
1. Validate the name.
- Lowercase letters, digits, underscores, hyphens only — no spaces or uppercase.
- Call `list-collections` and confirm `%s` does not already exist. If it does, stop
and tell the user.

2. Choose creation parameters.
- Defaults: configset `%s`, %d shard(s), replicationFactor %d. These work for most
single-node and small SolrCloud setups.
- Only override if the user asked: a custom configset for a pre-built schema, more
shards for a large dataset, or higher replicationFactor for redundancy.

3. Create.
- Call `create-collection` with `name=%s` and any non-default `configSet`,
`numShards`, `replicationFactor` values. Report the result (success flag +
message).

4. Verify.
- Call `list-collections` again; `%s` should appear.
- Call `check-health` on `%s`; it should respond to ping.

Next step suggestion: define the schema. Use the `design-schema` prompt to design
fields for the dataset the user wants to index.
""".formatted(name, purposeLine, name, DEFAULT_CONFIGSET, DEFAULT_NUM_SHARDS,
DEFAULT_REPLICATION_FACTOR, name, name, name);
}
}
Loading
Loading