MCP server
sift mcp [path] starts a stateless Model Context Protocol
server over stdio. It gives coding agents direct access to sift’s ranking,
signature compression, and token-budget optimizer, instead of making them
read whole files through generic filesystem tools and burn context on
outlines or unrelated diffs.
The server is hand-rolled on the standard library (encoding/json +
bufio.Scanner) — no mcp-go, no extra dependencies — and speaks
newline-delimited JSON-RPC 2.0 over stdin/stdout.
Setup: one global entry, or one per project?
Short answer: for terminal/editor-based agents, set it up once, globally, with no path — it just works per-project. For chat-first desktop apps, you need one entry per repo.
The [path] argument to sift mcp is optional. Omitted, sift serves the
process’s current working directory at launch. Whether that’s “dynamic”
comes down to how the client starts the subprocess:
- Workspace-aware clients (Claude Code, Cursor, Windsurf, Cline, Zed,
VS Code, OpenCode) spawn the local MCP server as a child process of the
session you have open, with cwd already set to the project you’re
working in. Point one global/user-scoped config entry at
sift mcp(no path), and every project you open gets its own correctly- scoped server automatically — nothing to edit per-repo. - Chat-first apps with no open project (Claude Desktop) don’t have a “current project” to inherit — the subprocess launches from wherever the app starts subprocesses (typically your home directory), not your repo. For these you must pass an absolute path explicitly, which in practice means one config block per repo you want to serve.
- The spec-correct mechanism for this is the MCP
rootscapability, where the client tells the server its workspace root over the protocol (roots/list) instead of the server inferring it from process cwd. Some harnesses (Claude Code, OpenCode) already lean on this model. sift’s server doesn’t implementroots/listyet — cwd-at-launch is the current mechanism, androotssupport is on the roadmap so a single long-lived server instance could serve multiple open folders correctly. Until then, cwd-per-launch is what makes the “global config” pattern work, and it covers every harness below except Claude Desktop.
Requirements
siftinstalled and resolvable as a binary (either onPATH, or an absolute path in the config).- Nothing else — no network port, no config file, no touching
~/.config/sift/state.json(see Statelessness).
stdio hygiene: the protocol is framed over stdout. Anything sift or a shell wrapper prints to stdout that isn’t a JSON-RPC message will break the client’s parser. If you invoke sift through a wrapper script, make sure it doesn’t echo anything.
Client setup
Global config (no path — recommended) unless noted otherwise.
Claude Code
CLI, one-time, user-scoped (applies to every project you open):
claude mcp add --scope user sift -- sift mcp
Or edit ~/.claude.json / ~/.claude/settings.json directly:
{
"mcpServers": {
"sift": {
"command": "sift",
"args": ["mcp"]
}
}
}
Use --scope local (or a project-level .mcp.json) instead if you want
it scoped to one repo, or need a different sift build per project.
Claude Desktop
Not project-scoped, so cwd can’t move with you — pass the path explicitly.
Settings → Developer → Edit Config → claude_desktop_config.json:
{
"mcpServers": {
"sift-myrepo": {
"command": "sift",
"args": ["mcp", "/absolute/path/to/myrepo"]
}
}
}
Add one entry per repo you want available (sift-myrepo, sift-otherrepo, …).
OpenAI Codex (CLI & IDE extension — shared config)
Codex uses TOML, not JSON. Either:
codex mcp add sift -- sift mcp
or edit ~/.codex/config.toml directly:
[mcp_servers.sift]
command = "sift"
args = ["mcp"]
This applies globally across the Codex CLI and the VS Code/IDE extension.
Drop a project-scoped .codex/config.toml in a repo instead if you want
per-project overrides (only loaded for trusted directories).
OpenCode
Global: ~/.config/opencode/opencode.json. Project (overrides global):
opencode.json in the repo root.
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"sift": {
"type": "local",
"command": ["sift", "mcp"],
"enabled": true
}
}
}
OpenCode also exposes an explicit cwd field on local servers (relative
paths resolve from the workspace root, which is already the default), so
you rarely need it — it’s there if you want to pin a server to a specific
subfolder.
Cursor
Global: ~/.cursor/mcp.json. Project: .cursor/mcp.json in the repo.
{
"mcpServers": {
"sift": {
"command": "sift",
"args": ["mcp"]
}
}
}
Windsurf
~/.codeium/windsurf/mcp_config.json:
{
"mcpServers": {
"sift": {
"command": "sift",
"args": ["mcp"]
}
}
}
Cline (VS Code extension)
Via Cline’s MCP settings UI, or cline_mcp_settings.json directly:
{
"mcpServers": {
"sift": {
"command": "sift",
"args": ["mcp"],
"disabled": false
}
}
}
Zed
settings.json, under context_servers:
{
"context_servers": {
"sift": {
"command": {
"path": "sift",
"args": ["mcp"]
}
}
}
}
VS Code (GitHub Copilot MCP)
.vscode/mcp.json (or the user-level MCP settings for a global entry):
{
"servers": {
"sift": {
"command": "sift",
"args": ["mcp"]
}
}
}
If a client can’t find
siftonPATH(common with GUI apps that don’t inherit your shell’s environment on macOS), replace"sift"with the absolute path fromwhich sift.
Tools
| Tool | Params | What it does |
|---|---|---|
pack_context |
budget (int, tokens), prompt (string, optional relevance hint), mode ("full" | "signatures", default "full"), style (output style, e.g. markdown/xml), ext (comma-separated string, e.g. "go,md"), ignore (comma-separated string, gitignore syntax) |
Packs the directory into one ranked, token-budgeted context document. This is the primary tool — most sessions only need this one. |
pack_diff |
from (git ref; omit/empty for uncommitted working-tree changes), patch (bool, default false) |
Packs only what changed since from, either as full file contents or a raw unified patch (patch: true). |
list_tree |
ext (comma-separated string), ignore (comma-separated string) |
Lists the repository structure — paths only, no content. Cheap way to orient before calling pack_context. |
There is no separate get_file_signatures tool — call pack_context with
mode: "signatures" and narrow with ext if you only want, say, Go or
TypeScript outlines.
Example: tools/call request/response
Request:
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "pack_context",
"arguments": { "budget": 8000, "mode": "signatures", "ext": "go" }
}
}
Response (truncated — a tree, then one fenced block per file):
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{ "type": "text", "text": "```\n.\n└── main.go\n```\n\nfile: main.go\n\n```go\n...\n```\n" }
]
}
}
Protocol surface
Only these methods are implemented:
initializenotifications/initializedtools/listtools/call
resources/* and prompts/* are explicitly deferred — the server returns
a “method not found” error rather than an empty result, so a well-behaved
client should recognize sift as tools-only rather than retrying those
calls in a loop.
Statelessness
The server never reads or writes the shared delta baseline
(~/.config/sift/state.json):
pack_diffrequires an explicitfromref (or defaults to uncommitted changes) — it never falls back to a recorded baseline.pack_contextnever records a dump.
An agent hammering these tools in a tight loop can’t perturb what a
human’s next interactive sift pick session considers “the last dump.”
The MCP surface and the interactive CLI are isolated from each other by
design.
Troubleshooting
| Symptom | Likely cause |
|---|---|
| Client shows “server disconnected” immediately | sift isn’t on PATH for that process — use an absolute path. |
pack_context returns the wrong repo’s content |
The client launched the subprocess with a stale/wrong cwd (rare, but happens with some plugin/user-scope combinations) — pass an explicit path as a workaround, or check the client’s scope settings. |
| Works in one client, not another, with identical config | The other client may not set cwd to the open project (see Setup — Claude Desktop is the common case) — pass an explicit path. |
pack_context returns far more than budget |
budget omitted or zero means unlimited, not “a small default” — always pass an explicit token budget. |
Client repeatedly retries resources/list |
Client assumes resources support; harmless, confirms this server is tools-only. |
| JSON parse errors in the client log | Something else (a wrapper script, a shell rc file) is writing to stdout before the JSON-RPC handshake. |