Manage Tracecat skills
Tracecat MCP ships six skill tools: list_skills, get_skill, prepare_skill_download, prepare_skill_upload, complete_skill_upload, and publish_skill.
Writes are staged, not inlined. File bytes never travel as tool arguments: prepare_skill_upload takes metadata only — path, SHA-256, size, content type — and returns a short-lived PUT URL per file; the bytes go to those URLs over plain HTTP; complete_skill_upload then attaches the staged blobs and replaces the draft. Do not look for an inline content_base64 argument, and never read file bytes into context to send them.
The bundled helper removes the parts that are error-prone by hand: the safe directory walk, symlink rejection, root SKILL.md enforcement, and SHA-256 digests.
Upload a skill
The same four steps create a skill and replace an existing one. The only difference is which identity arguments prepare_skill_upload takes.
-
Resolve the target workspace with
list_workspaces, then calllist_skills. For a new skill, confirm the name is free — omittingskill_idalways creates a new row, so a second create yields two skills with the samenameand different slugs. For an existing skill, take itsskill_id. -
Build the metadata array:
python3 <this-skill-dir>/scripts/upload_skill_files.py metadata <local-skill-dir>It prints the
filesarray verbatim: one entry per file withpath,sha256,size_bytes, andcontent_type. No file contents. -
Call
prepare_skill_uploadwithworkspace_idand thatfilesarray, plus either:name(and optionaldescription) and noskill_id, to create a skill; orskill_idand noname/description, to replace an existing skill's draft.
For a create, pass the same
namethe local frontmatter declares — the server mergesnameanddescriptioninto the stored rootSKILL.mdfrontmatter before validating, so a mismatched argument silently rewrites what gets stored.The response carries
skill_id,base_revision,created, and a per-fileupload_id,upload_url,method,headers, andexpires_at. -
PUTeach file's raw bytes to itsupload_urlwith the returned method and headers, straight from disk. The URLs are short-lived; checkexpires_atand re-prepare rather than retrying a stale one. -
Call
complete_skill_uploadwithworkspace_id,skill_id, thebase_revisionfrom step 3, and afilesarray of{"path": ..., "upload_id": ...}for every file. This replaces the draft and does not publish. -
Call
publish_skillwith theskill_idwhen the skill should be attachable to presets. An unpublished draft cannot be bound.
base_revision is a concurrency guard. A draft_revision_conflict means someone else edited the draft since step 3 — call get_skill to see the current draft_revision and what a complete-tree replacement would delete, then re-prepare.
Updating a live skill
A fix that stops after complete_skill_upload changes nothing that any agent runs. Three steps, in order:
prepare_skill_uploadwithskill_id→PUTthe bytes →complete_skill_upload. This replaces the draft. It does not publish.publish_skillwith the sameskill_id. Skills are versioned; this mints a new immutable version and moves the skill'scurrent_version_id.update_agent_presetwith the consuming preset'sskillslist — the same{"skill_id": ...}entries it already had. Each binding is re-pinned to that skill's current published version at write time, so a preset bound before step 2 keeps running the old version until this call rewrites it.
Skipping step 3 is the classic failure: the new version exists, the preset never sees it.
Verify by re-fetching. get_skill without path returns the draft manifest — paths, digests, sizes, draft_revision, is_publishable, validation_errors — and never file contents; get_skill with path reads one file, inline when it is small UTF-8 text. list_skills shows the skill's current_version_id; get_agent_preset shows skills[].skill_version_id and skills[].skill_version. Confirm both moved.
This is routine, reversible work. Every published version is immutable and still bound-able, so a bad push is undone by publishing a corrected tree and re-binding. Do it rather than narrating risk; reserve caution for operations that actually cannot be undone.
Declaring the skill's tools
A skill carries its own tools. Declare them in the root SKILL.md frontmatter under
metadata.tools, as registry action names or mcp.<slug> / mcp.<slug>.<tool> entries, up to
64 per skill:
---
name: slack-triage
description: Post and thread triage updates in Slack.
metadata:
tools:
- tools.slack.post_message
- tools.slack.list_replies
---
Every preset the skill is attached to receives those tools on top of its own actions
allowlist. They are granted as soon as the preset resolves, whether or not the model opens the
skill; only the skill's instructions load on demand.
Prefer this over listing the same tools on each preset's actions when the tools group
naturally and several agents reuse them — one edit and a re-publish then updates every consuming
preset. Keep a tool on the preset's actions when it is specific to that one agent.
One caveat: a preset's namespaces filter applies to the registry tools in the union and
silently blocks the ones outside it. After attaching a skill to a namespace-restricted preset,
check tool_policy.blocked_tools on get_agent_preset. MCP tools a skill declares
(mcp.<slug> / mcp.<slug>.<tool>) are not filtered by namespaces and do not appear there.
Changing metadata.tools changes what agents can execute, so it follows the same three steps as
any other fix: replace the draft, publish_skill, then update_agent_preset to re-pin the
binding. A read of the resulting preset shows the granted tools as one flat list in
tool_policy.actions, with no per-skill provenance — the union is what you can verify.
Size
Bytes no longer pass through model context, but each file still has a server-side size limit and a whole tree is still easier to review when it is small and text-only.
- Prefer Markdown, YAML, and scripts. Host a large binary asset elsewhere and reference it.
- Move long-form detail into
references/files the agent reads on demand rather than intoSKILL.md. - If a tree is too large to reason about, split the skill itself into two skills. Do not split one tree across several
complete_skill_uploadcalls — each call replaces the entire draft, so a partial call deletes everything it omits.
Hard rules
- Omitting
skill_idcreates; passing it replaces. Never create against a name that already exists — the slug retry makes the duplicate succeed instead of failing. - Every write replaces the entire draft. Files absent from the local directory are deleted from the skill. Always send the complete tree.
- Upload every prepared file before calling
complete_skill_upload, and pass every path back with itsupload_id. - Never inline file bytes or base64 into a tool argument, and never read an upload or download URL's bytes into context. Stream to and from disk.
- Publishing does not move existing preset bindings. Re-write the preset's
skillslist to pick up a new version. - Reject symlinked files and directories, and never send content from outside the selected skill root. The helper enforces this; do not work around it.
- The root file must be exactly
SKILL.md, at the top level, and valid UTF-8. The server rejects a file set without it. - If the client cannot execute local scripts, say so and give the user the helper command to run. Do not substitute model-generated digests.
References
- Read references/upload-contract.md for the literal request and response shapes and what this path does and does not protect.
- Read references/troubleshooting.md when an upload, publish, or preset re-bind does not land.