Docs

Author blueprints from MCP (Claude Desktop, Cursor, ChatGPT-with-MCP)

Audience: a Claude session connected to Amdahl via MCP that wants to read, draft, validate, and persist agent blueprints without ever leaving the chat. Prerequisites: an Amdahl MCP API key with artifact:write scope (the customer-agent bundle is sufficient); the Amdahl MCP server connected to your client. End state: a published tenant-owned blueprint authored interactively, gated through the same validation moat the artifact registry applies, ready to be read by the next session.

This guide pairs with tutorial.md (which walks through forking from the in-app copilot). The shape is the same — fork or compose, customize, validate, persist — but every step here goes through the blueprints MCP coarse tool so an external Claude session can run the whole loop without the Console UI.

Why a coarse tool for blueprints

Blueprints land as platform_artifacts rows with artifact_type = 'agent_blueprint'. You could already create one via artifacts create, but that path loses two things:

  1. Moat validation. The artifact registry's Zod schema catches structural errors, but six invariants beyond that (duplicate step ids, broken $step.field references, unknown prompt:// fragments, sub-blueprint slugs that don't resolve, sub-blueprint cycles, malformed tool ids in the policy allowlist) only catch with the moat. The blueprints tool runs the moat before every write.
  2. Catalog discovery. Authoring a v1 blueprint without knowing the 8 step kinds or the 19 reusable prompt fragments is brutal. The describe_step_kinds + list_prompt_fragments actions give you the full catalog with copy-pasteable examples.

blueprints is a recipe tool, not a one-shot runner. There is no run-blueprint tool on the MCP surface — when you (an MCP-connected Claude session) want to act on a blueprint inline, you ARE the runner: read the recipe (get, or read_resource agent_blueprint://<id>), walk the step graph in your own reasoning loop, make the primitive calls yourself. The blueprints tool is for authoring + discovery + scheduling, not inline execution — its create_schedule / update_schedule / delete_schedule actions DO register an unattended run on the headless SDK runner. (That headless runner also drives saved blueprints to completion for other platform-initiated runs — manual "Run now" (REST), event/webhook triggers, replay, and backtest sweeps; agents.run_blueprint exists for one-shot runs on REST + the Anthropic tool surface, just not on MCP. See ../../blueprint-runner-sdk.md.)

Action catalog

ActionPurposeRequired params
listCatalog every blueprint visible to this tenant (starters + tenant rows).
getFetch the full v1 DSL body for one blueprint.id (UUID or slug)
validateRun the moat against a draft body without writing it.content
describe_step_kindsReturn the v1 step grammar with example bodies.
list_prompt_fragmentsCatalog every registered prompt fragment.
createNew tenant blueprint.content
updatePatch tenant blueprint.id
deleteSoft-delete (archive).id
unarchiveRestore an archived row.id
forkWraps agents.fork_blueprint.source

Every write action returns the same {success, blueprint, error} envelope so the wire shape stays uniform regardless of which path you hit.

Walkthrough — author a new blueprint from scratch

Goal: produce a tenant blueprint that runs the standard research → drafting flow but pins the channel to LinkedIn and locks the writing voice to one author.

Step 1 — Orient on the v1 grammar

Before touching the body, read the step-kind catalog. This is a single MCP call:

text
blueprints describe_step_kinds

You'll see all 8 kinds (tool, llm, loop, branch, parallel, blueprint, transform, assert) with field tables and complete example bodies. Pay attention to:

  • The reference syntax ($inputs.X, $step_id.field, $now, $today, $random_uuid, $secret.X, $builtin.X). Every $-prefixed string in the blueprint must satisfy one of these resolvers, or the moat rejects it.
  • The prompt:// URI scheme. llm steps consume prompt fragments via this scheme; the renderer inlines the fragment body when the step prompt is composed.
  • The output_alias field common to every step. Aliasing an output makes downstream $alias.field references stable across step-id rename refactors.

Step 2 — Pull the prompt fragment catalog

For an llm step, you usually want to compose one or more shared fragments (e.g. content_writer/grounding_rules, content_writer/audience_scoping). List them:

text
blueprints list_prompt_fragments scheme="content_writer" include_body=true

Setting include_body: true inlines the fragment text so you don't have to make a separate prompt:// resource read for every one. The lean projection (default) is fine for browsing.

Step 3 — Draft the body

Compose the DSL in your reasoning loop. A minimal body looks like:

json
{
  "schema_version": "1.0.0",
  "identity": {
    "slug": "linkedin-thought-leader",
    "name": "LinkedIn Thought Leader",
    "version": "1.0.0",
    "authored_by": "tenant",
    "purpose": "Research a topic and draft a LinkedIn post pinned to one voice.",
    "tags": ["linkedin", "thought_leader"]
  },
  "inputs": [
    {
      "name": "topic",
      "type": "string",
      "required": true,
      "description": "The topic to write about."
    },
    {
      "name": "author_id",
      "type": "string",
      "required": true,
      "description": "Voice + author profile UUID."
    }
  ],
  "outputs": [
    {
      "name": "post_artifact_id",
      "type": "string",
      "description": "The created content_piece UUID."
    }
  ],
  "policy": {
    "tool_allowlist": [
      "data.cluster_search",
      "data.search",
      "artifacts.create",
      "external_search.execute"
    ]
  },
  "trigger": { "kind": "manual" },
  "steps": [
    {
      "id": "research",
      "kind": "tool",
      "tool": "external_search.execute",
      "args": {
        "action": "enrich_topic",
        "topic_query": "$inputs.topic"
      },
      "output_alias": "research_brief"
    },
    {
      "id": "draft",
      "kind": "llm",
      "effort": "medium",
      "prompt_resources": [
        "prompt://content_writer/audience_scoping",
        "prompt://content_writer/grounding_rules",
        "prompt://content_writer/hook_patterns"
      ],
      "instructions": "Draft a LinkedIn post grounded in $research_brief. Pin the voice to author $inputs.author_id.",
      "output_schema": { "type": "object", "properties": { "post_body": { "type": "string" } } },
      "output_alias": "draft"
    },
    {
      "id": "persist",
      "kind": "tool",
      "tool": "artifacts.create",
      "args": {
        "artifact_type": "content_piece",
        "title": "LinkedIn: $inputs.topic",
        "content_json": {
          "schema_version": "1.0.0",
          "channel": "linkedin",
          "body": "$draft.post_body",
          "author_id": "$inputs.author_id",
          "grounded_in": ["$research_brief.artifact_id"]
        }
      },
      "output_alias": "post"
    }
  ],
  "outputs_mapping": {
    "post_artifact_id": "$post.id"
  }
}

Step 4 — Dry-run through the moat

Before writing, check the body. The validate action runs the same moat the create path will apply:

text
blueprints validate content=<the body above>

The response shape is { valid: boolean, errors: ValidationError[] }. Errors carry a code discriminator (schema_error, duplicate_step_id, unknown_reference, unknown_fragment, unresolved_sub_blueprint, circular_sub_blueprint, invalid_tool_id) and a structured details blob the calling LLM can pattern-match to surface field-level UX.

Common moat hits during authoring:

  • unknown_reference — you wrote $research_breif.artifact_id (typo). The moat lists every reference it walked through plus the dot-path of the offending occurrence.
  • unknown_fragment — you used prompt://content_writer/grounded_rules (typo, real fragment is grounding_rules). Re-run list_prompt_fragments to fix.
  • duplicate_step_id — easy to hit when you copy-paste a step block. Each step within a blueprint (across loop / branch / parallel descents) must have a unique id.

Step 5 — Persist

Once validate returns { valid: true }, persist:

text
blueprints create content=<the body> status="draft"

The response carries { success: true, blueprint: { header, content } } where header is the lean catalog projection (id, slug, name, version, source, authored_by, status, purpose, tags, timestamps) and content is the full DSL body. Save the header.id — that's the artifact UUID you'll use to update or invoke later.

The default status is draft. Flip to published via update once you're satisfied:

text
blueprints update id="<the id>" status="published" change_summary="Promoted from draft after dry run"

Walkthrough — fork a starter

If the platform-shipped starter is 90% of what you need, fork instead of authoring from scratch:

text
blueprints fork source="plan-and-draft-window" new_slug="team-x-linkedin-flow" new_name="Team X LinkedIn Flow"

The fork is a thin wrapper around the existing agents.fork_blueprint Operation (same handler underneath as the in-app copilot's fork button). The fork:

  • Creates a fresh tenant artifact with a new UUID.
  • Resets identity.version to 1.0.0.
  • Sets identity.authored_by to tenant.
  • Lands at status: 'draft' and visibility: 'private'.
  • Stamps metadata.forked_from = { kind: 'starter'|'tenant', id, slug, version } on the artifact row so the ancestry graph is reconstructable.

The source is unmodified. Forking a tenant blueprint that has itself been forked works the same way — the new fork's metadata.forked_from points at the immediate parent; walking the chain reconstructs the full ancestry.

Walkthrough — iterate on a tenant blueprint

After the first create, the loop is read → edit → validate → update:

text
# 1. Read the current body
blueprints get id="<the id>"

# 2. Edit in your reasoning loop (mutate one or more fields)
# 3. Dry-run the patched body
blueprints validate content=<patched body>

# 4. Persist (content is REPLACE-semantic — pass the full new body, not a JSON Patch)
blueprints update id="<the id>" content=<patched body> change_summary="Tightened the hook step"

Three notes on update:

  • Content is replace-semantic. The patched body is the WHOLE new content_json — top-level keys you omit are dropped. This mirrors how the artifact registry handles content updates (the legacy merge semantic was retired because it bricked schema-tightening migrations).
  • Top-level patch fields are surface metadata. title, description, status, tags, metadata patch the artifact row's columns and stay independent from content_json. You can flip status without touching the recipe.
  • The state machine gates status transitions. agent_blueprint follows draft → published → archived. Trying to flip draft → archived without going through published returns a validation_failed envelope from the registry's state-machine gate.

Lifecycle — delete + recover

delete is soft (archive). The row stays in platform_artifacts with archived_at set; unarchive restores it. Hard delete is reserved for admin / GDPR paths and is NOT exposed on the blueprints MCP surface — use the generic artifacts.delete Operation (REST + Anthropic) if you really need the row gone.

text
blueprints delete id="<the id>"      # archive (reversible)
blueprints unarchive id="<the id>"   # restore

Archived rows are excluded from blueprints list by default. Pass include_archived: true to surface them.

Wire envelope reference

All write actions (create / update / delete / unarchive / fork) return one of:

json
{
  "success": true,
  "blueprint": {
    "header": {
      /* lean projection */
    },
    "content": {
      /* full DSL */
    }
  }
}
json
{
  "success": false,
  "error": {
    "code": "invalid_argument" | "not_found" | "slug_conflict" | "validation_failed" | "unauthorized" | "internal_error",
    "message": "...",
    "details": { /* structured field-level context, optional */ }
  }
}

slug_conflict carries details = { field: 'identity.slug', slug, conflict_artifact_id }. The frontend (and your reasoning loop) can map this to the slug field for inline correction.

validation_failed carries details.errors[] — the same ValidationError[] shape validate returns. Each entry has { code, path, message, severity, details? }.

Anti-patterns

  • Don't look for a one-shot run-blueprint tool here. There is no run-blueprint tool on the MCP surface. If you find yourself wanting to "run" a blueprint inline from here, you instead want to walk the recipe yourself. Read get, then make the primitive calls (data.query, external_search.execute, artifacts.create, etc.) directly. (To register an UNATTENDED run from MCP, use the blueprints create_schedule action — that hands the blueprint to the headless SDK runner. One-shot server-side runs live on REST + the Anthropic tool surface via agents.run_blueprint, not on MCP; see ../../blueprint-runner-sdk.md.)
  • Don't fork via artifacts.create({artifact_type: 'agent_blueprint', ...}). It works, but skips the lineage stamp + the moat. Use blueprints fork (or the underlying agents.fork_blueprint op).
  • Don't compose a body without validate. The artifact registry will reject moat violations on create / update anyway, but validate returns the full error list in one round-trip; the registry short-circuits on the first violation.
  • Don't bake secrets into the body. Every secret reference goes through $secret.X so the resolver substitutes from the tenant's keyring at render time. Literal API keys in a blueprint body are stored unencrypted on the artifact row and surface in get to every agent in the workspace.

Related docs