MCP server

Build with Headband

MCP server

Headband exposes an MCP (Model Context Protocol) server over HTTP. Add Headband to Claude Code with one command, and your AI assistant can search your indexes.

Auth uses the same API keys as the REST API. Over MCP an agent can search and read, and cannot change anything, whatever the key.

Search matches words, not meaning. Agents get the best results with keyword queries, as Queries and long documents explains.

Setup

The MCP server is available at /api/mcp and speaks JSON-RPC 2.0 over HTTP, with no local proxy needed. Use a search key (hb_src_) unless the agent needs stats or single documents, which take an admin key (hb_adm_). See the tools below.

Claude Code
export SEARCH_KEY="hb_src_..."
claude mcp add --transport http headband \
  https://headband.dev/api/mcp \
  --header "Authorization: Bearer $SEARCH_KEY"

By default the server is yours alone, in the current project. Add --scope project to write it to the project's .mcp.json instead, which is shared through git, so keep a key there only if everyone with the repository may use it. Run claude mcp get headband to check it.

We have tested four MCP clients: Claude Code, the MCP TypeScript SDK, the MCP Python SDK and the MCP Inspector. Each needs the same two things: the URL, and an Authorization header with your key. A client we have not tested needs the same two things, if it speaks MCP over HTTP.

GET/api/mcp

Returns MCP server capability descriptor.

Auth
admin or search key
Returns
200
Response
{
  "name": "headband",
  "transport": "http",
  "protocol": "json-rpc-2.0",
  "mcpProtocolVersion": "2025-06-18",
  "hint": "POST a JSON-RPC 2.0 request …"
}

Available tools

The MCP server is read-only, whatever the key. Its tools search, list indexes and read stats and documents. An agent cannot create or delete an index, add or delete documents, or change settings through MCP. MCP lets an agent see what your indexes hold. It does not set search up for you. For that, give the agent the REST API or the CLI with an admin key, on a machine you control, and point it at llms.txt.

The MCP server exposes 5 tools. All are engine-agnostic: the AI assistant never needs to know whether an index is backed by Meilisearch, Typesense, Elasticsearch or Visor.

A search key, the key most MCP client configs will hold, can call search_org, search_index and list_indexes. get_index_stats and get_document need an admin key, as their REST routes do. With a search key they answer an error result naming the key they need. Put an admin key only in a client that runs on your own machine or server, never in a browser.

ToolKeyArgumentsWhat it does
search_orgsearch or adminquery (required), limit, perIndexLimit, filter, mode, indexAllowlist, indexDenylist, includeSources, branchSearch the indexes visible to the API key, at most 10 per call, merge results with reciprocal rank fusion, and return a single ranked list. Takes mode (instant or retrieval) for its Visor indexes. The query argument is query, not q as on /v1/search.
search_indexsearch or adminindex (required), q (required, empty lists every document), limit, filter, mode, branchFull-text search a single index by UID. Returns ranked hits as JSON. Takes mode on a Visor index. There is no sort argument.
list_indexessearch or adminbranchList every index the API key can access, with engine type.
get_index_statsadminindex (required), branchGet document count, indexing health, and field distribution for an index.
get_documentadmin with documents.readindex (required), id (required), branchFetch a single document by its primary key. Long values are cut as in a search answer.

search_org limits

search_org searches at most 10 indexes per call. With more, it searches the first 10 and lists the rest in skippedIndexes with the reason over_limit, at most 20 of them. overLimitCount counts them all. The answer is still a success, so check skippedIndexes. To choose which indexes are searched, pass indexAllowlist (only these) or indexDenylist (never these, listed with the reason denylisted). An index that fails is listed there too, with its error, and the others still answer.

search_org result (shape)
{
  "query": "glacier mitts",
  "scope": "project",
  "results": [ ... ],
  "searchedIndexes": [
    "products",
    "articles",
    "..."
  ],
  "skippedIndexes": [
    {
      "index": "faq",
      "reason": "over_limit"
    },
    {
      "index": "drafts",
      "reason": "denylisted"
    }
  ],
  "overLimitCount": 1
}

limit (the merged list) defaults to 10 and perIndexLimit to 5; both go up to 20. Each index that answers counts as one search on your plan.

Queries and long documents

Search matches words, not meaning. The tools run each engine's keyword search, so send two to five distinctive keywords, such as tenant token, not a full question. If nothing comes back, try fewer or different words. On a Visor index, mode: "retrieval" copes better with a full sentence, and keywords still do best.

filter takes one syntax on every engine, for example price > 100 AND category = "shoes". On Meilisearch and Visor a field must be set up as filterable first, and a refused filter names the fields that are.

A search answer is cut to a size a model can read: about 8,000 tokens of JSON, by an estimate that counts four ASCII characters, or one other character, as a token. Each long text value is cut to at most 2,000 characters, shorter when needed, and marked with its full length. An array of more than 50 items keeps its first 50. When the hits still do not fit, the last ones are dropped, and a single hit that is still too big keeps its primary key and its first fields. The answer then carries a truncated field that says what was cut. Ask for fewer results with limit, or narrow the query. get_document is cut the same way, so it shows more of one document than a search does, but not always all of it. To read one passage of a long document, search for it with specific keywords.

Resources

Each index is exposed as an MCP resource at headband://index/{uid}. Reading a resource returns index stats and a sample of documents, useful for letting an AI assistant understand the shape of your data before searching. The same key rules apply: with a search key the resource answers with stats null and no sample documents, and an admin key with documents.read gets both.

Headband as a tool in your own agent

MCP is for clients that speak it, such as Claude Code. If you write the agent yourself, give it a search tool that makes one call to POST /v1/search with a search key. Return only the fields the model needs, keep limit small, and send keywords as above. Both snippets ran against production on 2026-11-12 (ai 7.0.171, zod 4.6.7).

Vercel AI SDK
import { tool } from "ai";
import { z } from "zod";

export const searchProducts = tool({
  description: "Search the product catalog by keywords. Returns the best matching products.",
  inputSchema: z.object({
    query: z.string().describe("Keywords to search for, for example 'waterproof jacket'"),
    limit: z.number().int().min(1).max(10).default(3),
  }),
  execute: async ({ query, limit }) => {
    const res = await fetch("https://api.headband.dev/v1/search", {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.HEADBAND_SEARCH_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ index: "products", q: query, limit }),
    });
    if (!res.ok) throw new Error(`Headband search failed: ${res.status}`);
    const { hits } = await res.json();
    return hits.map(({ id, name, description, price }) => ({ id, name, description, price }));
  },
});

// generateText({ model, tools: { searchProducts }, prompt: "..." })
Anthropic tool use
export const searchProductsTool = {
  name: "search_products",
  description: "Search the product catalog by keywords. Returns the best matching products.",
  input_schema: {
    type: "object",
    properties: {
      query: { type: "string", description: "Keywords to search for, for example 'waterproof jacket'" },
      limit: { type: "integer", minimum: 1, maximum: 10 },
    },
    required: ["query"],
  },
};

// Pass tools: [searchProductsTool] to client.messages.create(). When a reply holds a tool_use
// block, run it here and send the tool_result back in the next user message.
export async function runSearchProducts(block) {
  const res = await fetch("https://api.headband.dev/v1/search", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.HEADBAND_SEARCH_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ index: "products", q: block.input.query, limit: block.input.limit ?? 3 }),
  });
  if (!res.ok) {
    return { type: "tool_result", tool_use_id: block.id, is_error: true, content: `Search failed: ${res.status}` };
  }
  const { hits } = await res.json();
  const slim = hits.map(({ id, name, description, price }) => ({ id, name, description, price }));
  return { type: "tool_result", tool_use_id: block.id, content: JSON.stringify(slim) };
}

A search key reads every field you index, so leave private fields out of your documents. To limit an agent to some indexes, put them in their own project: a key limited to chosen indexes answers 403 all_indexes_key_required on /api/mcp, though it works on /v1/search. For a customer-scoped agent, search with a tenant token.