oll.am should stay the frozen capability layer. The "job agent" is not a new backend service — it's an external agent runtime (Sam's OpenCLAW, an agentic browser / computer-use runner) that is a thin, disposable client. The correct seam between them is MCP: build the capability surface once and any runtime can drive it — OpenCLAW today, Claude Desktop tomorrow (already on the roadmap as the Phase-3 "oll.am as a Claude Desktop plugin"). One build, two payoffs.
This matches the frozen-Core discipline exactly. We do not bolt a bespoke "job-application backend" onto the platform; we expose what we already have through a standard protocol, and we let the volatile, fashion-driven, replaceable part — the agent — live outside the wall where it can be thrown away and rebuilt without risk to the money-earning core.
The job-application pipeline is two halves, and being honest about which is which is the whole plan. One half we have already built and it is low-risk. The other half is un-built, hostile, and where every seductive "apply to 100 jobs in an hour" pitch secretly lives.
The agent runtime lives outside the wall. It reaches oll.am's frozen capabilities through exactly one thing — the MCP server — which is itself a thin adapter over the HTTP endpoints that already exist and already earn.
Plan the server before any code. It follows the house mcp-server-build pattern: the entry file is transport + tool registration only; all real work lives in a thin service layer that is an http-service-adapter over the existing oll.am endpoints (core.oll.am, write.oll.am, foto). No business logic is re-implemented in the MCP server — it translates tool calls into HTTP calls and shapes the results back into content blocks.
| Rule | Why |
|---|---|
Stable snake_case name, namespaced ollam_* | The name is an API. Namespacing avoids collisions when the runtime loads many servers. |
| Description written for the model | Say what it returns and when to call it, so the agent picks the right tool without a human in the loop. |
| Typed input schema, every param described + example'd | The schema is the contract the model fills in; a description + example on each param cuts malformed calls. |
| Handler never throws | Errors return isError text / a fallback, not exceptions — the runtime should recover, not crash. |
| Content-block returns | Structured returns the model can read and chain. |
| No stdout noise | stdio is the protocol channel; all logging goes to stderr or the protocol breaks. |
Six tools. Note the last column: only two require new backend code — everything else is a call to something that already runs.
| Tool | When the agent calls it | Upstream call | Notes |
|---|---|---|---|
| ollam_tailor_cv | Turn a base profile + a job description into a role-tailored CV | oll-write · NEW verb | Returns a markdown CV. |
| ollam_cover_letter | Generate a cover letter targeting a specific JD | oll-write · NEW verb | Returns letter text. |
| ollam_clarify | Tighten / humanize any application text | oll-write /clarify | Existing. Text in / out. |
| ollam_rewrite | Restyle or improve a passage | oll-write /rewrite | Existing. Text in / out. |
| ollam_draft | Braindump → polished prose (answers to application questions) | oll-write /draft | Existing. |
| ollam_headshot_order | Start a professional-headshot order | foto /checkout (guest) | Returns a Stripe checkout_url + job_id. Payment-gated — the agent hands the URL to the human; it cannot silently buy. |
| ollam_account_status | Resolve identity + live plan before spending gated calls | Core /api/auth/me | So the agent knows the user's tier first. |
ollam_headshot_order) returns a checkout URL, never a purchase. The agent is a drafting and orchestration assistant; every irreversible or paid step routes back through a human click. That is both the safe design and the honest one.Every oll-write verb requires Authorization: Bearer <Core JWT> — magic-link identity, a 72h HS256 token, with the plan resolved live from /me. So the MCP server's real job, beyond translating calls, is to obtain and hold a Core JWT on behalf of the user. This is where the skill's secrets/auth discipline applies.
| Concern | Decision |
|---|---|
| Resolve auth in priority order | Stored session file first, env-var fallback second. The server never blocks on interactive login mid-run. |
| Persist the token | Write to ~/.ollam-mcp/auth.json with an atomic write + 0o600 perms. Never echo it. |
| Login is a separate step | An ollam auth CLI subcommand runs the magic-link login before the server starts. |
| Core stays untouched | The MCP server is just another JWT-bearing client — the jwt-bearer-spine pattern. oll.am's frozen auth spine does not change at all. |
# the login step runs once, out of band — never inside a tool call $ ollam auth # magic-link → verify → token → wrote ~/.ollam-mcp/auth.json (0600) # then the runtime launches the server, which reads that token $ ollam serve # stdio MCP server, ready for OpenCLAW
The consequence worth naming: the MCP server introduces no new trust boundary inside oll.am. It holds a token the user already could have held; Core validates it the same way it validates specview's or ollwrite's. The platform's security surface is unchanged.
Everything above, except two tools, already exists. The only new backend code is two JD-aware verbs added to the oll-write service: tailor_cv(profile, job_description) and cover_letter(profile, job_description). Each follows the exact existing pattern — _authorize_and_gate → _run_model with a named prompt — differing only in the prompt template. Additive. No change to auth, billing, or frozen Core. Roughly one small PR to oll-write.
This is the leverage: a job-application product that looks like a big new build is, at the backend, two prompt templates dropped into an engine that already authorizes, gates, and calls a model. The rest is the agent — which lives outside the wall — and the MCP seam, which is thin.
Four steps, ordered so the cheap, safe, revenue-adjacent work comes first and the seductive-but-hard net-new build is explicitly last.
The single seam that serves OpenCLAW now and Claude Desktop later. Small, bounded, and it doesn't touch a frozen service — it wraps them.
Apply to real jobs — including the AlpineAI application (the meta-play: "I built an agent that applied to jobs for me"). Zero product burden, no ToS liability transferred to strangers, and it produces the build-in-public story that sells the product.
User pastes a job URL / JD → gets a tailored CV + cover letter + optional headshot, Swiss-formatted, ready to submit by hand. ~90% existing blocks. No browser automation, no stored credentials, no ToS risk. CHF 99.
Stored credentials, isolated browsers, cron, LinkedIn ToS exposure — the Phase-3 vision. Real, but it is the seductive-but-hard net-new thing, and it must not jump the queue.
| Risk | Where it bites / how v1 avoids it |
|---|---|
| ToS & legal | Automated submission to LinkedIn / Indeed violates ToS and risks the user's account. This is exactly why v1 is assisted, not autonomous — the human submits. |
| Positioning / ethics | Stay an application-quality tool, not a spam cannon — the same ethics line as humaniz.me (an honest tool, never a bypass). Mass-generic apps are both ineffective and dishonest to sell. |
| Where the agent runs | Local personal tool (step 2, cheap) vs per-user cloud with stored credentials + isolated browsers (step 4, a big lift). This is the main future cost — name it now, defer it deliberately. |
| Tailoring quality vs volume | Ten sharp applications beat a hundred generic ones. The product's value is quality-at-speed; volume is a trap that flags the applicant. |
tailor_cv, cover_letter) as the only new backend code — additive, prompt-template-only, one small PR, no touch to frozen Core.Everything above is the why. This is the executable spec for the one new-code piece — the MCP seam — written to be built and tested locally before anything is wired to a runtime. Sam approved starting here.
write.oll.am, memory.oll.am, core.oll.am. Build it once, test locally against Claude Desktop / Cursor, then wire OpenCLAW. It is a local / desktop stdio tool — not a deployed service, so it never touches prod. Sequenced behind the CiteBible franc.| Choice | Decision |
|---|---|
| What it is | One stdio MCP server named oll-mcp, Python + FastMCP (the house Python stack; locally testable via mcp dev → the MCP Inspector). Lives in the monorepo at services/oll-mcp/. ~250 lines, mostly the HTTP adapter. |
| Transport | stdio for v1 (local desktop clients). Optional later: an HTTP/SSE deployment for remote clients. |
| Pattern | Follows the house mcp-server-build pattern: entry file = registration + transport only; all work in a thin service layer; every tool has a model-facing description (what it returns and when to call it) + typed args with examples; handlers never throw (return isError text / fallback); content-block returns; no stdout noise (stdio is the protocol channel — log to stderr). |
services/oll-mcp/
oll_mcp/
server.py # FastMCP() + @mcp.tool registrations + mcp.run("stdio") — no logic
service.py # HTTP adapter: one httpx client per upstream (core/write/memory) — the ONLY network calls
auth.py # Core-JWT store (~/.oll-mcp/auth.json, 0600) + magic-link login + a DEV-ONLY mint helper
config.py # env: OLL_CORE_BASE/OLL_WRITE_BASE/OLL_MEMORY_BASE (default to the live domains),
# OLL_MEMORY_SERVICE_TOKEN, OLL_MCP_JWT
format.py # shape upstream JSON → clean markdown content blocks
tests/ # unit-test the pure fns (service/format/auth) + one tool smoke test
pyproject.toml
README.md # install + Claude Desktop / Cursor / OpenCLAW config snippets
All over existing endpoints — zero new backend. Eight tools:
| Tool | Upstream (LIVE) | Args | Auth |
|---|---|---|---|
| ollam_clarify | POST write.oll.am/api/write/clarify | text, tone? | Core JWT |
| ollam_rewrite | POST /api/write/rewrite | text, instructions?, strength? | Core JWT |
| ollam_draft | POST /api/write/draft | braindump, tone? | Core JWT |
| ollam_spec | POST /api/write/spec | braindump | Core JWT |
| ollam_memory_search | memory.oll.am (collection from JWT) | query, top_k? | JWT + service token |
| ollam_memory_chat | memory chat (grounded, cited) | message | JWT + service token |
| ollam_memory_ingest | memory ingest | text | JWT + service token |
| ollam_account | GET core.oll.am/api/auth/me | — | Core JWT |
ollam_*, with a description written for the model (what + when), typed args with a concrete example each, a handler that catches everything → returns text / isError (never an exception), and markdown content blocks (not raw JSON). Defer to v2: a foto guest-checkout tool, new tailor_cv/cover_letter verbs, an HTTP/SSE remote deploy, and rate-limits.oll.am's write / memory endpoints require a Core JWT (72h HS256, identity-only: sub=str(user_id)+email; plan resolved live from /api/auth/me). The MCP server acts as one user — it holds one Core JWT.
| Concern | Decision |
|---|---|
| Resolve order | OLL_MCP_JWT env → ~/.oll-mcp/auth.json (0600) → a clear error naming both fixes. Never log or echo the JWT / token. |
| Login | oll-mcp auth <email>: POST core/api/auth/magic-link → user clicks the emailed link → paste token → verify → store. (Confirm with Sam whether Core needs a PRODUCT_VERIFY_BASE_OLL_MCP context or can reuse an existing product — a small Core config detail, like CiteBible needed.) |
| Memory calls | Mirror the ollwrite BFF: derive collection mem:user:{sub} from the JWT and inject OLL_MEMORY_SERVICE_TOKEN server-side (never client-visible). |
| Dev ergonomics guarded · dev-only | oll-mcp mint-jwt --user <id> signs a 72h token using AUTH_JWT_SECRET so local testing needs no email round-trip. Grounded: core/.env already has AUTH_JWT_SECRET, so this works locally today — the write tools are testable immediately; memory tools need the service token, which is Coolify-set. |
cd services/oll-mcp && uv sync (or pip install -e .).OLL_*_BASE = https://{core,write,memory}.oll.am); optionally at a local stack.oll-mcp mint-jwt --user <valid-live-user-id> (dev-only; uses AUTH_JWT_SECRET) → export OLL_MCP_JWT. Fallback: real magic-link. Note: the user id must exist in the live oll_core DB or /me → 401 — get a valid id from Sam / a quick DB check at build time.mcp dev oll_mcp/server.py → opens the MCP Inspector (local web UI).ollam_clarify {text:"we was going to the store yesterday"} → expect clarified prose from live write.oll.am. Call ollam_account → expect the test user's plan. (Memory tools once OLL_MEMORY_SERVICE_TOKEN is supplied.)isError text (no crash); missing JWT → a clear error naming the fix.oll-mcp to Claude Desktop's claude_desktop_config.json (command runs uv run oll-mcp, env carries OLL_MCP_JWT), restart, confirm the tools appear + one call round-trips; repeat for Cursor./health is a real endpoint, /mcp + /docs are the SPA catch-all; it's the "OpenClaw Control" agent gateway). If OpenCLAW is OpenAI-tools-only rather than MCP, add a thin OpenAI-tools bridge instead — but that's a v2 detail; Claude Desktop / Cursor prove the seam first.0600 · errors degrade to text · zero secrets in logs · pytest green on the pure fns.write.oll.am / memory.oll.am / core.oll.am all live (200).AUTH_JWT_SECRET is in core/.env.OLL_MEMORY_SERVICE_TOKEN for the memory tools.oll-mcp sign-in.services/oll-mcp→
local smoke test (Inspector + dev-mint JWT, write tools first)→
wire Claude Desktop + Cursor→
verify OpenCLAW's model, then wire it→
open a feat-branch PR (NOT deployed — local/desktop tool)