Clients¶
Use from Neovim¶
contrib/nvim/ish.lua is an fzf-lua picker. Copy it to lua/utils/ish.lua and
bind it:
map('n', '<leader>fi', function() require('utils.ish').search() end,
{ desc = 'Semantic search (ish)' })
It reads --format grep, so the built-in previewer opens each result at its
line, and prints the rank in the leftmost column. search_lang({'cpp'}),
search_type({'doc'}), and search_here() narrow it, as does a lang:,
type:, or under: word typed into the query.
While the index refreshes, require('utils.ish').statusline() renders a bar
for a statusline — ish ███░░░░░ 38% — and an empty string when idle. It reads vim.g.ish_index_status, which the picker keeps up to date, and
shows ish ✓ briefly when a refresh finishes. Nothing is reported through
vim.notify: with cmdheight = 0 there is no command line to put a message
in, so nvim draws one over the last screen row — the statusline itself.
The picker never blocks the editor: results are written as they arrive, so typing stays smooth however long a search takes.
Saving a buffer tells the server to refresh, once a search has started it,
so an edit is searchable moments after the save. The server would otherwise
notice on its next poll, up to refresh_seconds later.
contrib/nvim/ish_server.lua keeps one ish-mcp process per session. It
starts on the first search and is reused after that, which cuts a keystroke
from about 500 ms to about 150 ms. Copy it beside the picker.
Use from VS Code¶
contrib/vscode/ is an extension that drives the same ish-mcp server
from a QuickPick. Press Ctrl+Alt+I and type. The highlighted chunk shows
in the editor beside the picker, Enter opens it with its lines selected, and
Tab finishes a filter word. While the index refreshes, the status bar shows
ish ███░░░░░ 38%. It needs ish-mcp on PATH, like the Neovim client, and
its README says how to build, install, and
release it. It ships on its own
schedule, apart from the PyPI package.
Use from Python¶
from ish.interfaces.python.api import Ish
with Ish("src/") as ish:
for chunk, score in ish.search("type:doc how to configure", limit=5):
print(score, chunk.path, chunk.symbol)
print(ish.status())
Ish holds the index open, so a second query costs a search rather than a
process start. It offers search(), chunks(), index(), refresh_all(),
and status(), and reads lang:, type:, and under: out of the query
exactly as the other interfaces do.
Use from an agent¶
ish-mcp serves the same search over the Model Context Protocol, so an agent
can query the index directly. Add it to a project with .mcp.json:
{
"mcpServers": {
"ish": { "command": "uv", "args": ["run", "ish-mcp"] }
}
}
It offers search_code, ask, list_chunks, index_status,
refresh_index, and complete_filter. ask answers a question from the
chunks a search finds and returns JSON: the answer with [n] markers, and
for each citation the path, the location, and a link. It needs a language
model, which Answer questions sets up. The server stays resident, so a query costs about 58 ms
rather than a process start, and a filter word completes from the registries
the server already holds rather than through a fresh ish-complete.
The server re-checks a tree every refresh_seconds, 30 by default. An
editor knows the moment a file is saved, so the Neovim and VS Code clients
call refresh_index then, and an edit is searchable moments later. A client
of your own should do the same.
A call may narrow one search with lang, under, type, and limit, or
write the same filters into the query text. It cannot change
what is indexed — those settings come from the config file only, so no single
call can shrink an index that another call depends on.
Serve over HTTP¶
ish-mcp serves stdio to the client that starts it. For clients on other
machines, serve the same tools over MCP Streamable HTTP instead:
export ISH_MCP_TOKEN="$(openssl rand -hex 32)"
ish-mcp --http 0.0.0.0:8765
Every request goes to /mcp and carries the token as
Authorization: Bearer <token>. A request without it, or with a wrong one,
gets 401 and runs nothing. The server refuses to start without a token. A
bare port, such as --http 8765, listens on 127.0.0.1 only. Each request
runs on its own thread, so a slow answer for one client does not hold up
another. Run it in the tree it serves, beside an ish-serve that keeps the
index current.
Point a client at it:
{
"mcpServers": {
"docs": {
"type": "http",
"url": "https://docs.example.com:8765/mcp",
"headers": { "Authorization": "Bearer ${ISH_MCP_TOKEN}" }
}
}
}
The server speaks plain HTTP. Put it behind a reverse proxy that holds the certificate when the token crosses a network you do not trust.