diff --git a/README.md b/README.md
index a6234f96..0e81a36f 100644
--- a/README.md
+++ b/README.md
@@ -105,6 +105,9 @@ Using a different client, or want STDIO/HTTP/Docker options? See the per-client
| `add-fields` | Add fields to a collection schema (additive only; existing fields cannot be modified) |
| `add-field-types` | Add field types — custom analyzers, `DenseVectorField` for semantic search, etc. |
| `get-schema` | Retrieve schema information for a collection |
+| `list-aliases` | List all Solr aliases and the collections they point to |
+| `create-alias` | Create or update a Solr alias pointing to one or more collections |
+| `delete-alias` | Delete a Solr alias (underlying collections are not affected) |
Every tool advertises MCP behavior hints (`readOnlyHint`, `destructiveHint`, `idempotentHint`) so clients can build sensible approval UX — `search` and the metadata tools are read-only, indexing is destructive but idempotent, schema modification is additive.
diff --git a/src/main/java/org/apache/solr/mcp/server/collection/AliasResult.java b/src/main/java/org/apache/solr/mcp/server/collection/AliasResult.java
new file mode 100644
index 00000000..34f7561b
--- /dev/null
+++ b/src/main/java/org/apache/solr/mcp/server/collection/AliasResult.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.mcp.server.collection;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import java.util.Date;
+import org.jspecify.annotations.Nullable;
+
+/**
+ * Result record for alias management operations.
+ *
+ *
+ * Returned by {@link AliasService} methods to communicate the outcome of
+ * create, update, and delete operations on Solr aliases.
+ *
+ * @param aliasName
+ * the alias that was operated on
+ * @param collections
+ * the target collection(s) (null for delete operations)
+ * @param success
+ * whether the operation completed successfully
+ * @param message
+ * human-readable description of the outcome
+ * @param timestamp
+ * when the operation was performed
+ */
+public record AliasResult(String aliasName, @Nullable String collections, boolean success, String message,
+ @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") Date timestamp) {
+}
diff --git a/src/main/java/org/apache/solr/mcp/server/collection/AliasService.java b/src/main/java/org/apache/solr/mcp/server/collection/AliasService.java
new file mode 100644
index 00000000..bf024844
--- /dev/null
+++ b/src/main/java/org/apache/solr/mcp/server/collection/AliasService.java
@@ -0,0 +1,215 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.mcp.server.collection;
+
+import io.micrometer.observation.annotation.Observed;
+import java.io.IOException;
+import java.util.Date;
+import java.util.Map;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.client.solrj.SolrServerException;
+import org.apache.solr.client.solrj.request.CollectionAdminRequest;
+import org.apache.solr.client.solrj.response.CollectionAdminResponse;
+import org.springaicommunity.mcp.annotation.McpTool;
+import org.springaicommunity.mcp.annotation.McpToolParam;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.stereotype.Service;
+
+/**
+ * Spring Service providing Solr alias management capabilities for MCP clients.
+ *
+ *
+ * Aliases are virtual collection names that point to one or more physical
+ * collections. They enable zero-downtime reindexing, blue-green deployments,
+ * and read/write separation by allowing applications to reference a stable name
+ * while the underlying collection is swapped transparently.
+ *
+ *
+ * Core Capabilities:
+ *
+ *
+ *
List Aliases: Discover all aliases and their target
+ * collections
+ *
Create/Update Alias: Point an alias to one or more
+ * collections (creates if new, updates if existing)
+ *
Delete Alias: Remove an alias without affecting the
+ * underlying collections
+ *
+ *
+ * @see CollectionAdminRequest
+ */
+@Service
+@Observed
+public class AliasService {
+
+ /**
+ * Error message for blank alias name validation
+ */
+ private static final String BLANK_ALIAS_NAME_ERROR = "Alias name must not be blank";
+
+ /**
+ * Error message for blank collections validation
+ */
+ private static final String BLANK_COLLECTIONS_ERROR = "Collections must not be blank";
+
+ /**
+ * SolrJ client for communicating with Solr server
+ */
+ private final SolrClient solrClient;
+
+ /**
+ * Constructs a new AliasService with the required dependencies.
+ *
+ * @param solrClient
+ * the SolrJ client instance for communicating with Solr
+ */
+ public AliasService(SolrClient solrClient) {
+ this.solrClient = solrClient;
+ }
+
+ /**
+ * Lists all aliases defined in the Solr cluster.
+ *
+ *
+ * Returns a map where each key is an alias name and the corresponding value is
+ * the comma-separated list of collection names that the alias points to.
+ *
+ *
+ * MCP Tool Usage:
+ *
+ *
+ * Invoked by AI clients with natural language requests like "list all aliases",
+ * "what aliases exist?", or "show me alias mappings".
+ *
+ * @return a map of alias names to their target collection(s); never null
+ * (returns an empty map when no aliases are defined)
+ * @throws SolrServerException
+ * if there are errors communicating with Solr
+ * @throws IOException
+ * if there are I/O errors during communication
+ */
+ @PreAuthorize("isAuthenticated()")
+ @McpTool(
+ name = "list-aliases",
+ annotations = @McpTool.McpAnnotations(readOnlyHint = true),
+ description = "List all Solr aliases and the collections they point to")
+ public Map listAliases() throws SolrServerException, IOException {
+ CollectionAdminRequest.ListAliases request = new CollectionAdminRequest.ListAliases();
+ CollectionAdminResponse response = request.process(solrClient);
+ Map aliases = response.getAliases();
+ return aliases != null ? aliases : Map.of();
+ }
+
+ /**
+ * Creates or updates a Solr alias pointing to one or more collections.
+ *
+ *
+ * If the alias already exists, it is updated to point to the new collection(s).
+ * This is the mechanism for zero-downtime collection swaps: reindex into a new
+ * collection, then update the alias to point to it.
+ *
+ *
+ * MCP Tool Usage:
+ *
+ *
+ * Invoked with requests like "create an alias ORDERS pointing to ORDERS_V2" or
+ * "swap the LIVE alias to PRODUCTS_V3".
+ *
+ * @param aliasName
+ * the name of the alias to create or update (must not be blank)
+ * @param collections
+ * comma-separated list of target collection names (must not be
+ * blank)
+ * @return result describing the outcome of the operation
+ * @throws IllegalArgumentException
+ * if aliasName or collections is blank
+ * @throws SolrServerException
+ * if Solr returns an error
+ * @throws IOException
+ * if there are I/O errors during communication
+ */
+ @PreAuthorize("isAuthenticated()")
+ @McpTool(
+ name = "create-alias",
+ annotations = @McpTool.McpAnnotations(idempotentHint = true),
+ description = "Create or update a Solr alias pointing to one or more collections. "
+ + "If the alias already exists, it is updated to point to the new collection(s).")
+ public AliasResult createAlias(
+ @McpToolParam(description = "Name of the alias to create or update") String aliasName,
+ @McpToolParam(
+ description = "Comma-separated list of collection names the alias should point to") String collections)
+ throws SolrServerException, IOException {
+
+ if (aliasName == null || aliasName.isBlank()) {
+ throw new IllegalArgumentException(BLANK_ALIAS_NAME_ERROR);
+ }
+ if (collections == null || collections.isBlank()) {
+ throw new IllegalArgumentException(BLANK_COLLECTIONS_ERROR);
+ }
+
+ CollectionAdminRequest.CreateAlias request = CollectionAdminRequest.createAlias(aliasName, collections);
+ CollectionAdminResponse response = request.process(solrClient);
+ boolean success = response.getStatus() == 0;
+
+ return new AliasResult(aliasName, collections, success,
+ success ? "Alias created/updated successfully" : "Alias creation/update failed", new Date());
+ }
+
+ /**
+ * Deletes an existing Solr alias.
+ *
+ *
+ * Removes the alias definition only — the underlying collection(s) are not
+ * affected. After deletion, the alias name is no longer resolvable.
+ *
+ *
+ * MCP Tool Usage:
+ *
+ *
+ * Invoked with requests like "delete the stale alias ORDERS_TEST" or "remove
+ * alias OLD_PRODUCTS".
+ *
+ * @param aliasName
+ * the name of the alias to delete (must not be blank)
+ * @return result describing the outcome of the operation
+ * @throws IllegalArgumentException
+ * if aliasName is blank
+ * @throws SolrServerException
+ * if Solr returns an error
+ * @throws IOException
+ * if there are I/O errors during communication
+ */
+ @PreAuthorize("isAuthenticated()")
+ @McpTool(
+ name = "delete-alias",
+ annotations = @McpTool.McpAnnotations(destructiveHint = true),
+ description = "Delete a Solr alias. The underlying collection(s) are not affected.")
+ public AliasResult deleteAlias(@McpToolParam(description = "Name of the alias to delete") String aliasName)
+ throws SolrServerException, IOException {
+
+ if (aliasName == null || aliasName.isBlank()) {
+ throw new IllegalArgumentException(BLANK_ALIAS_NAME_ERROR);
+ }
+
+ CollectionAdminRequest.DeleteAlias request = CollectionAdminRequest.deleteAlias(aliasName);
+ CollectionAdminResponse response = request.process(solrClient);
+ boolean success = response.getStatus() == 0;
+
+ return new AliasResult(aliasName, null, success,
+ success ? "Alias deleted successfully" : "Alias deletion failed", new Date());
+ }
+}
diff --git a/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java b/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java
index 2390626b..26fbe702 100644
--- a/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java
+++ b/src/main/java/org/apache/solr/mcp/server/config/SolrNativeHints.java
@@ -65,6 +65,7 @@ public SolrNativeHints() {
* image.
*/
private static final List MCP_RESPONSE_RECORDS = List.of(
+ "org.apache.solr.mcp.server.collection.AliasResult",
"org.apache.solr.mcp.server.collection.CollectionCreationResult",
"org.apache.solr.mcp.server.collection.SolrHealthStatus",
"org.apache.solr.mcp.server.collection.SolrMetrics", "org.apache.solr.mcp.server.collection.IndexStats",
diff --git a/src/test/java/org/apache/solr/mcp/server/collection/AliasServiceIntegrationTest.java b/src/test/java/org/apache/solr/mcp/server/collection/AliasServiceIntegrationTest.java
new file mode 100644
index 00000000..7d7ba8fd
--- /dev/null
+++ b/src/test/java/org/apache/solr/mcp/server/collection/AliasServiceIntegrationTest.java
@@ -0,0 +1,156 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.mcp.server.collection;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.Map;
+import org.apache.solr.mcp.server.TestcontainersConfiguration;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.context.annotation.Import;
+import org.testcontainers.junit.jupiter.Testcontainers;
+
+/**
+ * Integration tests for {@link AliasService} using Testcontainers with a real
+ * Solr instance.
+ *
+ *
+ * Tests create, list, update, and delete alias operations against a live Solr
+ * cluster.
+ */
+@SpringBootTest
+@Import(TestcontainersConfiguration.class)
+@Tag("integration")
+@Testcontainers(disabledWithoutDocker = true)
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+class AliasServiceIntegrationTest {
+
+ private static final String TEST_COLLECTION = "alias_test_collection";
+ private static final String TEST_COLLECTION_2 = "alias_test_collection_2";
+ private static final String TEST_ALIAS = "alias_test_alias";
+ private static final String TEST_ALIAS_2 = "alias_test_alias_2";
+ private static final String TEST_ALIAS_3 = "alias_test_alias_3";
+
+ @Autowired
+ private AliasService aliasService;
+
+ @Autowired
+ private CollectionService collectionService;
+
+ @BeforeAll
+ void setupCollection() throws Exception {
+ CollectionCreationResult created = collectionService.createCollection(TEST_COLLECTION, null, null, null);
+ assertThat(created.success()).as("Collection creation should succeed: %s", created.message()).isTrue();
+ CollectionCreationResult created2 = collectionService.createCollection(TEST_COLLECTION_2, null, null, null);
+ assertThat(created2.success()).as("Collection 2 creation should succeed: %s", created2.message()).isTrue();
+ }
+
+ @Test
+ @Order(1)
+ void listAliases_initiallyEmpty() throws Exception {
+ Map aliases = aliasService.listAliases();
+ // No aliases pointing to our test collection initially
+ assertThat(aliases).doesNotContainKey(TEST_ALIAS);
+ }
+
+ @Test
+ @Order(2)
+ void createAlias_createsNewAlias() throws Exception {
+ AliasResult result = aliasService.createAlias(TEST_ALIAS, TEST_COLLECTION);
+
+ assertThat(result.aliasName()).isEqualTo(TEST_ALIAS);
+ assertThat(result.collections()).isEqualTo(TEST_COLLECTION);
+ assertThat(result.success()).isTrue();
+ assertThat(result.timestamp()).isNotNull();
+ }
+
+ @Test
+ @Order(3)
+ void listAliases_containsCreatedAlias() throws Exception {
+ Map aliases = aliasService.listAliases();
+
+ assertThat(aliases).containsEntry(TEST_ALIAS, TEST_COLLECTION);
+ }
+
+ @Test
+ @Order(4)
+ void createAlias_updatesExistingAlias() throws Exception {
+ // Update alias to point to a different collection
+ AliasResult result = aliasService.createAlias(TEST_ALIAS, TEST_COLLECTION_2);
+
+ assertThat(result.success()).isTrue();
+ assertThat(result.collections()).isEqualTo(TEST_COLLECTION_2);
+
+ // Verify the alias now points to the second collection
+ Map aliases = aliasService.listAliases();
+ assertThat(aliases).containsEntry(TEST_ALIAS, TEST_COLLECTION_2);
+ }
+
+ @Test
+ @Order(5)
+ void createAlias_multipleAliasesCanExist() throws Exception {
+ // Assert only on aliases this test creates itself. Asserting on TEST_ALIAS
+ // here would couple the expectation to whichever collection an earlier
+ // test last repointed it at.
+ AliasResult first = aliasService.createAlias(TEST_ALIAS_2, TEST_COLLECTION);
+ AliasResult second = aliasService.createAlias(TEST_ALIAS_3, TEST_COLLECTION);
+ assertThat(first.success()).isTrue();
+ assertThat(second.success()).isTrue();
+
+ Map aliases = aliasService.listAliases();
+ assertThat(aliases).containsEntry(TEST_ALIAS_2, TEST_COLLECTION).containsEntry(TEST_ALIAS_3, TEST_COLLECTION);
+
+ // TEST_ALIAS_2 is left in place for the deletion tests that follow;
+ // TEST_ALIAS_3 is this test's own fixture, so clean it up here.
+ assertThat(aliasService.deleteAlias(TEST_ALIAS_3).success()).isTrue();
+ }
+
+ @Test
+ @Order(6)
+ void deleteAlias_removesAlias() throws Exception {
+ AliasResult result = aliasService.deleteAlias(TEST_ALIAS);
+
+ assertThat(result.aliasName()).isEqualTo(TEST_ALIAS);
+ assertThat(result.collections()).isNull();
+ assertThat(result.success()).isTrue();
+
+ // Verify it's gone
+ Map aliases = aliasService.listAliases();
+ assertThat(aliases).doesNotContainKey(TEST_ALIAS);
+ // But the other alias still exists
+ assertThat(aliases).containsEntry(TEST_ALIAS_2, TEST_COLLECTION);
+ }
+
+ @Test
+ @Order(7)
+ void deleteAlias_cleanupSecondAlias() throws Exception {
+ AliasResult result = aliasService.deleteAlias(TEST_ALIAS_2);
+ assertThat(result.success()).isTrue();
+
+ Map aliases = aliasService.listAliases();
+ assertThat(aliases).doesNotContainKey(TEST_ALIAS_2);
+ }
+}
diff --git a/src/test/java/org/apache/solr/mcp/server/collection/AliasServiceTest.java b/src/test/java/org/apache/solr/mcp/server/collection/AliasServiceTest.java
new file mode 100644
index 00000000..bf1f3262
--- /dev/null
+++ b/src/test/java/org/apache/solr/mcp/server/collection/AliasServiceTest.java
@@ -0,0 +1,173 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.solr.mcp.server.collection;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.util.Map;
+import org.apache.solr.client.solrj.SolrClient;
+import org.apache.solr.common.util.NamedList;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledInNativeImage;
+
+/**
+ * Unit tests for {@link AliasService}.
+ */
+@DisabledInNativeImage
+class AliasServiceTest {
+
+ private SolrClient solrClient;
+ private AliasService aliasService;
+
+ @BeforeEach
+ void setUp() {
+ solrClient = mock(SolrClient.class);
+ aliasService = new AliasService(solrClient);
+ }
+
+ @Nested
+ @DisplayName("list-aliases")
+ class ListAliases {
+
+ @Test
+ @DisplayName("returns aliases map when aliases exist")
+ void returnsAliasesWhenPresent() throws Exception {
+ // Given
+ NamedList