MCP Tokens
Un token MCP, c'est une chaîne
tly_ap_<64 hex>qui autorise un client (Claude Desktop, Cursor, script…) à appeler les tools MCP de ton instance. Chaque token a son propre set de scopes, ses propres rate limits, et est tracé dans l'audit log à chaque appel.
Concept
Comparable à une "API key" classique, mais conçue spécifiquement pour les agents LLM :
- Format :
tly_ap_+ 64 caractères hex (32 bytes random) - Stockage : seulement le SHA-256 du token est stocké en DB (
autopilot_tokens.token_hash). Le plaintext n'est montré qu'une seule fois à la création. Si tu le perds, tu dois rotater (= révoquer + recréer). - Identification rapide : les 12 premiers caractères (
tly_ap_a1b2) sont stockés en clair commetoken_prefix— affichés dans les listes et l'audit log pour distinguer les tokens sans révéler le reste.
Modèle de données
Schema autopilot_tokens :
| Colonne | Type | Note |
|---|---|---|
id |
SERIAL PK | |
name |
TEXT | label humain ("Claude Desktop laptop") |
token_prefix |
TEXT | tly_ap_a1b2 (12 chars) |
token_hash |
TEXT UNIQUE | SHA-256 du plaintext |
scopes |
JSONB | array de scope strings |
rate_limit_per_min |
INTEGER | bucket default (par défaut 100/min) |
ai_rate_limit_per_min |
INTEGER | bucket ai pour les tools coûteux (par défaut 20/min) |
daily_spend_cap_usd |
NUMERIC | limite cumulée par jour sur les tools qui dépensent (AI generation, ad budgets…) |
ip_whitelist |
JSONB | array d'IPs autorisées ; vide = pas de restriction |
expires_at |
TIMESTAMPTZ | date d'expiration ; NULL = jamais |
revoked_at |
TIMESTAMPTZ | date de révocation ; NULL = actif |
last_used_at |
TIMESTAMPTZ | mis à jour à chaque call |
use_count |
INTEGER | compteur cumulé |
created_by |
INTEGER FK users | qui a créé le token |
created_at |
TIMESTAMPTZ | |
updated_at |
TIMESTAMPTZ |
Créer un token
Via l'UI
Sidebar → MCP Autopilot → Tokens → New token. Tu remplis :
- Name — libellé que tu reverras dans la liste et l'audit log
- Scopes — soit un preset (
read_only,operator,full,admin), soit tu coches les scopes individuels (cf. Scopes) - Rate limit / min — par défaut 100, monte à 300 si tu fais des batches intensifs
- AI rate limit / min — par défaut 20, pour limiter les tools
ai:generatequi coûtent cher - Daily spend cap (USD) — optionnel, plafond cumulé par jour pour les opérations facturables
- IP whitelist — optionnel, array d'IPs autorisées (utile pour brancher Trackily depuis ton VPS de prod uniquement)
- Expires at — optionnel, date limite (token automatiquement révoqué après)
Au submit, l'UI affiche le plaintext token UNE seule fois dans un modal. Copie-le tout de suite dans ton client (Claude Desktop config, etc.). Si tu fermes le modal sans l'avoir copié, tu dois rotater.
Via l'API admin
POST /admin/api/autopilot/tokens HTTP/1.1
Cookie: tly_admin_session=...
{
"name": "Claude Desktop laptop",
"scopes": ["campaigns:read", "campaigns:write", "landings:read", "email:read"],
"rate_limit_per_min": 200,
"ai_rate_limit_per_min": 30,
"daily_spend_cap_usd": 5.00,
"ip_whitelist": []
}
Response :
{
"token": "tly_ap_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2",
"record": {
"id": 5,
"name": "Claude Desktop laptop",
"token_prefix": "tly_ap_a1b2",
"scopes": ["campaigns:read", "campaigns:write", "landings:read", "email:read"],
"rate_limit_per_min": 200,
"ai_rate_limit_per_min": 30,
"daily_spend_cap_usd": 5.00,
"ip_whitelist": [],
"expires_at": null,
"created_at": "2026-05-18T14:22:11.421Z"
}
}
Le champ token n'est présent que dans cette réponse. Toute query ultérieure (GET /admin/api/autopilot/tokens) retourne seulement le record sans le plaintext.
Lister les tokens
GET /admin/api/autopilot/tokens HTTP/1.1
Response (extrait) :
[
{
"id": 5,
"name": "Claude Desktop laptop",
"token_prefix": "tly_ap_a1b2",
"scopes": ["campaigns:read", "campaigns:write", "landings:read", "email:read"],
"rate_limit_per_min": 200,
"ai_rate_limit_per_min": 30,
"daily_spend_cap_usd": "5.00",
"ip_whitelist": [],
"expires_at": null,
"revoked_at": null,
"last_used_at": "2026-05-18T18:42:33.221Z",
"use_count": 247,
"created_at": "2026-05-18T14:22:11.421Z"
}
]
Par défaut, les tokens révoqués sont exclus. Ajoute ?include_revoked=true pour les voir.
Révoquer un token
DELETE /admin/api/autopilot/tokens/5 HTTP/1.1
Le token reste en DB (audit trail conservé), mais revoked_at est setté → toute call ultérieure retourne 401 Unauthorized (code -32001).
Rotater un token
POST /admin/api/autopilot/tokens/5/rotate :
- Révoque le token courant
- Crée un nouveau token avec les mêmes scopes / limits / IP whitelist
- Retourne le nouveau plaintext
Utile quand tu suspectes une fuite (token committed par mégarde dans Git, partagé sur Slack, etc.) — tu fais une rotation, tu mets à jour les configs client, sans avoir à recréer tous les réglages.
Rate limiting
Chaque token a deux buckets :
default— utilisé par 99% des tools. Quota =rate_limit_per_min.ai— utilisé par les tools coûteux (génération AI, scraping). Quota =ai_rate_limit_per_min.
Le bucket d'un tool est défini à l'enregistrement (registerTool({ ..., rateLimitBucket: 'ai' })). Tous les tools generate_* AI sont dans le bucket ai.
L'implémentation est une sliding window 60 secondes en mémoire process. Pas de Redis — Trackily est mono-process par instance. Si tu tournes plusieurs instances derrière un LB, chaque process a son propre compteur (donc le quota effectif est multiplié — à prévoir).
Quand le quota est dépassé, le serveur répond :
{
"jsonrpc": "2.0",
"id": 42,
"error": {
"code": -32003,
"message": "Rate limit exceeded",
"data": {
"bucket": "default",
"limit_per_min": 100,
"retry_after_ms": 12450
}
}
}
Plus un header HTTP Retry-After: 13 (secondes).
Daily spend cap
Pour les tools qui dépensent de l'argent réel — appels AI (OpenAI / Anthropic), budgets ad réseaux (update_source_campaign_budget) — Trackily accumule le coût estimé dans une fenêtre glissante de 24h par token.
Si daily_spend_cap_usd est défini et que le call dépasserait le plafond, le tool refuse :
{
"status": "rejected",
"error": "Daily spend cap exceeded",
"spent_24h_usd": "4.92",
"cap_usd": "5.00"
}
Tu peux raise le cap depuis l'UI ou via PATCH sur la row.
IP whitelist
Si non-vide, seules les IP listées peuvent utiliser le token. Implémenté côté handler MCP avant même le dispatch du payload :
const whitelist = Array.isArray(tokenRow.ip_whitelist) ? tokenRow.ip_whitelist : [];
if (whitelist.length && !whitelist.includes(ip)) {
// 403 Forbidden, audit_log entry 'auth_fail'
}
L'IP est lue depuis X-Forwarded-For (premier de la liste) ou req.socket.remoteAddress en fallback. Si tu es derrière un reverse proxy (Caddy, Cloudflare), assure-toi qu'il forwarde bien le X-Forwarded-For.
Audit log
Chaque appel MCP est loggé dans autopilot_audit_log :
| Colonne | Note |
|---|---|
token_id |
FK vers autopilot_tokens.id |
token_prefix |
redondant pour pouvoir lire l'audit sans join |
event_type |
tool, resource, prompt, rpc, auth_fail, rate_limit |
method |
méthode JSON-RPC (tools/call, etc.) |
tool_name |
si event_type='tool' |
resource_uri |
si event_type='resource' |
prompt_name |
si event_type='prompt' |
params_summary |
params sérialisés, tronqués à 4000 chars |
result_status |
ok, error |
result_summary |
résumé du résultat (bytes=…) ou message d'erreur |
error_message |
si erreur |
duration_ms |
temps total côté server |
ip |
IP du caller |
user_agent |
UA du caller |
created_at |
Une page admin (/admin#autopilot/audit) expose le log avec filtres (par token, par event_type, par status, par range de dates). Utile pour debugger pourquoi un agent a fait n'importe quoi à 3h du matin.
Bonnes pratiques
- Un token par client — pas un token "global" partagé. Si Claude Desktop et un script Python utilisent tous les deux Trackily, donne-leur deux tokens. Tu pourras révoquer l'un sans toucher à l'autre.
- Scope minimal — donne
read_onlypar défaut, ajoute:writequand tu as confirmé que l'agent en a besoin. Leadminpreset est à réserver à un token "break-glass" stocké en coffre. - IP whitelist en prod — si Trackily est joint depuis ton VPS de prod, set la whitelist à l'IP du VPS. Une exfiltration du token (via leak Git, mauvaise config) sera inutilisable.
- Expires_at sur les tokens temporaires — si tu donnes l'accès à un freelancer pour 2 semaines, set
expires_at = NOW() + INTERVAL '14 days'. Plus de cleanup à faire. - Daily spend cap obligatoire sur les tokens qui ont
ai:generateousources:write— un LLM qui hallucine et boucle peut générer 100$ d'AI en 10 minutes ou +1000% un budget Kadam. - Rotation tous les 90 jours — minimum, ou immédiatement si suspicion de fuite.
Erreurs courantes
- "Invalid or expired token" — soit typo dans le header, soit token révoqué, soit
expires_atdépassé. Check dans/admin#autopilot/tokens. - "Missing scope for X" — le tool nécessite un scope absent. La response contient
"data": { "required": ["scope:foo"] }pour savoir lequel ajouter. - "Rate limit exceeded" — soit ton client fait trop d'appels, soit le bucket
aiest trop restrictif. Augmente côté token. - "IP not allowed" — tu as ajouté une whitelist mais ton IP a changé (DHCP, CDN…). Mets à jour la whitelist ou vide-la.
Voir aussi
- Scopes — la liste des scopes à coller sur un token
- Tier System — comportement des destructives
- Concept — protocole JSON-RPC sous-jacent