Software Factory

MCP

Programmatic access to the factory — tasks, releases, the agent diary — over a bearer-token MCP connection. Any MCP client can read and act on your account exactly as the web UI does, gated the same way (reviewer-only actions stay reviewer-only).

Endpoint

https://factory.featured.com/api/mcp

Streamable HTTP only — there is no SSE endpoint.

Authentication

Every request carries a bearer API key in the Authorization header. Mint a key from your account settings — the secret is shown once, right after creation.

Authorization: Bearer <your-api-key>

Connect

Claude Code

claude mcp add --transport http factory https://factory.featured.com/api/mcp --header "Authorization: Bearer <your-api-key>"

Other clients (generic JSON config)

{
  "mcpServers": {
    "factory": {
      "url": "https://factory.featured.com/api/mcp",
      "headers": {
        "Authorization": "Bearer <your-api-key>"
      }
    }
  }
}

Tools

Each tool declares an output schema, so a client knows the response structure before it calls. Expand one to read it.

  • repo_list

    List every registered repository with its id, owner/name, default branch, and whether the GitHub App is connected. The id is what task_create needs — there is no other way to discover it over MCP.

    Response
    {
      "repos": [{
        "id": string,
        "repo": string,  // owner/name, the form most tools print.
        "owner": string,
        "name": string,
        "defaultBranch": string,
        "githubConnected": boolean,  // Whether the GitHub App installation can reach this repo.
      }],
    }
  • repo_config_get

    Read a repository's config: the `overrides` layer stored on the repo (what the config editor edits) and the `effective` config those resolve to against the schema defaults. The `.factory.yml` layer is agent-side and is NOT included — this is the same view the web config page shows.

    Response
    {
      "repo": string,  // owner/name.
      "overrides": object,  // The stored overrides layer — only what differs from the defaults.
      "effective": object,  // Defaults ⊕ overrides: what the app actually reads.
    }
  • repo_config_set

    Write a repository's overrides layer, validated exactly like the config editor's Save (unknown keys and schema violations are refused, nothing is written). Defaults to `mode: "merge"` — a top-level shallow merge, so passing one key changes only that key. Use `mode: "replace"` to set the whole overrides object, which is also the only way to REMOVE a key.

    Response
    {
      "repo": string,
      "overrides": object,  // The overrides layer as written.
      "effective": object,
      "changedKeys": [string],  // Top-level keys whose value actually changed. Empty means the write was a no-op — which a success response would otherwise be indistinguishable from.
    }
  • model_recommend

    Price a model tier's REAL measured token mix (from recent usage events) against the whole Vercel AI Gateway catalog, and rank what could run that tier more cheaply. Returns the observed mix, what runs the tier today, and candidates that clear hard constraints derived from your own data — context window against the p95 prompt, tool-use support, caching. Candidates are ranked by WORST-case cost (cache miss), not best, so the downside is bounded. Ranks the whole catalog and flags models absent from the repo's configured tier menu as unvetted; it does NOT rank on coding capability, since the catalog carries no benchmark data — treat the result as a shortlist for an eval, not a verdict.

    Response
    {
      "tier": string,
      "windowDays": number,
      "insufficientData": boolean,
      "observed": {
        "steps": number,
        "tasks": number,
        "cacheReadTokens": number,
        "freshInputTokens": number,
        "outputTokens": number,
        "avgPromptTokens": number,
        "p95PromptTokens": number,
        "cacheHitRate": number,
        "billedUsd": number,
        "modelIdCoverage": number,  // Share of steps with a recorded modelId; below 1, currentModels is partial.
        "currentModels": [{
          "modelId": string,
          "steps": number,
          "billedUsd": number,
        }],
      },
      "constraints": {
        "contextFloorTokens": number,
        "minHeadroom": number,
        "requireToolUse": boolean,
      },
      "candidates": [{
        "modelId": string,
        "bestUsd": number,
        "worstUsd": number,
        "contextWindow": number,
        "contextHeadroom": number,
        "caching": "explicit" | "implicit" | "none",
        "vetted": boolean,
        "worstCaseSavings": number | null,
      }],
    }
  • model_outcomes

    MONITORING, NOT SELECTION. Reports how models ALREADY IN USE are performing on this factory's own task history: how often their tasks reached done rather than blocked or archived, rework taken (implementation runs, CI failures), and cost per shipped task. Use it to verify a model still earns its slot — especially after switching to a new one. Do NOT use it to choose a model: it can only see models that have already run, so treating it as a selector would keep re-picking the incumbent and no new model could ever be considered. Choosing is model_recommend's job — that prices your measured token mix against the WHOLE gateway catalog, so every model gets a chance, and its shortlist goes to an eval. Only tasks run by exactly ONE model count toward a rate; tasks several models touched are reported as `sharedTasks` and excluded. Models below the settled-task floor come back under `insufficientData` rather than ranked. Read `unattributedTasks` first — it bounds how much of the history this can see at all.

    Response
    {
      "windowDays": number,
      "minTasks": number,  // Settled sole-model tasks a model needed to be ranked.
      "totalTasks": number,
      "unattributedTasks": number,
      "sharedTasks": number,
      "ranked": [{
        "modelId": string,
        "soleTasks": number,  // Tasks where this was the only model that ran.
        "completed": number,
        "abandoned": number,  // Reached blocked or archived — the work did not ship.
        "open": number,  // Still in flight; counts toward no rate.
        "completionRate": number | null,
        "runsPerCompletedTask": number | null,
        "ciFailuresPerCompletedTask": number | null,
        "usdPerCompletedTask": number | null,  // Spend on this model's sole tasks — abandoned ones included — per task shipped.
        "sharedTasks": number,
      }],
      "insufficientData": [{
        "modelId": string,
        "soleTasks": number,  // Tasks where this was the only model that ran.
        "completed": number,
        "abandoned": number,  // Reached blocked or archived — the work did not ship.
        "open": number,  // Still in flight; counts toward no rate.
        "completionRate": number | null,
        "runsPerCompletedTask": number | null,
        "ciFailuresPerCompletedTask": number | null,
        "usdPerCompletedTask": number | null,  // Spend on this model's sole tasks — abandoned ones included — per task shipped.
        "sharedTasks": number,
      }],
    }
  • task_list

    List factory tasks across every repo, newest-updated first. Optionally filter by status and/or kind.

    Response
    {
      "tasks": [{
        "id": string,
        "title": string,
        "status": "draft" | "refining" | "ready" | "in_progress" | "in_review" | "done" | "blocked" | "archived",
        "kind": "build" | "dogfood",
        "repo": string,
        "updatedAt": string,  // ISO 8601.
      }],
    }
  • task_get

    Get a single task's full spec, a recent thread digest (last 20 events, oldest first), and any unanswered human-input requests the agent is parked on — pass one of those requestIds to task_answer to unpark it.

    Response
    {
      "task": {
        "id": string,
        "title": string,
        "spec": string,
        "status": "draft" | "refining" | "ready" | "in_progress" | "in_review" | "done" | "blocked" | "archived",
        "kind": "build" | "dogfood",
        "repo": string,
        "readyAt": string | null,
        "updatedAt": string,
      },
      "recentActivity": [{
        "kind": string,
        "actor": string,
        "body": string | null,
        "at": string,
      }],  // Last 20 thread events, oldest first.
      "pendingRequests": [{
        "requestId": string,
        "prompt": string | null,
        "allowFreeform": boolean,
        "options": [{
          "id": string,
          "label": string,
        }],
      }],  // Unanswered human-input requests the agent is parked on. Empty when nothing is pending. Pass a requestId to task_answer to resolve one — without these ids there is no way to answer a parked session over MCP.
    }
  • task_create

    Create a draft task on a repo. Omit `kind` to leave classification to the agent at TASK_READY (recorded as kind_source=agent); pass it explicitly to pick build/dogfood yourself (kind_source=user).

    Response
    {
      "task": {
        "id": string,
        "title": string,
        "status": "draft" | "refining" | "ready" | "in_progress" | "in_review" | "done" | "blocked" | "archived",
        "kind": "build" | "dogfood",
      },
    }
  • task_ready

    Move a task to ready from draft or refining (a draft is walked through refining first), dispatching the same TASK_READY turn the web UI's ready button sends.

    Response
    {
      "task": {
        "id": string,
        "status": "draft" | "refining" | "ready" | "in_progress" | "in_review" | "done" | "blocked" | "archived",
      },
      "alreadyReady"?: boolean,  // Present and true when the task was already ready — no turn was dispatched.
    }
  • task_update

    Move a task to a new status as a human — the exact same legal hops the web UI's status buttons offer (actor "user"), with a clean error on an illegal one. Use this to lift a block, send an in_review task back to in_progress, or mark it done — from a local session with no browser open.

    Response
    {
      "task": {
        "id": string,
        "status": "draft" | "refining" | "ready" | "in_progress" | "in_review" | "done" | "blocked" | "archived",
      },
      "unchanged"?: boolean,  // Present and true when the requested status was already the current one.
    }
  • task_comment

    Post a comment to a task's thread. Moves a draft task to refining on its first comment, same as the web UI.

    Response
    {
      "eventId": string | null,  // The thread event the comment landed as.
    }
  • task_answerreviewer-gated

    Answer a pending input.requested approval card by its requestId — an `optionId` for a button click or `text` for freeform input, plus a human-readable `label` that lands in the thread as the durable record. Reviewer-gated exactly like the web UI's answer route.

    Response
    {
      "eventId": string | null,  // The thread event the answer landed as.
    }
  • release_list

    List release notes (drafts and published), newest-created first.

    Response
    {
      "releaseNotes": [{
        "id": string,
        "title": string,
        "category": string,
        "state": "draft" | "published",
        "repo": string,
        "taskId": string | null,
        "prNumber": integer | null,
        "prUrl": string | null,
        "publishedAt": string | null,
        "createdAt": string,
      }],
    }
  • release_publishreviewer-gated

    Publish a draft release note. Reviewer-gated exactly like the web UI — a non-reviewer key is rejected.

    Response
    {
      "releaseNote": {
        "id": string,
        "state": "draft" | "published",
      },
    }
  • diary_recent

    Read the latest agent diary entries (lessons/missteps/improvements/observations), optionally filtered by category.

    Response
    {
      "entries": [{
        "id": string,
        "category": "lesson" | "misstep" | "improvement" | "observation",
        "body": string,
        "repoId": string | null,
        "taskId": string | null,
        "createdAt": string,
      }],
    }
  • migrations_status

    Compare this build's bundled migration journal against the live database ledger: the mode (manual/auto), pending migration tags in order, and the backwards-journal guard state.

    Response
    {
      "mode": "manual" | "auto",
      "pending": [{
        "idx": integer,
        "tag": string,
      }],  // Bundled migrations not yet in the live ledger, in journal order.
      "guard": {
        "status": "ok" | "ledger-ahead-of-journal",
        "unknownHashes"?: [string],
      },  // Backwards-journal guard: `ledger-ahead-of-journal` means this build is older than the database.
    }
  • migrations_apply

    Apply every pending bundled migration to the live database — the exact same path as the web UI's Apply now button (advisory lock, backwards-journal guard, audit event recording the key's user with trigger "mcp"). Only reviewed, merged migration files can ever run; there is no arbitrary-SQL surface.

    Response
    {
      "status": "noop" | "success",
      "message"?: string,  // Present on `noop`.
      "tags"?: [string],  // Present on `success` — the tags applied, in order.
    }

Scoped to the factory’s data layer only — no sandbox or engine-run access. OAuth isn’t supported; bearer keys only.

For the rest of what the factory does, see /features.