Skills Auto-Injection¶
Skills auto-injection automatically detects a skills MCP server in the agent's tools, retrieves skill metadata via list_skills, and injects a skills catalog into the system prompt — giving the LLM awareness of available skills without requiring an explicit tool call.
How it works¶
When auto_inject_skills=True (the default), the agent performs the following on every run() call:
- Detection: Checks whether one server exposes both
list_skillsandload_skill(see below). - Fetch: Calls
list_skillson the detected MCP session to retrieve skill names and descriptions. - Injection: Formats the metadata and appends it to the system prompt.
Because detection and fetching happen on every run(), skills added mid-session are picked up automatically.
Detection logic¶
A server is a skills server when it exposes both the list_skills and load_skill tools. Nothing
else is required: the server's name is not consulted. (Until 0.5.3 it also had to contain skill,
which missed std-tools' composed server, doc-processing-std-tools, and any pre-built session named
otherwise, such as one called std_tools.) The two names are std-tools' own; a server that exposes
both for another purpose is treated as a skills server — one list_skills call per run, and when its
reply is not the std-tools JSON, one WARNING per run and its tools left alone (set
auto_inject_skills=False for such an agent). The check is per server, so two servers exposing one
name each do not combine, and servers are told apart by session, not by name, so two servers that
share a name (every unnamed pre-built session is mcp-session) are not merged — with one exception
that predates this release: two in-process FastMCP objects given the same name already share one
session in the loader, so give in-process servers distinct names. When more than one server
qualifies, the first in the toolkit's loaded order wins — servers whose tools are known at
construction (in-process FastMCP objects with a tool registry) before servers that publish them at
connect time (mcp_command, mcp_script, pre-built sessions, and a standalone fastmcp>=3 object,
which lists its tools at connect), each group in tools order — and the fetch and the
activation-tool filter below act on that server and no other. Attach one skills server.
Detection checks raw tools (FastMCP server objects) and loaded tools (from ToolLoader), and runs again
once the MCP servers are connected, so a server that only publishes its tools at connect time
(mcp_command, mcp_script, a pre-built session) is found on the first run().
Note
The skills tools (list_skills, load_skill) can be provided via any supported tool format — MCP server, local @tool decorated functions, FastMCP objects, etc. The toolkit's ToolLoader wraps local functions in a LocalFunctionSession with MCP-compatible interfaces, so detection works uniformly regardless of origin.
list_skills return format¶
list_skills is called with no argument: the server reads DPTTOOLS_SKILLS_DIR from its own
environment (see Skills directory). It must return JSON in this format:
{
"skills": [
{
"name": "my-data-analysis",
"description": "Analyzes CSV/Excel data and produces summary reports with charts.",
"path": "/path/to/.agent_skills/my-data-analysis/SKILL.md"
},
{
"name": "code-review",
"description": "Reviews code for bugs, security issues, and style violations.",
"path": "/path/to/.agent_skills/code-review/SKILL.md"
}
],
"skills_dir": "/path/to/.agent_skills",
"count": 2,
"diagnostics": []
}
The agent extracts the skills array and injects each skill's name, description, and path (when present) into the system prompt as <skill> entries. The path field gives the model an absolute SKILL.md location so it can resolve relative resource references inside the skill.
Which doc-processing-std-tools this is written against
Skills auto-injection is written against std-tools 0.2+, the release that added the
skills[].path field and the top-level diagnostics array it reads, and is exercised against
0.6.0, the current release (its documentation is at
https://doc-processing-std-tools.vercel.app/; which toolkit each std-tools release was verified
against is on its compatibility page). An older payload still works — both fields are read
defensively and simply omitted when missing. Two tests exercise the pair: std-tools'
tests/integration/test_toolkit_compat.py installs this toolkit and drives the in-process,
mcp_command and pre-built-session routes, and this repository's
tests/unit/test_skills_auto_injection_routes.py drives the same routes against a stand-in server
with std-tools' contract. Until 0.5.3 only the pre-built-session route worked: the toolkit passed
list_skills an argument it does not take, read the in-process result as an object, required
DPTTOOLS_SKILLS_DIR to name a directory on the agent's own machine, and required the server's
name to contain skill.
Older list_skills payloads that omit path continue to work (the attribute is simply elided from the rendered <skill> tag).
Diagnostics¶
If the std-tools skills server reports non-fatal parse warnings via a top-level diagnostics array, the toolkit emits one logger.warning("skills: ...") per entry. Diagnostics intentionally never enter the system prompt — they are an operator/dev signal, not LLM context.
System prompt injection¶
Skills metadata is appended to the system prompt in this format:
## Available Agent Skills
The following skills provide specialized instructions for specific tasks. When a
task matches a skill's description, load its full instructions before proceeding.
How to load a skill:
- Preferred: call the `load_skill` tool with the skill's name. The tool result
is wrapped in <skill_content>...</skill_content> tags and includes a list of
bundled resources (under <skill_resources>) and the skill's directory path.
Use the directory path to resolve any relative references inside the skill.
- Alternative: if you have a file-read tool, you may directly read the SKILL.md
at the listed `path`. Relative references inside the SKILL.md resolve
against the parent directory of that path.
<available_skills>
<skill name="my-data-analysis" path="/path/to/.agent_skills/my-data-analysis/SKILL.md">Analyzes CSV/Excel data and produces summary reports with charts.</skill>
<skill name="code-review" path="/path/to/.agent_skills/code-review/SKILL.md">Reviews code for bugs, security issues, and style violations.</skill>
</available_skills>
The LLM sees this catalog and can call load_skill to load full instructions for any skill it wants to use. Attribute values and the description body are XML-escaped defensively (via xml.sax.saxutils) so a stray <, &, or " in a skill description cannot break the structure the model parses.
Activation tools when no skills are configured¶
When list_skills returns an empty skills array, the toolkit hides three "activation" tools that std-tools advertises — load_skill, load_skill_resource, and execute_skill_script — from the LLM-visible tool list to save tokens.
Two tools intentionally stay advertised so the model can bootstrap from an empty state:
list_skills— so the model can re-poll on a later turn if a skill appears mid-session.create_skill— so the model can create the first skill in the directory itself. Without this, the suppression would lock the agent out of ever populating an empty skills directory.
The check runs on every run(), so adding a skill mid-session re-advertises the three suppressed tools immediately on the next call. A failed or malformed list_skills reply is not an empty catalog: since 0.5.3 the tools stay advertised and one WARNING names the server (before, a failure hid them like an empty catalog).
Configuration¶
| Parameter | Type | Default | Source |
|---|---|---|---|
auto_inject_skills |
bool |
True |
Agent.__init__, AgentBuilder, YAML config |
DPTTOOLS_SKILLS_DIR |
str |
the server's own default (./.agent_skills) |
the skills server's environment — see Skills directory and the std-tools configuration page |
Enabling/disabling¶
agent = Agent(model="claude-sonnet-5", auto_inject_skills=False)
from doc_processing_toolkit import AgentBuilder
# Direct instantiation
agent = AgentBuilder().auto_inject_skills(False).build()
# Or via Agent.builder()
# agent = Agent.builder().auto_inject_skills(False).build()
model: claude-sonnet-5
auto_inject_skills: false
Skills directory¶
The DPTTOOLS_SKILLS_DIR environment variable tells the skills server where to look for skill
definitions, and it is read by the server, in the server's own process — the toolkit does not read
it (since 0.5.3; see DPTTOOLS_SKILLS_DIR and, on the
std-tools side, configuration and servers/skills). Where to set it
depends on the route:
- In-process server (a FastMCP object in
tools=[...]): the server shares this process, so set it in this process's environment — an.envfile loaded withdotenvworks. -
mcp_command/mcp_script: a subprocess does not inherit this process's environment, so pass it in the config'senv(see Tools):tools=[{ "type": "mcp_command", "command": sys.executable, "args": ["-m", "doc_processing_std_tools"], "name": "std-tools", "env": {"DPTTOOLS_SKILLS_DIR": "/srv/skills"}, }] -
A pre-built session: set it where that server was started (its
StdioServerParameters(env=...), a sandbox's environment, ...).
When the variable is unset, or names a directory that does not exist, std-tools returns an empty catalog
with a note; the toolkit then hides the activation tools as described above and logs nothing above
INFO.
No-op conditions¶
Skills injection silently does nothing (no error) when:
auto_inject_skills=False- no server exposes both
list_skillsandload_skill - the instructions already contain an
<available_skills>block — the app built its own catalog, so for that agent the toolkit calls nothing, injects nothing and hides nothing (one INFO line per run; since 0.5.3) list_skillsreturns an empty skills array (also what std-tools returns when its directory is unset, missing or empty)
Failure handling¶
If the list_skills call fails (timeout, error, exception) or returns something other than the JSON above, the agent logs one WARNING naming the server and continues without skills injection; the activation tools stay advertised (since 0.5.3). This ensures a broken skills server never prevents the agent from running.
Anthropic native skills coexistence¶
If you provide Anthropic native skills via provider_kwargs and enable auto_inject_skills=True, both mechanisms coexist independently. No conflict resolution is performed.