This is an automated email from the ASF dual-hosted git repository.

epugh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-mcp.git


The following commit(s) were added to refs/heads/main by this push:
     new c1c2a8e  fix: correct indexing, collection-name and service defects 
(#176)
c1c2a8e is described below

commit c1c2a8ef86ddffc9a040e218e06a2e44c250f5ed
Author: Aditya Parikh <[email protected]>
AuthorDate: Tue Aug 18 17:25:48 2026 -0400

    fix: correct indexing, collection-name and service defects (#176)
    
    Verified findings from a CodeRabbit review. Two change observable behaviour
    and update the tests that pinned the old behaviour.
    
    Silent data loss:
    - JsonDocumentCreator returned an empty list for a bare JSON object, so
      indexing a single document indexed nothing and still reported success. It
      is now indexed as one document; testNonArrayJsonInput updated accordingly.
    - processArrayField filtered out objects but not nested arrays, so a nested
      array reached asString() and threw.
    
    Wrong collection resolved:
    - CollectionService.extractCollectionName truncated at any "_shard", so a
      collection named orders_shard_archive resolved to orders. Anchored to the
      real SolrCloud core suffix _shard<N>[_replica...]. Consequently
      "collection_shard" (no shard number, not a core name) now survives intact,
      and data_shard1_shard2_replica_n1 resolves to data_shard1 rather than data
      - correct for a collection whose own name ends in _shard1. The two test
      expectations that encoded the old behaviour are updated with comments.
    
    Other correctness:
    - SolrConfig detected the Solr path by searching the whole URL string, so a
      host named "solr" matched the "/solr/" inside the authority and skipped
      normalisation. Match on the URL path only. Also moved the SolrClient
      factory Javadoc, which sat above jsonResponseParser(ObjectMapper).
    - SearchService passed sort clauses straight to SolrQuery.SortClause, whose
      constructor calls ORDER.valueOf(): a missing "order" key threw NPE and an
      unknown one threw an opaque IllegalArgumentException. Validate both keys
      with messages that say what to send. The search prompt also advertised a
      "sortFields" parameter that does not exist; it is "sortClauses".
    - SchemaService cast definition.get("name") directly, recording a null name
      when absent and throwing a bare ClassCastException otherwise. It also
      concatenated an exception message into JSON, so a message containing a
      quote or newline produced invalid JSON.
    - FieldNameSanitizer used a locale-sensitive toLowerCase(); under a Turkish
      default locale 'I' maps to a dotless 'i' and changes the field name.
    - CollectionUtils.getFloat's Javadoc promised 0.0f for missing values while
      the method returns null.
    - Dtos pinned timezone = "UTC" on the @JsonFormat patterns. With the default
      ObjectMapper this is a no-op (Jackson defaults to UTC), but setting
      spring.jackson.time-zone would otherwise emit local time carrying a
      literal 'Z' - verified: the same instant serialises as
      1970-01-01T05:30:00.000Z under Asia/Kolkata.
    - logback-spring.xml scoped the HTTP CONSOLE appender to "http & !stdio".
      Activating both profiles attached a stdout appender while STDIO was
      transporting JSON-RPC over that same stream.
    
    Signed-off-by: Aditya Parikh <[email protected]>
    Co-authored-by: Claude Opus 5 (1M context) <[email protected]>
    Co-authored-by: Eric Pugh <[email protected]>
---
 .../mcp/server/collection/CollectionService.java   | 21 +++++-----
 .../mcp/server/collection/CollectionUtils.java     | 20 +++------
 .../apache/solr/mcp/server/config/SolrConfig.java  | 47 ++++++++++++++--------
 .../documentcreator/CsvDocumentCreator.java        |  3 ++
 .../documentcreator/FieldNameSanitizer.java        | 13 ++++--
 .../documentcreator/JsonDocumentCreator.java       | 15 ++++++-
 .../solr/mcp/server/schema/SchemaService.java      | 32 +++++++++++++--
 .../solr/mcp/server/search/SearchService.java      | 36 +++++++++++++++--
 src/main/resources/logback-spring.xml              |  8 +++-
 .../server/collection/CollectionServiceTest.java   | 23 +++++++++--
 .../indexing/IndexingServiceIntegrationTest.java   |  7 +++-
 11 files changed, 168 insertions(+), 57 deletions(-)

diff --git 
a/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java 
b/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java
index c082ce1..8a259e6 100644
--- a/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java
+++ b/src/main/java/org/apache/solr/mcp/server/collection/CollectionService.java
@@ -145,6 +145,14 @@ public class CollectionService {
        /** Suffix pattern used to identify shard names in SolrCloud 
deployments */
        private static final String SHARD_SUFFIX = "_shard";
 
+       /**
+        * Matches a SolrCloud shard/replica suffix at the end of a core name, 
e.g.
+        * {@code _shard1} or {@code _shard1_replica_n1}. Anchored so that 
collection
+        * names merely containing "_shard" are left intact.
+        */
+       private static final java.util.regex.Pattern SHARD_SUFFIX_PATTERN = 
java.util.regex.Pattern
+                       .compile("_shard\\d+(_replica.*)?$");
+
        /** Request parameter name for specifying response writer type */
        private static final String WT_PARAM = "wt";
 
@@ -948,15 +956,10 @@ public class CollectionService {
                        return collectionOrShard;
                }
 
-               // Check if this looks like a shard name (contains "_shard" 
pattern)
-               if (collectionOrShard.contains(SHARD_SUFFIX)) {
-                       // Extract collection name before "_shard"
-                       int shardIndex = 
collectionOrShard.indexOf(SHARD_SUFFIX);
-                       return collectionOrShard.substring(0, shardIndex);
-               }
-
-               // If it doesn't look like a shard name, return as-is
-               return collectionOrShard;
+               // Strip only a real SolrCloud shard/replica suffix. Matching a 
bare
+               // "_shard" anywhere would truncate legitimate collection names 
such as
+               // "orders_shard_archive" down to "orders".
+               return 
SHARD_SUFFIX_PATTERN.matcher(collectionOrShard).replaceFirst("");
        }
 
        /**
diff --git 
a/src/main/java/org/apache/solr/mcp/server/collection/CollectionUtils.java 
b/src/main/java/org/apache/solr/mcp/server/collection/CollectionUtils.java
index 2df9309..f64e878 100644
--- a/src/main/java/org/apache/solr/mcp/server/collection/CollectionUtils.java
+++ b/src/main/java/org/apache/solr/mcp/server/collection/CollectionUtils.java
@@ -148,12 +148,13 @@ public class CollectionUtils {
         * numeric types returned by Solr.
         *
         * <p>
-        * <strong>Default Value Behavior:</strong>
+        * <strong>Missing Value Behavior:</strong>
         *
         * <p>
-        * Returns {@code 0.0f} for missing or null values, which is typically 
the
-        * desired behavior for metrics like hit ratios, performance averages, 
and
-        * statistical calculations where missing data should be interpreted as 
zero.
+        * Returns {@code null} when the key is absent or its value is null, so 
callers
+        * can distinguish "no data reported" from a measured zero. A Solr 
endpoint that
+        * is unavailable reports no metric at all, which is not the same as a 
hit ratio
+        * of 0.
         *
         * <p>
         * <strong>Common Use Cases:</strong>
@@ -164,20 +165,11 @@ public class CollectionUtils {
         * <li>Statistical calculations and performance indicators
         * </ul>
         *
-        * <p>
-        * <strong>Note:</strong>
-        *
-        * <p>
-        * This method differs from {@link #getLong(NamedList, String)} by 
returning a
-        * default value instead of null, which is more appropriate for Float 
metrics
-        * that represent rates, ratios, or averages.
-        *
         * @param stats
         *            the NamedList containing the metric data to extract from
         * @param key
         *            the key to look up in the NamedList
-        * @return the Float value if found, or 0.0f if the key doesn't exist 
or value
-        *         is null
+        * @return the Float value if found and convertible, {@code null} 
otherwise
         * @see Number#floatValue()
         */
        public static Float getFloat(NamedList<Object> stats, String key) {
diff --git a/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java 
b/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java
index ae0eae4..dceb10c 100644
--- a/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java
+++ b/src/main/java/org/apache/solr/mcp/server/config/SolrConfig.java
@@ -17,6 +17,7 @@
 package org.apache.solr.mcp.server.config;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
+import java.net.URI;
 import java.util.concurrent.TimeUnit;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.impl.HttpJdkSolrClient;
@@ -167,33 +168,47 @@ public class SolrConfig {
         * @param properties
         *            the injected Solr configuration properties containing 
connection
         *            URL
+        * @param jsonResponseParser
+        *            the parser that converts Solr's JSON responses into the 
NamedList
+        *            tree SolrJ expects
         * @return configured SolrClient instance ready for use in application 
services
         * @see HttpJdkSolrClient.Builder
         * @see SolrConfigurationProperties#url()
         */
        @Bean
-       JsonResponseParser jsonResponseParser(ObjectMapper objectMapper) {
-               return new JsonResponseParser(objectMapper);
+       SolrClient solrClient(SolrConfigurationProperties properties, 
JsonResponseParser jsonResponseParser) {
+               return buildSolrClient(properties, jsonResponseParser);
        }
 
+       /**
+        * Response parser used by {@link #solrClient}, requesting {@code 
wt=json} and
+        * converting the response into SolrJ's
+        * {@link org.apache.solr.common.util.NamedList} tree.
+        *
+        * @param objectMapper
+        *            the application's Jackson mapper, reused so Solr 
responses are
+        *            parsed with the same configuration as the rest of the app
+        * @return the JSON response parser
+        */
        @Bean
-       SolrClient solrClient(SolrConfigurationProperties properties, 
JsonResponseParser jsonResponseParser) {
-               String url = properties.url();
+       JsonResponseParser jsonResponseParser(ObjectMapper objectMapper) {
+               return new JsonResponseParser(objectMapper);
+       }
 
-               // Ensure URL is properly formatted for Solr
-               // The URL should end with /solr/ for proper path construction
-               if (!url.endsWith("/")) {
-                       url = url + "/";
+       private static SolrClient buildSolrClient(SolrConfigurationProperties 
properties,
+                       JsonResponseParser jsonResponseParser) {
+               // Normalise against the URL's *path* only. Testing the whole 
URL string
+               // would see the "/solr/" inside an authority such as 
http://solr/ and
+               // wrongly conclude the path was already present.
+               URI parsed = URI.create(properties.url());
+               String path = parsed.getPath() == null ? "" : parsed.getPath();
+               if (!path.endsWith("/")) {
+                       path = path + "/";
                }
-
-               // If URL doesn't contain /solr/ path, add it
-               if (!url.endsWith("/" + SOLR_PATH) && !url.contains("/" + 
SOLR_PATH)) {
-                       if (url.endsWith("/")) {
-                               url = url + SOLR_PATH;
-                       } else {
-                               url = url + "/" + SOLR_PATH;
-                       }
+               if (!path.contains("/" + SOLR_PATH)) {
+                       path = path + SOLR_PATH;
                }
+               String url = parsed.resolve(path).toString();
 
                // JSON wire format for responses; XML wire format for update 
requests.
                // The default JavaBin request writer uses a binary codec that 
requires
diff --git 
a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/CsvDocumentCreator.java
 
b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/CsvDocumentCreator.java
index bf56679..6c0a664 100644
--- 
a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/CsvDocumentCreator.java
+++ 
b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/CsvDocumentCreator.java
@@ -109,6 +109,9 @@ public class CsvDocumentCreator implements 
SolrDocumentCreator {
         * @see FieldNameSanitizer#sanitizeFieldName(String)
         */
        public List<SolrInputDocument> create(String csv) throws 
DocumentProcessingException {
+               if (csv.isBlank()) {
+                       throw new DocumentProcessingException("CSV input cannot 
be empty");
+               }
                if (csv.getBytes(StandardCharsets.UTF_8).length > 
MAX_INPUT_SIZE_BYTES) {
                        throw new DocumentProcessingException(
                                        "Input too large: exceeds maximum size 
of " + MAX_INPUT_SIZE_BYTES + " bytes");
diff --git 
a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/FieldNameSanitizer.java
 
b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/FieldNameSanitizer.java
index fb175a5..7b63671 100644
--- 
a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/FieldNameSanitizer.java
+++ 
b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/FieldNameSanitizer.java
@@ -16,6 +16,7 @@
  */
 package org.apache.solr.mcp.server.indexing.documentcreator;
 
+import java.util.Locale;
 import java.util.regex.Pattern;
 
 /**
@@ -88,15 +89,21 @@ public final class FieldNameSanitizer {
         * @param fieldName
         *            the original field name to sanitize
         * @return sanitized field name compatible with Solr requirements, or 
"field" if
-        *         input is null/empty
+        *         the input is empty or sanitizes away to nothing
         * @see <a href=
         *      
"https://solr.apache.org/guide/solr/latest/indexing-guide/fields.html";>Solr
         *      Field Guide</a>
         */
        public static String sanitizeFieldName(String fieldName) {
 
-               // Convert to lowercase and replace invalid characters with 
underscores
-               String sanitized = 
INVALID_CHARACTERS_PATTERN.matcher(fieldName.toLowerCase()).replaceAll("_");
+               // No null guard: this package is @NullMarked and NullAway 
enforces the
+               // non-null contract at compile time. An empty input falls 
through to the
+               // isEmpty() check below and yields the default name.
+
+               // Convert to lowercase and replace invalid characters with 
underscores.
+               // Locale.ROOT keeps this deterministic - the default locale 
would map
+               // 'I' to a dotless 'ı' under a Turkish locale and produce a 
different field.
+               String sanitized = 
INVALID_CHARACTERS_PATTERN.matcher(fieldName.toLowerCase(Locale.ROOT)).replaceAll("_");
 
                // Remove leading/trailing underscores and collapse multiple 
underscores
                sanitized = 
LEADING_TRAILING_UNDERSCORES_PATTERN.matcher(sanitized).replaceAll("");
diff --git 
a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java
 
b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java
index 457a3cd..605e520 100644
--- 
a/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java
+++ 
b/src/main/java/org/apache/solr/mcp/server/indexing/documentcreator/JsonDocumentCreator.java
@@ -116,6 +116,9 @@ public class JsonDocumentCreator implements 
SolrDocumentCreator {
         * @see FieldNameSanitizer#sanitizeFieldName(String)
         */
        public List<SolrInputDocument> create(String json) throws 
DocumentProcessingException {
+               if (json.isBlank()) {
+                       throw new DocumentProcessingException("JSON input 
cannot be empty");
+               }
                if (json.getBytes(StandardCharsets.UTF_8).length > 
MAX_INPUT_SIZE_BYTES) {
                        throw new DocumentProcessingException(
                                        "Input too large: exceeds maximum size 
of " + MAX_INPUT_SIZE_BYTES + " bytes");
@@ -134,6 +137,14 @@ public class JsonDocumentCreator implements 
SolrDocumentCreator {
                                        addAllFieldsFlat(doc, item, "");
                                        documents.add(doc);
                                }
+                       } else if (rootNode.isObject()) {
+                               // A single document. Previously fell through 
and returned an empty
+                               // list, so indexing one object silently 
indexed nothing.
+                               SolrInputDocument doc = new SolrInputDocument();
+                               addAllFieldsFlat(doc, rootNode, "");
+                               documents.add(doc);
+                       } else {
+                               throw new DocumentProcessingException("JSON 
input must be an object or an array of objects");
                        }
                } catch (IOException e) {
                        throw new DocumentProcessingException("Failed to parse 
JSON document", e);
@@ -219,7 +230,9 @@ public class JsonDocumentCreator implements 
SolrDocumentCreator {
        private void processArrayField(SolrInputDocument doc, JsonNode 
arrayValue, String fieldName) {
                List<Object> values = new ArrayList<>();
                for (JsonNode item : arrayValue) {
-                       if (!item.isObject()) {
+                       // Skip objects and nested arrays alike: neither has a 
scalar
+                       // representation, and asString() on a container node 
throws.
+                       if (!item.isObject() && !item.isArray()) {
                                values.add(convertJsonValue(item));
                        }
                }
diff --git a/src/main/java/org/apache/solr/mcp/server/schema/SchemaService.java 
b/src/main/java/org/apache/solr/mcp/server/schema/SchemaService.java
index 07253b6..3f3bb96 100644
--- a/src/main/java/org/apache/solr/mcp/server/schema/SchemaService.java
+++ b/src/main/java/org/apache/solr/mcp/server/schema/SchemaService.java
@@ -185,7 +185,10 @@ public class SchemaService {
                try {
                        return toJson(objectMapper, getSchema(collection));
                } catch (Exception e) {
-                       return "{\"error\": \"" + e.getMessage() + "\"}";
+                       // Serialise via Jackson rather than concatenating: an 
exception message
+                       // containing a quote, backslash or newline would 
otherwise emit invalid
+                       // JSON to the MCP client.
+                       return toJson(objectMapper, Map.of("error", 
String.valueOf(e.getMessage())));
                }
        }
 
@@ -316,7 +319,7 @@ public class SchemaService {
                List<String> names = new ArrayList<>(fields.size());
                List<SchemaRequest.Update> updates = new 
ArrayList<>(fields.size());
                for (Map<String, Object> field : fields) {
-                       names.add((String) field.get("name"));
+                       names.add(requireName(field, "field"));
                        updates.add(new SchemaRequest.AddField(field));
                }
 
@@ -366,7 +369,7 @@ public class SchemaService {
                List<String> names = new ArrayList<>(fieldTypes.size());
                List<SchemaRequest.Update> updates = new 
ArrayList<>(fieldTypes.size());
                for (Map<String, Object> fieldType : fieldTypes) {
-                       names.add((String) fieldType.get("name"));
+                       names.add(requireName(fieldType, "field type"));
                        updates.add(new 
SchemaRequest.AddFieldType(toFieldTypeDefinition(fieldType)));
                }
 
@@ -374,6 +377,29 @@ public class SchemaService {
                return new SchemaUpdateResult(collection, names);
        }
 
+       /**
+        * Extracts and validates the {@code name} entry of a schema definition.
+        *
+        * <p>
+        * Casting {@code get("name")} directly would record a {@code null} 
name for a
+        * definition that omits it, or throw a bare {@link ClassCastException} 
if it is
+        * not a string - neither tells the caller which entry was malformed.
+        *
+        * @param definition
+        *            the caller-supplied field or field-type definition
+        * @param kind
+        *            human-readable noun used in the error message
+        * @return the validated name
+        */
+       private static String requireName(Map<String, Object> definition, 
String kind) {
+               Object name = definition.get("name");
+               if (!(name instanceof String s) || s.isBlank()) {
+                       throw new IllegalArgumentException(
+                                       "Each " + kind + " definition requires 
a non-empty string 'name'; got: " + name);
+               }
+               return s;
+       }
+
        /**
         * Builds a {@link FieldTypeDefinition} from a flat input map matching 
the Solr
         * Schema API add-field-type JSON shape. SolrJ's {@code 
FieldTypeDefinition}
diff --git a/src/main/java/org/apache/solr/mcp/server/search/SearchService.java 
b/src/main/java/org/apache/solr/mcp/server/search/SearchService.java
index 1235362..0619fec 100644
--- a/src/main/java/org/apache/solr/mcp/server/search/SearchService.java
+++ b/src/main/java/org/apache/solr/mcp/server/search/SearchService.java
@@ -21,6 +21,7 @@ import java.io.IOException;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import org.apache.solr.client.solrj.SolrClient;
 import org.apache.solr.client.solrj.SolrServerException;
@@ -293,9 +294,7 @@ public class SearchService {
 
                // sorting
                if (!CollectionUtils.isEmpty(sortClauses)) {
-                       solrQuery.setSorts(sortClauses.stream()
-                                       .map(sortClause -> new 
SolrQuery.SortClause(sortClause.get(SORT_ITEM), sortClause.get(SORT_ORDER)))
-                                       .toList());
+                       
solrQuery.setSorts(sortClauses.stream().map(SearchService::toSortClause).toList());
                }
 
                // pagination
@@ -321,6 +320,35 @@ public class SearchService {
                return new SearchResponse(documents.getNumFound(), 
documents.getStart(), documents.getMaxScore(), docs, facets);
        }
 
+       /**
+        * Builds a {@link SolrQuery.SortClause} from one caller-supplied map.
+        *
+        * <p>
+        * Both keys are validated up front: {@code SortClause}'s constructor 
calls
+        * {@code ORDER.valueOf(order)}, which throws {@link 
NullPointerException} on a
+        * missing order and an opaque {@link IllegalArgumentException} on an
+        * unrecognised one. Callers are LLMs, so the message needs to say what 
to send.
+        */
+       private static SolrQuery.SortClause toSortClause(Map<String, String> 
sortClause) {
+               String field = sortClause.get(SORT_ITEM);
+               String order = sortClause.get(SORT_ORDER);
+               if (field == null || field.isBlank()) {
+                       throw new IllegalArgumentException("Each sort clause 
requires a non-empty '" + SORT_ITEM + "' key");
+               }
+               if (order == null || order.isBlank()) {
+                       throw new IllegalArgumentException(
+                                       "Sort clause for '" + field + "' 
requires a '" + SORT_ORDER + "' key of 'asc' or 'desc'");
+               }
+               SolrQuery.ORDER parsed;
+               try {
+                       parsed = 
SolrQuery.ORDER.valueOf(order.toLowerCase(Locale.ROOT));
+               } catch (IllegalArgumentException e) {
+                       throw new IllegalArgumentException(
+                                       "Unsupported sort order '" + order + "' 
for '" + field + "'; expected 'asc' or 'desc'", e);
+               }
+               return new SolrQuery.SortClause(field, parsed);
+       }
+
        /**
         * MCP prompt that guides the client through translating a 
natural-language
         * question into a Solr query: inspect the schema, build the query, run 
the
@@ -376,7 +404,7 @@ public class SearchService {
 
                                3. Run the search.
                                   - Call `search` with `collection=%s` and the 
chosen `query` plus optional
-                                    `filterQueries`, `facetFields`, 
`sortFields`, `start`, `rows`. Set `rows=10` for a
+                                    `filterQueries`, `facetFields`, 
`sortClauses`, `start`, `rows`. Set `rows=10` for a
                                     focused look or `rows=0` if you only need 
counts / facets.
 
                                4. Interpret and refine.
diff --git a/src/main/resources/logback-spring.xml 
b/src/main/resources/logback-spring.xml
index e85a13b..0045aac 100644
--- a/src/main/resources/logback-spring.xml
+++ b/src/main/resources/logback-spring.xml
@@ -35,7 +35,13 @@
         under the STDIO profile. This avoids logback "not referenced" warnings
         and keeps STDIO stdout completely clean.
     -->
-    <springProfile name="http">
+    <!--
+        "http & !stdio": if both profiles are activated at once 
(PROFILES=stdio,http)
+        the CONSOLE appender would write diagnostics to stdout, corrupting the 
MCP
+        JSON-RPC stream that STDIO transports over it. Keeping stdio dominant 
here
+        means the combination degrades safely instead of silently breaking.
+    -->
+    <springProfile name="http &amp; !stdio">
         <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
             <encoder>
                 <pattern>${CONSOLE_LOG_PATTERN:-%d{yyyy-MM-dd HH:mm:ss.SSS} 
%5p --- [%15.15t] %-40.40logger{39} : %m%n}
diff --git 
a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java
 
b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java
index d482c17..d0772ec 100644
--- 
a/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java
+++ 
b/src/test/java/org/apache/solr/mcp/server/collection/CollectionServiceTest.java
@@ -152,11 +152,24 @@ class CollectionServiceTest {
        void extractCollectionName_EdgeCases_ShouldHandleCorrectly() {
                // Test various edge cases
                assertEquals("a", 
collectionService.extractCollectionName("a_shard1"));
-               assertEquals("collection", 
collectionService.extractCollectionName("collection_shard"));
+               // "collection_shard" has no shard number, so it is not a 
SolrCloud core
+               // name - it is a collection that merely ends in "_shard" and 
must survive
+               // unchanged. Stripping it would also corrupt names like
+               // "orders_shard_archive".
+               assertEquals("collection_shard", 
collectionService.extractCollectionName("collection_shard"));
                assertEquals("test_name", 
collectionService.extractCollectionName("test_name"));
                assertEquals("", 
collectionService.extractCollectionName("_shard1"));
        }
 
+       @Test
+       void extractCollectionName_WithShardWordInsideName_ShouldNotTruncate() {
+               // Regression: a bare "contains(_shard)" check truncated this 
to "orders".
+               assertEquals("orders_shard_archive", 
collectionService.extractCollectionName("orders_shard_archive"));
+               // ...while a genuine core name for that same collection still 
resolves.
+               assertEquals("orders_shard_archive",
+                               
collectionService.extractCollectionName("orders_shard_archive_shard2_replica_n1"));
+       }
+
        @Test
        void 
extractCollectionName_WithShardInMiddleOfName_ShouldExtractCorrectly() {
                // Given - "shard" appears in collection name but not as suffix 
pattern
@@ -170,15 +183,17 @@ class CollectionServiceTest {
        }
 
        @Test
-       void 
extractCollectionName_WithMultipleOccurrencesOfShard_ShouldUseFirst() {
-               // Given
+       void 
extractCollectionName_WithMultipleOccurrencesOfShard_ShouldStripOnlyTheCoreSuffix()
 {
+               // Given - a collection whose own name ends in "_shard1". 
SolrCloud names
+               // its cores "<collection>_shard<N>_replica_<type><M>", so this 
core belongs
+               // to the collection "data_shard1", not "data".
                String name = "data_shard1_shard2_replica_n1";
 
                // When
                String result = collectionService.extractCollectionName(name);
 
                // Then
-               assertEquals("data", result, "Should use first occurrence of 
'_shard'");
+               assertEquals("data_shard1", result, "Should strip only the 
trailing shard/replica suffix");
        }
 
        // Health check tests
diff --git 
a/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceIntegrationTest.java
 
b/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceIntegrationTest.java
index daa9561..e4f7516 100644
--- 
a/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceIntegrationTest.java
+++ 
b/src/test/java/org/apache/solr/mcp/server/indexing/IndexingServiceIntegrationTest.java
@@ -697,9 +697,12 @@ class IndexingServiceIntegrationTest {
                // Create documents
                List<SolrInputDocument> documents = 
indexingDocumentCreator.createSchemalessDocumentsFromJson(json);
 
-               // Verify no documents were created since input is not an array
+               // A bare object is indexed as a single document. This 
previously returned
+               // an empty list, so indexing one object silently indexed 
nothing and still
+               // reported success.
                assertNotNull(documents);
-               assertEquals(0, documents.size());
+               assertEquals(1, documents.size());
+               assertEquals("single_object_001", 
documents.getFirst().getFieldValue("id"));
        }
 
        @Test

Reply via email to