simple-job
Provides web search capabilities via DuckDuckGo, used in the search_and_summarize tool to find and read relevant pages before summarization.
Allows using Ollama as the local model endpoint, sending prompts to its OpenAI-compatible API for tasks like summarization, extraction, and reformatting.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@simple-job Download https://example.com/data.csv and verify the sha256 checksum matches the published one."
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
mcp-simple-job
Hand a subtask to a local model on another machine, and verify the result before returning it.
Built for a specific shape of problem: the capable, metered assistant runs on the machine you sit in front of, while a perfectly good GPU box sits idle in the corner. Reading forty files, summarising a long page, pulling down a dataset — none of that needs the expensive model, and doing it in the assistant's context spends the one resource that is genuinely scarce.
Measured on the author's setup, against doing the same four jobs in-context: ~2.7x faster, and 6.1x less context spent, because the raw pages never enter it.
The one rule
A job must come with a check. A task that cannot state how it would be known to have worked is refused — not warned about, refused.
This is not distrust of the local model. It is that the caller cannot tell. When the assistant delegates a summary and gets back four hundred confident words, it has no independent way to know whether those words describe the document or something else. Delegating without a check adds a second place where a green light means nothing.
Checks are deliberately boring: nonempty, contains, regex, json_keys,
line_count, shell (your argv, exit 0 passes, output piped to stdin), and summary_of
(shorter than the source and not a copy of it).
Related MCP server: Forgehand
The four subtasks
tool | what it does | where the work happens |
| a plain prompt with your own check | the model host |
| text, or url/urls fetched first | worker machine (fetch + model) |
| search, read the top pages, summarise | search alternates hosts, rest on the worker |
| fetch a file, report bytes + sha256 | worker by default, any configured host |
simple_job_stats reports how all of them have actually gone, from a ledger row written
for every job.
Running it
Requirements
Node 18+ on the machine running the MCP server.
An OpenAI-compatible chat endpoint. llama.cpp's
llama-server, Ollama, vLLM, LM Studio, or a hosted API — anything that answersPOST /v1/chat/completions.Python 3.9+ on whichever machine does the fetching. Standard library only; no pip install, no node, no beautifulsoup.
SSH key auth to the second machine, if you use one. It is optional (see below).
How it is wired on the author's machines
Two computers:
A Mac mini — the daily workstation. 16 GB, usually a few gigabytes into swap. It runs Claude Desktop and therefore this MCP server.
A Pop!_OS box with an RTX 5070 Ti — the laboratory. It runs a 35B model called ornith under
llama-serveron port 8080, and is idle most of the day.
An SSH tunnel makes the remote model look local to the Mac:
ssh -N -o ServerAliveInterval=30 -L 127.0.0.1:8081:127.0.0.1:8080 pop-ospop-os is a ~/.ssh/config alias with a key and IdentitiesOnly yes, so the server can
reach it non-interactively with BatchMode=yes.
The MCP client entry is just:
{ "mcpServers": { "simple-job": { "command": "node", "args": ["/path/to/mcp-simple-job/index.js"] } } }Defaults do the rest: the model at 127.0.0.1:8081, the worker machine at pop-os, and
the ledger in an existing ~/Code/harness/ if there is one.
Nothing is ever called by hand. The assistant picks the tool — which is a harder problem than it sounds, and is covered below.
Running it on yours
Two machines, one running the client and one running the model:
{
"mcpServers": {
"simple-job": {
"command": "node",
"args": ["/path/to/mcp-simple-job/index.js"],
"env": {
"ORNITH_URL": "http://127.0.0.1:8081/v1/chat/completions",
"ORNITH_MODEL": "your-model-name",
"POP_HOST": "your-ssh-alias"
}
}
}
}One machine — everything local, no SSH anywhere:
{
"env": {
"ORNITH_URL": "http://127.0.0.1:11434/v1/chat/completions",
"ORNITH_MODEL": "qwen3:8b",
"SEARCH_HOSTS": "mac"
}
}SEARCH_HOSTS=mac is the switch that says "there is no second machine". Fetching,
downloading and searching all happen locally, and search simply has one rate budget
instead of two. Everything else behaves the same.
Environment variables
variable | default | what it does |
|
| the chat endpoint |
|
| model name sent in the request |
|
| SSH alias of the worker machine |
|
| machines to alternate searches across; set to |
|
| minimum gap between searches from one machine |
|
| how long a machine sits out after being throttled |
| next to | where search timing state is kept |
|
| the SQLite ledger |
|
| optional trace id to stamp on rows |
The ledger is optional. It is created on first use, and if it cannot be written the
jobs still run — logging is best-effort and never blocks work. HARNESS_TRACE is a hook
for the author's own tracing setup; ignore it and rows simply have a null trace id.
Getting it actually used
This is the part most people skip, and it is the part that decides whether any of the above matters.
A tool that nothing routes to is invisible no matter how well it works. The author has a separate MCP server that works perfectly and had zero calls in months, purely because nothing ever told the assistant to reach for it. Building a capability and routing to it are two different jobs, and finishing the first one feels like finishing.
Three ways to close that gap, cheapest first. Most people want the second one.
1. Do nothing, and see. Some clients read tool descriptions well enough that a sufficiently obvious request — "summarise these forty pages" — finds the tool on its own. Worth trying for a day before adding machinery. Watch whether it actually gets called.
2. Put a rule where your client keeps standing instructions. Claude Desktop project
instructions, a CLAUDE.md for Claude Code, .cursorrules, a custom GPT's instructions —
whatever your client reads on every turn. Something like:
There is a local model available through
simple-job. Use it when the material is not already in context and the job is mechanical: summarising pages or files, web search plus reading, fetching downloads, extraction and reformatting. It is free and does not spend context on the source.Do it yourself when the text is already in context, when the job needs interpretation rather than transcription, or when being right matters more than being checkable. Never delegate judgment calls, code that must be correct, or file edits.
Every job must carry a check — the server refuses work it cannot verify.
Spend as many words on when not to delegate as on when to. The failure mode of routing a delegation tool is over-delegation, and an assistant that ships everything downhill will hand you faithful transcription where you wanted judgment.
3. Wire it into a router, if you have one. If your setup already matches situations to tools, add an entry for "bulk reading or fetching material not yet in context". The advantage over a standing instruction is that it is measurable — you can count whether it fired when it should have. A standing instruction either works or it does not, and nothing records which.
What not to send it
Anything where "looks right" is the only test. Judgment calls. Code that must be correct. Editing files.
And one boundary found by measurement rather than taste: a small local model transcribes faithfully but does not interpret. In testing it reproduced a source's ambiguous phrasing verbatim instead of resolving what it meant, and summarised a repository's star count as though it were part of a bug report. Send it transcription. Keep interpretation.
Notes from building it
Everything below is a measurement, not an opinion. The numbers are in the code comments too.
Thinking is off by default
Reasoning models emit their deliberation and their answer from the same token budget. On one summarising job run three times identically, two of the three spent 5,500–6,000 characters thinking, hit the ceiling, and returned an empty answer with HTTP 200.
Raising max_tokens did not fix it. reasoning_effort: "low" did not fix it. A
/no_think system tag did not fix it. Only chat_template_kwargs: {enable_thinking: false} did, and the same job then answered in 258 tokens. Pass think: true for a job
that genuinely needs deliberation, and raise max_tokens with it.
Search is rationed, and DuckDuckGo lies about why
DuckDuckGo does not rate-limit politely:
a served query is HTTP 200, ~28 KB, ten result links
a refused one is HTTP 202, ~14.2 KB, and its text reads "Please complete the following challenge... Select all squares containing a duck"
It is a captcha flag on the IP, not a timed limit, and spacing does not clear one. After four minutes of silence, six queries at 30 s spacing from one machine and six at 15 s from the other were 0 for 12. Polling every 5 s during a block never recovered in 162 s — retrying feeds it. The flag decayed on its own in roughly twenty minutes.
So searches are spaced, alternated across machines (two IPs are two budgets), and a
throttle is reported as throttled, never as "no results". Those mean opposite things.
Reading follows the question
A long page gets cut to fit the model's window, and cutting from the top answers the wrong question silently. Asked about "sparse gating and load balancing" in a 40,063-character page with an 8,000-character window, the first version returned a fluent summary of the article's opening — in which "load balancing" never appears (it starts at character 16,181) and "sparse" never appears (14,671).
So focus steers the window: a head for context, then the passages around each named
term, one window guaranteed per term before any term gets a second. Two earlier
versions were not enough — substring matching found "load" inside "download" and reported
42 hits of noise, and taking passages in document order spent the budget before reaching
character 16,181.
If the page never uses those words, the call returns ok:false with focus_not_found.
Zero hits is a better answer than a plausible summary of other material.
A page that is mostly script is refused
One site served 68,896 bytes containing 40 characters of text ("Loading..."), which the
original if not text guard passed — so the shell went into a summary as source material
and the model wrote a confident benchmark figure citing it.
Two tests now, because either alone is fooled: an absolute floor, and a text-to-bytes ratio that only condemns a page which is also short. A GitHub issue is 290,000 bytes of markup around 3,896 characters of real discussion, and the ratio alone threw it away.
Citations are numbered so they can be checked
search_and_summarize numbers the pages and asks for [1], [2] rather than urls. Asked
for urls, the model attributed a figure it had read in a blog to a documentation page —
the fact was real and in the material, the attribution was not, and summary_of cannot
see that because a mislabelled bullet is the right length and is not a copy.
A url is a long opaque string to copy correctly. An integer is not, and it can be range-checked against the pages actually read — which the code does, failing the call on an out-of-range number and counting bullets with no source at all.
Which machine is faster?
Verified by identical sha256 on both sides:
client machine | worker machine | |
ssh round-trip | — | ~185 ms per call |
fetch 4 pages | ~1.7 s | ~2.0 s |
download 20 MB | 17.5 MB/s | 12.6 MB/s |
The client machine was faster at both. Speed is not the reason to send work to the worker. The reasons are that it holds the model, that it is idle while the other machine is in use, and that a second machine is a second search budget. Choose a download's host by where the file is needed, not by throughput.
Tests
node test_e2e.mjs # 24 assertions, spawns the real server over JSON-RPC
node --test test/simple-job.test.mjs # 19 unit assertions on the checksThe end-to-end suite spawns the actual server the way a client would, against a throwaway ledger. An in-process test would not catch a PATH or environment bug, and those are exactly the ones that only appear after a restart.
Changes to index.js take effect when the client next starts the server. pop_agent.py
is re-read on every call, so changes to fetching, searching and downloading are live
immediately.
Available Tools
5 toolsdownloadA
Download a file to pop (default) or to this Mac. Returns the path, byte count and sha256 — a download that reports success without those is not verified. Put it on pop when pop will use it (models, datasets, anything for the GPU); put it on the mac when you will open it.
Inputs: url (required), dest, host, max_mb, +3 more.
search_and_summarizeA
Search the web (DuckDuckGo), read the top pages on pop, and have ornith summarise them — one call, no Claude context spent on the raw pages. THE ONLY SEARCH TOOL HERE, and deliberately: DuckDuckGo rate-limits hard, so searches are spaced and alternated between this Mac and pop. A throttle is reported as a throttle, never as "no results".
Inputs: query (required), check, count, focus, +7 more.
simple_jobA
Hand a plain subtask to ornith (35B, local, free) on pop and verify the result before returning it. REQUIRES a check — a task that cannot state how it would be known to have worked is refused. Good for: extraction against a schema, classifying, reformatting. Bad for: judgment calls, code that must be correct, editing files — do those yourself.
Inputs: task (required), check (required), model, think, +3 more.
simple_job_statsB
How delegation has actually gone: jobs by kind, how often the check passed, and speed. Answers whether this server is earning its place.
Inputs: no named parameters.
summarizeB
Summarise text with ornith on pop. Free, local, and it does not spend Claude context on the source. Pass the text directly, or a url/urls to fetch first (fetched on pop). The default check is a real one: the summary must be substantially shorter than the source and not a copy of it.
Inputs: url, text, urls, check, +7 more.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
Loading tool schema changelog…
TDQS
Scored across 5 tools
Each tool has a clear, distinct purpose: simple_job delegates a generic subtask, summarize condenses text, search_and_summarize combines web search with summarization, simple_job_stats reports on job execution, and download retrieves files. No two tools overlap in functionality.
All tool names follow a consistent lowercase_with_underscores convention, and related tools (simple_job, simple_job_stats) share a common prefix while remaining descriptive. No mixed camelCase or other styles.
With 5 tools, the server is well-scoped—neither bare nor cluttered. Each tool serves a distinct, essential function, and the count aligns with the apparent purpose of task delegation and information retrieval.
The tool set covers the core operations needed for the server's domain: executing a job, summarizing, searching and summarizing, tracking job stats, and downloading files. No obvious missing functionality for the stated purpose.
Maintenance
Related MCP Connectors
AI that does the work and checks it. EU-hosted gateway: memory, the codai router, opt-in toolsets.
Hire AI workers that run on schedule, report back and act with your OK. Or deploy private models.
Hand tasks, bugs and finished work to AI coding agents, and get back a write-up with evidence.
Exact IBAN, VAT, cron, regex answers; HTML/URL to hosted PDF or screenshot; agent memory; workflows.
Related MCP Servers
- AlicenseBqualityDmaintenanceDelegates heavy, repetitive, and verifiable tasks like PDF extraction, code analysis, and log processing to a local LLM to reduce token consumption for frontier AI models, while keeping decision-making with the main AI.8MIT
- AlicenseAqualityBmaintenanceEnables delegating complete coding tasks to a local OpenAI-compatible model through MCP, using bounded Git worktrees and compact receipts so conversation history is not carried.4MIT
- FlicenseNot gradedqualityCmaintenanceEnables delegating mechanical or high-volume subtasks to a local LLM through an MCP tool, letting the assistant query the local model without using its own output tokens for content.-
- AlicenseNot gradedqualityFmaintenanceEnables a primary coding agent to delegate bounded implementation and repair tasks to a local OpenAI-compatible model that edits a copy of the project, runs configured checks, and returns artifacts for review. Exposes stdio tools for capability discovery, task submission, progress polling, artifact reading, and cancellation.MIT