Using Indx for AI Agents

Getting started with an agent instead? The onboarding path — skill install, agent-readable docs, MCP connection — is in Build with an AI Agent. This page is the runtime and MCP tool reference.

Indx is a good fit for AI agents. It is a lightweight embedded library with no external service dependencies — when used via the C# NuGet package, it runs inside the agent's own process with sub-millisecond query times. There is no network round-trip, no managed service to provision, and no cold-start latency. Agents can fire as many searches as needed without debouncing or batching.

When used via the HTTP API, Indx is equally fast — search is in-memory and designed for repeated high-frequency queries.


Model Context Protocol (MCP)

Indx ships a built-in MCP server, so any MCP client — Claude Desktop, Claude Code, or your own agent framework — can search your datasets with no glue code. It exposes the same in-memory engine, and your saved boost rules, over the Model Context Protocol.

Endpoint: https://<your-host>/mcp — authenticate with an API key (Account → API keys) as a Bearer token. Admins can turn the server on or off under Admin → Settings.

The agent sees exactly what the key reaches: its team, its datasets if you limited it, and its access level. A Read only key gives the agent all five tools; a Search only key allows list_datasets, search and get_document, and refuses describe_dataset and get_synonyms with a message saying why. No level lets an agent change data.

The server is read-only and exposes five tools:

  • list_datasets — datasets the token can reach, with document counts and state.
  • describe_dataset — the queryable surface: configured fields with their capabilities (searchable / filterable / facetable / sortable) plus value hints — the distinct values for facetable fields and numeric ranges — an owner-authored description, and a sample document. Call this first so the agent filters with real values instead of guessing.
  • search — ranked results with relevance scores. Declarative filters on filterable fields ({field, value} or {field, min, max}), optional facets, and full documents (or a projected subset).
  • get_document — the full JSON document for a key.
  • get_synonyms — the dataset's synonym list, or null when it has none. Lets an agent see why a search matched more than its literal words.

Precision by default

search uses the same precision-first matching described below — near-exact only (IncludePatternMatches = false), with typo tolerance retained. An empty result is a trustworthy "nothing matches", so the agent can stop or ask rather than hallucinate. Pass broaden: true to opt into broad pattern matching for a single call. Saved boost rules are applied automatically, so agent searches inherit your ranking policy.

Connecting a client

Add the server with the endpoint URL and your API key as a Bearer token. Clients without a custom-header field can use the mcp-remote bridge. Pin its version, and pass the key through env so it does not show up in the process list:

json
{
  "mcpServers": {
    "indx": {
      "command": "npx",
      "args": ["mcp-remote@0.14.3", "https://<your-host>/mcp",
               "--header", "Authorization:${AUTH_HEADER}"],
      "env": { "AUTH_HEADER": "Bearer <your-api-key>" }
    }
  }
}

There are no spaces around the colon in Authorization:${AUTH_HEADER}; the space belongs inside the variable.

The tools return your dataset's content as it is, and an agent reads it. If documents hold text from outside sources, such as reviews or scraped pages, keep in mind that some of it could be written to look like instructions to the agent. A Search key limits what such text could lead to: it can only list, search and fetch.

A typical agent flow: describe_dataset to learn the fields and valid filter values → search with text and filters → get_document for full detail on a hit.


Precision Over Recall

Human search benefits from typo tolerance and fuzzy matching. Agents query with well-formed text and need a clear signal: did something match, or not? Configure coverage to reflect that.

csharp
var cov = new CoverageSetup
{
    CoverFuzzyWords    = false,  // agents don't make typos
    CoverWholeWords    = true,
    CoverPrefixSuffix  = true,
    CoverJoinedWords   = true,
    IncludePatternMatches = false, // only return confirmed hits — no soft pattern results
};

var query = new Query("invoice payment overdue", 20) { CoverageSetup = cov };
var result = engine.Search(query);

With IncludePatternMatches = false, the result contains only coverage-confirmed matches. If nothing was confirmed, the result is empty — a clean no-match signal the agent can act on.

HTTP API

json
{
  "text": "invoice payment overdue",
  "maxNumberOfRecordsToReturn": 20,
  "coverageSetup": {
    "coverFuzzyWords": false,
    "coverWholeWords": true,
    "coverPrefixSuffix": true,
    "coverJoinedWords": true,
    "includePatternMatches": false
  }
}

Using the Truncation Point

When IncludePatternMatches is true (or not set), results up to and including truncationIndex are coverage-confirmed and results after it are softer pattern matches. An agent can use this boundary as a quality gate:

csharp
var result = engine.Search(query);
// TruncationIndex is the last confirmed result; -1 means none was confirmed.
var confirmed = result.TruncationIndex >= 0
    ? result.Records[..(result.TruncationIndex + 1)]
    : [];

Multiple Queries for Better Recall

Agents can compensate for turning off fuzzy matching by issuing multiple searches with reformulated queries — synonyms, related terms, or decomposed phrases. Merge results by document key, keeping the highest score per document:

csharp
var queries = new[] { "payment overdue", "outstanding invoice", "unpaid bill" };

var merged = new Dictionary<long, ushort>();
foreach (var text in queries)
{
    var r = engine.Search(new Query(text, 20) { CoverageSetup = cov });
    foreach (var entry in r.Records)
    {
        if (!merged.TryGetValue(entry.DocumentKey, out var existing) || entry.Score > existing)
            merged[entry.DocumentKey] = entry.Score;
    }
}

var ranked = merged.OrderByDescending(kv => kv.Value).ToList();

Because Indx queries are fast, firing three to five searches adds negligible latency compared to a single LLM call.


Structured Lookups with Filters

When the agent knows field values, use filters instead of relying on text search alone:

csharp
var statusFilter = engine.CreateValueFilter("status", "overdue", out _);
var query = new Query("payment", 20)
{
    Filter = statusFilter,
    CoverageSetup = cov
};

Combine filters for multi-condition lookups:

csharp
var overdueFilter   = engine.CreateValueFilter("status", "overdue", out _);
var highValueFilter = engine.CreateRangeFilter("amount", 10000, int.MaxValue, out _);
var combined        = overdueFilter & highValueFilter; // AND; use | for OR, ! to negate

Letting the Agent Build the Index

An agent doesn't need a pre-existing dataset. It can spin up a fresh in-memory index from scratch — useful when the agent is handed a large JSON file, a collection of PDFs, or any content it discovers at runtime. The engine is initialised from the first batch of documents and made searchable immediately.

csharp
var engine = new SearchEngine();

// Build the schema from your first batch of documents
using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(firstBatchJson));
engine.Init(stream);

// Configure which fields to search
engine.GetField("title")!.Searchable   = true;
engine.GetField("content")!.Searchable = true;

// Load the first batch and build the initial index
stream.Position = 0;
engine.Load(stream);
engine.Index();

// Insert additional documents as the agent discovers them — no rebuild needed
engine.InsertJsonRecords(additionalDocuments, monitor: null, error: out _);

This pattern works well when an agent processes a large document set at task start: parse the content into JSON chunks, load them all, then search freely. Because the engine is embedded and in-memory, the full pipeline — from raw files to searchable index — completes in seconds even for tens of thousands of documents.

Incremental Updates

Agents that discover or generate data over time can continue inserting without rebuilding:

csharp
// Single document
engine.InsertJsonRecord(
    documentKey: 42,
    json: """{"id":42,"title":"Q2 Report","content":"Revenue up 12%..."}""",
    monitor: null,
    error: out _
);

// Batch
engine.InsertJsonRecords(newDocuments, monitor: null, error: out _);

Updates and deletes work the same way — the index stays in sync after every operation. A full Index() rebuild is never required after mutations, though calling it periodically re-normalises scoring for better result quality over time.


Empty Search for Enumeration

An agent that needs to enumerate or scan all documents can use an empty search with sorting and filters:

csharp
var query = new Query("", 100)
{
    EnableFacets = true,
    SortBy       = engine.GetField("date")!,
    SortAscending = false,
    Filter        = statusFilter
};

Facets are calculated across the full dataset regardless of maxNumberOfRecordsToReturn, giving the agent a complete picture of value distributions without retrieving all documents.