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:writescope (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:
- Moat validation. The artifact registry's Zod schema catches structural errors, but six invariants beyond that (duplicate step ids, broken
$step.fieldreferences, unknownprompt://fragments, sub-blueprint slugs that don't resolve, sub-blueprint cycles, malformed tool ids in the policy allowlist) only catch with the moat. Theblueprintstool runs the moat before every write. - 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_fragmentsactions 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
| Action | Purpose | Required params |
|---|---|---|
list | Catalog every blueprint visible to this tenant (starters + tenant rows). | — |
get | Fetch the full v1 DSL body for one blueprint. | id (UUID or slug) |
validate | Run the moat against a draft body without writing it. | content |
describe_step_kinds | Return the v1 step grammar with example bodies. | — |
list_prompt_fragments | Catalog every registered prompt fragment. | — |
create | New tenant blueprint. | content |
update | Patch tenant blueprint. | id |
delete | Soft-delete (archive). | id |
unarchive | Restore an archived row. | id |
fork | Wraps 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:
blueprints describe_step_kindsYou'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.llmsteps consume prompt fragments via this scheme; the renderer inlines the fragment body when the step prompt is composed. - The
output_aliasfield common to every step. Aliasing an output makes downstream$alias.fieldreferences 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:
blueprints list_prompt_fragments scheme="content_writer" include_body=trueSetting 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:
{
"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:
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 usedprompt://content_writer/grounded_rules(typo, real fragment isgrounding_rules). Re-runlist_prompt_fragmentsto 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:
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:
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:
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.versionto1.0.0. - Sets
identity.authored_bytotenant. - Lands at
status: 'draft'andvisibility: '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:
# 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,metadatapatch the artifact row's columns and stay independent fromcontent_json. You can flipstatuswithout touching the recipe. - The state machine gates
statustransitions.agent_blueprintfollowsdraft → published → archived. Trying to flipdraft → archivedwithout going throughpublishedreturns avalidation_failedenvelope 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.
blueprints delete id="<the id>" # archive (reversible)
blueprints unarchive id="<the id>" # restoreArchived 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:
{
"success": true,
"blueprint": {
"header": {
/* lean projection */
},
"content": {
/* full DSL */
}
}
}{
"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 theblueprintscreate_scheduleaction — that hands the blueprint to the headless SDK runner. One-shot server-side runs live on REST + the Anthropic tool surface viaagents.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. Useblueprints fork(or the underlyingagents.fork_blueprintop). - Don't compose a body without
validate. The artifact registry will reject moat violations oncreate/updateanyway, butvalidatereturns 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.Xso 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 ingetto every agent in the workspace.
Related docs
- Blueprint DSL reference (auto-generated) — the canonical Zod schema dump for every field on every step kind.
- Tutorial — fork the calendar starter — same loop from the in-app copilot UI.