Skip to content

dhruvvarshney1/fanout-mcp

v0.1.0Apache-2.0

Run many independent subtasks in parallel on cheap, local or open-source models and return only the distilled result.

Changelog

[Unreleased]

Added

  • Core pydantic contracts: SubTask, WorkerResult, RunSummary, FanOutResult.

  • Project scaffolding: uv, ruff, mypy strict, pytest.

  • Provider layer: Provider protocol, OpenAICompatibleProvider, OllamaNativeProvider.

  • providers.yaml loader with ${ENV_VAR} interpolation and named profiles with fallback.

  • fan_out engine: global + per-provider throttling, per-task timeout, wall-clock budget, jittered retry on 429/5xx/connection errors, and an optional cost ceiling.

  • .mcp.json registering the server for Claude Code, docs/local-setup.md, and scripts/dogfood/ (three real fan-out tasks run against this repo).

  • ${VAR:-default} fallbacks in providers.yaml interpolation, so OLLAMA_BASE_URL is overridable without being required.

  • FastMCP server (dhruvvarshney.server, fanout-mcp) exposing fan_out, list_profiles and get_result, with full/truncated/reference return modes, untrusted-output delimiters and fanout://runs/... resources.

  • fan_out takes per_task_timeout_s and wall_clock_budget_s; local models are far slower than the old fixed 60s ceiling allowed.

  • think on ProviderConfig, forwarded by the native Ollama provider, plus Field descriptions on every SubTask field.

  • list_profiles() reports each profile's effective concurrency.

  • JSONL run log at $FANOUT_LOG: one line per task plus a run-summary line.

  • fanout report CLI (argparse, no new deps): runs, cost by provider/model, p50/p95 latency, failure rate, and savings against a configurable flagship rate.

  • Optional OpenTelemetry spans, one per run and one per task, behind FANOUT_OTEL=1 and the otel extra. The import is guarded, so neither is required.

  • Pluggable aggregation via AGGREGATORS, selected per call: passthrough, concat, vote (majority with deterministic tie-breaking, counts and dissent), rank (a cheap model scores each output against original_intent, top-k wins) and map_reduce (one model reduces them all; only the reduction is inline). rank and map_reduce call the engine recursively and degrade to passthrough with a note if their own model call fails.

  • Claude Code plugin packaging: .claude-plugin/plugin.json (with userConfig for the Ollama URL and model tag, optional Groq/OpenRouter keys, default profile and default return mode), a relocatable .mcp.json launching the server through uvx --from ${CLAUDE_PLUGIN_ROOT}, .claude-plugin/marketplace.json, the fanout skill (when fan-out pays off, how to decompose, when not to bother) and the /fanout-report command.

  • FANOUT_PROFILE and FANOUT_RETURN_MODE set the defaults for a fan_out call that names neither; an empty or unrecognised return mode reads as truncated, since a plugin option arrives as an env var either way.

  • docs/distribution.md: uvx vs. a PyInstaller binary vs. an HTTP container, and why uvx now and HTTP later.

  • Multi-harness packaging generated from the canonical .mcp.json and .claude-plugin/plugin.json by scripts/gen_manifests.py: root mcp.json and plugin.json (Agent Plugins 1.0, read by Copilot and Cursor), .agents/plugins/marketplace.json, .codex-plugin/{plugin,mcp}.json (with the Codex interface block) and .cursor-plugin/plugin.json. Generated, not symlinked: the harnesses disagree on JSON shape and on path-variable vocabulary. --check fails on drift, from CI and from pytest.

  • docs/install/: one page per harness — Claude Code, Codex, Copilot CLI, Cursor, Gemini CLI, Zed, OpenCode.

  • GitHub Actions CI: ruff, mypy, the manifest drift check, pytest.

  • The default providers.yaml ships inside the package, so uvx --from git+<repo> fanout-mcp runs with no config path and no environment — the only launch form harnesses without a plugin-root variable can express. FANOUT_CONFIG, then a providers.yaml in the working directory, still win.

Removed

  • providers.example.yaml at the repo root; it is now src/dhruvvarshney/providers.default.yaml and ships in the wheel.
  • No OpenCode TypeScript plugin. OpenCode already speaks MCP via opencode.json, so a wrapper would add an npm package, a release step and a second language to ship a SKILL.md that AGENTS.md already reads. Reasoning in docs/distribution.md.

Changed

  • ProviderConfig and Pricing reject unknown keys, so a misspelt rate like input_per_1m_usd fails loudly instead of silently costing 0.
  • return_mode defaults to truncated, not full.
  • Result envelope states the untrusted-data warning, provider and model once per run instead of once per task, and carries chars= on every block. Cuts per-task overhead ~56% inline and ~65% in reference mode.
  • Engine per-task timeout default 60s -> 120s.
  • aggregation went from a one-value placeholder to a real registry.

Fixed

  • A worker that returns empty text is an error carrying finish_reason and the token counts, not an ok with nothing in it.
  • Native Ollama replies that put everything in message.thinking and stopped are no longer read as empty. Same for reasoning / reasoning_content on the OpenAI-compatible path (NVIDIA NIM, vLLM).