Curated workflow authoring standard
Every new intent-level workflow (client diagnosis, site/device health, WLAN changes, NAC, firmware, compliance, alerts, incident response) MUST follow this standard. The docstring is the tool’s UI — the LLM selects and calls tools from it alone.
Reference implementation for every pattern below: list_clients and the alert-action helpers in src/hpe_networking_mcp/mcp_servers/monitoring.py.
1. Annotations
Declare capability via the shared enums, never ad-hoc: READ_ONLY, WRITE, IDEMPOTENT_WRITE, DESTRUCTIVE (mcp_servers/shared.py). Reads use READ_ONLY; anything mutating uses a write annotation AND the write gate (§5). Annotation and actual behavior must agree — mislabeled writes are the bug class this exists to prevent.
2. Docstring contract
First line: imperative intent plus the selection cue an LLM needs (“List connected clients. ALWAYS filter — unfiltered returns all clients.”). Then, in order:
- Parameters — name server-side filters vs client-side substring filters explicitly, and say which to prefer for natural-language queries.
- Enums and synonyms — list valid values verbatim (“severity: CRITICAL/MAJOR/MINOR”). If the API’s vocabulary differs from user vocabulary, document the accepted synonyms and coerce (§3).
- Pagination contract — state cursor vs offset behavior in the docstring, verbatim, including which response field carries the cursor (
_pagination.next_cursor). - Limits and defaults — state the default and the hard cap.
Never document a behavior the code does not implement; the repo runs docs-consistency tests and this standard extends that expectation to docstrings.
3. Parameter coercion
- Clamp every
limitthroughclamp_limit— never trust the caller’s number. - Accept natural-language input and coerce: enum-synonym maps (see
_REBOOT_REASON_MAPfor the translation-map pattern), case-insensitive substring matching, MAC/IP ambiguity resolution (find_client). - Central’s v1 and v1alpha1 responses use different field names for the same datum. Client-side filters MUST check the tuple of known field names, not a single key (see
_matchinlist_clients). - Normalize scope identifiers via
pipeline.scope_ids.normalize_scope_id; reject invalid scopes with aValueErrorthat names the field.
4. Pagination and bounded output
- Prefer
next_cursor; accept legacyoffsetand translate it to an approximate starting cursor, documenting that translation is approximate. - Wrap collections in
maybe_bound/bound_collection_response; when the wrapper returns the bounded form, populate_pagination.offsetand_pagination.next_cursorfrom the client’s returned cursor. - Item and byte budgets are enforced by the shared bounding path — a curated tool must not bypass it by returning raw API payloads.
5. Writes (until the transactional model lands)
- Gate registration/execution with
enforce_platform_write(platform, tool); a blocked write returns the structured blocked payload, never an exception. - Destructive actions elicit confirmation via
ctx.elicitwith an explicit schema. On unsupported elicitation return{"status": "CONFIRMATION_UNAVAILABLE", ...}and DO NOT perform the operation; on decline return{"status": "CANCELLED", ...}(pattern:_confirm_alert_action). - Every write response includes
endpoint_used; HTTP errors go throughcompact_http_error(response, endpoint)— never return raw response text (credential-redaction rules live in the shared path). - Validate mutation results with
validate_write_result/WriteResultError.
6. Errors
Return structured error payloads ({"error": ..., "endpoint_used": ...}), not exceptions, for expected API failures. Exception text leaving the process is redacted by the shared middleware — do not reconstruct raw vendor errors in tool responses.
7. Required tests per workflow
- Coercion: synonyms, case variants, ambiguous identifiers, invalid scope.
- Pagination: cursor forward, offset translation, empty final page.
- Bounded output: oversized collection returns the bounded envelope with
_pagination, never the full payload. - Write gate (writes only): gate unset → structured blocked payload, zero HTTP calls (assert on mocked transport).
- Confirmation (destructive only): elicitation unavailable →
CONFIRMATION_UNAVAILABLEand no mutating call; decline →CANCELLED. - Dry-run (once the transactional model lands):
dry_run=trueperforms no mutating HTTP call.
8. Module header
Each mcp_servers/*.py module keeps its first docstring current: tool count, coverage summary, and module-wide pagination notes (see monitoring.py’s header). Tool counts in docs are CI-checked via docs/project-facts.json — regenerate it, never hand-edit.