Skip to main content
Glama

mcp-local-file-llm

An MCP server that lets an orchestrating model (Claude Code) hand read-and-answer work to a local LLM without paying to move the source text through its own context.

Why this shape

The obvious design — an MCP tool that takes a text parameter — saves nothing. Tool-call arguments are assistant output tokens, billed at roughly five times the input rate, so passing a document into such a tool costs more than simply reading and answering it directly. The saving only appears when the bulk text never reaches the orchestrator at all.

So the tool takes a reference to the data (a file path, a line range, a shell command) rather than the data itself. The server reads it locally, sends it to the local model, and returns only the answer.

Related MCP server: Agent Helper

Tools

local_llm(prompt, data?, max_words?, max_tokens?, model?, json_schema?, background?)

  • data.files — paths with optional line ranges: /a/b.py, /a/b.py:120-160, /a/b.py:120- (to end), /a/b.py:-80 (from start), /a/b.py:42 (one line)

  • data.urls — URLs to fetch; HTML is reduced to text first

  • data.command — a shell command whose stdout becomes the input

  • data.text — literal text, for short framing only

  • Omitting data entirely makes it a plain generation call

  • json_schema constrains the answer to a JSON Schema, via ollama's native format field, so extraction returns structured data rather than prose

  • background: true returns a job id immediately instead of waiting

local_find(prompt, files, max_files?, model?) — search many files, given as paths or globs, for whatever the prompt describes, and return only a ranked shortlist with a one-line reason per hit. Files are batched to roughly 50KB per request and judged against a JSON schema, one verdict per file. Use plain grep/rg when a literal string would do; this is for when the target is described rather than spelled.

local_result(job?, wait_ms?) — collect a background job. Omit job to list this session's jobs and their state. A finished job is returned once, then dropped.

local_status() — check the endpoint and list available models.

Inputs larger than one context window are split and processed map-reduce: the prompt runs against each chunk (a few chunks at a time, see LOCAL_LLM_CONCURRENCY), then a final pass merges the partial answers.

Background jobs

The point of background: true is to overlap local inference with the orchestrator's own work — fire off "read this page and tell me whether it mentions X" and keep searching elsewhere while it runs, then collect the answer with local_result. Jobs live in the server process, so they last as long as the session and do not survive a restart.

Note that the inference host has one GPU. Several background jobs in flight at once contend for it and each one gets slower; the overlap is with the orchestrator, not a way to get more throughput out of the box.

A note on concurrency

LOCAL_LLM_CONCURRENCY does much less than it looks like it should. Ollama sizes its parallel slots from whatever VRAM is free, and at num_ctx 65536 a single slot already claims what remains after a 19GB model is loaded, so large requests are served one at a time no matter what this is set to.

Measured on an RTX 4090 with qwen3-coder:30b, a 253KB five-chunk run:

concurrency

1

2

3

4

6

wall clock

8.6s

8.5s

8.6s

8.5s

8.3s

Flat. Concurrency only helps when the individual requests are small enough for several slots to fit at once. The default of 2 is kept because it costs nothing and does help on smaller inputs.

Benchmarks against this backend are easy to get wrong: any other job on the same GPU distorts them badly. An earlier run of this same test appeared to show four-way concurrency taking over five minutes, which turned out to be an unrelated background job competing for the card. Serialise everything before measuring.

Routing

The tool descriptions carry explicit size thresholds, because the economics invert depending on how much data is involved:

data size

what to do

under 20KB

read it directly; the round trip costs about 5s and saves a fraction of a cent

20-75KB

delegate; roughly 10-50x cheaper for a few seconds more latency

over 75KB

always delegate; reading it directly is billed once and then rides along in every later request

Text that is already in the caller's context should never be passed back in: tool arguments are billed as output tokens, so returning it costs several times more than simply using it where it already is.

Usage tracking

Every call appends one JSON object to a usage log, so the question "is this actually worth having?" can be answered from data rather than argued about. Each record holds the tool, the model, a truncated prompt, the source paths, bytes and estimated tokens in and out, chunk count, duration, and any error.

npm run report                 # or: node report.mjs
node report.mjs --days 7       # last week only
node report.mjs --calls        # also list recent individual calls

The report gives per-tool call counts, how many tokens were kept out of the caller's context against how many came back, a compression ratio, and a cost comparison: what reading that text directly would have cost against what delegating actually cost. That figure is a floor — it counts the text once, and ignores the fact that text read directly also rides along in every later request of the session.

It also flags calls whose input was under 20KB. Those are below the threshold where delegation pays, so a high count there means the tool is being reached for too eagerly and the routing guidance needs tightening.

Set LOCAL_LLM_LOG=off to disable logging entirely, or LOCAL_LLM_LOG_PROMPTS=off to record only sizes and timings without any prompt text.

Configuration

Variable

Default

Purpose

LOCAL_LLM_URL

http://localhost:11434

Ollama root (native /api/chat, so num_ctx is settable per request)

LOCAL_LLM_MODEL

qwen3-coder:30b

Model tag

LOCAL_LLM_NUM_CTX

65536

Context window requested per call

LOCAL_LLM_CHUNK_CHARS

140000

Chunk size before map-reduce kicks in

LOCAL_LLM_CONCURRENCY

2

Chunks in flight during the map phase (see below)

LOCAL_LLM_FIND_BATCH_CHARS

50000

Bytes of files per local_find request

LOCAL_LLM_TIMEOUT_MS

300000

Per-request timeout

LOCAL_LLM_MAX_BYTES

4000000

Refuse inputs larger than this

LOCAL_LLM_ALLOW_SECRETS

unset

Set to 1 to disable the credential-path guard

LOCAL_LLM_LOG

$XDG_DATA_HOME/mcp-local-file-llm/usage.jsonl

Usage log path, or off

LOCAL_LLM_LOG_PROMPTS

on

Set to off to log sizes and timings but no prompt text

Security note

Everything passed as data leaves this machine for the inference host, so paths that look like credentials (.ssh/id_*, .env, *.pem, *.key, .aws/credentials, secrets.yaml, and similar) are refused unless LOCAL_LLM_ALLOW_SECRETS=1. The data.command field runs a shell command, which is the same privilege the caller already has through its own shell tool, but it is worth knowing it is there. data.urls makes outbound requests from this machine to whatever URL it is given.

Related MCP Connectors

Related MCP Servers

  • F
    license
    B
    quality
    D
    maintenance
    Provides LLMs with safe, read-only access to local codebases for searching, reading files, and finding function definitions. All source code remains local, ensuring privacy while enabling AI assistants to explore project structures and functionality.
    4
    -
  • A
    license
    A
    quality
    D
    maintenance
    Enables searching and retrieving documents from a local folder to ground LLM answers in your files.
    2
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    Provides LLMs with secure, read-only access to local documentation by scanning directories, extracting content from PDF, DOCX, Markdown, and text files, and performing keyword searches.
    3
    5 npm
    MIT