Recipe: Skills Setup¶
Goal¶
Create a skills folder, write a skill with a reference file and a script, check that the skills server reads it cleanly, and attach the server so the agent knows the skill exists.
Prerequisites¶
- std-tools with no extra: PyYAML is a base dependency. The skill below also uses Documents tools,
which need the
docsextra. - For the agent step, doc-processing-toolkit. Toolkit 0.5.2 and earlier need the pre-built session shown in step 5.
Steps¶
1. Choose the skills folder¶
The server reads DPTTOOLS_SKILLS_DIR, by default ./.agent_skills relative to the server's
working directory. Set it to an absolute path so it doesn't depend on where the server starts:
DPTTOOLS_SKILLS_DIR=/workspace/.agent_skills
In a container, mount the folder read-only unless the agent should create skills (Deployment).
2. Write the skill¶
Each skill is a folder whose name is the skill's name: 1–64 lowercase letters, digits and single
hyphens. It holds a SKILL.md, plus any files the instructions refer to:
.agent_skills/
invoice-extraction/
SKILL.md
references/
fields.md
scripts/
check_totals.py
SKILL.md starts with YAML frontmatter. The description is what the agent sees before it loads
the skill, so say what the skill does and when to use it:
---
name: invoice-extraction
description: Extracts the header fields and line items of a supplier invoice into JSON. Use when asked to read, check or summarise an invoice.
metadata:
owner: finance-team
---
# Invoice extraction
1. Run `get_file_overview` on the invoice. If it has no text, use `ocr_pdf`.
2. Read the field names in `references/fields.md`.
3. Put the line items in a JSON file and run `scripts/check_totals.py <file>`.
4. Report the fields as JSON, and say whether the totals add up.
scripts/check_totals.py is ordinary Python. It runs with the server's interpreter, in the
script's own folder:
import json, sys
data = json.load(open(sys.argv[1], encoding="utf-8"))
lines = sum(l["amount"] for l in data["lines"])
print(json.dumps({"lines_total": lines, "stated_total": data["total"],
"matches": abs(lines - data["total"]) < 0.005}))
An agent can also start a skill itself: create_skill(skill_name="invoice-extraction",
description="...") writes a template SKILL.md with the frontmatter and a TODO body, which the
agent then fills in with a file tool.
3. Check what the server sees¶
Call list_skills once, by hand or from a test, before giving the folder to an agent:
{
"skills": [
{"name": "draft-notes", "description": "Notes: things to remember",
"path": "/workspace/.agent_skills/draft-notes/SKILL.md",
"directory": "/workspace/.agent_skills/draft-notes"},
{"name": "invoice-extraction",
"description": "Extracts the header fields and line items of a supplier invoice into JSON. Use when asked to read, check or summarise an invoice.",
"path": "/workspace/.agent_skills/invoice-extraction/SKILL.md",
"directory": "/workspace/.agent_skills/invoice-extraction",
"metadata": {"owner": "finance-team"}}
],
"skills_dir": "/workspace/.agent_skills",
"count": 2,
"diagnostics": [
{"skill": "draft-notes", "level": "warning", "code": "yaml_lenient_retry",
"message": "description value contained unquoted colons; lenient quoting applied"}
]
}
diagnostics lists what was wrong. A warning still loads the skill, as here, where a colon in an
unquoted description was quoted for you. An error, such as a missing description, skips the
skill. The toolkit logs diagnostics and never shows them to the model, so fix them at this step.
The skills server page lists every rule.
4. Walk through the skill as the agent will¶
load_skill(skill_name="invoice-extraction")
<skill_content name="invoice-extraction" directory="/workspace/.agent_skills/invoice-extraction">
---
name: invoice-extraction
...
4. Report the fields as JSON, and say whether the totals add up.
<skill_resources>
<file>references/fields.md</file>
<file>scripts/check_totals.py</file>
</skill_resources>
Skill directory: /workspace/.agent_skills/invoice-extraction
Relative paths in this skill resolve against the skill directory.
</skill_content>
load_skill_resource(skill_name="invoice-extraction", resource_path="references/fields.md")
execute_skill_script(skill_name="invoice-extraction", script_path="scripts/check_totals.py",
args="/workspace/invoices/inv-0042.json")
{"stdout": "{\"lines_total\": 150.5, \"stated_total\": 150.5, \"matches\": true}\n", "stderr": "", "exit_code": 0}
args is split with POSIX shell rules, so quote a path that contains spaces. A script that runs
longer than DPTTOOLS_SANDBOX_TIMEOUT (30 seconds) returns exit_code: -1.
5. Attach the server to an agent¶
The toolkit reads the skills server automatically: it calls list_skills and puts each skill's
name, description and path into the system prompt
(Skills auto-injection). With toolkit
0.5.2 and earlier this works only through a session you open yourself:
import os
import sys
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import get_default_environment, stdio_client
from doc_processing_toolkit import Agent
os.environ["DPTTOOLS_SKILLS_DIR"] = "/workspace/.agent_skills" # the toolkit checks it too
env = get_default_environment()
env["DPTTOOLS_SKILLS_DIR"] = os.environ["DPTTOOLS_SKILLS_DIR"]
env["LLMWHISPERER_API_KEY"] = os.environ.get("LLMWHISPERER_API_KEY", "") # the skill may OCR
params = StdioServerParameters(
command=sys.executable, args=["-m", "doc_processing_std_tools"], env=env
)
async with stdio_client(params) as (read, write), ClientSession(read, write) as session:
await session.initialize()
agent = Agent(
model="claude-sonnet-5",
tools=[{"session": session, "name": "std-tools-skills"}],
)
result = await agent.run("Check the totals on invoices/inv-0042.pdf.")
- The session's
namecontainsskill, so the toolkit treats it as a skills server. - The server gets
DPTTOOLS_SKILLS_DIRinenv, and your application has it set too, pointing at a folder that exists. - The session runs the composed launcher, so the agent has the tools the skill calls for
(
get_file_overview,ocr_pdf, a file tool to write the JSON) as well as the skills tools. To give an agent only the skills tools, run-m doc_processing_std_tools.skillsinstead.
Agent(tools=[skills_server]) and mcp_command configs return an empty catalog with toolkit 0.5.2
and earlier, and the agent never hears of the skills
(Compatibility).
What to validate¶
list_skillsreturns every skill with an absolutepath, anddiagnosticsonly for what was wrong.load_skillreturns theSKILL.mdwith a<skill_resources>list of the skill's files.load_skill_resourceandexecute_skill_scriptwork with paths relative to the skill folder, and a path that leaves the skills folder is refused.- A skill name that is not lowercase letters, digits and hyphens is refused.
- Through the pre-built session, the toolkit finds the skills (the agent's system prompt lists them).
Test sources¶
tests/unit/test_skills.py: frontmatter parsing and diagnostics (TestParseFrontmatter,TestBuildSkillRecord), names (TestValidateSkillName), and each tool.tests/integration/test_skills_mcp.py: create, list and load through the server.tests/integration/test_toolkit_compat.py:test_skills_catalog_through_a_prebuilt_stdio_sessionpasses with the toolkit installed.test_skills_catalog_in_processandtest_skills_catalog_through_the_toolkit_command_configare expected to fail on toolkit 0.5.2.
Related pages¶
- Skills server: the tools, the payload and the frontmatter rules.
- Agent Skills specification.