Skip to main content
Run metalworks mcp serve (stdio is the keyless default) to expose metalworks as MCP tools. Each tool is a thin async wrapper over a plain, unit-testable body; the bodies own the error-envelope contract. The authoritative registered set is the _TOOL_WRAPPERS tuple in metalworks.mcp.server, and each tool’s docstring in metalworks.mcp.tools begins with TIER 1 or TIER 2 — that prefix is the source of truth for its tier. 26 tools are registered.

The tier model

  • Tier 1 — zero-key. Data and deterministic tools: Reddit search/intel ([reddit] extra, no API key), the Arctic corpus readers ([arctic] extra), local-store readers, the offline compliance check, and the two deterministic report projections (channel_plan_build, content_plan_from_report). No provider key needed.
  • Tier 2 — key-gated. Anything that calls a model: planning, the report-derived tools, the research job pattern, reply drafting, discovery, and posting. These need a chat provider key (and some need an embedding key). A missing credential surfaces a MissingKeyError envelope naming the env var and extra — it does not crash.

Tier 1 — zero-key

ToolPurposeKey params
compliance_lintDeterministic, fully-offline compliance check on reply text; emits a confirm_token over the exact text on pass.text, subreddit_rules
reddit_search_postsSearch public Reddit submissions ([reddit] extra, no key).query, subreddit, limit (15)
reddit_get_post_commentsTop-level comments for a public post URL ([reddit]).url, limit (10)
reddit_subreddit_infoSubreddit intel: description, rules, top titles ([reddit]).name
reddit_subreddit_rulesA subreddit’s posting rules ([reddit]).name
arctic_list_monthsLatest available month in the Arctic corpus ([arctic]).content_type (submissions)
arctic_pull_threadsPull submissions for one subreddit from the Arctic corpus. Scoped: months ≤ 3 (default 1), limit ≤ 1000 (default 200), so a Tier-1 caller can’t trigger an unbounded read ([arctic]).subreddit, months, limit
corpus_statsCounts of persisted runs/reports in the local store (offline).store_path
research_list_runsList runs (including in-flight) from the local store.store_path, limit (50)
research_get_reportFetch a finished report from the local store by id.report_id, store_path
channel_plan_buildDeterministic, human-executed launch channel plan for a stored report. Every step is requires_human + posting_gated; no LLM.report_id, store_path
content_plan_from_reportProject a stored report into a deterministic content/SEO plan — pure, no LLM, no embeddings.report_id, store_path

Tier 2 — key-gated

ToolPurposeKey params
research_plan_briefWalk the D1-D8 planner with default answers → an assembled ResearchBrief (chat key).prompt, store_path
positioning_from_reportDerive grounded positioning from a stored report — one LLM call, synchronous (chat key).report_id, store_path
competitor_map_from_reportMap the competitive landscape — grounded names, cited gaps, synchronous (chat + embedding keys).report_id, store_path
surface_recommendRecommend a product surface — grounded rubric + trade-offs, synchronous (chat + embedding keys).report_id, store_path
ux_skeleton_buildBuild a UX skeleton for a stored report on the given surface, synchronous (chat + embeddings).report_id, surface, store_path
site_renderBuild a grounded marketing site + a self-contained index.html (chat + embeddings).report_id, store_path
launch_assets_buildDraft grounded, channel-native launch assets — one LLM call per surface; [] on a no-go report. Drafting only (chat key).report_id, store_path
build_specDerive an evidence-grounded BuildSpec — each feature maps to a real demand cluster with quotes; un-grounded features dropped. Returns the spec; does not write files (that is the metalworks build init CLI) (chat + embedding keys).report_id, surface (web), stack (empty), store_path
research_startStart the pipeline as a background job and return a run_id immediately (chat + embedding keys).brief, months, store_path
research_statusStatus of a background research job.run_id, store_path
research_resultThe finished report for a completed job, or a status payload while running.run_id, store_path
generate_replyDraft a Reddit reply for a thread + run the compliance gate; emits a confirm_token on pass (chat key).thread_url, voice
discovery_runRun discovery over queries → gated draft opportunities. Never posts (chat key, [reddit]).queries, subreddits, max_opportunities (10), voice
reddit_post_commentSecurity boundary — post a reply to a public thread (see below).url, text, confirm_token, username
The report-derived tools (positioning_from_report, competitor_map_from_report, surface_recommend, ux_skeleton_build, site_render, launch_assets_build, build_spec) are synchronous — run them after a stored report exists. They are distinct from the minutes-long pipeline, which uses the async job pattern below.

The async job pattern

Research takes minutes, so it never runs inline. Call research_start to get a run_id immediately, then poll research_status (run state) and research_result (the finished report, or a status payload while it’s still running).

The error-envelope contract

Every tool body returns either its success payload or an error envelope. The guard decorator turns any raised exception — typed or not — into that envelope, so a host model never sees a raw traceback:
{ "error": { "error_code": "...", "message": "...", "fix": "...", "docs_url": "..." } }
Relay the fix string verbatim. Tier-2 bodies that need a credential surface a MissingKeyError envelope naming the env var and extra.

The posting security gate

reddit_post_comment is the security boundary and is triple-gated:
  1. METALWORKS_ALLOW_POSTING=1 must be set (operator opt-in) — there is no override.
  2. The confirm_token must be the one a prior compliance_lint (or generate_reply) pass emitted over this exact text — proof the text cleared the deterministic gate unchanged. Tokens are per-process HMAC and do not survive a server restart.
  3. The compliance gate is re-run server-side and must still pass (defense in depth).
It also needs the [reddit] extra plus REDDIT_CLIENT_ID / REDDIT_CLIENT_SECRET and a connected account (metalworks reddit auth login). A model cannot post without a passing compliance check on the identical text and an explicit operator opt-in.