archive/2026-07-25/AAA/skills/ARCHIVE/mcp-builder-doctrine/SKILL.md
Canonical MCP server-builder doctrine — naming laws, capability declaration discipline, _meta envelope contract, lifecycle notification rules, error envelope contract, and
npx skillsauth add ariffazil/openclaw-workspace mcp-builder-doctrineInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
The LLM discovers and invokes tools/resources/prompts via the model + the host. Your server's
nameanddescriptionfields are not documentation. They are the interface contract to agentic intelligence. Bad naming causes silent misfires — the model picks the wrong tool, fills wrong arguments, skips a tool entirely. No error is thrown. The system just produces wrong outputs.
DITEMPA BUKAN DIBERI — Naming is the first act of creation.
| Primitive | Decided by | Surfaced via | GEOX example |
|---|---|---|---|
| Tools | Model (LLM) | tools/call | geox_basin(mode='macrostrat') |
| Resources | Host (Cursor/app) | resources/read after host injects | geox://literature/sabah/madon-2021 |
| Prompts | User | /slash_name | /analyse-well-log |
Critical split: Resources are application-driven — server exposes, host decides context inclusion, server only hints via annotations. DO NOT rely on resources being model-callable. Use TOOLS for that.
| # | Law | Consequence of violation |
|---|---|---|
| 1 | name is a machine contract — never rename after publish | every agent, cached plan, every reference breaks |
| 2 | description is the model's only briefing | vague = misfire, hallucinated args, tool skipped silently. Must be 50-300 chars. Must include "Use when..." sentence. |
| 3 | title and name must diverge deliberately | human readable + machine stable |
| 4 | Argument names are semantic, not syntactic | latitude: float, longitude: float — never p1, p2 |
| 5 | Prompt names are user vocabulary | /analyse-well-log matches how a geologist thinks |
{
"name": "query_macrostrat_columns", // machine, snake_case, stable
"title": "Macrostrat Geological Columns", // human, readable, can change
"description": "Query Macrostrat for columnar stratigraphy at lat/lon — returns columns JSON with units, lithology, age. Use when the user asks about stratigraphic columns at a location."
}
| Field | Role in agentic intelligence |
|---|---|
| name | Unique identifier — the model calls this exactly |
| title | Human display only — model ignores |
| description | The model's decision surface — it reads this to decide whether + how to call |
| inputSchema.properties | Per-parameter intent — model fills arguments from description |
| Field | Role |
|---|---|
| name | Machine identifier — used in URI resolution |
| title | Host UI display |
| description | Host uses this to decide whether to inject into context |
| uri | The address — must be unambiguous, no collisions |
| annotations.audience | ["assistant"] for papers; ["user"] for index/README |
| annotations.priority | 0.7-0.9 papers, 1.0 LAS, 0.5 index |
| annotations.lastModified | ISO 8601 — clients sort by recency |
| mimeType | text/plain preferred for agents; application/pdf only for small blobs |
| size | Include byte count — clients budget context |
| Field | Role |
|---|---|
| name | Unique identifier — user types /this-exact-name |
| title | Slash command menu label |
| description | Tells user what the prompt does before trigger |
| arguments[].description | Drives autocomplete via completion API |
/mcp initialize response declares everything the client may use:
{
"capabilities": {
"tools": { "listChanged": true },
"resources": { "subscribe": false, "listChanged": true },
"prompts": { "listChanged": true },
"logging": {},
"completions": {}
}
}
Hard rules:
subscribe, omit it — don't declare false and silently fail.completions, declare {} to signal API presence — clients may try.| Error type | Mechanism | When to use |
|---|---|---|
| Protocol error | JSON-RPC error field | unknown tool, invalid args, server crash |
| Execution error | isError: true in result | API timeout, bad response, business logic |
Live API failures (isError: true) NEVER throw protocol errors. The Macrostrat tool returns isError: true for API down — not JSON-RPC error.
| Decision | Use |
|---|---|
| Live API call (Macrostrat, USGS, etc.) | Tool with strict inputSchema |
| Cached snapshot of API response | Resource, mimeType application/json |
| Schema/citation metadata only | Resource (index) |
| Tool returns URIs + content | Tool result with embedded type: "resource" |
Hybrid pattern: tool returns content: [{type: "resource", resource: {uri, ...}}] to bridge cache + freshness.
_meta Envelope — Shape A (per MCP 2025-11-25)The _meta extension lives on the contents object, not the response envelope. Carries seal_id, evidence_class, authority, sha256 — survives client-side caching.
{
"contents": [{
"uri": "geox://literature/sabah/madon-2021",
"mimeType": "text/markdown",
"text": "...",
"_meta": {
"seal_id": "VAULT999-2026-0731-A7",
"evidence_class": "DERIVED",
"authority": "EVIDENCE",
"sha256": "c01031...",
"contract_version": "geox.resource.v2"
}
}]
}
audit_class enum (F2 TRUTH): OBSERVED / DERIVED / INTERPRETED / SPECULATED.
supportsListTemplates are pure URI shapes for resources/read. Discovery = resources/list (with cursor pagination). Completion = completion/complete for {param} enum.
# Templates are URI SHAPES:
geox://literature/{basin}/{paper_id} # shape for read
geox://wells/{basin}/{well_id}/logs # shape for read
# Discovery always via:
resources/list → cursor-paginated list
resources/templates/list → paginated list of shapes
# Completion for parameter hints:
completion/complete {ref: "geox://literature/{basin}", argument: {name: "basin"}}
→ returns ["sabah", "malay-basin", "sarawak", ...] (cap 100)
New tool → notifications/tools/list_changed (no payload)
New resource → notifications/resources/list_changed (no payload)
New prompt → notifications/prompts/list_changed (no payload)
Resource update → notifications/resources/updated { uri } (per-URI; only if subscribe: true)
Discipline: Never fire for undeclared capabilities. Client MUST send notifications/initialized before server sends anything except pings/logs. Timeouts on every request — use cancellation notifications; never hang.
| Type | audience | priority |
|---|---|---|
| Published papers | ["assistant"] | 0.7–0.9 |
| LAS well data | ["assistant"] | 1.0 |
| README/index | ["user"] | 0.5 |
| Internal analyses | ["user","assistant"] | 0.8 |
| Operator-private wells (F13) | ["assistant"] | 0.95 |
Always include lastModified for recency sort.
| Surface | Mechanism | Authority |
|---|---|---|
| Client fetches directly | https:// URI allowed | Client + server both see bytes |
| Server proxies | geox:// (custom scheme) + URL field | Server-stamped sha256 |
| File-backed (real or virtual) | file:// (no real FS required per spec) | Per organ |
| Version control | git:// built-in | For VCS integration |
For paywalled/internal papers: NEVER fake https:// you'll proxy. Use geox:// + server-side sha256.
cursor = base64url(json.dumps({
"p": page_idx, # 0-indexed
"f": filter_hash, # sha256 of filter args
"s": page_size,
"v": 1 # version
}))
Clients MUST NOT persist cursors across sessions. Server chooses page size dynamically under load; clients handle silently.
{ "code": -32002, "message": "Resource not found", "data": { "uri": "geox://..." } }
JSON-RPC codes for MCP:
-32002 not found-32003 forbidden (F13 gate)-32602 invalid params-32603 internal-32601 method not foundname is machine contract — never change after publishdescription is model's only briefing — vague = silent misfire. Must be 50-300 chars, include "Use when..." sentencetitle and name must diverge deliberately/mcp initialize)subscribe: false declared ONLY if not implemented; omit otherwiseisError: true for execution vs JSON-RPC error for protocol_meta Shape A on contents object (not envelope)supportsListlist_changed notifications have no payloadresources/updated per-URI fires only if subscribe: true negotiatedhttps:// only when client fetches directly — never fake for proxyfile:// no real FS requiredgit:// built-in for VCS integrationdata: {uri})messages[] can embed type: "resource" content blocks — pre-loads context before model reasoning (MCP 2025-06-18 spec)PromptArgument[]; no-default = required, has-default = optionalArgs: section becomes argument description — drives completion API autocompleteError Envelope Contract — every tool MUST return structured errors, never raw tracebacks. Minimum shape:
{
"error_code": "EXTERNAL_API_TIMEOUT",
"message": "Macrostrat API did not respond within 5s",
"retry_allowed": true,
"suggested_action": "Retry with shorter timeout or check Macrostrat status page"
}
Rules:
error_code = UPPER_SNAKE_CASE, domain-prefixed (e.g., GEOX_*, WEALTH_*, FORGE_*)message = human-readable, one sentence, no stack tracesretry_allowed = boolean — model uses this to decide whether to retrysuggested_action = what the model should tell the user OR try nextisError: true in MCP result envelope (per binding 10) wraps this payloaderror field (binding 10)External MCP Ingress — connecting to third-party MCP servers (e.g., DeepWiki, public MCP endpoints) MUST go through an ingress boundary:
forge_fetch SSRF rules (no internal IPs, no metadata endpoints)forge_work/ receipt□ name field present + snake_case + stable across versions?
□ description field rich enough to brief an LLM in isolation? (50-300 chars, includes "Use when..." sentence)
□ title distinct from name (human ≠ machine)?
□ inputSchema properties each described (intent, not just type)?
□ capability declared in /mcp initialize response?
□ subscribe omitted if not implemented (NOT false)?
□ external live API — using TOOL not RESOURCE?
□ _meta lives on contents object (not envelope)?
□ template is URI shape only (no supportsList)?
□ annotations cover audience + priority + lastModified?
□ error codes include data.uri (per JSON-RPC convention)?
□ tool execution errors use structured envelope (error_code, message, retry_allowed, suggested_action)?
□ external MCP connections go through ingress boundary (SSRF + F12 + trust state)?
□ ISO 8601 timestamps (lastModified, read_at_iso, etc.)?
□ F2 evidence label (OBS/DERIVED/INT/SPEC) in every output?
□ F11 audit trail maintained (read ledger append, sha256, actor)?
□ F13 sensitive resource requires actor_signature?
□ cursor opaque + non-traversable + session-scoped?
| Floor | MCP binding |
|---|---|
| F1 AMANAH | reversible-first; backups before edits; irrefutable ops explicit |
| F2 TRUTH | evidence labels in _meta; capability declaration honest |
| F3 WITNESS | tri-witness on SEAL-grade claims; _meta.actor_signature slot |
| F4 CLARITY | single-URI-namespace; templates fixed param order |
| F5 PEACE² | de-escalate in description; never weaponize "must" |
| F6 MARUAH | pii_redacted field; dignity in error messages; error envelope never exposes internals |
| F7 HUMILITY | confidence cap 0.90 on claims; declare unknowns in description |
| F8 GENIUS | simplest correct path: description first, code second |
| F9 ANTI-HANTU | no soul claims; bundled cross-checks; tier caps on blobs |
| F10 ONTOLOGY | categorical scope: AI-only, substrate ≠ being |
| F11 AUDIT | every read appended to ledger; sha256 verified |
| F12 INJECTION | sanitize inputs; external ≠ authority |
| F13 SOVEREIGN | operator-private resources gated by actor_signature |
from fastmcp import FastMCP
from mcp.types import EmbeddedResource, TextResourceContents
from fastmcp.prompts.base import Message
from pydantic import AnyUrl
mcp = FastMCP("organ-name", version="v2026.07.x")
# Tool
@mcp.tool(name="query_macrostrat_columns",
annotations={"readOnlyHint": True, "openWorldHint": True})
async def query_macrostrat_columns(latitude: float, longitude: float) -> str:
"""Query Macrostrat for columnar stratigraphy at lat/lon — returns columns JSON ..."""
...
# Resource
@mcp.resource("geox://literature/{basin}/{paper_id}",
name="geox-literature-paper",
title="Geological Literature Paper",
mime_type="text/markdown",
annotations={"audience": ["assistant"], "priority": 0.85})
async def read_paper(basin: str, paper_id: str) -> str:
...
# Prompt — SIMPLE (string return, no args)
@mcp.prompt(name="geox_guard",
description="CONSTRAIN: F10 ontology enforcement.")
async def guard() -> str:
return GUARD_PROMPT_TEXT
# Prompt — SPEC-ALIGNED (messages[] with embedded resources + arguments)
# FastMCP infers PromptArgument[] from function signature automatically.
# Parameters with defaults = optional. Parameters without = required.
@mcp.prompt(name="analyse-well-log",
description="ANALYSE: single-well petrophysics + interpretation...")
async def analyse_well_log(las_path: str, well_name: str = "") -> list[Message]:
"""Analyse a single well log end-to-end.
Args:
las_path: Path to the LAS file
well_name: Well identifier for URI resolution
"""
return [
Message(f"Analyse this well log:\n\n{PROMPT_TEXT}", role="user"),
Message(
EmbeddedResource(
type="resource",
resource=TextResourceContents(
uri=AnyUrl(f"las://{well_name or 'unknown'}"),
mimeType="text/plain",
text=f"LAS path: {las_path}\nWell: {well_name}",
),
),
role="user",
),
]
import json
import httpx
# Tool with structured error envelope
@mcp.tool(name="fetch_external_data",
annotations={"readOnlyHint": True, "openWorldHint": True})
async def fetch_external_data(url: str) -> str:
"""Fetch data from an external API with governed error handling.
Use when the user requests data from a specific external source.
Returns JSON on success, structured error envelope on failure.
"""
try:
result = await httpx.get(url, timeout=5)
result.raise_for_status()
return result.text
except httpx.TimeoutException:
return json.dumps({
"error_code": "EXTERNAL_API_TIMEOUT",
"message": f"Request to {url} timed out after 5s",
"retry_allowed": True,
"suggested_action": "Retry with shorter timeout or check if the URL is reachable"
})
except httpx.HTTPStatusError as e:
return json.dumps({
"error_code": f"EXTERNAL_API_{e.response.status_code}",
"message": f"External API returned {e.response.status_code}",
"retry_allowed": e.response.status_code >= 500,
"suggested_action": "Check URL validity or try again later" if e.response.status_code >= 500 else "Do not retry — client error"
})
Why this matters: The FreeCodeCamp pattern returns raw text/errors. The model sees isError: true with a Python traceback and has to guess whether to retry. Our envelope tells the model: "here's what went wrong, here's whether you can retry, here's what to do next." The model makes better decisions. Fewer hallucinated retry loops.
Per MCP 2025-06-18 spec: prompts/get response messages[] supports type: "resource" content blocks. This lets prompts pre-load context before the model touches any tool.
When to embed resources in prompts:
When NOT to embed:
FastMCP argument inference:
async def fn(las_path: str) → PromptArgument(name="las_path", required=True)async def fn(name: str = "") → PromptArgument(name="name", required=False)Args: section → description field on each argumentname containing spaces, hyphens, or capitalsdescription < 50 chars (model's only briefing — must be 50-300 chars)description missing "Use when..." sentence (model needs trigger guidance)description > 300 chars (model skims, not reads — long descriptions get truncated mentally)uri mapping to https:// you'll proxysubscribe: true declared when handler not implemented_meta on envelope instead of contentstitle: matching name (must diverge)p1, p2, x, y, data (use semantic names)| Field | Value | |---|---| | Name | unchanged since stable | | Description | brief on its own | | Capability declaration | honest | | Evidence class | OBS / DER / INT / SPEC | | Floor active | F1, F2, F11 (always); F13 if sensitive | | Audit trail | append-only ledger entry per read |
forge_work/YYYY-MM-DD/MCP-AUDIT-<organ>.md is the canonical receipt.
For every MCP-related change:
forge_work/YYYY-MM-DD/MCP-AUDIT-<organ>.md (per organ audit receipt)forge_work/YYYY-MM-DD/MCP-CONSOLIDATED.md (cross-organ summary)F1 AMANAH → /mcp read = non-mutating
F2 TRUTH → _meta.evidence_class present
F3 WITNESS → _meta.actor_signature on sealed claims
F4 CLARITY → no double registrations, no duplicate URIs
F5 PEACE² → de-escalate in tool descriptions
F6 MARUAH → pii_redacted + dignity-first error messages
F7 HUMILITY → confidence cap in description (never claim certainty)
F8 GENIUS → simplest correct path: description-first
F9 ANTI-HANTU → never claim consciousness via tool output
F10 ONTOLOGY → AI-vs-human categories preserved
F11 AUDIT → read_ledger.append + sha256 verified
F12 INJECTION → input sanitized; external ≠ authority
F13 SOVEREIGN → actor_signature on operator-private URIs
These bindings are derived from MCP 2025-06-18 spec text + 2025-11-25 server-card extensions + docs-agent 2026-07-10 consensus. Every binding cites its source primitive.
Naming is the first act of creation. Every server registers a contract with the model. Honour it.
testing
OpenClaw edge agent bridge — operational triage, doctor, restart, and A2A bridge routing for the federation edge (Telegram surface). USE WHEN: "openclaw unhealthy", "gateway down", "edge bot not responding", "a2a bridge disconnected", "watchdog tripped", "openclaw doctor", "openclaw restart". NOT for token/security audit — use FORGE-telegram-audit.
tools
Generate images, videos, TTS, voice clone, and music via MiniMax MCP server. Use when user asks to "draw", "generate image", "create picture", "make a photo", "text to image", "image generation".
testing
Single load-bearing constitutional-judgment skill. Routes all F1–F13, verdict, hold, seal, scope, authority and floor-check calls through the live arif_judge surface. Replaces 7 overlapping predecessors (arifos-constitutional-judge, arifos-constitutional-judge, arifos-constitutional-judge, arifos-constitutional-judge, arifos-constitutional-judge, arifos-constitutional-judge, arifos-constitutional-judge).
development
MANDATORY LSP grounding gate BEFORE any code mutation on .ts, .py, .js, .tsx, .jsx files. Forces the agent to read real-time compiler diagnostics and structural project context before editing — eliminating blind guesses and anchoring every mutation in F2 (TRUTH). Routes through arifOS kernel (:8088) for centralized gate logic.