An open protocol for cross-MCP server content tracking and deduplication.
Spec (EN) · Spec (RU) · Examples · README на русском
You have multiple MCP servers — one fetches content, another posts to Telegram, a third cross-posts to a social network. Each server is isolated by design. No server sees the others.
Now answer these questions:
- Was this image already posted to Telegram?
- Did the social network post succeed or fail?
- Where did the pipeline break yesterday at 3am?
You can't. There's no standard way to track content across isolated MCP servers.
TRAIL solves this.
LLM Orchestrator (Claude, GPT, etc.)
/ | \
/ | \
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Civitai │ │ Telegram │ │ Facebook │
│ MCP │ │ MCP │ │ MCP │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │
trail.jsonl trail.jsonl trail.jsonl
Each server keeps its own trail.jsonl — an append-only log with a shared schema. The orchestrator reads all logs and connects the dots using a universal Content ID (content_id).
One pipeline run across three servers:
# Civitai trail.jsonl
{"version":2,"timestamp":"2026-04-05T14:07:00Z","content_id":"civitai:image:12345","action":"selected","requester":"daily-post","trace_id":"run-001"}
# Telegram trail.jsonl
{"version":2,"timestamp":"2026-04-05T14:07:05Z","content_id":"civitai:image:12345","action":"posted","requester":"daily-post","trace_id":"run-001","details":{"platform":"telegram","platform_id":"42"}}
# Facebook trail.jsonl
{"version":2,"timestamp":"2026-04-05T14:07:30Z","content_id":"civitai:image:12345","action":"posted","requester":"daily-post","trace_id":"run-001","details":{"platform":"facebook","platform_id":"99"}}The orchestrator sees: civitai:image:12345 → selected → posted to Telegram (#42) → posted to Facebook (#99). Full pipeline traced via trace_id.
- Zero shared state — no database, no message bus, no inter-server communication
- Append-only JSONL — atomic writes, no corruption risk, trivially parseable
- Self-documenting — readable field names:
content_id,action,requester,timestamp,version - Universal Content ID —
source:type:idformat traces content across any number of servers - Trace correlation — optional
trace_idlinks entries across servers into one pipeline trace - 15 standard actions —
fetched,selected,posted,failed,skipped,retrying,transformed,moderated,expired,delivered,delegated,received,evaluated,guarded,acknowledged - Multi-agent patterns — delegation, evaluation, guardrails, and human-in-the-loop via standard actions and
caused_bychains - Standard tools —
get_trail,mark_trail,get_trail_stats— same API everywhere - Standardized details — error types, cost tracking, content metadata, platform IDs, guardrail results, evaluation scores
- Auto-logging — publishing tools log automatically when
content_idandrequesterare passed - 3 conformance levels — Basic (5 fields + 2 tools), Standard (+ tracing, auto-logging, discovery), Full (+ causality chains, OTel export, all 15 actions)
- Zero dependencies — stdlib only, no external packages
- OTel-native mapping —
caused_by→parentSpanId,server→service.name, full span tree in any OTel backend
from trail import Trail
trail = Trail("./data")
# Log an event
await trail.append(
content_id="civitai:image:12345",
action="posted",
requester="daily-post",
details={"platform": "telegram", "platform_id": "42"},
trace_id="run-001"
)
# Query the trail
entries, total = await trail.query(content_id="civitai:image:12345")
# Check if already posted
if await trail.is_used("civitai:image:12345"):
print("Already posted, skipping")
# Get pipeline stats
stats = await trail.stats(requester="daily-post")
print(f"Posted: {stats['by_action'].get('posted', 0)}")import { Trail } from "./trail";
const trail = new Trail("./data");
// Log an event
await trail.append("civitai:image:12345", "posted", "daily-post", {
details: { platform: "telegram", platform_id: "42" },
trace_id: "run-001",
});
// Query the trail
const { entries, total } = await trail.query({ content_id: "civitai:image:12345" });
// Check if already posted
if (await trail.isUsed("civitai:image:12345")) {
console.log("Already posted, skipping");
}
// Get pipeline stats
const stats = await trail.stats("daily-post");
console.log(`Posted: ${stats.by_action.posted ?? 0}`);{"version":2,"timestamp":"2026-04-05T14:07:00.123Z","content_id":"civitai:image:12345","action":"posted","requester":"daily-post","server":"telegram-mcp","trace_id":"run-001","details":{"platform":"telegram","platform_id":"42"}}| Field | Type | Required | Description |
|---|---|---|---|
version |
int |
yes | Protocol version (2) |
timestamp |
string |
yes | ISO 8601 timestamp (UTC, milliseconds) |
content_id |
string |
yes | Content ID: source:type:id |
action |
string |
yes | Action performed (see below) |
requester |
string |
yes | Workflow/task ID that initiated this |
details |
object |
no | Platform-specific data with standard sub-fields |
trace_id |
string |
no | Groups entries across servers into one trace |
server |
string |
no | MCP server that wrote this entry (auto-set) |
entry_id |
string |
no | Unique entry identifier (for causality chains) |
caused_by |
string |
no | entry_id of the causing entry (maps to OTel parentSpanId) |
tags |
string[] |
no | Free-form labels for filtering |
| Action | When |
|---|---|
fetched |
Content retrieved from source (candidate list) |
selected |
Chosen from candidates by the orchestrator |
posted |
Successfully published |
failed |
Publish attempt failed (details.error has structured error info) |
skipped |
Intentionally not used (details.reason explains why) |
retrying |
Scheduling retry after failure (details.attempt = attempt number) |
transformed |
Content modified (resize, transcode, translate) |
moderated |
Passed or failed moderation (details.result: "pass" / "reject") |
expired |
Content no longer eligible (TTL, source removed) |
delivered |
Delivery confirmed by platform (webhook, read receipt) |
delegated |
Task delegated to another agent/server (details.delegate_to, details.delegation_reason) |
received |
Content received from another agent/server (details.received_from) |
evaluated |
Quality/relevance scored (details.score: 0.0–1.0, details.evaluator) |
guarded |
Guardrail check (details.guardrail, details.passed, details.reason) |
acknowledged |
Human-in-the-loop approval (details.acknowledged_by, details.decision) |
Every TRAIL-compatible MCP server exposes:
| Tool | Purpose |
|---|---|
get_trail(content_id?, action?, requester?, trace_id?, tags?, since?, limit?, offset?) |
Query the log with filters. Returns {entries, total} |
mark_trail(content_id, action, requester, details?, trace_id?, tags?) |
Write an entry explicitly |
get_trail_stats(requester?, since?) |
Summary statistics (counts by action, unique content IDs, date range) |
Publishing tools (send_photo, publish_post, etc.) accept optional content_id + requester + trace_id for automatic logging.
TRAIL defines standard sub-fields in details for cross-server consistency:
{
"details": {
"platform": "telegram",
"platform_id": "42",
"url": "https://t.me/channel/42",
"error": {"type": "rate_limit", "message": "429", "retry_after": 60},
"cost": {"tokens_in": 150, "tokens_out": 50, "usd": 0.003},
"content": {"type": "image", "width": 1024, "height": 1024, "model": "Flux.1"},
"duration_ms": 1200,
"delegate_to": "optimizer-mcp",
"score": 0.92,
"evaluator": "llm-judge",
"guardrail": "nsfw-filter",
"passed": true,
"acknowledged_by": "[email protected]",
"decision": "approve"
}
}See SPEC.md — Details Field for the full specification.
The orchestrator handles deduplication, not the servers:
1. Before fetching → get_trail(requester="daily-post", since="...") on source
2. Before posting → get_trail(content_id="civitai:image:12345", action="posted") on destination
3. If already posted → skip
Servers MAY offer convenience filters (e.g., exclude_used=true) that use the trail internally.
Servers advertise TRAIL support via capabilities:
{
"capabilities": {
"trail": {
"version": 2,
"server": "telegram-mcp",
"conformance": "standard",
"actions": ["fetched", "selected", "posted", "failed", "skipped", "guarded"],
"auto_log_tools": ["send_photo", "send_message", "publish_post"],
"supports": {
"trace_id": true,
"entry_id": true,
"caused_by": true,
"tags": true,
"server_field": true
},
"retention_days": 90
}
}
}Basic (Level 0) — deduplication and simple tracking:
Standard (Level 1) — production pipelines:
4. Set server name in Trail constructor
5. Add get_trail_stats tool
6. Add content_id + requester + trace_id params to publishing tools
7. Advertise TRAIL in capabilities with "conformance": "standard"
Full (Level 2) — multi-agent observability:
8. Enable entry_id auto-generation and caused_by support
9. Implement all 15 standard actions
10. Add OTel export capability
Full specification: SPEC.md | SPEC.ru.md
| Alternative | Why TRAIL is better for content tracking |
|---|---|
| Shared database | Creates coupling, deployment complexity, single point of failure. MCP servers are isolated by design. |
| Message queue | Overkill. The LLM orchestrator already mediates all servers — it IS the message bus. |
| OpenTelemetry | Traces tool calls, not content semantics. Doesn't know what was published where. TRAIL has an OTel bridge for combining both. |
| IETF AAT | Compliance-focused (hash chains, ECDSA signatures). TRAIL is developer-first — lightweight and zero-dependency. |
| Langfuse / LangSmith | LLM observability platforms — trace API calls, not content lifecycle. Require cloud/self-hosted backend. |
| Google A2A | Agent-to-agent communication protocol, not a content tracking log. Different layer. |
| Agent Protocol | Defines agent API, not a logging format. Tasks/Steps, not content semantics. |
| ActivityPub | Designed for social federation, not AI tool orchestration. Massive overhead. |
Q: Why readable field names instead of short ones?
A: A protocol for decades must be self-documenting. content_id is clear on first sight. The overhead is ~15 KB/year at typical rates — negligible.
Q: Do I need all the optional fields? A: No. The five required fields are the whole protocol. Optional fields unlock advanced features when you need them.
Q: Orchestrator crashes mid-pipeline?
A: Use trace_id to find all entries for that run. The last entry's action shows where to resume.
Q: What are conformance levels?
A: Three tiers — Basic (5 fields + 2 tools), Standard (+ tracing, server, auto-logging), Full (+ causality chains, all 15 actions, OTel export). Start at Basic.
Q: Multi-agent pipelines?
A: delegated/received action pairs + caused_by chains + server field. The orchestrator reconstructs the full DAG. See SPEC.md — Multi-Agent Patterns.
We searched extensively for existing solutions. As of April 2026, no cross-MCP content tracking protocol exists:
- MCP Spec — no server-to-server communication by design
- CA-MCP (arXiv 2601.11595) — shared context store for transient state, not persistent content logs
- lokryn/mcp-log — JSONL audit logging for operations (SOC2/HIPAA), not content tracking
- IBM ContextForge — gateway proxy with OTel tracing, not content semantics
- OpenTelemetry GenAI — semantic conventions for LLM calls (Development status), not content lifecycle
- IETF AAT (draft-sharif-agent-audit-trail) — compliance-focused audit trail with hash chains, no content semantics
- Google A2A — agent-to-agent communication protocol with traceability extension, not a logging format
- Langfuse / LangSmith / Arize Phoenix — LLM observability platforms, trace API calls not content
- Agent Protocol (agentprotocol.ai) — REST API for interacting with agents, not a log format
TRAIL fills a unique gap: lightweight, zero-dependency content tracking with multi-agent support — no other protocol combines content semantics (content_id), zero shared state, and agent patterns (delegation, evaluation, guardrails).
| Server | Description | Language |
|---|---|---|
| civitai-mcp-ultimate | Civitai API — models, images, videos, prompts | Python |
| telegram-api-mcp | Telegram Bot API v9.6 — full coverage | TypeScript |
Implementing TRAIL in your server? Open a PR to add it here.
Other Projects by @timoncool
| Project | Description |
|---|---|
| ACE-Step Studio | AI music studio — songs, vocals, covers, videos |
| VideoSOS | AI video production in the browser |
| Bulka | Live-coding music platform |
| GitLife | Your life in weeks — interactive calendar |
| ScreenSavy.com | Ambient screen generator |
I build open-source software and do AI research. Most of what I create is free and available to everyone. Your donations help me keep creating without worrying about where the next meal comes from =)
All donation methods | dalink.to/nerual_dreming | boosty.to/neuro_art
- BTC:
1E7dHL22RpyhJGVpcvKdbyZgksSYkYeEBC - ETH (ERC20):
0xb5db65adf478983186d4897ba92fe2c25c594a0c - USDT (TRC20):
TQST9Lp2TjK6FiVkn4fwfGUee7NmkxEE7C
MIT License · Made with Claude Code