{
  "markdown": "# Limner: A Harnessed Image-generation Agent\n\n<p align=\"center\"><img src=\"docs/assets/hero.webp\" alt=\"The limner's empty studio: a finished portrait of an armored knight on the easel, daylight and open sky entering through a balcony arch\" width=\"100%\"></p>\n\n> A Harnessed Image-generation Agent: Claude reasons, Cloudflare executes, D1 remembers, OAuth is the only way in.\n\n[![Model Context Protocol](https://img.shields.io/badge/MCP-server-1f2937)](https://modelcontextprotocol.io)\n[![Cloudflare Workers](https://img.shields.io/badge/Cloudflare-Workers-F38020?logo=cloudflare&logoColor=white)](https://developers.cloudflare.com/workers/)\n[![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)\n[![Node](https://img.shields.io/badge/node-%3E%3D22.13-339933?logo=nodedotjs&logoColor=white)](https://nodejs.org)\n[![CI](https://github.com/vinsonconsulting/limner/actions/workflows/ci.yml/badge.svg)](https://github.com/vinsonconsulting/limner/actions/workflows/ci.yml)\n[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n[![DCO](https://img.shields.io/badge/DCO-required-orange)](https://developercertificate.org/)\n\nLimner is an Agent Harness and Model Context Protocol (MCP) server for orchestrating image\ngeneration across multiple pipelines, with durable memory for project\ncontext. This repository is **rasa**, the foundation variant of the Limner\nfamily. You deploy rasa as your own instance: free as in beer, no strings or data pipelines attached.\n\n## Contents\n\n- [The Harnessed Agent](#the-harnessed-agent)\n- [Decisions and dead-ends](#decisions-and-dead-ends)\n- [Architecture](#architecture)\n- [Stack](#stack)\n- [Tool Surface](#tool-surface)\n- [Memory](#memory)\n- [Managed Agent Setup](#managed-agent-setup)\n- [Skills and Capability Surfaces](#skills-and-capability-surfaces)\n- [Security](#security)\n- [Privacy Policy](#privacy-policy)\n- [Prerequisites](#prerequisites)\n- [Quickstart](#quickstart)\n- [Testing and feedback](#testing-and-feedback)\n- [Roadmap](#roadmap)\n- [The Limner family](#the-limner-family)\n- [Contributing](#contributing)\n- [License](#license)\n- [Acknowledgments](#acknowledgments)\n\n## The Harnessed Agent\n\nLimner is explicitly built as a composable harness with clean seams:\n\n- **The reasoning loop runs on Anthropic's Claude Managed Agents platform.**\n  The model does the planning and the tool choice. It never holds provider\n  credentials and never executes code.\n- **Tool execution runs in Cloudflare V8 isolates.** Every tool call lands in\n  a Worker: deterministic TypeScript with explicit bindings, not a shell.\n  Image composition happens in-isolate (WASM codecs, raster ops, text\n  rendering) so the common path costs nothing and leaks nothing.\n- **The seam between them is an OAuth-gated MCP surface.** The agent reaches\n  the tools the same way any MCP client does: dynamic client registration,\n  a bearer token, and a typed tool contract at `/mcp`. There is no private\n  side channel.\n- **Memory is a database, not a context window.** Project briefs, style\n  decisions, and progress notes live in D1 and survive across sessions.\n  The agent recalls what it recorded last week instead of being re-told.\n\nThe same tool surface ships three ways from one codebase: the OAuth-gated\nWorkers endpoint, a local stdio server, and a `.mcpb` one-click bundle for\nClaude Desktop. The stdio server identifies itself as `limner-mcp (preview)`:\nstdio is the preview transport at v1, pending a refresh against the next MCP\nspec revision. The Workers and `.mcpb` surfaces carry no preview tag.\n\n## Decisions and dead-ends\n\nThe design did not arrive fully formed. The full record lives in\n[docs/Limner_Cloudflare_CMA_Architecture.md](docs/Limner_Cloudflare_CMA_Architecture.md):\n22 dated decision records, a cost model, and five named off-ramp triggers that\nstate in advance what would make us walk away from the approach.\n\nSeveral of those records are reversals. The FastAPI shim that fronted the first\ndesign was killed. The Sharp/libvips composition path was abandoned for a hybrid\nWASM stack that runs inside the isolate. Recraft began as a composed first-party\nMCP and was later amended to a direct REST call, with the transport seam kept as\na reusable adapter. One record is a correction: a request timed out at 30s during\nintegration testing and was first blamed on an upstream API, until the API's own\ndocs turned up an async mode and the fault turned out to be ours.\n\nThe \"Deploy to Cloudflare\" button is the cleanest dead-end (currently). We ran the spike on\n2026-06-12. The button's monorepo mode extracts a single package into a standalone\nrepo, which severs the `@limner/core` workspace dependency and breaks the build.\nProvisioning and secrets worked; the repo shape did not. The full note, and the\nscript that does the same job plus migrations and a smoke test, is under\n[Where is the Deploy to Cloudflare button?](#where-is-the-deploy-to-cloudflare-button).\n\n## Architecture\n\n<p align=\"center\"><img src=\"docs/assets/architecture.svg\" alt=\"Architecture, top to bottom: Claude on CMA and any MCP client cross the OAuth seam into the limner-mcp Cloudflare Worker, whose 18 tools reach OpenAI, Recraft, the Images binding, D1, and R2; a local stdio and .mcpb flavor runs the same surface on node:sqlite\" width=\"480\"></p>\n\n## Stack\n\n- TypeScript pnpm monorepo: `@limner/core` (pipelines, composition, state),\n  `@limner/mcp` (the server, all three transports), `@limner/cma-tools`\n  (the same tool contract packaged for CMA custom-tool consumption)\n- Cloudflare Workers, D1, KV, R2, Durable Objects, Images\n- Composition: Photon and jSquash (WASM), Satori + resvg for typography,\n  Cloudflare Images for the network-side ops\n- MCP over Streamable HTTP and stdio\n\nThe architecture document with decision records lives at\n[docs/Limner_Cloudflare_CMA_Architecture.md](docs/Limner_Cloudflare_CMA_Architecture.md).\n\n## Tool Surface\n\nAll tools are namespaced `limner_*`. Full schemas in\n[packages/limner-mcp/README.md](packages/limner-mcp/README.md).\n\n| Tool | What it does | Needs |\n| --- | --- | --- |\n| `limner_generate_dalle` | OpenAI Images API (gpt-image-1 default) | `OPENAI_API_KEY`, your OpenAI credit |\n| `limner_generate_recraft` | Recraft, via their REST API (`external.api.recraft.ai`) | `RECRAFT_API_KEY`, your Recraft credit |\n| `limner_generate_midjourney` | Composes a Midjourney prompt string; a human carries it the rest of the way | Nothing |\n| `limner_upscale` / `limner_vectorize` | Recraft crisp upscale, and raster-to-SVG vectorize | `RECRAFT_API_KEY`, your Recraft credit |\n| `limner_compose` | 16 image ops behind one discriminated union: resize, crop, brightness, contrast, blur, sharpen, watermark, encode, decode, convert, renderText run in-isolate; cfTransform, cfOverlay, cfBlur, cfSmartCrop, cfBackgroundFill use Cloudflare Images | Images binding for the five cf* ops only |\n| `limner_record` / `limner_recall` / `limner_forget` / `limner_list_categories` | Durable memory with categories and idempotent upserts | D1 (or local SQLite) |\n| `limner_create_project` / `limner_list_projects` / `limner_get_project_context` / `limner_record_project_note` | Project briefs and running notes | D1 (or local SQLite) |\n| `limner_health` / `limner_version` / `limner_list_pipelines` / `limner_pipeline_capabilities` | Discovery and diagnostics | Nothing |\n\n## Memory\n\nMost image tools start every session from zero. Limner records what a project\nis and what you decided, then reads it back on the next run. Two stores back\nthis, both in D1 (or local SQLite for the stdio and `.mcpb` flavors):\n\n- **Categorized memory.** `limner_record` writes a fact under a category with\n  an idempotent upsert, so re-recording the same key updates in place instead\n  of piling up duplicates. `limner_recall` reads a category back,\n  `limner_forget` removes an entry, and `limner_list_categories` enumerates\n  what exists. Use it for durable preferences: a house palette, a default\n  aspect ratio, a model that works for a given look.\n- **Project context.** `limner_create_project` opens a project,\n  `limner_record_project_note` appends running notes (briefs, style decisions,\n  progress), `limner_get_project_context` returns the brief and notes for a\n  project, and `limner_list_projects` lists them. This is the \"what were we\n  doing last week\" layer.\n\nNothing here is a vector database or an embedding model. It is plain structured\nstate with explicit keys, which is the point: the agent recalls what it\nactually recorded, not the nearest fuzzy match.\n\n## Managed Agent Setup\n\nLimner runs as a Claude Managed Agent: Anthropic's model does the reasoning\n(the brain), Cloudflare's Worker does the executing (the hands), and the two\nmeet at the OAuth-gated MCP surface described above. The agent definition\nitself is narrow by design.\n\n- **What's on the agent.** Model `claude-sonnet-4-6`, an A4-framed system\n  prompt (the framing is code-enforced: `assertA4()` refuses to build the\n  agent plan if the verbatim disclaimer is missing), and 16 Agent Skills\n  uploaded through the Skills API. That's it.\n- **What's deliberately not on the agent.** Tools and MCP servers never appear\n  in the agent definition. They bind to whichever consumer Worker you deploy,\n  not to the agent's identity, so one agent definition works against any\n  Worker you point it at.\n\nStand up your own agent:\n\n1. Deploy your own `limner-mcp` Worker (Quickstart above) and note its\n   OAuth-protected MCP URL.\n2. Set `ANTHROPIC_API_KEY` in your environment.\n3. Build the agent package: `pnpm --filter @limner/core build && pnpm --filter @limner/limner-agent build`.\n4. Dry run the registration: `pnpm --filter @limner/limner-agent create-agent`.\n   Review the printed request payloads, then re-run with `-- --execute` to\n   create the agent and upload all 16 skills.\n5. Connect the standing agent to your Worker's MCP URL the same way any MCP\n   client would, then verify the wiring with the Test 4 harness\n   (`pnpm --filter @limner/limner-agent test-agent`).\n\nFull reference, including the exact API endpoints, frontmatter validation\nrules, and the Path A / Path B tool-lockstep detail, is in\n[packages/limner-agent/README.md](packages/limner-agent/README.md).\n\n## Skills and Capability Surfaces\n\nOne guidance core renders into three public surfaces: a **skill** for\nautonomous multi-step agent work, a **prompt** for a launchable client\ntemplate, and a **resource** for a fact worth consulting. 19 guidance entries\nin `@limner/core` render into 16 skills, 12 prompts, and 3 resources today. A\nCI drift test enforces that a committed skill can never diverge from the\nguidance entry that generated it.\n\n**16 skills** (attach order; S = skill only, S+P = skill and prompt, S+R =\nskill and resource):\n\n| Skill | Surface |\n| --- | --- |\n| `file-types` | S+R |\n| `external-tools` | S+R |\n| `midjourney` | S+P |\n| `dalle` | S+P |\n| `recraft` | S+P |\n| `illuminated-manuscript` | S+P |\n| `pipeline-router` | S+P |\n| `brand-stamp` | S+P |\n| `multi-size-export` | S+P |\n| `captioned-graphic` | S+P |\n| `iterate-on-asset` | S |\n| `style-from-images` | S+P |\n| `brand-kit` | S |\n| `art-research` | S |\n| `vectorize` | S+P |\n| `print-ready` | S+R |\n\n**12 prompts:** `capability-tour`, `midjourney-builder`, `dalle-builder`,\n`recraft-builder`, `illuminated-manuscript`, `pipeline-router`, `brand-stamp`,\n`multi-size-export`, `captioned-graphic`, `aspect-ratio-crops`,\n`style-from-images`, `vectorize`.\n\n**3 resources:** `limner://reference/file-types`,\n`limner://reference/external-tools`, `limner://reference/print-ready`.\n\nTogether with the 18 tools in Tool Surface above, that's the whole public\ncapability surface. The full matrix (which guidance entry renders into which\nsurface, and why `style-profile` backs no standalone surface at all) is in\n[packages/limner-agent/README.md](packages/limner-agent/README.md).\n\n## Security\n\nA few properties hold by construction, worth stating for anyone running their\nown instance:\n\n- **One way in.** The Workers endpoint is OAuth 2.1 with dynamic client\n  registration and PKCE. First authorization shows a consent screen with a\n  double-submit CSRF token (a `__Host-` cookie plus a signed hidden field);\n  the issued scope is pinned, and deny paths validate the redirect URI against\n  registration before redirecting.\n- **Artifacts are signed and expiring.** Generated images are served from R2\n  behind HMAC-signed capability URLs with a short TTL, verified in constant\n  time. On a public origin, delivery fails closed if the signing key is unset.\n  SVG is served with content-type pinning and a sandbox policy; raster is\n  served inline.\n- **Outbound fetches are guarded.** Every fetch the server makes on your behalf\n  (reference images, provider calls) runs through an SSRF guard that blocks\n  loopback, link-local, unique-local, and metadata ranges, and re-resolves\n  hostnames to catch rebinding.\n- **The common path leaks nothing.** In-isolate composition needs no network\n  and no credentials; only the generation tools and the five `cf*` ops reach\n  outside the isolate, and only with the provider key you supplied.\n\n## Privacy Policy\n\nLimner is bring-your-own-key software. It collects no analytics and sends no\ntelemetry to the author. Anything that leaves your machine leaves only because\na tool you called needs it:\n\n- **Provider calls.** When you run a generation tool, the prompt text and any\n  reference images you pass are sent to the provider that backs it: OpenAI for\n  `limner_generate_dalle`, Recraft for `limner_generate_recraft` and the\n  `limner_upscale` / `limner_vectorize` tools. Those providers process that data\n  under their own privacy terms. `limner_generate_midjourney` builds a prompt\n  string locally and calls nothing. In-isolate composition (`limner_compose`,\n  apart from the five `cf*` ops that use Cloudflare Images) needs no network and\n  no credentials.\n- **Your keys.** OpenAI and Recraft API keys are read from your local\n  configuration (environment variables or the client's `user_config`) and used\n  only to authenticate those provider calls. They are never written to logs or\n  sent anywhere else.\n- **Durable memory and projects.** Facts, decisions, and notes you save persist\n  in Cloudflare D1 on the Workers transport, or in a local SQLite file (default\n  `~/.limner/limner.db`) on the stdio and `.mcpb` builds. This data stays until\n  you remove it with `limner_forget` or delete the database.\n- **Generated artifacts.** On the hosted Workers transport, images are stored in\n  Cloudflare R2 and served behind HMAC-signed capability URLs with a short TTL.\n  On local transports, artifacts stay on your machine.\n\nQuestions or requests about data handling go to jim@vinson.org.\n\n## Prerequisites\n\nFor a self-deployed Workers stack:\n\n- A Cloudflare account with Workers, D1, and KV available (the free plan\n  covers all three, including SQLite Durable Objects)\n- R2 enabled on the account (free tier; the artifact bucket is created for\n  you)\n- **A Cloudflare Images paid plan** for the five `cf*` compose ops at real\n  usage volumes\n- An OpenAI API key and a Recraft API key. Both are required up front; the\n  generation tools spend your own provider credit\n- Node per [.nvmrc](.nvmrc) (22; floor 22.13) and pnpm 10\n\nA note on the Images free tier: Cloudflare includes 5,000 unique\ntransformations per month on every account, after which the cf* ops return\nerrors until the month rolls over. That allowance is fine for kicking the\ntires; treat the paid plan as the requirement for actual use. The eleven\nin-isolate compose ops never touch Images and stay free everywhere.\n\nLocal stdio and `.mcpb` need no Cloudflare account at all: state goes to\nlocal SQLite, and the cf* compose ops refuse cleanly.\n\n## Quickstart\n\nPick the heaviest option you have patience for. They all end at the same\ntool surface.\n\n### Provisioning script\n\n```bash\ngit clone https://github.com/vinsonconsulting/limner.git\ncd limner\npnpm install --frozen-lockfile\npnpm setup:cloudflare\n```\n\nThe script checks your wrangler login, provisions D1, KV, and R2 by name\n(re-running is safe), pins the resource ids into the wrangler config,\napplies the schema migration, builds, deploys, prompts for both provider\nkeys, smoke-tests the deployed endpoint, and prints connect instructions\nfor Claude Desktop, Claude Code, and MCP Inspector.\n\nOptional: `pnpm setup:cloudflare --with-example-seed` loads a small generic\nmemory seed (a fictional postcard project) so the memory tools have\nsomething to recall on day one. `--env production` provisions the separate\nproduction environment, and `--dry-run` shows the plan without changing\nanything.\n\n### Where is the Deploy to Cloudflare button?\n\nSigh... we tried, in a live spike (2026-06-12). The button's monorepo mode\nextracts `packages/limner-mcp` into a standalone repository, which severs\nthe `@limner/core` workspace dependency, so the build cannot succeed\nregardless of build settings. Its provisioning and secrets flow worked\nwell; the repo shape is the blocker. The script above does everything the\nbutton would, plus migrations and a smoke test. If the button gains\nin-place monorepo support, it returns here.\n\nFor push-to-deploy CI/CD in the meantime, connect your fork through the\ndashboard: Workers and Pages, import the repository, root directory `/`,\nbuild command `pnpm install --frozen-lockfile && pnpm -r build`, deploy\ncommand `pnpm --filter @limner/mcp run deploy`. That flow keeps the full\nrepository, so the workspace resolves.\n\n### Manual\n\nThe script above is a convenience wrapper over five wrangler commands; the\nby-hand version is in the comments of\n[packages/limner-mcp/wrangler.toml](packages/limner-mcp/wrangler.toml).\nShort form: `wrangler d1 create limner-rasa-dev`, `wrangler kv namespace\ncreate OAUTH_KV`, `wrangler r2 bucket create limner-rasa-artifacts-dev`,\npin the printed ids in the config, then `wrangler d1 migrations apply\nlimner-rasa-dev --remote`, `wrangler secret put` both keys, and\n`wrangler deploy`.\n\n### Local stdio (no Cloudflare account)\n\n```bash\ngit clone https://github.com/vinsonconsulting/limner.git\ncd limner\npnpm install --frozen-lockfile\npnpm -r build\nOPENAI_API_KEY=sk-... RECRAFT_API_KEY=... node packages/limner-mcp/dist/stdio.js\n```\n\nFor Claude Desktop, add to\n`~/Library/Application Support/Claude/claude_desktop_config.json`:\n\n```jsonc\n{\n  \"mcpServers\": {\n    \"limner\": {\n      \"command\": \"node\",\n      \"args\": [\"/absolute/path/to/limner/packages/limner-mcp/dist/stdio.js\"],\n      \"env\": { \"OPENAI_API_KEY\": \"sk-...\", \"RECRAFT_API_KEY\": \"...\" }\n    }\n  }\n}\n```\n\n### `.mcpb` bundle (Claude Desktop one-click)\n\nDownload the `.mcpb` from a GitHub release (built on `mcpb-v*` tags), open\nit with Claude Desktop, and fill in the API-key prompts. Or build your own:\n`pnpm pack:mcpb`.\n\n### Connecting a client to a deployed Worker\n\n```bash\nclaude mcp add --transport http limner https://limner-mcp.<your-subdomain>.workers.dev/mcp\n```\n\nClaude Desktop: Settings, Connectors, add a custom connector with the same\nURL. MCP Inspector: `npx @modelcontextprotocol/inspector`, then connect\nwith the Streamable HTTP transport. OAuth dynamic client registration\nhandles credentials in all three cases.\n\n## Testing and feedback\n\n[docs/TESTING.md](docs/TESTING.md) is the tester's checklist, ordered\nfree-first and paid-last, with the cost of every step stated up front.\nFindings are public issues: use the\n[test finding](.github/ISSUE_TEMPLATE/test_finding.yml) or\n[bug report](.github/ISSUE_TEMPLATE/bug_report.yml) template.\n\n## Roadmap\n\nNear-term, in rough order:\n\n- **MCP spec refresh (`2026-07-28`).** Move the transports to the stateless\n  model the new revision defines and drop the per-session id. This is what\n  promotes stdio out of `preview`; the Workers and `.mcpb` surfaces are already\n  current.\n- **Token-footprint instrumentation.** Measure real per-turn token cost and\n  round-trip counts in live traffic before pulling any schema-reduction lever,\n  so the decision is driven by numbers rather than guesses.\n- **More MCP clients.** Keep validating rasa against additional MCP hosts and\n  conformance harnesses, and fold the findings back into the tool contract.\n- **The family.** `limner-pixel` (pixel art) and `limner-ascii` (ASCII\n  workflows) build on rasa; see [The Limner family](#the-limner-family).\n\n## The Limner family\n\nLimner is a family of model harnesses. Each variant builds on **rasa** (this\nrepo) and adds opinions for a specific creative niche.\n\n| Repo | Status | Focus |\n|---|---|---|\n| `vinsonconsulting/limner` (this repo) | rasa, foundation | General-purpose; OSS |\n| `vinsonconsulting/limner-pixel` | in development, not yet public | Pixel art, sprite work, retro game asset pipelines |\n| `vinsonconsulting/limner-ascii` | pre-development skill building, not yet public | ASCII art workflows |\n\nSpecialty pipelines (pixel-art generators like Pixellab and RetroDiffusion)\nare being built in `limner-pixel` (not yet public); rasa stays\ngeneral-purpose. The legacy proprietary work that preceded the OSS pivot is\nkept private at `vinsonconsulting/limner-pixel-legacy`.\n\n## Contributing\n\nContributions welcome. See [CONTRIBUTING.md](CONTRIBUTING.md). All commits\nneed a [Developer Certificate of Origin](https://developercertificate.org/)\nsign-off via `git commit -s`; the DCO check gates merge.\n\n## License\n\n[Apache-2.0](LICENSE) © Jim Vinson · [jimvinson.com](https://jimvinson.com)\n\n## Acknowledgments\n\nLimner is built on [Anthropic's Claude Managed Agents](https://claude.com/blog/claude-managed-agents-updates) platform and [Cloudflare's CMA hosting](https://blog.cloudflare.com/claude-managed-agents/). Limner is an independent project; \"built on\" does not imply endorsement by Anthropic or Cloudflare.\n\n---\n\n<p align=\"center\"><img src=\"docs/assets/hero-pixel.webp\" alt=\"The hero scene translated to 16-bit pixel art\" width=\"720\"></p>\n<p align=\"center\"><sub>The hero above, translated to pixel art by PixelLab, the pipeline being built as <code>limner-pixel</code>. The family renders its parent.</sub></p>\n",
  "bytes": 22150,
  "sha": "bad9c46ceeece93b98759fc2a04c89f8a300750ec4c2a4552e828b2599d1f978",
  "repo_slug": "vinsonconsulting/limner",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_us_limner_limner_9478cf7e/readme"
}