Arranger MCP server

August 31, 2026 · View on GitHub

The Arranger MCP server exposes a running Arranger instance's catalogue data and query tools to AI models, scripts, and pipelines. It is one of two surfaces for that purpose; the other is the Introspection API, a set of read-only REST endpoints that any client can call directly.

The arranger-mcp-server package implements the Model Context Protocol over Streamable HTTP. Connect any MCP-compatible AI client to it and the client can discover available catalogues, retrieve field metadata and the SQON schema, and construct search queries: without needing Arranger-specific integration code on the model side.


Quick start

# from the monorepo root
npm run mcp-server:dev

The server starts on http://localhost:3100/mcp by default. Two environment variables are required:

VariableDescription
ARRANGER_BASE_URLURL of the running Arranger search-server
ARRANGER_CATALOGUESComma-separated list of catalogue IDs to expose

All other variables (host, port, path, log level, request timeout) have sensible defaults. Copy apps/mcp-server/.env.schema to apps/mcp-server/.env to start from a working local baseline.

What the server exposes

Instructions (sent once, in the initialize response):

The server returns a short set of usage instructions that most clients fold into the model's system prompt. It describes what the server is for, requires the model to discover catalogue names, field names, and SQON syntax through the tools below rather than recalling them, and gives the call order (list_cataloguesget_catalogue_fieldsbuild_sqonexecute_query). Clients that ignore instructions still get the same rules from the tool descriptions, though later in the exchange.

Tools (callable actions):

  • list_catalogues: returns the catalogues registered on this Arranger instance
  • get_sqon_schema: returns the SQON JSON Schema and operator metadata
  • get_catalogue_fields: returns field metadata for one catalogue (input: catalogueId)
  • build_sqon: builds a validated SQON from plain field, operator, and value inputs, so a model never has to write SQON itself (input: { catalogueId, combination: 'and' | 'or', clauses: [{ fieldName | fieldNames, operator, value, negate? }], existingSqon? })
  • execute_query: builds, confirms, and executes a SQON-filtered query against one catalogue (input: { catalogueId, sqon, queryType = 'hits', fields [], first = 20, offset = 0, sort, aggregationFields = [], includeMissing = true, aggregationsFilterThemselves = false })

build_sqon returns { sqon, summary, clauseCount, filterCount, notes? } and executes nothing. Pass the resulting sqon to execute_query unchanged. Every clause is validated against the catalogue before a SQON is built, and one error is reported per invalid clause so that a whole batch can be corrected in a single resubmission. summary is a plain-English rendering of the built SQON, using the catalogue's display names, meant to be read back to the user for confirmation. clauseCount and filterCount differ when equivalent clauses on the same field merged, either during the build (two lower bounds on one field collapse to the stricter one, for example) or because a supplied existingSqon was already redundant on arrival (two same-field clauses inside it that merge on their own); notes explains the difference when they do.

Most clauses name one field with fieldName and take a value operator: in, not-in, some-not-in, all, gt, gte, lt, lte, between. A wildcard clause is the exception: it names several fields with fieldNames (plural) and matches when any one of them matches. Include * for a substring search, since "TP53" matches only a value that is exactly TP53 while "*TP53*" matches one containing it; negate: true expresses "does not contain". Which operators a field accepts is decided by the catalogue, not this tool, so read operators from get_catalogue_fields.

An asterisk inside an in, not-in, some-not-in, or all value is rejected, because Arranger runs such a value as a regular expression rather than matching it literally: use wildcard instead. execute_query's raw sqon parameter still accepts it, so an asterisk-bearing keyword value is reachable there but not through build_sqon.

Two in clauses on the same field also merge, by combining their value lists, when the field is declared single-valued (isArray: false): status in ['active'] together with status in ['pending'] becomes status in ['active', 'pending'], meaning "either", the correct reading there since no document could satisfy both clauses at once. On a field that can hold several values at once (isArray: true), or one whose cardinality is undeclared (isArray: null, or absent entirely on a server that predates the field), the other reading, "every one of these must be present", is equally legitimate, so build_sqon refuses to guess and returns an error naming both readings instead of merging silently. Use all for "every one of these" once the field's isArray confirms it can hold more than one value; all carrying more than one value is refused the same way on a field that isn't confirmed multi-valued.

One combination applies to the whole call. Mixed AND/OR nesting and the planned fuzzy operator are not yet supported: a query needing either still requires a hand-written sqon. An unfiltered query needs no build_sqon call at all; pass {"op":"and","content":[]} to execute_query directly.

execute_query takes field names exactly as get_catalogue_fields reports them, but its results are keyed by the names Arranger's generated GraphQL schema uses, which are not always the same string. A name carrying a character GraphQL disallows is rewritten for the schema: dots separating an aggregation path become __, any other disallowed character becomes _, and a leading digit gets an _ prefix. So a request for donor-info.age-at-diagnosis comes back as donor_info { age_at_diagnosis } under hits, and as donor_info__age_at_diagnosis under aggregations. Field names inside a sqon are never rewritten, in either direction: a SQON travels as a query variable rather than as part of the query document, so it always uses the raw name.

Resources (readable data by URI):

  • arranger://introspection/server: server-wide catalogue inventory
  • arranger://introspection/sqon: SQON schema and operator metadata
  • arranger://introspection/catalog/{catalogueId}: per-catalogue field metadata

Prompts (callable by clients):

  • query_arranger: accepts the user's goal as an input, and returns two messages containing the "system prompt" (workflow instructions, which route SQON construction through build_sqon) and the user's goal

Connecting a client

Any MCP-compatible client that supports Streamable HTTP can connect. Point it at the MCP server URL (http://127.0.0.1:3100/mcp with default config) and use transport type streamable-http.

MCP Inspector is useful during development: it's a browser-based UI for browsing resources and calling tools:

npm run mcp-server:inspect

For LM Studio and other model hosts, follow the client's documentation to add an MCP server entry. The connection config lives at apps/mcp-server/mcp-inspector.json as a starting point.


SQON generation

A model connected over MCP should not construct SQON at all: build_sqon does it, from field, operator, and value inputs the model selects out of get_catalogue_fields. That is the whole point of the tool, so the rules below are enforced rather than merely documented, and a mistake is reported per clause instead of surfacing as an Arranger query error.

The rest of this section is for a client constructing SQON directly, without the MCP server: a script, a pipeline, or the cases build_sqon does not yet cover (mixing AND and OR in one query, and the planned fuzzy operator). Use the introspection API to derive field names, types, and valid operators at runtime rather than hard-coding them. This keeps the client current when a catalogue mapping changes.

Safe defaults for programmatic SQON construction:

  • Use canonical operator names (in, not-in, gt, wildcard, etc.); do not use aliases (=, !=, filter)
  • A single condition can be a bare leaf node: no wrapping and is needed
  • gt, gte, lt, lte take a single scalar value; between takes exactly [min, max]
  • Preserve falsy values: 0, "", and false are valid and must not be filtered out before construction
  • Every operator except wildcard uses fieldName (string); wildcard uses fieldNames (array or string): this is a schema constraint, not a convention
  • Do not invent pivot values; derive them from the live catalogue mapping or omit them
  • Use not-in for value exclusion, not not { in: [...] }: combining the two is a double negative

For a detailed walkthrough of the SQON format and how to compose queries, see Building SQON queries.


What's coming

  • build_sqon mixed combinators and fuzzy search: mixing AND and OR in one query, and the fuzzy (edit-distance) operator, are still to come
  • Authentication: the MCP server currently requires no auth; support is planned
  • Chat interface: a conversational front-end for non-technical users to search catalogues in plain language