{"id":"aa19ff7f-c50b-4849-8f6e-43fadcb11d3e","slug":"software-mansion-labs-mcp-jobs","kind":"mcp","name":"mcp-jobs","summary":"Turn Oban workers into MCP tools. Each tool call runs as an Oban job, and the client follows it as an MCP task.","owner":"software-mansion-labs","repoUrl":"https://github.com/software-mansion-labs/mcp_jobs","license":"MIT","licenseTier":"permissive","trust":"community","stars":0,"match":null,"fewStars":true,"format":"mcp","createdAt":"2026-10-05T11:46:59.000Z","addedAt":"2026-10-21T06:54:59.652Z","logoUrl":"https://avatars.githubusercontent.com/u/6952717?v=4","linkOnly":false,"collection":null,"skillPath":null,"categories":["productivity","memory-knowledge"],"tags":["ai-tools","background-jobs","elixir","oban"],"runtime":"binary","local":true,"cloudCapable":false,"cloudCapability":"unknown","cloudReadyOnPlatform":false,"needsApiKey":false,"reviewed":false,"syncedAt":"2026-10-22T15:40:03.213Z","sample":false,"url":"https://conduits.dev/tools/software-mansion-labs-mcp-jobs","language":"Elixir","transports":["stdio"],"install":{},"requirements":{"env":[],"upstreamMayCharge":false},"tools":null,"readme":"# MCPJobs\n\n[![Version](https://img.shields.io/hexpm/v/mcp_jobs.svg)](https://hex.pm/packages/mcp_jobs)\n[![Hex Docs](https://img.shields.io/badge/documentation-gray.svg)](https://hexdocs.pm/mcp_jobs)\n[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)\n\nMCPJobs is a small Elixir library that turns [Oban](https://hex.pm/packages/oban) workers into MCP tools. Each tool call becomes an Oban job, and the client follows it as an MCP task.\n\nAn MCP tool call usually blocks until the tool is done. This is a problem for slow work, such as reports, exports, file processing, slow external APIs, data imports, or bulk emails. The client waits, the connection can time out, and a restart loses the work.\n\nWith MCPJobs, the tool call gives back an MCP task at once, and Oban does the work in the background.\n\n## Features\n\n### Reliable\n\n- **Task ID at once.** A client with MCP Tasks never waits on an open request.\n- **Oban does the work.** You get Oban queues, retries, backoff, and Oban Web.\n- **Durable.** Tasks are in PostgreSQL. They survive restarts and deploys.\n- **Retries do not fail tasks.** A task fails only when Oban discards the job.\n- **No races.** The first status change wins, in one conditional update.\n- **Self-repair.** If a node stops early, the next read restores the status from the Oban job.\n- **Safe duplicates.** The same task ID gives the same task. A unique index enforces it.\n- **Private tasks.** With ExMCP, only the owner of a task can read or cancel it.\n\n### Built for long work\n\n- **Progress.** `MCPJobs.progress(job, 2, 5, \"Rendering\")` shows as `\"Rendering (2/5)\"`.\n- **Push notifications.** A client can listen instead of polling: for progress, completion, failure, and cancel.\n- **Cancellation.** A waiting job is cancelled in Oban. A running worker stops at `MCPJobs.cancelled?/1`, or at once with `kill: true`.\n- **Safe errors.** Clients see only the error message. The details stay on the server.\n\n### Easy to adopt\n\n- **Plain Oban workers.** They need no MCP code, and they still run as normal jobs.\n- **Tools from your code.** The description comes from `@moduledoc`. The schema comes from the options or from an Oban Pro `args_schema`.\n- **Arguments checked first.** A bad call gets a list of the problems at once, and no job starts.\n- **Current and older clients.** It supports MCP `2026-07-28` and is tested with Claude Code. A client without tasks gets the result directly.\n- **One-command setup.** `mix mcp_jobs.install` creates everything. `--phoenix` also adds the route.\n- **Small.** It plugs into [ExMCP](https://hex.pm/packages/ex_mcp) or [FastestMCP](https://hex.pm/packages/fastest_mcp). The core API needs no MCP library.\n\n### Ready for production\n\n- **Telemetry.** `[:mcp_jobs, :task, ...]` events for each status change and for progress.\n- **Cleanup.** `MCPJobs.Cleaner` deletes old finished tasks on an Oban Cron schedule.\n\nThe [guides](#guides) explain each feature in detail.\n\n## Usage\n\nWrite a plain Oban worker. The `@moduledoc` becomes the tool description, and the input schema tells the AI client which arguments to send:\n\n```elixir\ndefmodule MyApp.Workers.GenerateReport do\n  @moduledoc \"Generates a report in the background.\"\n\n  use Oban.Worker, queue: :reports, max_attempts: 3\n\n  use MCPJobs.Tool,\n    input_schema: %{\n      \"type\" => \"object\",\n      \"properties\" => %{\"report_id\" => %{\"type\" => \"integer\"}},\n      \"required\" => [\"report_id\"]\n    }\n\n  @impl Oban.Worker\n  def perform(%Oban.Job{args: %{\"report_id\" => report_id}} = job) do\n    MCPJobs.progress(job, 1, 2, \"Loading data\")\n\n    {:ok, %{\"url\" => MyApp.Reports.generate(report_id)}}\n  end\nend\n```\n\nList your workers in the MCP server. Each worker becomes a tool:\n\n```elixir\ndefmodule MyApp.MCPServer do\n  use MCPJobs.ExMCP, tools: [MyApp.Workers.GenerateReport]\nend\n```\n\n`mix mcp_jobs.install` prints how to serve it over HTTP. The client calls `generate_report` and gets a task ID at once. It then reads the task with `tasks/get`, or listens for `notifications/tasks`, until the task is done:\n\n```json\n{\"taskId\": \"...\", \"status\": \"working\", \"statusMessage\": \"Loading data (1/2)\"}\n{\"taskId\": \"...\", \"status\": \"completed\", \"result\": {\"structuredContent\": {\"url\": \"...\"}}}\n```\n\nThe MCP task status follows the Oban job state:\n\n| Oban job state                                                  | MCP task status |\n| --------------------------------------------------------------- | --------------- |\n| `available`, `scheduled`, `executing`, `retryable`, `suspended` | `working`       |\n| `completed`                                                     | `completed`     |\n| `discarded` (all attempts failed)                               | `failed`        |\n| `cancelled`, or the job is deleted                              | `cancelled`     |\n\n## Installation\n\nMCPJobs needs Oban with PostgreSQL.\n\n```elixir\ndef deps do\n  [\n    {:mcp_jobs, \"~> 0.1\"},\n    # Optional, for the ExMCP adapter:\n    {:ex_mcp, \"~> 1.5\"}\n  ]\nend\n```\n\nSet up Oban first, then run:\n\n```sh\nmix mcp_jobs.install\n```\n\nIt creates the migration and an MCP server module, and prints the next steps. In a Phoenix app, add `--phoenix` to also add the `/mcp` route.\n\n## Guides\n\n- [Installation](guides/installation.md): installer options, manual setup, and Phoenix\n- [Workers](guides/workers.md): results, progress, errors, cancellation\n- [Use with ExMCP](guides/ex_mcp.md): tools, Oban Pro schemas, clients without tasks, notifications\n- [Use with FastestMCP](guides/fastest_mcp.md): tools and durable tasks\n- [Core API, telemetry and cleanup](guides/operations.md)\n\n## Example\n\n`examples/deep_thought` is a small app with one tool, `answer_ultimate_question`. It takes 7.5 million years (about 3 seconds). Its demo shows all features:\n\n- **Progress and push notifications:** \"Thinking. 3 million years have passed (3/6)\"\n- **Retry:** on the first attempt a mouse interrupts the calculation. Oban retries the job, and the task stays `working`.\n- **Cancel:** the Vogons demolish the Earth during the calculation.\n- **Safe errors:** Deep Thought cannot compute the Ultimate Question itself. The client gets only the message, and the details stay on the server.\n\n```sh\ncd examples/deep_thought\nmix deps.get\nmix ecto.create && mix ecto.migrate\nmix run demo.exs\n```\n\nStop `serve.exs` and `serve_fastest.exs` before you run the demo. They use the same queue, so they can take its jobs.\n\nTo use it from another MCP client, start it over HTTP on port 4000:\n\n```sh\nmix run --no-halt serve.exs\nnpx @modelcontextprotocol/inspector --cli http://localhost:4000/ --transport http \\\n  --method tools/call --tool-name answer_ultimate_question\n```\n\nThe same worker with FastestMCP runs on port 4001: `mix run serve_fastest.exs`, then use `http://localhost:4001/mcp`.\n\n## Development\n\nTests need a local Postgres.\n\n```sh\nmix test\n```\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n"}