Guide · Search Integration
MCP Tools for Algolia — API key scoping, async indexing, facet filtering
Three Algolia behaviours trip up MCP server authors: indexing operations are asynchronous and saveObjects returns before records are searchable — a tool that indexes a document then immediately runs a search to confirm it may return zero hits because the indexing task has not completed; the fix is to call index.waitTask(taskID) (or use saveObjects({ autoGenerateObjectIDIfNotExist: true }) with the algoliasearch v5 waitForTask helper) before any dependent search; the Admin API key must never appear in MCP tool handler code that logs requests or stores them in agent context — use Algolia's secured API keys (algoliaClient.generateSecuredApiKey(searchOnlyKey, restrictions)) to scope each tool to the indices and operations it needs; and search responses return all indexed attributes by default, which can balloon MCP tool responses to many kilobytes per hit — always set attributesToRetrieve: ['id','title','price'] to limit the payload to fields the agent actually consumes.
TL;DR
Use search-only keys for read tools, secured API keys with index restrictions for write tools. Await waitForTask(taskID) before any query that depends on recently indexed data. Set attributesToRetrieve on every search call. Keep hitsPerPage bounded (default 20, max 1000). Health probe: GET /1/health via the REST API or customGet({ path: '/1/health' }) with the v5 client.
API key scoping — never use the Admin key in tool handlers
Algolia issues three categories of keys: the Admin API key (full read/write/configure access on all indices), Search-only API keys (read access across all indices), and secured API keys generated from a parent key with restrictions applied at runtime. MCP tool handlers should never receive the Admin key — if the tool's request is logged or its parameters are stored in agent memory, the key is compromised. Use a search-only key for search tools and generate scoped secured keys for any tool that must write.
import { algoliasearch } from 'algoliasearch';
// Read-only search client — safe to instantiate with search-only key
const searchClient = algoliasearch(
process.env.ALGOLIA_APP_ID!,
process.env.ALGOLIA_SEARCH_ONLY_KEY! // Not the Admin key
);
// Write client — Admin key, kept server-side only, never logged
const writeClient = algoliasearch(
process.env.ALGOLIA_APP_ID!,
process.env.ALGOLIA_ADMIN_KEY!
);
// Generate a secured API key scoped to one index with an expiry
// Use this when a tool needs temporary write access to a specific index
function generateIndexScopedKey(indexName: string, expiresInSeconds = 3600): string {
const expiresAt = Math.floor(Date.now() / 1000) + expiresInSeconds;
return searchClient.generateSecuredApiKey({
parentApiKey: process.env.ALGOLIA_SEARCH_ONLY_KEY!,
restrictions: {
validUntil: expiresAt,
restrictIndices: [indexName],
restrictSources: process.env.ALGOLIA_ALLOWED_IP_RANGE, // optional IP restrict
},
});
}
// MCP tool: search products — uses search-only client
server.tool('search_products', async ({ query, category, maxPrice }) => {
const results = await searchClient.searchSingleIndex({
indexName: 'products',
searchParams: {
query,
filters: [
category ? `category:${category}` : '',
maxPrice ? `price <= ${maxPrice}` : '',
].filter(Boolean).join(' AND '),
hitsPerPage: 20,
attributesToRetrieve: ['objectID', 'title', 'price', 'category', 'url'],
attributesToHighlight: ['title'],
},
});
return {
hits: results.hits,
totalHits: results.nbHits,
page: results.page,
totalPages: results.nbPages,
};
});
Never log the raw algoliasearch client configuration object — it contains the API key in the apiKey field. Structure tool handlers so the client is instantiated once at module load time from environment variables, not passed as a tool argument.
Async indexing — wait for tasks before dependent queries
Every write operation in Algolia (saveObjects, partialUpdateObjects, deleteObjects, setSettings) is asynchronous. The API returns a taskID immediately; the index reflects the change only after Algolia has processed the task on its indexing servers, which typically takes under one second but can take several under load. MCP tools that index data and then run a search to return the indexed result will get stale results unless they wait for the task.
import { algoliasearch } from 'algoliasearch';
const client = algoliasearch(
process.env.ALGOLIA_APP_ID!,
process.env.ALGOLIA_ADMIN_KEY!
);
// MCP tool: index a product and confirm it is searchable
server.tool('index_product', async ({ product }) => {
// saveObjects returns taskID — index not yet updated
const { taskID } = await client.saveObject({
indexName: 'products',
body: {
objectID: product.id,
title: product.title,
price: product.price,
category: product.category,
description: product.description,
updatedAt: Date.now(),
},
});
// Wait until the task is processed before returning
// Default timeout: 5 minutes; polling interval: 100ms → 500ms (exponential)
await client.waitForTask({ indexName: 'products', taskID });
return { indexed: true, objectID: product.id, taskID };
});
// Batch indexing with progress tracking
server.tool('bulk_index_products', async ({ products }) => {
// Split into chunks of 1000 (Algolia's recommended batch size)
const batchSize = 1000;
const tasks: Array<{ taskID: number; batchIndex: number }> = [];
for (let i = 0; i < products.length; i += batchSize) {
const batch = products.slice(i, i + batchSize).map(p => ({
objectID: p.id,
...p,
}));
const { taskID } = await client.saveObjects({
indexName: 'products',
objects: batch,
});
tasks.push({ taskID, batchIndex: Math.floor(i / batchSize) });
}
// Wait for all batches to complete before confirming
await Promise.all(
tasks.map(({ taskID }) =>
client.waitForTask({ indexName: 'products', taskID })
)
);
return {
indexed: products.length,
batches: tasks.length,
};
});
Rate limits on indexing depend on your Algolia plan. Free plans allow roughly 10,000 records and limited indexing operations per day. If your MCP tool indexes frequently (e.g., syncing a product catalog on each agent run), implement a change-detection layer so only modified records are sent to Algolia rather than the full dataset.
Search tool patterns — filters, facets, and response shaping
Algolia distinguishes between filters (raw filter string, does not require attributes to be declared as facets) and facetFilters (requires attributes to be declared in attributesForFaceting in the index settings). Use filters for simple numeric and boolean conditions; use facetFilters for multi-select faceted navigation where the UI needs facet counts.
// Configure index settings once at setup time (not per tool call)
await client.setSettings({
indexName: 'products',
indexSettings: {
searchableAttributes: ['title', 'description', 'brand'],
attributesForFaceting: ['category', 'brand', 'filterOnly(price)'],
ranking: ['typo', 'geo', 'words', 'filters', 'proximity', 'attribute', 'exact', 'custom'],
customRanking: ['desc(popularity)', 'desc(updatedAt)'],
// Only return these attributes in search results — reduces bandwidth
attributesToRetrieve: ['objectID', 'title', 'price', 'category', 'brand', 'url'],
// Highlight only title — avoids processing large description fields
attributesToHighlight: ['title'],
hitsPerPage: 20,
paginationLimitedTo: 1000, // Max records reachable via pagination
},
});
// MCP tool: faceted product search
server.tool('search_products_faceted', async ({
query,
categories,
brands,
minPrice,
maxPrice,
page = 0,
}) => {
const facetFilters: string[][] = [];
if (categories?.length) facetFilters.push(categories.map(c => `category:${c}`));
if (brands?.length) facetFilters.push(brands.map(b => `brand:${b}`));
const numericFilters: string[] = [];
if (minPrice != null) numericFilters.push(`price >= ${minPrice}`);
if (maxPrice != null) numericFilters.push(`price <= ${maxPrice}`);
const results = await searchClient.searchSingleIndex({
indexName: 'products',
searchParams: {
query: query ?? '',
facetFilters,
numericFilters,
facets: ['category', 'brand'], // Return facet counts for these attributes
page,
hitsPerPage: 20,
attributesToRetrieve: ['objectID', 'title', 'price', 'category', 'brand', 'url'],
attributesToHighlight: ['title'],
snippetEllipsisText: '…',
},
});
return {
hits: results.hits,
facets: results.facets, // { category: { Electronics: 45, Clothing: 12 } }
totalHits: results.nbHits,
page: results.page,
totalPages: results.nbPages,
};
});
// MCP tool: multi-index search (search across products AND docs in one request)
server.tool('search_all', async ({ query }) => {
const { results } = await searchClient.search({
requests: [
{
indexName: 'products',
query,
hitsPerPage: 5,
attributesToRetrieve: ['objectID', 'title', 'price', 'url'],
},
{
indexName: 'documentation',
query,
hitsPerPage: 5,
attributesToRetrieve: ['objectID', 'title', 'excerpt', 'url'],
},
],
});
return {
products: (results[0] as any).hits,
docs: (results[1] as any).hits,
};
});
Algolia's paginationLimitedTo defaults to 1000, meaning you cannot paginate past the 1000th result via page and hitsPerPage. For MCP tools that need to export full result sets, use the Browse API (client.browseObjects) which iterates without pagination limits.
Health probe — Algolia reachability from an MCP server
Algolia exposes a /1/health REST endpoint that returns HTTP 200 when the API is operational. Use it as a lightweight liveness check — it does not consume indexing quota and does not touch your index data.
import { algoliasearch } from 'algoliasearch';
const client = algoliasearch(
process.env.ALGOLIA_APP_ID!,
process.env.ALGOLIA_SEARCH_ONLY_KEY!
);
async function checkAlgoliaHealth(): Promise<{
healthy: boolean;
latencyMs?: number;
error?: string;
}> {
const start = Date.now();
try {
// Returns { status: 'available' } when operational
await client.customGet({ path: '/1/health' });
return { healthy: true, latencyMs: Date.now() - start };
} catch (err: any) {
return {
healthy: false,
error: err?.message ?? String(err),
};
}
}
// Startup gate — verify API connectivity before accepting tool calls
async function startMcpServer() {
const health = await checkAlgoliaHealth();
if (!health.healthy) {
console.error('Algolia unreachable at startup:', health.error);
process.exit(1);
}
console.log(\`Algolia ready (\${health.latencyMs}ms). Registering MCP tools.\`);
}
// Expose as a diagnostic tool so agents can self-report search health
server.tool('search_health', async () => {
return checkAlgoliaHealth();
});
AliveMCP monitors your MCP server's Algolia-backed search tools by probing the health endpoint on a 60-second interval and alerting when latency spikes or when the probe returns an error — catching Algolia regional outages, rate limit exhaustion, and misconfigured API keys before your agents encounter silent search failures.
Related guides
- MCP Tools for Typesense — collection schema, import batching, multi-search
- MCP Tools for Meilisearch — task lifecycle, filterable attributes, tenant tokens
- MCP Tools for OpenSearch — bulk indexing, scroll API, AWS SigV4 auth
- MCP Tools for Elasticsearch — mapping, bool queries, index lifecycle
- MCP Tools for vector search — embedding pipeline, ANN index, hybrid retrieval