Skip to main content

Agent Skills -- Claude

Take a look at hydrotrek-ssc-suite/openswmm.mcp/src/openswmm_mcp/skills implementation. Is this the gold standard way of implementing agent skills? Are there any alternative skill designs that could have been used in the same environment?

We are implementing our own MCP server under hydrotrek-dwd-suite/dwd-mcp-fastmcp that will serve as a kind of parallel to the openswmm_mcp implementation. The use cases are similar since our DWD stack is built on the EPANET engine which is very similar to the SWMM engine. In our implementation, however, we are API based with cross-network API calls instead of in-memory and cross-process operations that you would see in the openswmm_mcp design.

For our Fast MCP server, would our skills be designed the same way openswmm_mcp designs theirs, or is there a better design fit for Fast MCP?

The short answer: the openswmm_mcp skills are a good, idiomatic version of the common workaround. They are not a gold standard, because there isn't one yet: MCP has no "skill" primitive. For your DWD server, keep the same file format but deliver it differently. Your production agent is the MAF agent in dwd-api-fastapi, and it talks to MCP through tools, so DWD skills should be exposed as tools, not mainly as resources and prompts.

What openswmm_mcp does​

  • File format: each skill is a folder containing SKILL.md (frontmatter name and description, then a workflow body), with optional references/ and scripts/ folders. This is Anthropic's Agent Skills format, which is the closest thing to a standard.
  • Discovery: skills/__init__.py scans */SKILL.md once at import time.
  • Exposure: a resource index (swmm://skills), one resource per skill (swmm://skills/{name}), and a use_skill prompt.
  • Nudging: the server's instructions tell the model to check the index before improvising, and route by intent to capacity-assessment, calibrate-model and operational-optimization.
  • Claude Code / Cowork: docs/developer/SKILLS.md shows the same folder bundled as a plugin, so those clients auto-discover the skills natively.

What it does well:

  • One source of truth that ships with the server.
  • The description fields are strong triggers, with "use when" and "do NOT use when" keywords.
  • Each workflow step names a specific tool and has a "→ verify" checkpoint.
  • Guardrails are stated explicitly.

Where it falls short​

  1. Reference files can't be reached over MCP. The operational-optimization skill tells the model to use references/market_config.schema.json, control_loop.py and the dashboard template, but only the SKILL.md body is served:
@skills_mcp.resource("swmm://skills/{skill_name}")
async def get_skill(skill_name: str) -> str:
"""Return the full ``SKILL.md`` body for *skill_name*."""
skill = _SKILLS.get(skill_name)
if skill is None:
available = ", ".join(_SKILLS) or "(none)"
raise ToolError(f"Unknown skill '{skill_name}'. Available: {available}")
return skill["body"]

Only the Claude Code plugin path can open those files. An MCP-only client cannot load the third tier of the skill (its reference files).

  1. Resources and prompts don't belong to the model. In MCP, the client application decides when resources are read, and the user chooses prompts. Many clients never give the model a way to "read swmm://skills". The server instructions ask the model to do something it may not be able to do. Tools are the only primitive the model reliably calls on its own.

  2. Nothing catches drift. No test in openswmm.mcp/tests covers skills. A skill can name a tool that has been renamed or removed, and nothing fails.

  3. Some steps assume a local, stateful engine:

    • stepping the solver with lifecycle_stride;
    • hot-start cloning to branch experiments;
    • running control_loop.py;
    • writing output files to a folder the user can browse.

    That fits an in-process engine. It doesn't carry over to a remote API.

Other ways skills could have been designed​

DesignHow it worksBest fit
Skills as toolslist_skills(), load_skill(name), read_skill_file(name, path)Any client where the model should decide when to load a skill
Resources and promptsThe current openswmm designClients with a resource picker or prompt (slash-command) UI
Eager indexPut each skill's name and one-line description in the system prompt; load the body on demandCheap, reliable triggering
Composite or "macro" toolsWrite the workflow as code behind one tool or API endpoint, e.g. run_fireflow_assessment(...)Deterministic, fixed sequences
Sub-agent per skillA specialist agent with its own instructions and a smaller tool set, reached by handoffLong, complex workflows such as calibration
Tool scoping by tagFastMCP tags, so a skill exposes only the tools it needsLarge tool sets; less useful on a stateless server
Client-side skillsClaude Code plugins, Cursor skillsIDE use only; never reaches a production agent

Recent FastMCP 3.x releases may include a built-in skills provider. Your DWD server requires fastmcp>=3.3.0, so check the docs for that version before writing your own loader.

What I'd recommend for dwd-mcp-fastmcp​

The two environments differ in ways that change the design:

  • Client: in DWD, the model uses MCPStreamableHTTPTool inside MAF, and its system prompt comes from the composed playbooks in app/agent/instructions/. As far as I know, MAF passes MCP tools (and possibly prompts) to the model, but not resources. A resource-only skill design would probably be invisible to your agent.
  • Statelessness: main.py runs stateless_http=True under 4 Gunicorn workers. Skills can't rely on a "default session", so every step must pass twin_id and scenario_id explicitly. Loading skills statically at import time works fine across workers.
  • Runs are queued: run_scenario returns 202 when accepted. Skills should describe "enqueue, poll status, fetch results", not stepping a solver.
  • No code sandbox: a gpt-4o-mini MAF agent can't run a control_loop.py. Heavy logic should live in the API or worker, and the skill should orchestrate it.

Concretely:

  1. Keep the SKILL.md folder format under dwd-mcp-fastmcp/skills/<name>/SKILL.md, next to tools.py, so skills are versioned with the tool names they reference. Add skills/ to compress.py so it ships to Azure.
  2. Expose skills as tools first: list_skills() returns names and descriptions, load_skill(name) returns the body, and read_skill_reference(name, path) returns a reference file (restricted to that skill's folder). Add resources and a prompt as well, for other MCP clients.
  3. Put the index in the system prompt. Add a short playbook such as 05_skills.md, or generate the index at FastAPI startup, saying that substantive workflows (fireflow assessment, leakage investigation, calibration against SCADA) should call load_skill first. This keeps 00_core.md thin, as your dev guide requires.
  4. Push deterministic steps into tools. For example, "clone the scenario, set the SCADA window, run, wait, summarize" is better as one API workflow plus a thin tool than as eight steps the model has to get right. The skill then covers judgement: which parameters, which acceptance criteria, how to present trade-offs. This follows your guide's layer order: enforce in the API first, then tool shape, and only then instructions.
  5. Skills vs playbooks: playbooks stay always-loaded and short (identity, presentation rules, tool etiquette). Skills are lazy-loaded, multi-step domain workflows. That split stops the composed instructions from growing with every new workflow.
  6. Add a drift test that parses each SKILL.md and checks every backticked tool name against the tools registered in register_tools. openswmm_mcp would benefit from the same test.

In short: copy openswmm's file format and description style, but not its delivery. Delivering skills through tools fits FastMCP behind MAF, and API-driven orchestration fits a remote, stateless EPANET stack.