Purpose
Enable collaborating projects to "produce messages correctly" — publish a message via the NATS MCP tool according to a cross-team contract, ensuring subject / headers / payload / QoS are strictly aligned specs/nats-messaging.md. When there is no contract for an event for the first time, guide the generation of a contract and persist it to the producer repo.
Core Objective
Primary Objective: Publish a contract-compliant message via the NATS MCP tool; if the contract does not exist, generate the contract first, then send.
Success Criteria:
- ✅ Project cache
.cortex/nats.yamlhas been read (if it does not exist, guide its generation) - ✅ The
<event>-contract.mdcorresponding to the event is in place (if it exists, read it; if it does not exist, draft it and prompt to commit) - ✅ The actually sent subject / headers / payload strictly conform to the contract definition
- ✅
Nats-Msg-Idis the UUID v7 generated for this time (or reuse the original ID on retry of the same message) - ✅ JetStream send receives ack; at-most-once only does fire-and-forget
- ✅ The user gets the send result (success + actual headers digest / failure + reason)
Acceptance Test: On the consumer side, use consume-nats-message to subscribe to the same subject, and it can correctly decode headers + payload with no validation failure.
Scope Boundaries
This skill is responsible for:
- Reading the project cache + loading the contract
- Drafting the contract according to specs/nats-messaging.md + specs/cross-team-contract.md when the contract is missing for the first time
- Constructing a compliant message (subject + headers + payload)
- Completing publish / jetstream_publish / request via the NATS MCP tool
- Ack validation and failure retry (according to the contract retry policy)
This skill is not responsible for:
- Actually writing publisher code → not within the Skill's scope (application engineering responsibility)
- Creating / modifying JetStream stream / consumer / KV → IaC responsibility (see spec §5.5.2)
- Subscribing / consuming messages → use consume-nats-message
Prerequisites
- Already connected to the NATS MCP server (provides
mcp__nats__publish/mcp__nats__jetstream_publish/mcp__nats__requestand other tools; the specific tool names are subject to the connected server) - The current working directory is the producer repo (used to read
.cortex/nats.yamland the contract file) - The user can authorize publish permission to the broker
Execution Flow
Phase 0: Announcement
"I am using the publish-nats-message skill to send a NATS message."
Phase 1: Read Project Cache
- Read
.cortex/nats.yaml(producer repo root directory) - Extract fields:
broker_url/service_source/default_stream/contract_dir/iac_owner - Cache does not exist: Enter the "cache initialization subflow" — ask for the above fields, write
.cortex/nats.yaml, prompt the user to review and commit; subsequent steps reuse the values filled in this time - Cache exists but fields are missing: Ask only about the missing fields, update the cache
Phase 2: Locate Contract
- Parse the event description from user input (e.g., "send clarification.session.requested")
- Glob
<contract_dir>/**/<event>-contract.md - Hit: Read the contract → proceed to Phase 4
- Miss: Proceed to Phase 3 to draft the contract
Phase 3: Contract Drafting Subflow (only the first time for an event)
Ask for the minimum necessary information (the rest is obtained from the cache):
- Consumer name (one or more)
- QoS selection (default at-least-once; user may choose at-most-once but must specify the lossy scenario)
- Whether it is a multi-turn session scenario (if yes →
X-Correlation-Idmarked as conditionally required, plan the sessionId position within the subject) - Business field table (payload schema draft; user may be asked to list the main field types and requiredness)
- DLQ + retry parameters (default values: DLQ
<subject>.dlq、max_deliver=5、exponential backoff base 1s max 30s)
Generate contract file:
- Path:
<contract_dir>/<consumer>/<event>-contract.md(per cross-team-contract.md §3 flat layout) - frontmatter includes
contract_version: 1.0.0+ CHANGELOG first entry### 1.0.0 — YYYY-MM-DD Initial Release - Body per specs/nats-messaging.md §7.1 template
After completion:
- Prompt the user to review + commit the contract file
- Wait for user confirmationAfter the contract is ok, proceed to Phase 4 actual sending (avoid sending polluted messages based on an unconfirmed contract)
Phase 4: Construct message
Fill fields per contract:
| Item | Value |
|---|---|
| subject | Subject defined in the contract's "Contract Scope" section |
Nats-Msg-Id |
Generate UUID v7 on site (reuse the original ID on retry) |
X-Source |
Cache service_source |
X-Type |
Event type string defined by the contract |
Traceparent |
If the runtime environment has an OTel context, inject it; otherwise skip |
X-Correlation-Id |
Required for multi-turn session scenarios; the caller provides the original ID |
X-Schema-Url |
Fill in when the contract defines a schema URL |
| payload | User-provided business data, validated field by field per the contract field table (all required fields present, types correct, enum values valid) |
Validation failure → report an error indicating the violating fields,do not send。
Phase 5: Call the MCP NATS tool to send
Select the tool by QoS:
| Scenario | MCP tool | Notes |
|---|---|---|
| at-most-once (telemetry / heartbeat) | mcp__nats__publish |
No ack, fire-and-forget |
| at-least-once (cross-team default) | mcp__nats__jetstream_publish |
Must wait for ack; triggers duplicate_window deduplication |
| Synchronous request-response (< 5s) | mcp__nats__request |
Includes reply subject + X-Correlation-Id |
The specific tool names are subject to what the connected NATS MCP server provides; if the tool signatures are inconsistent, the Skill adapts the mapping (headers parameter / subject parameter).
Phase 6: Self-check and receipt
- JetStream: wait for ack; timeout / no ack → treat as failure
- Failure retry: resend per the contract retry policy,reuse the same
Nats-Msg-Id(broker-side deduplication); if it still fails after reaching max_deliver → report to the user - Success receipt: output
- subject
- Actual header values (sensitive fields may be masked if any)
- ack information (JetStream sequence / stream / domain)
- Duration
Error handling
| Situation | Handling |
|---|---|
| MCP NATS server 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 sub-flow (Phase 1) |
| Contract does not exist | Enter the contract drafting sub-flow (Phase 3) |
| Payload field validation failed | List the violating fields and require correction before resending |
| JetStream ack timeout | Retry with contract-based backoff; report to the user on final failure |
| Stream does not exist (Subject domain not IaC-managed) | The error indicates a violation of specs/nats-messaging.md §5.5.2; prompt to contact the IaC owner |
Anti-patterns
- ❌ Creating Stream / Consumer / KV at application code runtime (violates spec §5.5.2) —— error out and point to IaC
- ❌ Cross-team messages going over Core NATS without persistence —— default to JetStream; only downgrade with explicit user confirmation
- ❌ Using a new
Nats-Msg-Idon retry —— must be reused, otherwise broker deduplication fails - ❌ Stuffing metadata into the payload JSON envelope (
id/source/time, etc.) —— must go through Headers - ❌ Repeatedly asking for
broker_url/service_sourceand other basic information without reading the cache - ❌ Sending directly when no contract exists —— must first draft and prompt to commit
Relationships with other assets
- Structural contract ruler:specs/nats-messaging.md——the authority on subject / headers / payload / QoS / validation rules
- Contract document skeleton:specs/cross-team-contract.md——apply its frontmatter + section structure when drafting a contract for the first time
- Consumer-side companion:consume-nats-message——the other end of the same contract
- Broker behavior authority:https://docs.nats.io——NATS protocol / JetStream / Services API defer to the official documentation