Build a tool/MCP skill
This guide builds a complete skill that drafts text with the model and then writes it to a file —
end to end, with zero MCP setup. It uses the built-in skiller_createFile
tool, so it runs out of the box. The second half shows how to swap in a real MCP tool you have
configured in VS Code.
By the end you will have a working note-writer skill and understand the two ways skills use tools:
- a
toolstep that invokes one tool directly with templated parameters, and - an agentic
llmstep that lets the model decide whether and when to call tools.
What you are building
Section titled “What you are building”A two-step skill:
- An
llmstep asks the model to draft a short note and return it as JSON. - A
toolstep writes that draft to a file withskiller_createFile.
The tool step never talks to the model — it calls one tool with the parameters you give it. That is
the key difference from an agentic llm step, which you will meet below.
Step 1 — Create the skill folder
Section titled “Step 1 — Create the skill folder”Skills are folders discovered under .skiller/skills/ in your workspace. Create one:
.skiller/skills/note-writer/├── skill.yaml└── steps/ └── 01-draft.mdThe folder name (note-writer) becomes the skill’s default id.
Step 2 — Write the draft prompt
Section titled “Step 2 — Write the draft prompt”Create steps/01-draft.md. Ask the model to return JSON so the next step can read individual fields:
---id: draftdescription: Draft a short note as JSON---Write a short note about {{ inputs.topic }}.
Reply with ONLY a JSON object, no prose and no code fence:{ "title": "<a short title>", "text": "<two or three sentences>" }When an llm reply is valid JSON, Skiller parses it into an object, so {{ outputs.draft.title }} and
{{ outputs.draft.text }} resolve to the fields above. If the reply is not valid JSON it is stored
as a raw string — and then {{ outputs.draft.text }} renders empty instead of erroring. That is
the JSON-fallback trap; Debug a skill explains how to spot it.
Step 3 — Write the manifest
Section titled “Step 3 — Write the manifest”Create skill.yaml. It collects a topic, drafts the note, then writes it. The tools.aliases map is
the bridge between a friendly name you use in steps (create_file) and the real tool
(skiller_createFile):
name: Note Writerdescription: Draft a short note and write it to a fileversion: "1.0.0"
inputs: - name: topic type: string required: true prompt: "What should the note be about?"
tools: aliases: create_file: skiller_createFile # alias -> built-in LM tool
steps: - id: draft type: llm file: steps/01-draft.md output: draft
- id: save type: tool tool: create_file params: filePath: "notes.md" content: "{{ outputs.draft.text }}" output: saved
on_error: abort
output: summary: "Saved your note to notes.md."The save step calls the create_file alias and passes two params, both
Liquid-interpolated. filePath is relative, so the file lands in your workspace root. Because
skiller_createFile is workspace-confined, a path that escapes the workspace is rejected unless you
opt in via skiller.skills.allowOutsideWorkspaceWrites.
A tool step takes only tool and params. It must not carry tools or tool_mode — those
belong to agentic llm steps.
Step 4 — Run it
Section titled “Step 4 — Run it”Reload so Skiller picks up the new folder, then launch the skill:
@skiller /reload@skiller /skill note-writer topic="our release process"The draft step calls the model; the save step writes notes.md to your workspace and reports the
resolved path. Open notes.md to confirm the text is there. The whole run animates in the
live execution graph as each step fires.
Call a real MCP tool
Section titled “Call a real MCP tool”The skill above needs no MCP server. To use a tool from an MCP server you have configured in VS Code,
alias it the same way — then let an agentic llm step decide when to call it. Confirm the tool is
visible to Skiller first:
@skiller /toolsThat lists every tool Skiller can see, including ones from your MCP servers. Use the exact name it shows as the alias target. Here the model researches a topic and may call a search tool while it works:
tools: aliases: search: my_mcp_searchTool # a real MCP tool you configured in VS Codesteps: - id: research type: llm file: steps/01-research.md tools: [search] tool_mode: auto # "auto" (model decides) | "required" (must call a tool; needs non-empty tools) output: findingsHere the tools list on the step says which aliases the model is allowed to call during that step.
The model runs an agentic loop: it may call search, read the result, call it again, and keep going
until it produces a final reply — stored under output: findings.
tool_mode: auto vs required
Section titled “tool_mode: auto vs required”auto— the model decides whether to call a tool. It might answer directly or callsearchfirst. Use this for “help if you need it” steps.required— the model must call at least one tool before it can finish. Use this when a step is only meaningful with fresh tool data.requiredneeds a non-emptytoolslist, or the manifest is rejected.
Optional tools with ?
Section titled “Optional tools with ?”A skill that aliases an MCP tool stops being self-contained: if the server is not configured on someone
else’s machine, the alias resolves to nothing. Append ? to a tool name to mark it optional:
tools: aliases: search: my_mcp_searchTool? # optional — trailing "?"When an optional tool is missing, the step that depends on it is skipped rather than failing the
whole skill — and the skip shows in the execution graph. Without the ?, a missing tool is an error.
This lets you ship a skill that enhances itself with an MCP tool when present but still runs without
it.
Related
Section titled “Related”- Reference: Built-in tools —
skiller_createFile/skiller_replaceInFileparameters and workspace confinement. - Reference: skill.yaml manifest — the
toolstep,tools.aliases, andtool_modeschema. - Concept: Step types —
llm,confirmation, andtool, and howoutputscarry state between them.