Skip to content

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 docs extra.
  • 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 name contains skill, so the toolkit treats it as a skills server.
  • The server gets DPTTOOLS_SKILLS_DIR in env, 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.skills instead.

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_skills returns every skill with an absolute path, and diagnostics only for what was wrong.
  • load_skill returns the SKILL.md with a <skill_resources> list of the skill's files.
  • load_skill_resource and execute_skill_script work 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_session passes with the toolkit installed. test_skills_catalog_in_process and test_skills_catalog_through_the_toolkit_command_config are expected to fail on toolkit 0.5.2.