Skill: Publish NATS Message
Purpose
Let collaborating projects "produce messages correctly" — publish one message through the NATS MCP tools according to the cross-team contract, with subject / headers / payload / QoS aligned strictly to specs/nats-messaging.md. The first time an event has no contract, walk through generating one and write it into the producer repo.
Core Objective
Primary goal: publish one contract-conforming message through the NATS MCP tools; when the contract does not exist, generate it before sending.
Success criteria:
- ✅ The project cache
.cortex/nats.yamlwas read (walk through creating it when it is absent) - ✅ The
<event>-contract.mdfor the event is in place (read it when it exists; draft it and prompt for a commit when it does not) - ✅ The subject / headers / payload actually sent match the contract definition strictly
- ✅
Nats-Msg-Idis a UUID v7 generated for this send (or the original ID reused when retrying the same message) - ✅ A JetStream send received its ack; at-most-once is fire-and-forget only
- ✅ The user got the send result (success + a summary of the actual headers / failure + the reason)
Acceptance test: on the consumer side, consume-nats-message subscribes to the same subject and decodes headers + payload correctly, with no validation failure.
Scope Boundary
This skill owns:
- Reading the project cache and loading the contract
- Drafting the contract per specs/nats-messaging.md + specs/cross-team-contract.md when it is missing on first use
- Building the conforming message (subject + headers + payload)
- Carrying out publish / jetstream_publish / request through the NATS MCP tools
- Ack verification and retry on failure (per the contract's retry policy)
This skill does not own:
- Writing the actual publisher code → outside the Skill's remit (an application-engineering responsibility)
- Creating / modifying a JetStream stream / consumer / KV → an IaC responsibility (see spec §5.5.2)
- Subscribing to / consuming messages → use consume-nats-message
Preconditions
- A NATS MCP server is connected (offering tools such as
mcp__nats__publish/mcp__nats__jetstream_publish/mcp__nats__request; the exact tool names come from the connected server) - The current working directory is the producer repo (for reading
.cortex/nats.yamland the contract file) - The user can authorize publish permission on the broker
Execution
Stage 0: announce
"I am using the publish-nats-message skill to send a NATS message."
Stage 1: read the project cache
- Read
.cortex/nats.yaml(at the producer repo root) - Extract the fields:
broker_url/service_source/default_stream/contract_dir/iac_owner - Cache absent: enter the "cache initialization subflow" — ask for the fields above, write
.cortex/nats.yaml, and prompt the user to review and commit it; later steps reuse the values filled in here - Cache present but a field is missing: ask about the missing field only, and update the cache
Stage 2: locate the contract
- Parse the event description the user gave ("send clarification.session.requested", for example)
- Glob
<contract_dir>/**/<event>-contract.md - Hit: read the contract → go to Stage 4
- Miss: go to Stage 3 and draft the contract
Stage 3: contract drafting subflow (first time for a given event only)
Ask for the minimum information needed (the rest comes from the cache):
- The consumer name (one or several)
- The QoS choice (at-least-once by default; the user can pick at-most-once but must state where loss is acceptable)
- Whether this is a multi-turn session (if so → mark
X-Correlation-Idconditionally required, and plan a slot for sessionId inside the subject) - The business field table (a draft payload schema; the user can list the main fields, their types and whether each is required)
- DLQ + retry parameters (defaults: DLQ
<subject>.dlq, max_deliver=5, exponential backoff base 1s max 30s)
Generate the contract file:
- Path:
<contract_dir>/<consumer>/<event>-contract.md(the flat layout of cross-team-contract.md §3) - The frontmatter carries
contract_version: 1.0.0, and the first CHANGELOG entry is### 1.0.0 — YYYY-MM-DD Initial Release - The body follows the template in specs/nats-messaging.md §7.1
Afterwards:
- Prompt the user to review and commit the contract file
- Wait for the user to confirm the contract is fine before entering Stage 4 and sending for real (this avoids polluting the stream with a message sent against an unconfirmed contract)
Stage 4: build the message
Fill the fields per the contract:
| Field | Value |
|---|---|
| subject | the subject defined in the contract's "Contract Scope" section |
Nats-Msg-Id |
a UUID v7 generated on the spot (reuse the original ID when retrying) |
X-Source |
the cached service_source |
X-Type |
the event-type string defined in the contract |
Traceparent |
inject it when the runtime has an OTel context; otherwise skip |
X-Correlation-Id |
required in a multi-turn session; the caller supplies the original ID |
X-Schema-Url |
filled in when the contract defines a schema URL |
| payload | the business data the user supplied, validated field by field against the contract's field table (every required field present, types correct, enum values legal) |
Validation fails → report the offending fields and send nothing.
Stage 5: send through the MCP NATS tools
Pick the tool by QoS:
| Case | MCP tool | Note |
|---|---|---|
| at-most-once (telemetry / heartbeat) | mcp__nats__publish |
no ack, fire-and-forget |
| at-least-once (the cross-team default) | mcp__nats__jetstream_publish |
must wait for the ack; triggers duplicate_window deduplication |
| synchronous request-response (< 5s) | mcp__nats__request |
carries a reply subject + X-Correlation-Id |
The exact tool names come from the connected NATS MCP server; when a tool signature differs, the Skill maps onto it adaptively (the headers argument / the subject argument).
Stage 6: self-check and receipt
- JetStream: wait for the ack; a timeout / no ack → treat it as a failure
- Retry on failure: resend per the contract's retry policy, reusing the same
Nats-Msg-Id(the broker deduplicates); still failing at max_deliver → report to the user - Success receipt: emit
- subject
- the actual header values (sensitive fields can be redacted where present)
- the ack information (JetStream sequence / stream / domain)
- the elapsed time
Error Handling
| Situation | Handling |
|---|---|
| The MCP NATS server is not connected | prompt the user to check the MCP configuration, and list the expected tool names |
.cortex/nats.yaml does not exist |
enter the cache initialization subflow (Stage 1) |
| The contract does not exist | enter the contract drafting subflow (Stage 3) |
| Payload field validation fails | list the offending fields and ask for a correction before resending |
| The JetStream ack times out | retry with the contract's backoff; report the final failure to the user |
| The stream does not exist (the subject domain was never put under IaC) | report the violation of specs/nats-messaging.md §5.5.2, and point at the IaC owner |
Anti-Patterns
- ❌ Application code creating a Stream / Consumer / KV at runtime (a violation of spec §5.5.2) — report the error and point at IaC
- ❌ Cross-team messages going over Core NATS with no persistence — switch to JetStream by default; a downgrade takes an explicit user confirmation
- ❌ A fresh
Nats-Msg-Idon retry — it must be reused, or the broker's deduplication stops working - ❌ Stuffing metadata into the payload JSON envelope (
id/source/timeand the like) — it must go in the Headers - ❌ Asking again for basics such as
broker_url/service_sourcewithout reading the cache - ❌ Sending with no contract in place — it must be drafted first, with a prompt to commit
Relation to Other Assets
- The structural contract yardstick: specs/nats-messaging.md — the authority on subject / headers / payload / QoS / validation rules
- The contract document skeleton: specs/cross-team-contract.md — apply its frontmatter and section structure when drafting a contract for the first time
- The consumer-side counterpart: consume-nats-message — the other end of the same contract
- The authority on broker behavior: https://docs.nats.io — the official docs govern the NATS protocol / JetStream / Services API