October 11–14, 2026·Glasgow, Scotland

#CommunityOverCode

Talk to Apache Solr in plain English.

Solr MCP is a Model Context Protocol server that hands your AI assistant fifteen Solr tools — search, indexing, schema, collection and alias management. This card takes you from nothing installed to running faceted queries against a real index. Ten minutes, no Solr syntax required.

Time ~10 min Needs Docker, or Java 25 Dataset 61 streaming shows Tools 15 · 2 resources · 6 prompts
0

Before anything else

Pick your runtime

Two ways to run the server. Docker is the shorter road — the image carries its own Java, so nothing on your machine needs to change.

Your OS
Run via
Transport
Data format
Client

These five choices drive every command on this page. Set them once. STDIO is the default: your client launches the server as a subprocess, nothing else to run. HTTP starts one server in a terminal that any client connects to over a URL. The data format picks which flavour of the same 61-show dataset step 3 indexes; JSON is the path the rest of the card was written against.

Check Docker

docker --version

Any recent version works. If this errors, install Docker Desktop (macOS/Windows) or Docker Engine (Linux).

Check your Java

java -version
Read this before you file a bug

The JAR is built for Java 25. On Java 17 or 21 it fails with UnsupportedClassVersionError — and because STDIO servers are launched silently by your client, most clients show only "server failed to start" with no reason. If the server won't connect, check this first.

Get the JAR

Prebuilt, so you don't need Git or Gradle — 52 MB:

curl -LO https://github.com/adityamparikh/solr-mcp/releases/download/coc2026/solr-mcp-1.0.0-SNAPSHOT.jar

Built from the same commit as the container image, so both paths behave identically. Unofficial pre-release build — not an Apache release.

Install Java 25

# SDKMAN (recommended — no sudo, easy to switch back)
sdk install java 25-tem

# or Homebrew
brew install --cask temurin@25
winget install EclipseAdoptium.Temurin.25.JDK
# SDKMAN (recommended — no sudo, easy to switch back)
curl -s "https://get.sdkman.io" | bash
source "$HOME/.sdkman/bin/sdkman-init.sh"
sdk install java 25-tem
1

One container, no clone

Start Solr

SolrCloud mode with embedded ZooKeeper, which is what the collection tools need. Ready in about seven seconds. The first line creates a private Docker network: the server's container joins it later and reaches Solr by its name, solr-mcp-demo. Solr's port is published on 127.0.0.1 only, so it answers on your laptop and nobody else on the conference Wi-Fi can reach it.

docker network create solr-mcp
docker run -d --name solr-mcp-demo --network=solr-mcp -p 127.0.0.1:8983:8983 solr:9-slim solr start -c -f

While that boots, pull the server image too. It is ~170 MB, and several clients time out a first connection at 60 seconds — long enough to look like a broken config when it is really just a download:

docker pull ghcr.io/adityamparikh/solr-mcp:coc2026

Confirm Solr is up — this should return an empty collection list:

curl "http://localhost:8983/solr/admin/collections?action=LIST"

The admin UI is at localhost:8983/solr. Keep it open on a second screen if you have one — watching collections appear as you talk to the assistant is half the fun.

Why not docker compose

The repo ships a compose.yaml, but it exists to preload the films and books samples. This card builds its own collection from scratch, so a single container is all you need — no clone, no Gradle, no build.

2

Wire it to your assistant

Connect the server

Commands below follow the choices you made in step 0. Change them there and everything on this page updates.

Unofficial build

ghcr.io/adityamparikh/solr-mcp:coc2026 is a personal pre-release build for this event — not an Apache release. Apache Solr MCP is an incubating project and has not cut a voted release yet, so no official image exists. Building it yourself is covered under Build it yourself.

Windows paths in JSON

Backslashes are escape characters in JSON, so a Windows path must double them — "C:\\\\Users\\\\you\\\\solr-mcp-1.0.0-SNAPSHOT.jar" — or just use forward slashes, which Java accepts on Windows: "C:/Users/you/solr-mcp-1.0.0-SNAPSHOT.jar". Forward slashes are the safer habit.

Start the server

In HTTP mode the server is a process you start yourself, in a terminal you keep open. Every client then connects to http://localhost:8080/mcp.

docker run -p 127.0.0.1:8080:8080 --rm \
  --network=solr-mcp \
  -e PROFILES=http \
  -e HTTP_SECURITY_ENABLED=false \
  -e SOLR_URL=http://solr-mcp-demo:8983/solr/ \
  ghcr.io/adityamparikh/solr-mcp:coc2026
PROFILES=http SERVER_ADDRESS=127.0.0.1 HTTP_SECURITY_ENABLED=false \
  SOLR_URL=http://localhost:8983/solr/ \
  java -jar /absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar
# PowerShell
$env:PROFILES="http"; $env:SERVER_ADDRESS="127.0.0.1"; $env:HTTP_SECURITY_ENABLED="false"; $env:SOLR_URL="http://localhost:8983/solr/"
java -jar C:/path/to/solr-mcp-1.0.0-SNAPSHOT.jar

Ready when curl http://localhost:8080/actuator/health answers {"status":"UP"}. Leave the terminal open; closing it stops the server for every client.

Security is off on purpose

HTTP mode requires OAuth2 out of the box (HTTP_SECURITY_ENABLED defaults to true, the issuer is OAUTH2_ISSUER_URI). HTTP_SECURITY_ENABLED=false is for a laptop at a hackathon, bound to localhost: the commands above listen on 127.0.0.1 only (-p 127.0.0.1:8080:8080 for Docker, SERVER_ADDRESS for the JAR), so nobody else on the conference Wi-Fi can reach it. Never run it that way on anything other people can reach; the Security setup panel below walks through turning it on, start to finish.

Point your client at it

Claude Code

claude mcp add solr-mcp -- docker run -i --rm \
  --network=solr-mcp \
  -e SOLR_URL=http://solr-mcp-demo:8983/solr/ \
  ghcr.io/adityamparikh/solr-mcp:coc2026
claude mcp add solr-mcp \
  -e SOLR_URL=http://localhost:8983/solr/ \
  -- java -jar /absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar

The -- matters: it stops Claude Code reading the server's own flags as its own. Verify with claude mcp list.

claude mcp add --transport http solr-mcp http://localhost:8080/mcp

Verify with claude mcp list; the server must already be running or the entry shows as failed.

Codex

codex mcp add solr-mcp -- docker run -i --rm \
  --network=solr-mcp \
  -e SOLR_URL=http://solr-mcp-demo:8983/solr/ \
  ghcr.io/adityamparikh/solr-mcp:coc2026
codex mcp add solr-mcp \
  --env SOLR_URL=http://localhost:8983/solr/ \
  -- java -jar /absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar

The -- separates Codex's flags from the server's launch command. This writes a [mcp_servers.solr-mcp] table to ~/.codex/config.toml, which you can also edit by hand. Verify with codex mcp list, or /mcp inside a Codex session.

Codex gives a server about 10 seconds to start. A cold docker run that has to pull the image will miss that — run docker pull ghcr.io/adityamparikh/solr-mcp:coc2026 once first, or add startup_timeout_sec = 60 to the [mcp_servers.solr-mcp] table.

codex mcp add solr-mcp --url http://localhost:8080/mcp

Codex prints may or may not require login — ignore it, there is no login on an unsecured server. Verify with codex mcp list; the server must already be running when Codex starts.

ChatGPT

HTTP only

ChatGPT connects to a remote MCP server; it cannot launch a local process, so there is no STDIO setup. Switch Transport to HTTP above.

Needs a public HTTPS URL

localhost is unreachable from ChatGPT. Put a tunnel in front of the server you started above, for example ngrok http 8080, and use the https://… address it prints with /mcp appended. Because security is off on this server, anyone with that URL can read and change your Solr — demo against a throwaway Solr and stop the tunnel afterwards. ChatGPT supports OAuth 2.1 or no auth only, and this server's secured mode is not compatible yet, so there is no secured variant.

  1. Settings → Security and login: turn on Developer mode (Pro, Plus, Business, Enterprise, Education; workspace admins may need to allow it).
  2. Open ChatGPT Plugins, press +, and create a developer-mode app.
  3. Name it solr-mcp; under Connection enter https://<your-tunnel>/mcp; choose No authentication.
  4. Create it and check the discovered tools — there should be fifteen. Enable the app in a chat.

ChatGPT asks before running tools it treats as writes, which is most of them. After restarting the server, press Refresh on the app.

Claude Desktop

Edit the config file, then fully quit and reopen Claude Desktop:

~/Library/Application Support/Claude/claude_desktop_config.json
%APPDATA%\Claude\claude_desktop_config.json
~/.config/Claude/claude_desktop_config.json
On WSL

Claude Desktop runs on the Windows host, not inside WSL — so use the Windows path above and let Docker Desktop's WSL integration handle the container.

On Linux

There is no official Claude Desktop build for Linux. That path is where community builds look; if you're on Linux, Claude Code or MCP Inspector is the smoother route.

{
  "mcpServers": {
    "solr-mcp": {
      "command": "docker",
      "args": ["run", "-i", "--rm",
               "--network=solr-mcp",
               "-e", "SOLR_URL=http://solr-mcp-demo:8983/solr/",
               "ghcr.io/adityamparikh/solr-mcp:coc2026"]
    }
  }
}
{
  "mcpServers": {
    "solr-mcp": {
      "command": "java",
      "args": ["-jar", "/absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar"],
      "env": { "SOLR_URL": "http://localhost:8983/solr/" }
    }
  }
}

Claude Desktop's own Connectors screen cannot reach this server: custom connectors are contacted from Anthropic's cloud, not from your laptop, so a localhost URL is unreachable there and would need a public HTTPS tunnel and OAuth switched back on. Skip that. Bridge it locally instead with the mcp-remote shim (needs Node.js), which talks plain HTTP to the server on your machine:

{
  "mcpServers": {
    "solr-mcp": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "http://localhost:8080/mcp"]
    }
  }
}

Same config file as the STDIO route (paths above). Then fully quit and reopen Claude Desktop. No ngrok needed.

VS Code / GitHub Copilot

Needs VS Code 1.99+. Create .vscode/mcp.json in your workspace, then use Copilot Chat in Agent mode.

{
  "servers": {
    "solr-mcp": {
      "type": "stdio",
      "command": "docker",
      "args": ["run", "-i", "--rm",
               "--network=solr-mcp",
               "-e", "SOLR_URL=http://solr-mcp-demo:8983/solr/",
               "ghcr.io/adityamparikh/solr-mcp:coc2026"]
    }
  }
}
{
  "servers": {
    "solr-mcp": {
      "type": "stdio",
      "command": "java",
      "args": ["-jar", "/absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar"],
      "env": { "SOLR_URL": "http://localhost:8983/solr/" }
    }
  }
}

Same file, .vscode/mcp.json, with a URL instead of a command:

{
  "servers": {
    "solr-mcp": {
      "type": "http",
      "url": "http://localhost:8080/mcp"
    }
  }
}

Cursor

Create .cursor/mcp.json in your project root:

{
  "mcpServers": {
    "solr-mcp": {
      "command": "docker",
      "args": ["run", "-i", "--rm",
               "--network=solr-mcp",
               "-e", "SOLR_URL=http://solr-mcp-demo:8983/solr/",
               "ghcr.io/adityamparikh/solr-mcp:coc2026"]
    }
  }
}
{
  "mcpServers": {
    "solr-mcp": {
      "command": "java",
      "args": ["-jar", "/absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar"],
      "env": { "SOLR_URL": "http://localhost:8983/solr/" }
    }
  }
}

Same file, .cursor/mcp.json, with a URL instead of a command:

{
  "mcpServers": {
    "solr-mcp": { "url": "http://localhost:8080/mcp" }
  }
}

Zed

Zed has a form for this, which is the easier route here — no JSON, and it validates as you type. Settings → AI → General → MCP Servers → Add Server → Add Local MCP Server, then:

Server Namesolr-mcp
Commanddocker
Argumentsrun -i --rm --network=solr-mcp -e SOLR_URL=http://solr-mcp-demo:8983/solr/ ghcr.io/adityamparikh/solr-mcp:coc2026
Timeout60 is fine once the image is pulled — see below
Server Namesolr-mcp
Commandjava
Arguments-jar /absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar
Environment VariablesSOLR_URL = http://localhost:8983/solr/

Arguments go in one field, space-separated — not as a JSON array. The form writes the same settings entry you'd hand-edit below.

Pull the image first

Zed's default timeout is 60 seconds, and a first run has to fetch ~170 MB. On venue wifi that can outrun the timeout, and the failure looks like a broken config rather than a slow download. Run docker pull ghcr.io/adityamparikh/solr-mcp:coc2026 once beforehand and the problem disappears.

Prefer JSON? zed: open settings from the command palette, and add a context_servers entry:

{
  "context_servers": {
    "solr-mcp": {
      "command": "docker",
      "args": ["run", "-i", "--rm",
               "--network=solr-mcp",
               "-e", "SOLR_URL=http://solr-mcp-demo:8983/solr/",
               "ghcr.io/adityamparikh/solr-mcp:coc2026"],
      "env": {}
    }
  }
}
{
  "context_servers": {
    "solr-mcp": {
      "command": "java",
      "args": ["-jar", "/absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar"],
      "env": { "SOLR_URL": "http://localhost:8983/solr/" }
    }
  }
}
Zed schema — verified 1.16.2

Verified against Zed 1.16.2. Zed's schema takes command as a plain string with args and env alongside it — there is no source key, and adding one makes Zed's settings validator reject the entry. If a future release moves this again, check zed.dev/docs/ai/mcp.

Settings → AI → General → MCP Servers → Add Server → Add Remote Server, name it solr-mcp, URL http://localhost:8080/mcp. Or in zed: open settings:

{
  "context_servers": {
    "solr-mcp": { "url": "http://localhost:8080/mcp" }
  }
}

Zed starts an OAuth flow only if the server answers 401. With security off it will not; if it prompts anyway, add "headers": { "Authorization": "Bearer x" } to the entry, which the server ignores.

IntelliJ IDEA / JetBrains

Needs the AI Assistant plugin. Either drop a file at .junie/mcp/mcp.json in your project root:

{
  "mcpServers": {
    "solr-mcp": {
      "command": "docker",
      "args": ["run", "-i", "--rm",
               "--network=solr-mcp",
               "-e", "SOLR_URL=http://solr-mcp-demo:8983/solr/",
               "ghcr.io/adityamparikh/solr-mcp:coc2026"]
    }
  }
}
{
  "mcpServers": {
    "solr-mcp": {
      "command": "java",
      "args": ["-jar", "/absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar"],
      "env": { "SOLR_URL": "http://localhost:8983/solr/" }
    }
  }
}

…or add it through Settings → Tools → AI Assistant → MCP Servers → Add, transport STDIO.

Same file, .junie/mcp/mcp.json, with a URL instead of a command:

{
  "mcpServers": {
    "solr-mcp": { "url": "http://localhost:8080/mcp" }
  }
}

…or Settings → Tools → AI Assistant → MCP Servers → Add, transport HTTP.

MCP Inspector

No editor, no assistant — a browser UI that shows the raw protocol. The best way to see what MCP actually is, and the fastest way to prove the server works.

npx @modelcontextprotocol/inspector

Opens at http://localhost:6274. Choose STDIO, then:

Commanddocker
Argumentsrun -i --rm --network=solr-mcp -e SOLR_URL=http://solr-mcp-demo:8983/solr/ ghcr.io/adityamparikh/solr-mcp:coc2026
Commandjava
Arguments-jar /absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar

Hit Connect, then List Tools. You should see fifteen. Click list-collections and run it — that round-trip is the whole protocol in one click.

npx @modelcontextprotocol/inspector

Opens at http://localhost:6274. Choose Streamable HTTP, URL http://localhost:8080/mcp, then Connect and List Tools. Click list-collections and run it.

Confirm the connection

Ask your assistant:

What Solr collections are available?calls list-collections — expect an empty list, which is correct right now

An empty list means everything works. If the assistant says it has no Solr tools, see When it breaks.

3

The lazy way, on purpose

Index with no schema at all

Solr's _default configset is schemaless: throw data at it and it invents field types for you. We're going to do exactly that, watch it work, and then watch it bite. That contrast is the point of this whole exercise.

Create a Solr collection called shows-auto.calls create-collection

Get the data in

Pick whichever suits your client. File-aware clients (Claude Code, Codex, Cursor, Copilot, Zed, IntelliJ) can read a downloaded file; chat-only clients want a paste.

Option A — 61 shows from a file Claude Code, Codex, Cursor, Copilot, Zed, IntelliJ
curl -O https://raw.githubusercontent.com/apache/solr-mcp/main/src/test/resources/shows.json
Index the contents of ./shows.json into the shows-auto collection.calls index-json-documents — expect 61 of 61
curl -O https://raw.githubusercontent.com/apache/solr-mcp/main/src/test/resources/shows.csv
Index the contents of ./shows.csv into the shows-auto collection.calls index-csv-documents — Solr accepts the payload; the collection then holds 61

The server hands the file to Solr's own CSV handler, which reports that it accepted the payload rather than a document count. Multi-valued fields (genres, cast…) are repeated column headers; every non-empty cell becomes one value. Numbers arrive as text and Solr's parsers still guess numeric types, so the next step plays out the same.

curl -O https://raw.githubusercontent.com/apache/solr-mcp/main/src/test/resources/shows.xml
Index the contents of ./shows.xml into the shows-auto collection.calls index-xml-documents — Solr accepts the payload; the collection then holds 61

The file is Solr's own update format — <add><doc><field name="…">. The server checks that it is an <add> block (no deletes, no commits) and passes it to Solr unchanged, so the field names are the same as in JSON.

curl -O https://raw.githubusercontent.com/apache/solr-mcp/main/src/test/resources/shows-markdown.md
Index the contents of ./shows-markdown.md into the shows-auto collection.calls index-markdown-documents once — expect 61 of 61

The file holds 61 Markdown documents, each opening with its own YAML front matter, and the server splits it into one Solr document per show. The fields live in the front matter; the description is the body, which lands in content, with the title in headings.

Option B — a dozen shows, paste anywhere Claude Desktop, or any chat-only client

Same shape, same lesson, small enough to paste into a chat box. Copy this whole block and send it with the sentence at the top.

Index this JSON into the shows-auto collection:

[
  {"id":"hbo-001","title":"Game of Thrones","platform":"HBO Max","genres":["Fantasy","Drama","Action"],"release_year":2011,"end_year":2019,"status":"Ended","seasons":8,"episodes":73,"creators":["David Benioff","D.B. Weiss"],"cast":["Emilia Clarke","Kit Harington","Peter Dinklage"],"country":"USA","language":"English","rating":"TV-MA","imdb_rating":9.2,"description":"Noble families vie for control of the Iron Throne while an ancient threat returns.","tags":["epic","fantasy","adaptation","politics"]},
  {"id":"netflix-001","title":"Stranger Things","platform":"Netflix","genres":["Sci-Fi","Horror","Drama"],"release_year":2016,"end_year":2025,"status":"Ended","seasons":5,"episodes":42,"creators":["Matt Duffer","Ross Duffer"],"cast":["Millie Bobby Brown","Finn Wolfhard","Winona Ryder"],"country":"USA","language":"English","rating":"TV-14","imdb_rating":8.7,"description":"A group of kids in 1980s Indiana uncover supernatural mysteries and government conspiracies.","tags":["80s","supernatural","coming-of-age","monsters"]},
  {"id":"hulu-001","title":"The Bear","platform":"Hulu","genres":["Drama","Comedy"],"release_year":2022,"end_year":0,"status":"Ongoing","seasons":3,"episodes":28,"creators":["Christopher Storer"],"cast":["Jeremy Allen White","Ayo Edebiri","Ebon Moss-Bachrach"],"country":"USA","language":"English","rating":"TV-MA","imdb_rating":8.6,"description":"A fine-dining chef returns to Chicago to run his late brother's sandwich shop.","tags":["food","family","anxiety"]},
  {"id":"apple-001","title":"Ted Lasso","platform":"Apple TV+","genres":["Comedy","Drama","Sport"],"release_year":2020,"end_year":2023,"status":"Ended","seasons":3,"episodes":34,"creators":["Bill Lawrence","Jason Sudeikis"],"cast":["Jason Sudeikis","Hannah Waddingham","Brett Goldstein"],"country":"USA","language":"English","rating":"TV-MA","imdb_rating":8.8,"description":"An American football coach is hired to manage an English Premier League soccer team.","tags":["feel-good","sports","british"]},
  {"id":"disney-001","title":"The Mandalorian","platform":"Disney+","genres":["Sci-Fi","Action","Adventure"],"release_year":2019,"end_year":0,"status":"Ongoing","seasons":3,"episodes":24,"creators":["Jon Favreau"],"cast":["Pedro Pascal","Carl Weathers","Giancarlo Esposito"],"country":"USA","language":"English","rating":"TV-14","imdb_rating":8.7,"description":"A lone bounty hunter guards a mysterious child across the outer reaches of the galaxy.","tags":["star-wars","space-western","adventure"]},
  {"id":"hbo-002","title":"Succession","platform":"HBO Max","genres":["Drama","Comedy"],"release_year":2018,"end_year":2023,"status":"Ended","seasons":4,"episodes":39,"creators":["Jesse Armstrong"],"cast":["Brian Cox","Jeremy Strong","Sarah Snook"],"country":"USA","language":"English","rating":"TV-MA","imdb_rating":8.9,"description":"A media dynasty's children fight over control of the family empire.","tags":["satire","family","politics","business"]},
  {"id":"prime-001","title":"The Boys","platform":"Amazon Prime Video","genres":["Action","Comedy","Sci-Fi"],"release_year":2019,"end_year":0,"status":"Ongoing","seasons":4,"episodes":32,"creators":["Eric Kripke"],"cast":["Karl Urban","Jack Quaid","Antony Starr"],"country":"USA","language":"English","rating":"TV-MA","imdb_rating":8.7,"description":"A group of vigilantes set out to take down corrupt superheroes.","tags":["superhero","satire","violent"]},
  {"id":"netflix-002","title":"Dark","platform":"Netflix","genres":["Sci-Fi","Thriller","Drama"],"release_year":2017,"end_year":2020,"status":"Ended","seasons":3,"episodes":26,"creators":["Baran bo Odar","Jantje Friese"],"cast":["Louis Hofmann","Lisa Vicari","Oliver Masucci"],"country":"Germany","language":"German","rating":"TV-MA","imdb_rating":8.7,"description":"Four families search for a missing child and uncover a time-travel conspiracy.","tags":["time-travel","mystery","german"]},
  {"id":"prime-002","title":"Fleabag","platform":"Amazon Prime Video","genres":["Comedy","Drama"],"release_year":2016,"end_year":2019,"status":"Ended","seasons":2,"episodes":12,"creators":["Phoebe Waller-Bridge"],"cast":["Phoebe Waller-Bridge","Sian Clifford","Andrew Scott"],"country":"UK","language":"English","rating":"TV-MA","imdb_rating":8.7,"description":"A sharp-tongued woman navigates grief and family in London.","tags":["british","dark-comedy","fourth-wall"]},
  {"id":"apple-002","title":"Severance","platform":"Apple TV+","genres":["Sci-Fi","Thriller","Drama"],"release_year":2022,"end_year":0,"status":"Ongoing","seasons":2,"episodes":19,"creators":["Dan Erickson"],"cast":["Adam Scott","Britt Lower","Patricia Arquette"],"country":"USA","language":"English","rating":"TV-MA","imdb_rating":8.7,"description":"Office workers surgically divide their memories between work and personal life.","tags":["dystopian","workplace","mystery"]},
  {"id":"hbo-003","title":"The Last of Us","platform":"HBO Max","genres":["Drama","Horror","Adventure"],"release_year":2023,"end_year":0,"status":"Ongoing","seasons":2,"episodes":16,"creators":["Craig Mazin","Neil Druckmann"],"cast":["Pedro Pascal","Bella Ramsey","Gabriel Luna"],"country":"USA","language":"English","rating":"TV-MA","imdb_rating":8.7,"description":"A smuggler escorts a teenage girl across a post-apocalyptic United States.","tags":["adaptation","post-apocalyptic","survival"]},
  {"id":"hulu-002","title":"Only Murders in the Building","platform":"Hulu","genres":["Comedy","Mystery","Crime"],"release_year":2021,"end_year":0,"status":"Ongoing","seasons":4,"episodes":40,"creators":["Steve Martin","John Hoffman"],"cast":["Steve Martin","Martin Short","Selena Gomez"],"country":"USA","language":"English","rating":"TV-14","imdb_rating":8.1,"description":"Three true-crime obsessed neighbours investigate a death in their apartment building.","tags":["whodunnit","comedy","new-york"]}
]
Index this CSV into the shows-auto collection:
id,title,platform,genres,genres,genres,release_year,end_year,status,seasons,episodes,creators,creators,creators,cast,cast,cast,cast,country,language,rating,imdb_rating,description,tags,tags,tags,tags
hbo-001,Game of Thrones,HBO Max,Fantasy,Drama,Adventure,2011,2019,Ended,8,73,David Benioff,D.B. Weiss,,Emilia Clarke,Peter Dinklage,Kit Harington,Lena Headey,USA,English,TV-MA,9.2,"Nine noble families fight for control over the lands of Westeros, while an ancient enemy returns after being dormant for millennia.",fantasy,dragons,epic,george-rr-martin
netflix-001,Stranger Things,Netflix,Sci-Fi,Horror,Drama,2016,2025,Ended,5,42,Matt Duffer,Ross Duffer,,Millie Bobby Brown,Finn Wolfhard,Winona Ryder,David Harbour,USA,English,TV-14,8.7,A group of kids in 1980s Indiana uncover supernatural mysteries and government conspiracies tied to a parallel dimension.,80s,supernatural,coming-of-age,monsters
hulu-001,The Handmaid's Tale,Hulu,Drama,Sci-Fi,Dystopian,2017,2025,Ended,6,66,Bruce Miller,,,Elisabeth Moss,Joseph Fiennes,Yvonne Strahovski,Samira Wiley,USA,English,TV-MA,8.4,"Set in a dystopian future, a woman is forced to live as a concubine under a fundamentalist theocratic dictatorship.",dystopian,feminist,atwood,theocracy
disney-001,The Mandalorian,Disney+,Sci-Fi,Action,Adventure,2019,,Ongoing,3,24,Jon Favreau,,,Pedro Pascal,Carl Weathers,Giancarlo Esposito,,USA,English,TV-14,8.6,"The travels of a lone bounty hunter in the outer reaches of the galaxy, far from the authority of the New Republic.",star-wars,space-western,grogu,baby-yoda
hbo-002,Succession,HBO Max,Drama,Comedy,,2018,2023,Ended,4,39,Jesse Armstrong,,,Brian Cox,Jeremy Strong,Kieran Culkin,Sarah Snook,USA,English,TV-MA,8.9,"The Roy family controls the biggest media and entertainment company in the world, but their patriarch's health is failing.",media,family-drama,satire,wealth
prime-001,The Boys,Amazon Prime Video,Action,Sci-Fi,Drama,2019,,Ongoing,4,32,Eric Kripke,,,Karl Urban,Jack Quaid,Antony Starr,Erin Moriarty,USA,English,TV-MA,8.7,A group of vigilantes set out to take down corrupt superheroes who abuse their powers.,superheroes,satire,violent,comic-adaptation
netflix-002,The Crown,Netflix,Drama,Historical,Biography,2016,2023,Ended,6,60,Peter Morgan,,,Claire Foy,Olivia Colman,Imelda Staunton,Matt Smith,UK,English,TV-MA,8.6,The reign of Queen Elizabeth II from her wedding in 1947 through the early 21st century.,royalty,british,period-drama,politics
prime-002,The Marvelous Mrs. Maisel,Amazon Prime Video,Comedy,Drama,Period,2017,2023,Ended,5,43,Amy Sherman-Palladino,,,Rachel Brosnahan,Alex Borstein,Michael Zegen,Tony Shalhoub,USA,English,TV-MA,8.7,A 1950s New York housewife discovers she has a talent for stand-up comedy.,50s,stand-up,feminist,new-york
hbo-003,The Last of Us,HBO Max,Drama,Horror,Sci-Fi,2023,,Ongoing,2,16,Craig Mazin,Neil Druckmann,,Pedro Pascal,Bella Ramsey,Anna Torv,,USA,English,TV-MA,8.7,"After a global pandemic destroys civilization, a hardened survivor takes charge of a 14-year-old girl who may be humanity's last hope.",post-apocalyptic,zombies,video-game-adaptation,fungal
hulu-002,The Bear,Hulu,Comedy,Drama,,2022,,Ongoing,3,28,Christopher Storer,,,Jeremy Allen White,Ebon Moss-Bachrach,Ayo Edebiri,,USA,English,TV-MA,8.6,A young chef from the fine dining world returns to Chicago to run his family's sandwich shop after a heartbreaking death.,cooking,chicago,family,anxiety
netflix-003,Squid Game,Netflix,Thriller,Drama,Survival,2021,2025,Ended,3,22,Hwang Dong-hyuk,,,Lee Jung-jae,Park Hae-soo,Wi Ha-joon,HoYeon Jung,South Korea,Korean,TV-MA,8.0,Hundreds of cash-strapped contestants accept an invitation to compete in deadly children's games for a tempting prize.,korean,survival,dystopian,social-commentary
netflix-004,Wednesday,Netflix,Comedy,Horror,Mystery,2022,,Ongoing,2,16,Alfred Gough,Miles Millar,,Jenna Ortega,Catherine Zeta-Jones,Luis Guzmán,Gwendoline Christie,USA,English,TV-14,8.1,Wednesday Addams navigates a supernatural boarding school while solving a murder mystery.,gothic,teen,supernatural,addams-family
Index this XML into the shows-auto collection:
<add>
  <doc>
    <field name="id">hbo-001</field>
    <field name="title">Game of Thrones</field>
    <field name="platform">HBO Max</field>
    <field name="genres">Fantasy</field>
    <field name="genres">Drama</field>
    <field name="genres">Adventure</field>
    <field name="release_year">2011</field>
    <field name="end_year">2019</field>
    <field name="status">Ended</field>
    <field name="seasons">8</field>
    <field name="episodes">73</field>
    <field name="creators">David Benioff</field>
    <field name="creators">D.B. Weiss</field>
    <field name="cast">Emilia Clarke</field>
    <field name="cast">Peter Dinklage</field>
    <field name="cast">Kit Harington</field>
    <field name="cast">Lena Headey</field>
    <field name="country">USA</field>
    <field name="language">English</field>
    <field name="rating">TV-MA</field>
    <field name="imdb_rating">9.2</field>
    <field name="description">Nine noble families fight for control over the lands of Westeros, while an ancient enemy returns after being dormant for millennia.</field>
    <field name="tags">fantasy</field>
    <field name="tags">dragons</field>
    <field name="tags">epic</field>
    <field name="tags">george-rr-martin</field>
  </doc>
  <doc>
    <field name="id">netflix-001</field>
    <field name="title">Stranger Things</field>
    <field name="platform">Netflix</field>
    <field name="genres">Sci-Fi</field>
    <field name="genres">Horror</field>
    <field name="genres">Drama</field>
    <field name="release_year">2016</field>
    <field name="end_year">2025</field>
    <field name="status">Ended</field>
    <field name="seasons">5</field>
    <field name="episodes">42</field>
    <field name="creators">Matt Duffer</field>
    <field name="creators">Ross Duffer</field>
    <field name="cast">Millie Bobby Brown</field>
    <field name="cast">Finn Wolfhard</field>
    <field name="cast">Winona Ryder</field>
    <field name="cast">David Harbour</field>
    <field name="country">USA</field>
    <field name="language">English</field>
    <field name="rating">TV-14</field>
    <field name="imdb_rating">8.7</field>
    <field name="description">A group of kids in 1980s Indiana uncover supernatural mysteries and government conspiracies tied to a parallel dimension.</field>
    <field name="tags">80s</field>
    <field name="tags">supernatural</field>
    <field name="tags">coming-of-age</field>
    <field name="tags">monsters</field>
  </doc>
  <doc>
    <field name="id">hulu-001</field>
    <field name="title">The Handmaid's Tale</field>
    <field name="platform">Hulu</field>
    <field name="genres">Drama</field>
    <field name="genres">Sci-Fi</field>
    <field name="genres">Dystopian</field>
    <field name="release_year">2017</field>
    <field name="end_year">2025</field>
    <field name="status">Ended</field>
    <field name="seasons">6</field>
    <field name="episodes">66</field>
    <field name="creators">Bruce Miller</field>
    <field name="cast">Elisabeth Moss</field>
    <field name="cast">Joseph Fiennes</field>
    <field name="cast">Yvonne Strahovski</field>
    <field name="cast">Samira Wiley</field>
    <field name="country">USA</field>
    <field name="language">English</field>
    <field name="rating">TV-MA</field>
    <field name="imdb_rating">8.4</field>
    <field name="description">Set in a dystopian future, a woman is forced to live as a concubine under a fundamentalist theocratic dictatorship.</field>
    <field name="tags">dystopian</field>
    <field name="tags">feminist</field>
    <field name="tags">atwood</field>
    <field name="tags">theocracy</field>
  </doc>
  <doc>
    <field name="id">disney-001</field>
    <field name="title">The Mandalorian</field>
    <field name="platform">Disney+</field>
    <field name="genres">Sci-Fi</field>
    <field name="genres">Action</field>
    <field name="genres">Adventure</field>
    <field name="release_year">2019</field>
    <field name="status">Ongoing</field>
    <field name="seasons">3</field>
    <field name="episodes">24</field>
    <field name="creators">Jon Favreau</field>
    <field name="cast">Pedro Pascal</field>
    <field name="cast">Carl Weathers</field>
    <field name="cast">Giancarlo Esposito</field>
    <field name="country">USA</field>
    <field name="language">English</field>
    <field name="rating">TV-14</field>
    <field name="imdb_rating">8.6</field>
    <field name="description">The travels of a lone bounty hunter in the outer reaches of the galaxy, far from the authority of the New Republic.</field>
    <field name="tags">star-wars</field>
    <field name="tags">space-western</field>
    <field name="tags">grogu</field>
    <field name="tags">baby-yoda</field>
  </doc>
  <doc>
    <field name="id">hbo-002</field>
    <field name="title">Succession</field>
    <field name="platform">HBO Max</field>
    <field name="genres">Drama</field>
    <field name="genres">Comedy</field>
    <field name="release_year">2018</field>
    <field name="end_year">2023</field>
    <field name="status">Ended</field>
    <field name="seasons">4</field>
    <field name="episodes">39</field>
    <field name="creators">Jesse Armstrong</field>
    <field name="cast">Brian Cox</field>
    <field name="cast">Jeremy Strong</field>
    <field name="cast">Kieran Culkin</field>
    <field name="cast">Sarah Snook</field>
    <field name="country">USA</field>
    <field name="language">English</field>
    <field name="rating">TV-MA</field>
    <field name="imdb_rating">8.9</field>
    <field name="description">The Roy family controls the biggest media and entertainment company in the world, but their patriarch's health is failing.</field>
    <field name="tags">media</field>
    <field name="tags">family-drama</field>
    <field name="tags">satire</field>
    <field name="tags">wealth</field>
  </doc>
  <doc>
    <field name="id">prime-001</field>
    <field name="title">The Boys</field>
    <field name="platform">Amazon Prime Video</field>
    <field name="genres">Action</field>
    <field name="genres">Sci-Fi</field>
    <field name="genres">Drama</field>
    <field name="release_year">2019</field>
    <field name="status">Ongoing</field>
    <field name="seasons">4</field>
    <field name="episodes">32</field>
    <field name="creators">Eric Kripke</field>
    <field name="cast">Karl Urban</field>
    <field name="cast">Jack Quaid</field>
    <field name="cast">Antony Starr</field>
    <field name="cast">Erin Moriarty</field>
    <field name="country">USA</field>
    <field name="language">English</field>
    <field name="rating">TV-MA</field>
    <field name="imdb_rating">8.7</field>
    <field name="description">A group of vigilantes set out to take down corrupt superheroes who abuse their powers.</field>
    <field name="tags">superheroes</field>
    <field name="tags">satire</field>
    <field name="tags">violent</field>
    <field name="tags">comic-adaptation</field>
  </doc>
  <doc>
    <field name="id">netflix-002</field>
    <field name="title">The Crown</field>
    <field name="platform">Netflix</field>
    <field name="genres">Drama</field>
    <field name="genres">Historical</field>
    <field name="genres">Biography</field>
    <field name="release_year">2016</field>
    <field name="end_year">2023</field>
    <field name="status">Ended</field>
    <field name="seasons">6</field>
    <field name="episodes">60</field>
    <field name="creators">Peter Morgan</field>
    <field name="cast">Claire Foy</field>
    <field name="cast">Olivia Colman</field>
    <field name="cast">Imelda Staunton</field>
    <field name="cast">Matt Smith</field>
    <field name="country">UK</field>
    <field name="language">English</field>
    <field name="rating">TV-MA</field>
    <field name="imdb_rating">8.6</field>
    <field name="description">The reign of Queen Elizabeth II from her wedding in 1947 through the early 21st century.</field>
    <field name="tags">royalty</field>
    <field name="tags">british</field>
    <field name="tags">period-drama</field>
    <field name="tags">politics</field>
  </doc>
  <doc>
    <field name="id">prime-002</field>
    <field name="title">The Marvelous Mrs. Maisel</field>
    <field name="platform">Amazon Prime Video</field>
    <field name="genres">Comedy</field>
    <field name="genres">Drama</field>
    <field name="genres">Period</field>
    <field name="release_year">2017</field>
    <field name="end_year">2023</field>
    <field name="status">Ended</field>
    <field name="seasons">5</field>
    <field name="episodes">43</field>
    <field name="creators">Amy Sherman-Palladino</field>
    <field name="cast">Rachel Brosnahan</field>
    <field name="cast">Alex Borstein</field>
    <field name="cast">Michael Zegen</field>
    <field name="cast">Tony Shalhoub</field>
    <field name="country">USA</field>
    <field name="language">English</field>
    <field name="rating">TV-MA</field>
    <field name="imdb_rating">8.7</field>
    <field name="description">A 1950s New York housewife discovers she has a talent for stand-up comedy.</field>
    <field name="tags">50s</field>
    <field name="tags">stand-up</field>
    <field name="tags">feminist</field>
    <field name="tags">new-york</field>
  </doc>
  <doc>
    <field name="id">hbo-003</field>
    <field name="title">The Last of Us</field>
    <field name="platform">HBO Max</field>
    <field name="genres">Drama</field>
    <field name="genres">Horror</field>
    <field name="genres">Sci-Fi</field>
    <field name="release_year">2023</field>
    <field name="status">Ongoing</field>
    <field name="seasons">2</field>
    <field name="episodes">16</field>
    <field name="creators">Craig Mazin</field>
    <field name="creators">Neil Druckmann</field>
    <field name="cast">Pedro Pascal</field>
    <field name="cast">Bella Ramsey</field>
    <field name="cast">Anna Torv</field>
    <field name="country">USA</field>
    <field name="language">English</field>
    <field name="rating">TV-MA</field>
    <field name="imdb_rating">8.7</field>
    <field name="description">After a global pandemic destroys civilization, a hardened survivor takes charge of a 14-year-old girl who may be humanity's last hope.</field>
    <field name="tags">post-apocalyptic</field>
    <field name="tags">zombies</field>
    <field name="tags">video-game-adaptation</field>
    <field name="tags">fungal</field>
  </doc>
  <doc>
    <field name="id">hulu-002</field>
    <field name="title">The Bear</field>
    <field name="platform">Hulu</field>
    <field name="genres">Comedy</field>
    <field name="genres">Drama</field>
    <field name="release_year">2022</field>
    <field name="status">Ongoing</field>
    <field name="seasons">3</field>
    <field name="episodes">28</field>
    <field name="creators">Christopher Storer</field>
    <field name="cast">Jeremy Allen White</field>
    <field name="cast">Ebon Moss-Bachrach</field>
    <field name="cast">Ayo Edebiri</field>
    <field name="country">USA</field>
    <field name="language">English</field>
    <field name="rating">TV-MA</field>
    <field name="imdb_rating">8.6</field>
    <field name="description">A young chef from the fine dining world returns to Chicago to run his family's sandwich shop after a heartbreaking death.</field>
    <field name="tags">cooking</field>
    <field name="tags">chicago</field>
    <field name="tags">family</field>
    <field name="tags">anxiety</field>
  </doc>
  <doc>
    <field name="id">netflix-003</field>
    <field name="title">Squid Game</field>
    <field name="platform">Netflix</field>
    <field name="genres">Thriller</field>
    <field name="genres">Drama</field>
    <field name="genres">Survival</field>
    <field name="release_year">2021</field>
    <field name="end_year">2025</field>
    <field name="status">Ended</field>
    <field name="seasons">3</field>
    <field name="episodes">22</field>
    <field name="creators">Hwang Dong-hyuk</field>
    <field name="cast">Lee Jung-jae</field>
    <field name="cast">Park Hae-soo</field>
    <field name="cast">Wi Ha-joon</field>
    <field name="cast">HoYeon Jung</field>
    <field name="country">South Korea</field>
    <field name="language">Korean</field>
    <field name="rating">TV-MA</field>
    <field name="imdb_rating">8.0</field>
    <field name="description">Hundreds of cash-strapped contestants accept an invitation to compete in deadly children's games for a tempting prize.</field>
    <field name="tags">korean</field>
    <field name="tags">survival</field>
    <field name="tags">dystopian</field>
    <field name="tags">social-commentary</field>
  </doc>
  <doc>
    <field name="id">netflix-004</field>
    <field name="title">Wednesday</field>
    <field name="platform">Netflix</field>
    <field name="genres">Comedy</field>
    <field name="genres">Horror</field>
    <field name="genres">Mystery</field>
    <field name="release_year">2022</field>
    <field name="status">Ongoing</field>
    <field name="seasons">2</field>
    <field name="episodes">16</field>
    <field name="creators">Alfred Gough</field>
    <field name="creators">Miles Millar</field>
    <field name="cast">Jenna Ortega</field>
    <field name="cast">Catherine Zeta-Jones</field>
    <field name="cast">Luis Guzmán</field>
    <field name="cast">Gwendoline Christie</field>
    <field name="country">USA</field>
    <field name="language">English</field>
    <field name="rating">TV-14</field>
    <field name="imdb_rating">8.1</field>
    <field name="description">Wednesday Addams navigates a supernatural boarding school while solving a murder mystery.</field>
    <field name="tags">gothic</field>
    <field name="tags">teen</field>
    <field name="tags">supernatural</field>
    <field name="tags">addams-family</field>
  </doc>
</add>

Three shows, three Markdown documents, one tool call:

Index the following three Markdown documents into the shows-auto collection:

---
id: hbo-001
title: Game of Thrones
platform: HBO Max
genres:
  - Fantasy
  - Drama
  - Adventure
release_year: 2011
end_year: 2019
status: Ended
seasons: 8
episodes: 73
creators:
  - David Benioff
  - D.B. Weiss
cast:
  - Emilia Clarke
  - Peter Dinklage
  - Kit Harington
  - Lena Headey
country: USA
language: English
rating: TV-MA
imdb_rating: 9.2
tags:
  - fantasy
  - dragons
  - epic
  - george-rr-martin
---

# Game of Thrones

Nine noble families fight for control over the lands of Westeros, while an ancient enemy returns after being dormant for millennia.

---
id: netflix-001
title: Stranger Things
platform: Netflix
genres:
  - Sci-Fi
  - Horror
  - Drama
release_year: 2016
end_year: 2025
status: Ended
seasons: 5
episodes: 42
creators:
  - Matt Duffer
  - Ross Duffer
cast:
  - Millie Bobby Brown
  - Finn Wolfhard
  - Winona Ryder
  - David Harbour
country: USA
language: English
rating: TV-14
imdb_rating: 8.7
tags:
  - 80s
  - supernatural
  - coming-of-age
  - monsters
---

# Stranger Things

A group of kids in 1980s Indiana uncover supernatural mysteries and government conspiracies tied to a parallel dimension.

---
id: hulu-001
title: The Handmaid's Tale
platform: Hulu
genres:
  - Drama
  - Sci-Fi
  - Dystopian
release_year: 2017
end_year: 2025
status: Ended
seasons: 6
episodes: 66
creators:
  - Bruce Miller
cast:
  - Elisabeth Moss
  - Joseph Fiennes
  - Yvonne Strahovski
  - Samira Wiley
country: USA
language: English
rating: TV-MA
imdb_rating: 8.4
tags:
  - dystopian
  - feminist
  - atwood
  - theocracy
---

# The Handmaid's Tale

Set in a dystopian future, a woman is forced to live as a concubine under a fundamentalist theocratic dictatorship.
What just happened

No schema, no field definitions, no configuration — and every document landed. That is genuinely useful, and it's why schemaless mode exists. Now let's find its edge.

4

Where guessing runs out

Hit the wall

Ask the obvious business question:

Show me the breakdown of shows-auto by platform.calls search with a facet — this may not go the way you expect
Show me the schema for shows-auto.calls get-schema — here's the actual explanation

What Solr guessed

The table below was captured from the JSON path. CSV, XML and Markdown deliver the same values as text; Solr's schemaless chain parses numeric-looking text before choosing a type, so expect the same guesses.

FieldGuessed typeConsequence
platformtext_generalTokenized, so platform:prime now matches "Amazon Prime Video" — nonsense for a category. Faceting it returns nothing at all, not even wrong buckets.
titletext_generalSearchable, but not sortable or exact-matchable.
imdb_ratingpdoublesPlural — multi-valued. Every rating is now a list.
release_yearplongsMulti-valued too. Range filters get awkward.

Look at any returned document and you'll see it: every single field came back wrapped in an array — "title":["Stranger Things"]. Solr had one sample of each field and no reason to assume it wouldn't repeat, so it hedged.

What you'll see

Either an empty breakdown — "facets": { "platform": {} } — or, if your assistant read the schema first, real buckets on platform_str. Schemaless mode quietly copies every text field into a *_str string field, and a schema-aware assistant facets on the copy instead. Ask it to facet on the platform field itself and you get the empty result: text_general yields no facet buckets at all.

The copies are a patch, not a design: one per text field, capped at 256 characters, and no help for the numbers. Step 5 gives platform real buckets, and the question starts answering itself.

The lesson: schemaless is a fast on-ramp, not a destination. Solr guessed from a single document with no idea what you'd want to ask later. Facets, ranges and sorting all need types chosen deliberately.

5

Types chosen on purpose

Build a real schema

Reset first — this is not optional

Solr field types cannot be changed once created, and every collection here shares the same _default configset. The guesses from step 3 are already baked in, so a new collection would inherit them. Wipe the slate:

docker rm -f solr-mcp-demo
docker run -d --name solr-mcp-demo --network=solr-mcp -p 127.0.0.1:8983:8983 solr:9-slim solr start -c -f

Takes about seven seconds. Wait for the collections endpoint to answer before continuing.

Create the collection and define fields before indexing

Create a Solr collection called shows.calls create-collection

Then paste this. Order matters — fields first, documents second:

Add these fields to the shows collection schema:

- title         text_general  single-valued   (full-text search)
- description   text_general  single-valued   (full-text search)
- platform      string        single-valued   docValues  (faceting)
- status        string        single-valued   docValues
- country       string        single-valued   docValues
- language      string        single-valued   docValues
- rating        string        single-valued   docValues
- genres        string        multi-valued    docValues  (faceting)
- tags          string        multi-valued    docValues
- cast          string        multi-valued    docValues
- creators      string        multi-valued    docValues
- release_year  pint          single-valued   docValues  (range filters)
- end_year      pint          single-valued   docValues
- seasons       pint          single-valued   docValues
- episodes      pint          single-valued   docValues
- imdb_rating   pdouble       single-valued   docValues  (sorting)

Now index the same data into shows — same file or same paste as step 3, just the new collection name.

Why these types

ChoiceBuys you
string not text_generalExact values. "Amazon Prime Video" stays one facet bucket instead of three tokens.
docValues: trueThe column-oriented structure that makes faceting and sorting fast.
pint / pdoubleReal numbers — range filters like [2020 TO *] and numeric sort.
single-valued where trueYou can sort on it. Multi-valued fields make "highest rated" meaningless.
text_general kept for proseAnalysis and tokenizing is exactly what you want in title and description.

Ask the same question that failed in step 4:

Show me the breakdown of shows by platform.now returns clean buckets — Netflix 20, Amazon Prime Video 20, HBO Max 7…
6

Eight questions, eight Solr features

Actually search

Every prompt below is plain English. The note under each one names the Solr parameter it exercises, and the result you should get from the full 61-show set.

Which shows have "dragon" in the title?q · full-text on an analyzed field → 1 hit, House of the Dragon
Find shows whose description contains the exact phrase "a group of".q · phrase query → 2 hits, The Boys and Stranger Things
Find shows whose content field contains the exact phrase "a group of".q · phrase query → 2 hits, The Boys and Stranger Things (Markdown puts the description in content)
Show me everything on Netflix.fq · exact string filter → 20 hits
Which shows were released in 2020 or later?fq · numeric range release_year:[2020 TO *] → 31 hits
Find all the comedies.fq · membership in a multi-valued field → 15 hits
Break the collection down by genre and by status.facet · two fields at once → Ended 33, Ongoing 28
What are the five highest rated shows?sort + rows → Game of Thrones 9.2, Succession 8.9, True Detective 8.9…
Show me shows from 2015 onward rated 8 or above, sorted by rating, with a platform breakdown.everything at once — fq × 2, sort, facet, rows → 39 hits
Which shows star Pedro Pascal?fq · exact match inside a multi-valued field → 3 hits
Worth noticing

You never wrote fq=release_year:[2020 TO *]&facet.field=platform. The assistant did — because the tool descriptions told it how. That translation layer is the entire product.

7

Beyond search

Ask about the index itself

Search is the headline, but the operational tools are where this earns its place in a real workflow.

Is the shows collection healthy?check-health — ping, response time, doc count
Give me the stats for the shows collection.get-collection-stats — segments, cache hit ratios, handler timings
What's my query result cache hit ratio, and what does that tell me?the interesting one — real metrics, interpreted
Explain the shows schema — which fields can I facet on, and which can I sort by?get-schema — and a genuinely useful answer

The full toolbox

ToolDoes
searchQuery, filter, facet, sort, paginate
index-json-documentsIndex a JSON array of documents
index-csv-documentsIndex CSV through Solr's CSV handler
index-xml-documentsIndex Solr update XML — <add> blocks only
index-markdown-documentsIndex from Markdown — YAML front matter plus body text
create-collectionNew collection, optional configset / shards / replicas
list-collectionsEverything in the cluster
list-aliasesAliases and the collections they point to
create-aliasPoint an alias at one or more collections, or repoint it
delete-aliasRemove an alias — the collections stay
get-collection-statsIndex, query, cache and handler metrics
check-healthPing and document count
get-schemaFields, types, dynamic fields, copy fields
add-fieldsAdd fields — additive only
add-field-typesCustom analyzers, DenseVectorField for vector search

Plus 2 resources (solr://collections, solr://{collection}/schema) and 6 guided prompts your client may surface as slash commands.

Going further

Nothing below is on the numbered path. When it breaks is where steps 2 and 3 send you if something fails; Build it yourself replaces the prebuilt download; the rest is for after today.

Build it yourself no image, no download

Needs Git, JDK 25 and about three minutes.

git clone https://github.com/apache/solr-mcp.git
cd solr-mcp
./gradlew build

Produces build/libs/solr-mcp-1.0.0-SNAPSHOT.jar. Use that absolute path anywhere this card says JAR.

…and as a local Docker image

./gradlew jibDockerBuild

Produces solr-mcp:latest locally. Substitute that for the ghcr.io/… reference in any config above.

Want faster startup and a smaller image? See Native builds and every way to run it below.

Native builds and every way to run it jar · native binary · Docker × stdio · http

Each form below has a build command and a run command, for the Run via and Transport you picked in step 0. JAR shows what runs directly on your machine, the JVM jar and the native binary. Docker shows the two images. Everything here uses the clone from Build it yourself, with Solr from step 1 running.

FormstdiohttpNeeds
JVM jarone build, bothsame jarJDK 25
Native binarybuild with stdiobuild with httpGraalVM JDK 25
JVM Docker image (Jib)one image, bothsame imageJDK 25, Docker
Native Docker image…-native-stdio…-native-httpDocker only

The JVM forms are dual-mode: the PROFILES environment variable picks the transport at run time. Native forms are pinned to one transport at build time — Spring AOT bakes the web-application type into the binary — so you build one per transport. In the commands below, SOLR_URL points at the Solr from step 1.

JVM jar

./gradlew build                                  # build/libs/solr-mcp-1.0.0-SNAPSHOT.jar

# stdio: your client runs this
SOLR_URL=http://localhost:8983/solr/ \
  java -jar /absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar
./gradlew build                                  # build/libs/solr-mcp-1.0.0-SNAPSHOT.jar

# http: you run this
PROFILES=http SERVER_ADDRESS=127.0.0.1 HTTP_SECURITY_ENABLED=false \
  SOLR_URL=http://localhost:8983/solr/ \
  java -jar /absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar

Native binary (host OS only)

Needs GraalVM JDK 25 on JAVA_HOME. The first compile takes several minutes. The binary only runs on the OS and CPU you built it on.

# stdio: your client runs the binary
./gradlew nativeCompile -Pnative
SOLR_URL=http://localhost:8983/solr/ build/native/nativeCompile/solr-mcp
# http (the transport and the security switch are baked in at build time)
HTTP_SECURITY_ENABLED=false ./gradlew nativeCompile -Pnative -Pprofile=http
PROFILES=http SERVER_ADDRESS=127.0.0.1 HTTP_SECURITY_ENABLED=false \
  SOLR_URL=http://localhost:8983/solr/ \
  build/native/nativeCompile/solr-mcp

JVM Docker image (Jib)

./gradlew jibDockerBuild                         # solr-mcp:latest

# stdio: your client runs this (-i keeps stdin open)
docker run -i --rm --network=solr-mcp \
  -e SOLR_URL=http://solr-mcp-demo:8983/solr/ \
  solr-mcp:latest
./gradlew jibDockerBuild                         # solr-mcp:latest

# http: you run this
docker run -p 127.0.0.1:8080:8080 --rm --network=solr-mcp \
  -e PROFILES=http -e HTTP_SECURITY_ENABLED=false \
  -e SOLR_URL=http://solr-mcp-demo:8983/solr/ \
  solr-mcp:latest

Native Docker image

Prebuilt for this event, for Intel and Apple-silicon machines alike — nothing to build:

# stdio: your client runs this
docker run -i --rm --network=solr-mcp \
  -e SOLR_URL=http://solr-mcp-demo:8983/solr/ \
  ghcr.io/adityamparikh/solr-mcp:coc2026-native-stdio
# http: you run this
docker run -p 127.0.0.1:8080:8080 --rm --network=solr-mcp \
  -e PROFILES=http -e HTTP_SECURITY_ENABLED=false \
  -e SOLR_URL=http://solr-mcp-demo:8983/solr/ \
  ghcr.io/adityamparikh/solr-mcp:coc2026-native-http

Or build it yourself, inside a Linux builder container, so it works on macOS, Linux and Windows without installing GraalVM. Needs Docker and a few minutes.

./gradlew bootBuildImage -Pnative                # solr-mcp:latest-native-stdio
docker run -i --rm --network=solr-mcp \
  -e SOLR_URL=http://solr-mcp-demo:8983/solr/ \
  solr-mcp:latest-native-stdio
HTTP_SECURITY_ENABLED=false ./gradlew bootBuildImage -Pnative -Pprofile=http # solr-mcp:latest-native-http
docker run -p 127.0.0.1:8080:8080 --rm --network=solr-mcp \
  -e PROFILES=http -e HTTP_SECURITY_ENABLED=false \
  -e SOLR_URL=http://solr-mcp-demo:8983/solr/ \
  solr-mcp:latest-native-http
Mixing them up fails quietly

The -native-stdio image has no web server in it, and the -native-http image has no stdio transport. Pointing a client at the wrong one gives a timeout, not a helpful error.

A native binary built without -Pprofile=http has no web server in it, and one built with it has no stdio transport. Pointing a client at the wrong one gives a timeout, not a helpful error.

Native http picks its security when it is built

Spring AOT fixes the security setup inside the binary, so HTTP_SECURITY_ENABLED only counts at build time; at run time a native http server ignores it. That is why the build commands above set it. Built without it, every tool answers Access Denied until you configure OAuth2 (Security setup). The prebuilt coc2026-native-http image is built with security off, for use on your own machine only.

To use one of these images with a client, take the matching config from step 2 and put the image tag above where it says ghcr.io/adityamparikh/solr-mcp:coc2026.

To use these with a client, take the matching config from step 2. For the native binary, put its absolute path in place of java -jar /absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar.

HTTP transport remote access, multiple clients, one server

STDIO has your client launch the server as a subprocess. HTTP mode runs one server that many clients connect to over http://localhost:8080/mcp. Pick HTTP under Transport in step 0 and step 2 shows how to start it and how each client connects.

The hackathon commands turn security off with HTTP_SECURITY_ENABLED=false. Anything reachable by other people needs the OAuth2 setup in the Security setup panel.

Observability — traces and logs in Grafana HTTP mode, one extra container

In HTTP mode the server exports traces, logs and request metrics over OpenTelemetry. Point it at Grafana's all-in-one LGTM container and every tool call shows up as a trace, with a span per tool and any log lines it wrote linked to it. About five minutes. Assumes Solr from step 1 is running. Apart from the server command, commands are bash (macOS, Linux, WSL or Git Bash). STDIO mode exports nothing, so pick HTTP under Transport in step 0 first.

1. Start LGTM

Grafana on 3000, the OTLP receiver on 4317 (gRPC) and 4318 (HTTP). The loop waits for the collector; Grafana answers first, and export only works once the collector is up.

docker run -d --name lgtm --network=solr-mcp -p 127.0.0.1:3000:3000 -p 127.0.0.1:4317:4317 -p 127.0.0.1:4318:4318 grafana/otel-lgtm:0.33.0

until docker logs lgtm 2>&1 | grep -q "up and running"; do sleep 2; done
echo "LGTM ready"

2. Restart the server pointed at it

Stop the step 2 server first — this one listens on 8080 too. The server exports over OTLP/HTTP to localhost:4318 by default, which inside a container is the container itself, so these three variables send traces, metrics and logs to the lgtm container instead, by name over the solr-mcp network. Each is the full URL for its signal.

Stop the step 2 server first — this one listens on 8080 too. From a plain JAR the default localhost:4318 is already right, so the command is the same as step 2: nothing to add.

docker run -p 127.0.0.1:8080:8080 --rm \
  --network=solr-mcp \
  -e PROFILES=http \
  -e HTTP_SECURITY_ENABLED=false \
  -e SOLR_URL=http://solr-mcp-demo:8983/solr/ \
  -e OTEL_TRACES_URL=http://lgtm:4318/v1/traces \
  -e OTEL_METRICS_URL=http://lgtm:4318/v1/metrics \
  -e OTEL_LOGS_URL=http://lgtm:4318/v1/logs \
  ghcr.io/adityamparikh/solr-mcp:coc2026
PROFILES=http SERVER_ADDRESS=127.0.0.1 HTTP_SECURITY_ENABLED=false \
  SOLR_URL=http://localhost:8983/solr/ \
  java -jar /absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar
# PowerShell
$env:PROFILES="http"; $env:SERVER_ADDRESS="127.0.0.1"; $env:HTTP_SECURITY_ENABLED="false"; $env:SOLR_URL="http://localhost:8983/solr/"
java -jar C:/path/to/solr-mcp-1.0.0-SNAPSHOT.jar

3. Make some traffic

Any prompt from steps 3–7 in your client works. Or, without a client, a few tool calls with curl — each one is a trace:

for q in "*:*" "title:dragon" "genres:drama"; do
  curl -s -X POST http://localhost:8080/mcp -H "Content-Type: application/json" \
    -H "Accept: application/json, text/event-stream" \
    -d "{\"jsonrpc\":\"2.0\",\"method\":\"tools/call\",\"id\":1,\"params\":{\"name\":\"search\",\"arguments\":{\"collection\":\"shows-auto\",\"query\":\"$q\"}}}" >/dev/null
done

# one call that fails on purpose, so there is a log line to follow
curl -s -X POST http://localhost:8080/mcp -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","method":"tools/call","id":2,"params":{"name":"check-health","arguments":{"collection":"no-such-collection"}}}' >/dev/null

4. Look at it

Open localhost:3000 (no login) → Explore. Pick Tempo, switch the query type to TraceQL, and run:

{resource.service.name="solr-mcp"}

Open an http post /mcp trace. Inside it sit the security filter chain and one span named for the tool — SearchService#search, IndexingService#… — with its duration. Traces take up to a minute to become searchable — an empty result right after the calls is normal, so give it thirty seconds and run the query again.

Now open the CollectionService#checkHealth trace from the failing call and click Logs for this span: Loki opens on its WARN Health check failed line, matched by trace ID. Each log line links back the same way. Metrics land in Prometheus once a minute, in milliseconds; pick it in Explore and run:

http_server_requests_milliseconds_count{job="solr-mcp"}
What you won't see

Logs on successful calls — the services only log when something fails, so for the searches Logs for this span comes up empty. That's expected, not broken. And the call to Solr has no span of its own; its time is inside the tool span. JVM, Tomcat and per-tool metrics arrive the same way — try jvm_memory_used_bytes{job="solr-mcp"} or method_observed_milliseconds_count{job="solr-mcp"}.

Tear down

LGTM keeps everything in memory, so removing the container discards all of it.

docker rm -f lgtm
Security setup — HTTP mode with OAuth2 on Keycloak on your laptop, start to finish

Everything above runs with HTTP_SECURITY_ENABLED=false. This panel turns security on and proves it, with Keycloak as the identity provider. About ten minutes. It assumes Solr from step 1 is running, the JAR from step 0 is downloaded with Java 25 on the PATH, and you have docker, curl and jq. Commands are bash (macOS, Linux, WSL or Git Bash). Stop any unsecured server from step 2 first — both listen on 8080.

How it fits together: the server is a resource server. Keycloak issues a JWT, the server checks its signature against Keycloak's keys, the issuer, the expiry, and that the aud claim contains the URL the client dials — http://localhost:8080/mcp. Every step below exists to make one of those checks pass.

1. Start Keycloak

Port 8180, so it does not collide with the MCP server. The loop waits until Keycloak actually answers; the container is "up" long before that.

docker run -d --name keycloak -p 127.0.0.1:8180:8080 \
  -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin \
  quay.io/keycloak/keycloak:26.0 start-dev

until curl -sf http://localhost:8180/realms/master/.well-known/openid-configuration >/dev/null; do sleep 2; done
echo "Keycloak ready"

2. Create the realm, client and test user

One paste. It logs in as admin, creates a realm solr-mcp, a public client solr-mcp-client with the audience mapper that puts http://localhost:8080/mcp into every token (Keycloak does not honour the standard resource= parameter, so without this mapper every token is rejected), and a user testuser / testpassword. First and last name are not decoration — Keycloak refuses to issue a token to a user missing either. Expect three 201s.

KC=http://localhost:8180
ADMIN_TOKEN=$(curl -s -X POST "$KC/realms/master/protocol/openid-connect/token" \
  -d client_id=admin-cli -d username=admin -d password=admin -d grant_type=password | jq -r .access_token)

curl -s -o /dev/null -w "realm %{http_code}\n" -X POST "$KC/admin/realms" \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
  -d '{"realm":"solr-mcp","enabled":true}'

curl -s -o /dev/null -w "client %{http_code}\n" -X POST "$KC/admin/realms/solr-mcp/clients" \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
  -d '{"clientId":"solr-mcp-client","publicClient":true,"directAccessGrantsEnabled":true,
       "redirectUris":["http://localhost:6274/*"],"webOrigins":["http://localhost:6274"],
       "protocolMappers":[{"name":"mcp-audience","protocol":"openid-connect",
         "protocolMapper":"oidc-audience-mapper",
         "config":{"included.custom.audience":"http://localhost:8080/mcp",
                   "access.token.claim":"true","id.token.claim":"false"}}]}'

curl -s -o /dev/null -w "user %{http_code}\n" -X POST "$KC/admin/realms/solr-mcp/users" \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
  -d '{"username":"testuser","email":"test@example.com","firstName":"Test","lastName":"User",
       "enabled":true,"emailVerified":true,
       "credentials":[{"type":"password","value":"testpassword","temporary":false}]}'

# Tokens last 5 minutes by default; one hour is kinder for a trial. Expect 204.
curl -s -o /dev/null -w "lifetime %{http_code}\n" -X PUT "$KC/admin/realms/solr-mcp" \
  -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json" \
  -d '{"accessTokenLifespan":3600}'

curl -sf "$KC/realms/solr-mcp/.well-known/openid-configuration" >/dev/null && echo "realm ready"

3. Start the server with security on

The realm must answer before this: the server fetches the issuer's configuration while starting and exits with code 1 if it cannot. That is the "realm ready" line above. Use the JAR here rather than the Docker image — inside a container localhost:8180 is not your laptop, and the issuer name has to be identical in the container and in the tokens, which takes more setup than a card.

PROFILES=http SERVER_ADDRESS=127.0.0.1 HTTP_SECURITY_ENABLED=true \
OAUTH2_ISSUER_URI=http://localhost:8180/realms/solr-mcp \
SOLR_URL=http://localhost:8983/solr/ \
java -jar /absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar
# PowerShell
$env:PROFILES="http"; $env:SERVER_ADDRESS="127.0.0.1"; $env:HTTP_SECURITY_ENABLED="true"; $env:OAUTH2_ISSUER_URI="http://localhost:8180/realms/solr-mcp"; $env:SOLR_URL="http://localhost:8983/solr/"
java -jar C:/path/to/solr-mcp-1.0.0-SNAPSHOT.jar

Leave that terminal open. In another one, wait for curl http://localhost:8080/actuator/health to answer {"status":"UP"}, then read back the audience the server expects. This is the value the mapper in step 2 has to match, character for character:

curl -s http://localhost:8080/.well-known/oauth-protected-resource/mcp | jq -r .resource
# http://localhost:8080/mcp

4. Get a token

The password grant, which is what the client in step 2 allows. The second command decodes the token and prints its audience — it must list http://localhost:8080/mcp.

TOKEN=$(curl -s -X POST http://localhost:8180/realms/solr-mcp/protocol/openid-connect/token \
  -d client_id=solr-mcp-client -d username=testuser -d password=testpassword \
  -d grant_type=password | jq -r .access_token)

python3 - "$TOKEN" <<'EOF'
import sys, base64, json
p = sys.argv[1].split('.')[1]; p += '=' * (-len(p) % 4)
print(json.loads(base64.urlsafe_b64decode(p))['aud'])
EOF
# ['http://localhost:8080/mcp', 'account']

5. Prove the gate with curl

The same tool call twice. Without the header the server answers HTTP 200 with an Access Denied result; with it you get the collection names. Both lines are the test — one alone proves nothing.

CALL='{"jsonrpc":"2.0","method":"tools/call","id":1,"params":{"name":"list-collections","arguments":{}}}'

curl -s -X POST http://localhost:8080/mcp -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" -d "$CALL"
# ... "text":"Access Denied" ... "isError":true

curl -s -X POST http://localhost:8080/mcp -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" -H "Authorization: Bearer $TOKEN" -d "$CALL"
# ... "text":"[\"shows-auto\", ...]" ... "isError":false   (your collections from steps 3 and 5)

6. Connect your client — with the token in a header

The header is the whole point of this step. The server never answers an anonymous /mcp request with 401, and MCP clients start their OAuth login only when they see a 401 — so a client given just the URL reports connected, lists every tool, and gets Access Denied from every call, with no browser opening. Nothing looks broken. Pass $TOKEN from step 4 explicitly.

HTTP transport only

OAuth2 bearer token security applies only to the HTTP transport. In STDIO mode, your client launches the server directly as a local subprocess over standard input/output without network exposure or bearer authentication headers. Switch Transport to HTTP in step 0 to see client connection snippets with the token in a header.

Make sure your Solr MCP Docker container is running and port 8080 is published (-p 127.0.0.1:8080:8080) so your client can reach http://localhost:8080/mcp.

Make sure your Solr MCP JAR process from step 3 is running and listening on port 8080 before connecting your client.

Dataset: shows-auto was indexed from JSON in step 3. The verification query checks that collection.

Dataset: shows-auto was indexed from CSV in step 3. The verification query checks that collection.

Dataset: shows-auto was indexed from XML in step 3. The verification query checks that collection.

Dataset: shows-auto was indexed from Markdown in step 3. The verification query checks that collection. Markdown keeps each description in the body, so it is in content, not description: the phrase question finds its hits there.

Claude Code

claude mcp add --transport http solr-mcp http://localhost:8080/mcp \
  --header "Authorization: Bearer $TOKEN"
claude mcp list      # ✔ Connected — true with or without the header, so also run:
claude -p "Call the list-collections tool on the solr-mcp MCP server and reply with its raw result"
# ["shows-auto", ...]   ← the proof. "Access Denied" here means the header did not reach the server.
# Tools missing, or the server fails to connect: the token reached it and was rejected (expired, or wrong aud) with HTTP 401.
# PowerShell
claude mcp add --transport http solr-mcp http://localhost:8080/mcp `
  --header "Authorization: Bearer $TOKEN"
claude mcp list      # ✔ Connected — true with or without the header, so also run:
claude -p "Call the list-collections tool on the solr-mcp MCP server and reply with its raw result"
# ["shows-auto", ...]   ← the proof. "Access Denied" here means the header did not reach the server.
# Tools missing, or the server fails to connect: the token reached it and was rejected (expired, or wrong aud) with HTTP 401.

Codex

Codex reads the token from an environment variable you name, so the token never lands in config.toml. Export it in the shell that launches Codex; re-running codex mcp add replaces an earlier unsecured entry.

export SOLR_MCP_TOKEN="$TOKEN"
codex mcp add solr-mcp --url http://localhost:8080/mcp --bearer-token-env-var SOLR_MCP_TOKEN
codex mcp list       # Auth: Bearer token — true with or without a valid token, so also run:
codex exec "Call the list-collections tool on the solr-mcp MCP server and reply with its raw result"
# ["shows-auto", ...]   ← the proof. "Access Denied" here means the token did not reach the server.
# Tools missing, or the server fails to connect: the token reached it and was rejected (expired, or wrong aud) with HTTP 401.
# PowerShell
$env:SOLR_MCP_TOKEN = $TOKEN
codex mcp add solr-mcp --url http://localhost:8080/mcp --bearer-token-env-var SOLR_MCP_TOKEN
codex mcp list       # Auth: Bearer token — true with or without a valid token, so also run:
codex exec "Call the list-collections tool on the solr-mcp MCP server and reply with its raw result"
# ["shows-auto", ...]   ← the proof. "Access Denied" here means the token did not reach the server.
# Tools missing, or the server fails to connect: the token reached it and was rejected (expired, or wrong aud) with HTTP 401.

The token expires after about five minutes; restart Codex after refreshing it so it re-reads the variable.

ChatGPT

Not supported

ChatGPT supports OAuth 2.1 or no authentication — not a static bearer token — and it has no way to start a login against this server's secured mode, which never answers an anonymous request with 401. Use another client for this step.

Claude Desktop (via mcp-remote)

Paste the token into env; mcp-remote forwards it on every request. Edit your config file, then fully quit and reopen Claude Desktop:

~/Library/Application Support/Claude/claude_desktop_config.json
%APPDATA%\Claude\claude_desktop_config.json
~/.config/Claude/claude_desktop_config.json
On WSL

Claude Desktop runs on the Windows host, not inside WSL — so use the Windows path above and let Docker Desktop's WSL integration handle the container.

On Linux

There is no official Claude Desktop build for Linux. That path is where community builds look; if you're on Linux, Claude Code or MCP Inspector is the smoother route.

{
  "mcpServers": {
    "solr-mcp": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "http://localhost:8080/mcp", "--allow-http",
               "--header", "Authorization: Bearer ${TOKEN}"],
      "env": { "TOKEN": "paste-the-token-here" }
    }
  }
}

VS Code / GitHub Copilot

In .vscode/mcp.json, add the headers object to the server entry from step 2. Using an inputs prompt keeps the token out of source control:

{
  "inputs": [
    { "type": "promptString", "id": "solr-token", "description": "Solr MCP token", "password": true }
  ],
  "servers": {
    "solr-mcp": {
      "type": "http",
      "url": "http://localhost:8080/mcp",
      "headers": { "Authorization": "Bearer ${input:solr-token}" }
    }
  }
}

Cursor

In .cursor/mcp.json, add the headers object to your server entry. Cursor expands ${env:SOLR_MCP_TOKEN} from your environment:

{
  "mcpServers": {
    "solr-mcp": {
      "url": "http://localhost:8080/mcp",
      "headers": { "Authorization": "Bearer ${env:SOLR_MCP_TOKEN}" }
    }
  }
}

Zed

In zed: open settings (JSON), add the headers object to your remote server entry:

{
  "context_servers": {
    "solr-mcp": {
      "url": "http://localhost:8080/mcp",
      "headers": { "Authorization": "Bearer paste-the-token-here" }
    }
  }
}

IntelliJ IDEA / JetBrains (via mcp-remote)

In .junie/mcp/mcp.json or IDE Settings, proxy through mcp-remote to forward the bearer header on each request:

{
  "mcpServers": {
    "solr-mcp": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "http://localhost:8080/mcp", "--allow-http",
               "--header", "Authorization: Bearer ${TOKEN}"],
      "env": { "TOKEN": "paste-the-token-here" }
    }
  }
}

MCP Inspector

# web UI
npx @modelcontextprotocol/inspector --server-url http://localhost:8080/mcp --transport http \
  --header "Authorization: Bearer $TOKEN"
# or one call from the CLI, no browser
npx @modelcontextprotocol/inspector --cli http://localhost:8080/mcp --transport http \
  --header "Authorization: Bearer $TOKEN" --method tools/call --tool-name list-collections
# PowerShell web UI
npx @modelcontextprotocol/inspector --server-url http://localhost:8080/mcp --transport http `
  --header "Authorization: Bearer $TOKEN"
# or one call from the CLI, no browser
npx @modelcontextprotocol/inspector --cli http://localhost:8080/mcp --transport http `
  --header "Authorization: Bearer $TOKEN" --method tools/call --tool-name list-collections

When it breaks

SymptomCause & fix
Server exits with code 1 right after starting The realm was not answering when the server resolved the issuer. Re-run the last line of step 2 until it prints realm ready, then start the server again.
401 with The aud claim is not valid The mapper's audience and the URL the client dials differ — 127.0.0.1 vs localhost is enough. Compare step 3's .resource with step 4's aud, and use the same spelling in the client.
401 with Jwt expired Re-run step 4 and re-add the server in the client. A client with a configured header reports a failed connection rather than logging in again.
Access Denied with the header $TOKEN is empty or null: the token request in step 4 failed. Run it without jq and read the error. Account is not fully set up means the user is missing first/last name or has a temporary password.
Client shows connected, calls denied, no browser opens Expected without the header — see step 6. This is not an OAuth-capable-client problem; the server never sends the 401 that would start the flow.
Want to see why a token was rejected curl -si -H "Authorization: Bearer $TOKEN" localhost:8080/actuator/metrics | grep -i www-authenticate — a protected actuator endpoint returns a plain 401 with the reason.

Tear down

docker rm -f keycloak
claude mcp remove solr-mcp

Using Auth0 instead of Keycloak

Same server, same checks; only steps 1–4 change. You need your own Auth0 tenant (the free tier works) and administrative access.

  1. Applications → APIs → Create API. Name it Solr MCP API. Set the Identifier to exactly http://localhost:8080/mcp with signing algorithm RS256. The identifier becomes the token's aud (audience) claim, which the server matches character-for-character against its incoming resource URI. It cannot be edited after creation.
  2. Authorize your Machine to Machine application. Auth0 creates a default test application named Solr MCP API (Test Application). Go to Applications → APIs → Solr MCP API → Machine to Machine Applications, and ensure your app is toggled to Authorized. On the application's Settings tab (under Applications → Applications), copy your Domain, Client ID, and Client Secret.
  3. Start the server with security on. Because Auth0 is a public HTTPS provider, both Docker and JAR run directly without host-networking hurdles. The trailing slash in OAUTH2_ISSUER_URI is mandatory — Auth0 writes its iss claim with a trailing slash:
docker run -p 127.0.0.1:8080:8080 --rm \
  --network=solr-mcp \
  -e PROFILES=http \
  -e HTTP_SECURITY_ENABLED=true \
  -e OAUTH2_ISSUER_URI=https://YOUR-TENANT.us.auth0.com/ \
  -e SOLR_URL=http://solr-mcp-demo:8983/solr/ \
  ghcr.io/adityamparikh/solr-mcp:coc2026
PROFILES=http SERVER_ADDRESS=127.0.0.1 HTTP_SECURITY_ENABLED=true \
OAUTH2_ISSUER_URI=https://YOUR-TENANT.us.auth0.com/ \
SOLR_URL=http://localhost:8983/solr/ \
java -jar /absolute/path/to/solr-mcp-1.0.0-SNAPSHOT.jar
# PowerShell
$env:PROFILES="http"; $env:SERVER_ADDRESS="127.0.0.1"; $env:HTTP_SECURITY_ENABLED="true"; $env:OAUTH2_ISSUER_URI="https://YOUR-TENANT.us.auth0.com/"; $env:SOLR_URL="http://localhost:8983/solr/"
java -jar C:/path/to/solr-mcp-1.0.0-SNAPSHOT.jar
  1. Get a token using the Client Credentials grant. Auth0 M2M tokens default to 24 hours (86,400s), so unlike Keycloak no lifetime adjustment is needed. Mint the token with curl:
TOKEN=$(curl -s -X POST https://YOUR-TENANT.us.auth0.com/oauth/token \
  -H 'content-type: application/json' \
  -d '{"client_id":"YOUR_CLIENT_ID","client_secret":"YOUR_CLIENT_SECRET",
       "audience":"http://localhost:8080/mcp","grant_type":"client_credentials"}' | jq -r .access_token)

Or use the repository helper script with your .env file:

TOKEN=$(./scripts/get-auth0-token.sh --quiet)

Confirm the token audience:

python3 - "$TOKEN" <<'EOF'
import sys, base64, json
p = sys.argv[1].split('.')[1]; p += '=' * (-len(p) % 4)
print(json.loads(base64.urlsafe_b64decode(p))['aud'])
EOF
# http://localhost:8080/mcp

Now continue to step 5 above to test the gate with curl, then step 6 to connect your client.

Beyond the laptop

For a team deployment — TLS, a public issuer, the public URL as the audience, an application that refreshes its own token — the full guides are the reference:

When it breaks the five things that actually go wrong
SymptomCause & fix
Client says the server failed to start, no detail Almost always Java version. Run java -version — the JAR needs 25. STDIO swallows the error. Switch to the Docker path if you'd rather not install a JDK.
Connects, but every Solr call fails SOLR_URL is wrong for the context. From inside a container use http://solr-mcp-demo:8983/solr/, which resolves because both containers are on the solr-mcp network from step 1; from a plain JAR use localhost. If Docker says network solr-mcp not found, run docker network create solr-mcp and restart Solr with the step 1 command.
Solr instance is not running in SolrCloud mode The container was started without -c. Collections need SolrCloud. Re-run the step 1 command exactly.
New collection already has fields you didn't add Every collection shares the _default configset, so the first collection's field types leak into later ones — and types can't be changed once set. Restart the container to reset (step 5). Filed as #183.
Results ignore your sort The search tool takes sortClauses as objects shaped {"item":"imdb_rating","order":"desc"}. Unknown parameters are silently dropped rather than rejected, so a wrong key looks like it worked. Ask the assistant to retry and name the field explicitly.

Start completely over

docker rm -f solr-mcp-demo
docker run -d --name solr-mcp-demo --network=solr-mcp -p 127.0.0.1:8983:8983 solr:9-slim solr start -c -f
Take it home contribute, or just keep poking

Good first issues tend to be client guides, tool descriptions and error messages — all of which directly affect how well an assistant uses the server. If something on this card confused you, that's a bug worth filing.

Ideas worth trying while you're here

  • Index your own JSON, CSV or XML and see what the schema guesser makes of it.
  • Add a DenseVectorField with add-field-types and try vector search.
  • Ask the assistant to design a schema for a dataset you describe in words.
  • Point it at a Solr you already run — SOLR_URL is all it takes.