Guide · Search Integration
MCP Tools for Typesense — collection schema, import batching, scoped API keys
Three Typesense behaviours catch MCP server authors off-guard: collections must exist and have a declared schema before documents can be indexed — calling client.collections('products').documents().import(docs) against a non-existent collection throws a Collection not found error at indexing time, not at startup, so the first agent write tool call fails; the fix is to call client.collections().create(schema) idempotently at server startup (catch the ObjectAlreadyExists error); the default import action is create which throws on duplicate id fields — MCP tools that sync from an external database should use action: 'upsert' to insert new documents and update existing ones in a single call; and auto_detect_schema: true infers field types but marks all string fields as non-facetable by default, so a collection created with auto-detect cannot use those fields in facet_by queries — declare the schema explicitly in production with facet: true on string fields you need to facet.
TL;DR
Create collections with explicit schema at startup — catch ObjectAlreadyExists. Use action: 'upsert' for idempotent imports. Declare facet: true on string fields you need to filter or aggregate. Generate scoped API keys per tool with client.keys().generate(keyId, restrictions). Health probe: GET /health returns {"ok": true}.
Collection schema — create at startup, catch ObjectAlreadyExists
Typesense stores documents in collections. Each collection has a schema that defines field names, types, and faceting options. Unlike Algolia or Elasticsearch, Typesense requires the collection to exist before any document can be indexed — there is no implicit creation. For MCP servers this means collection setup must happen in the startup sequence, before any tool handlers are registered.
import Typesense from 'typesense';
import { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
const client = new Typesense.Client({
nodes: [{
host: process.env.TYPESENSE_HOST ?? 'localhost',
port: Number(process.env.TYPESENSE_PORT ?? 8108),
protocol: process.env.TYPESENSE_PROTOCOL ?? 'http',
}],
apiKey: process.env.TYPESENSE_API_KEY!,
// How long to wait for a response before trying the next node
connectionTimeoutSeconds: 10,
// Number of retries on transient network errors
numRetries: 3,
// How long between retries (seconds)
retryIntervalSeconds: 0.1,
});
// Define the schema explicitly — auto_detect_schema: true is not suitable for production
const productsSchema: CollectionCreateSchema = {
name: 'products',
fields: [
{ name: 'id', type: 'string', facet: false },
{ name: 'title', type: 'string', facet: false },
{ name: 'description', type: 'string', facet: false, optional: true },
{ name: 'brand', type: 'string', facet: true }, // Facetable for filtering
{ name: 'category', type: 'string', facet: true },
{ name: 'price', type: 'float', facet: false },
{ name: 'in_stock', type: 'bool', facet: true },
{ name: 'tags', type: 'string[]',facet: true },
{ name: 'created_at', type: 'int64', facet: false },
],
// Field used as document identifier — must be unique within the collection
default_sorting_field: 'created_at',
};
// Idempotent startup: create collection if it doesn't exist
async function ensureCollections() {
try {
await client.collections().create(productsSchema);
console.log('Collection "products" created');
} catch (err: any) {
// ObjectAlreadyExists means the collection is already set up — safe to continue
if (err.httpStatus === 409) {
console.log('Collection "products" already exists — skipping creation');
} else {
throw err;
}
}
}
async function main() {
await ensureCollections();
// Now safe to register MCP tools that use this collection
buildMcpServer(client).start();
}
If your schema needs to change after data is already in the collection, Typesense requires you to either drop and recreate the collection (losing data) or use the schema update API (available in Typesense 0.24+). Plan your schema carefully at the start — adding optional fields later is supported, but changing field types requires a full reindex.
Import batching — upsert action for idempotent MCP tool writes
Typesense's import endpoint processes documents in bulk. The action parameter controls how duplicates are handled: create (default) throws an error if a document with the same id already exists; upsert inserts new documents and updates existing ones; update merges fields into existing documents (partial update); emplace behaves like upsert but also allows id auto-generation. For MCP tools that sync from external systems, use upsert — it is safe to call repeatedly without tracking which records already exist.
// MCP tool: sync products from an external catalog API
server.tool('sync_products', async ({ products }) => {
// Prepare documents — 'id' field is required and must be a string
const documents = products.map(p => ({
id: String(p.productId),
title: p.name,
description: p.description ?? '',
brand: p.brand ?? 'Unknown',
category: p.category,
price: Number(p.price),
in_stock: p.inventory > 0,
tags: p.tags ?? [],
created_at: Math.floor(Date.now() / 1000),
}));
// import() accepts a JSON Lines string or an array of objects
// action: 'upsert' — insert if not exists, replace if exists
const results = await client.collections('products').documents().import(
documents,
{ action: 'upsert', batch_size: 100 }
);
// results is an array of per-document import results
// Each entry is { success: true } or { success: false, error: '...' }
const failures = results.filter(r => !r.success);
if (failures.length > 0) {
console.warn(`${failures.length} documents failed to import:`, failures);
}
return {
total: documents.length,
succeeded: documents.length - failures.length,
failed: failures.length,
errors: failures.slice(0, 5), // Return first 5 errors for diagnosis
};
});
// MCP tool: partial update a single document
server.tool('update_product_price', async ({ productId, newPrice }) => {
// update action merges only the provided fields, does not replace the document
await client.collections('products').documents().import(
[{ id: String(productId), price: Number(newPrice) }],
{ action: 'update' }
);
return { updated: true, productId, price: newPrice };
});
Typesense imports are synchronous — unlike Algolia, the imported documents are immediately available in search results without waiting for a task. However, very large batches (over 10,000 documents) can block the import endpoint. Split large syncs into chunks of 1,000–5,000 documents to avoid timeout errors in long-running MCP tool calls.
Search tool patterns — filter, facet, and multi-search
Typesense search parameters differ from Elasticsearch in that filter syntax is a string expression rather than a DSL object. Fields used in filter_by must be declared as facet: true in the collection schema for string fields, or as any numeric/bool type (which are always filterable).
// MCP tool: search products with filters and facets
server.tool('search_products', async ({
query,
category,
brand,
minPrice,
maxPrice,
inStockOnly,
page = 1,
}) => {
// Build filter expression — only include non-null conditions
const filterParts: string[] = [];
if (category) filterParts.push(`category:=${category}`);
if (brand) filterParts.push(`brand:=${brand}`);
if (minPrice != null) filterParts.push(`price:>=${minPrice}`);
if (maxPrice != null) filterParts.push(`price:<=${maxPrice}`);
if (inStockOnly) filterParts.push(`in_stock:=true`);
const searchParameters = {
q: query ?? '*', // '*' matches all documents
query_by: 'title,description,tags', // Fields searched with typo tolerance
filter_by: filterParts.join(' && ') || undefined,
facet_by: 'category,brand,in_stock',
per_page: 20,
page,
// Sort: most relevant first, then by price
sort_by: query ? '_text_match:desc,price:asc' : 'created_at:desc',
// Number of typos tolerated (0 = exact, 1 = 1 typo, 2 = 2 typos)
num_typos: 2,
// Prefix search — matches partial words (good for autocomplete)
prefix: true,
// Include only these fields in the response to reduce payload size
include_fields: 'id,title,brand,category,price,in_stock',
// Snippet description around the matched query term
snippet_threshold: 30,
};
const result = await client.collections('products').documents().search(
searchParameters
);
return {
hits: result.hits?.map(h => h.document) ?? [],
facets: result.facet_counts,
totalHits: result.found,
page: result.page,
searchTimeMs: result.search_time_ms,
};
});
// MCP tool: multi-collection search in one request
server.tool('search_all', async ({ query }) => {
const response = await client.multiSearch.perform(
{
searches: [
{
collection: 'products',
q: query,
query_by: 'title,description',
per_page: 5,
include_fields: 'id,title,price,category',
},
{
collection: 'articles',
q: query,
query_by: 'title,body',
per_page: 5,
include_fields: 'id,title,excerpt,url',
},
],
},
{ query_by: 'title' } // Common parameters applied to all searches
);
return {
products: response.results[0].hits?.map(h => h.document) ?? [],
articles: response.results[1].hits?.map(h => h.document) ?? [],
};
});
Typesense's filter_by syntax uses := for exact match (string), :>=/:<=/:>/:< for numeric comparisons, and :[val1, val2] for multi-value OR. The && operator combines conditions with AND. Combine with || for OR between filter groups: category:=Electronics || category:=Computers.
Scoped API keys — per-tool access control
Typesense supports generating API keys scoped to specific collections and operations. Use a master key only server-side; generate scoped keys for tool handlers that need to expose search to end-users or that operate on a subset of collections. Scoped keys are generated client-side from a master key — no API call required.
// Generate a search-only key scoped to the 'products' collection
// This can be passed to frontend agents without exposing the master key
async function generateSearchKey(tenantId: string): Promise<string> {
// First, create a parent search key if you don't have one
const searchOnlyKeyResponse = await client.keys().create({
description: `Search key for tenant ${tenantId}`,
actions: ['documents:search'],
collections: ['products', 'articles'],
});
// Generate a scoped key from the parent, embedding tenant filters
// This is a HMAC-signed token — no network call required
return client.keys().generateScopedSearchKey(
searchOnlyKeyResponse.value!,
{
// Force all searches to include this filter — tenant data isolation
filter_by: `tenant_id:=${tenantId}`,
// This scoped key expires in 1 hour
expires_at: Math.floor(Date.now() / 1000) + 3600,
}
);
}
Health probe — Typesense reachability from an MCP server
Typesense exposes a /health endpoint that returns {"ok": true} when the server is operational. Use it as a lightweight startup gate and periodic liveness check.
async function checkTypesenseHealth(): Promise<{
healthy: boolean;
latencyMs?: number;
error?: string;
}> {
const start = Date.now();
try {
// health() is a built-in client method that calls GET /health
const result = await client.health.retrieve();
return {
healthy: result.ok === true,
latencyMs: Date.now() - start,
};
} catch (err: any) {
return {
healthy: false,
error: err?.message ?? String(err),
};
}
}
async function startMcpServer() {
const health = await checkTypesenseHealth();
if (!health.healthy) {
console.error('Typesense unreachable at startup:', health.error);
process.exit(1);
}
console.log(\`Typesense ready (\${health.latencyMs}ms). Registering MCP tools.\`);
}
server.tool('search_health', async () => {
return checkTypesenseHealth();
});
AliveMCP monitors your MCP server's Typesense-backed search tools by probing the health endpoint every 60 seconds and alerting on latency spikes or connection failures — catching disk-full conditions, memory pressure from large index loads, and network partitions between the MCP server and Typesense cluster before they surface as agent tool failures.
Related guides
- MCP Tools for Algolia — API key scoping, async indexing, facet filtering
- 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