Un MCP local. Plusieurs périmètres. Plusieurs agents en parallèle. Chacun ne voit que sa fenêtre.
Version 0.4 · Date 2026-08-11 · Statut Alignée sur l’implémentation actuelle (allowlist + binding URL + historique filtré + surface ops /mcp/root + /api Bearer)
Donner à un agent un accès GitHub borné, sans lui donner le token, et sans lui montrer ce qui existe hors de son cadre.
git-blinder-mcp est un programme local qui :
- s’authentifie auprès de GitHub avec un token ;
- expose un serveur MCP HTTP (multi-path) ;
- laisse l’opérateur définir plusieurs périmètres (outils + chemins + branches autorisées) ;
- permet à plusieurs agents de travailler en même temps, chacun collé à un périmètre et à une branche figée par l’URL de connexion.
L’agent ne doit jamais tenir le token GitHub. Il ne doit jamais découvrir, par erreur ou par sondage, ce qui est hors périmètre. Il ne gère pas les branches : l’orchestrateur choisit la branche en connectant l’agent à la bonne URL.
Token GitHub: git_pat-…
┌──────────────────────────────────────────────┐
│ git-blinder-mcp (1 process) │
Agent A ──/mcp/infra/feat-aws──►│ périmètre infra │──► GitHub
│ read infra/** │
│ write infra/aws/** │
│ branches: [main, feat-aws] │
│ │
Agent B ──/mcp/landing─────────►│ périmètre landing (→ main) │
│ read/write apps/landing/** │
│ branches: [main] │
└──────────────────────────────────────────────┘
- L’opérateur a allowlisté
feat-awssurinfra(après création de la branche sur GitHub). - Agent A est branché sur
/mcp/infra/feat-aws: il lit/écrit du contenu sur cette branche, path-filtré ; pas d’outil de découverte de branches. - Agent B est branché sur
/mcp/landing: branche par défaut du dépôt. - Les deux passent par le même process, en parallèle, sans mélange.
Un secret GitHub (PAT ou équivalent) détenu uniquement par git-blinder-mcp. Il définit le plafond absolu de ce que le process peut faire. Fourni via --token ou GITHUB_TOKEN au serve — jamais dans la config versionnable.
Double rôle (V0) : le même secret sert de Bearer ops pour /api/* et pour les commandes CLI opérateur (perimeter*, repos, branches). Les endpoints MCP périmètre (/mcp/<id>[/<branch>]) ne sont pas authentifiés par Bearer — l’isolation repose sur l’URL (capability-by-path). Les URLs opaques / secrets par périmètre sont hors V0.
Une fenêtre découpée dans ce plafond. Un périmètre déclare :
| Champ | Rôle |
|---|---|
id |
Identifiant URL-safe ([a-z0-9-]+), unique ; root est réservé (surface ops) |
repo |
Dépôt cible (owner/name) |
tools |
Sous-ensemble d’outils autorisés (optionnel ; sinon catalogue agent) |
read |
Globs de chemins lisibles (relatifs à la racine du dépôt) |
write |
Globs de chemins modifiables (relatifs à la racine) |
default_branch |
Branche par défaut du dépôt (résolue à la création) |
branches |
Allowlist de branches auxquelles l’orchestrateur peut binder |
read et write sont indépendants. Écrire n’implique pas lire. Lire n’implique pas écrire.
À la création, branches = [default_branch]. L’opérateur ajoute explicitement d’autres branches (vérifiées sur GitHub).
Exemple :
perimeters:
infra:
repo: acme/platform
tools: [get_tree, get_file, get_files, push_files, list_commits, get_commit]
read:
- "infra/**"
write:
- "infra/aws/**"
# default_branch / branches sont gérés au runtime (registry)Une connexion MCP = un périmètre + une branche allowlistée.
| URL | Branche liée |
|---|---|
/mcp/<id> |
default_branch du périmètre |
/mcp/<id>/<branch> |
<branch> si elle est dans l’allowlist ; sinon 404 |
Hors binding agent :
| URL | Rôle |
|---|---|
/mcp |
Health / status (pas d’outils MCP) |
/mcp/root |
Surface ops non filtrée — jamais pour un agent |
Changer de branche = déconnecter / reconnecter sur une autre URL. Pas d’état « checkout » côté serveur. Pas d’outil agent pour lister ou créer des branches.
| Rôle | Décide |
|---|---|
| Opérateur | périmètres, globs, tools, allowlist de branches |
| Orchestrateur | quelle URL MCP (donc quelle branche) donner à l’agent |
| Agent | contenu (lire / écrire / historique filtré) — rien sur les branches |
Le contexte runtime d’un agent sur une connexion : périmètre résolu, branche liée, outils exposés, règles de chemins. Plusieurs sessions coexistent dans le même process.
┌──────────────────────────────────────────────┐
│ CLI │ serve, perimeter*, branches, repos
├──────────────────────────────────────────────┤
│ HTTP / MCP transport │ /mcp (health), /mcp/root (ops),
│ (Option B : connexion = périmètre×branche) │ /mcp/<id>[/<branch>], /api/* (Bearer)
├──────────────────────────────────────────────┤
│ Core (pur) │ decide Allow / Deny / Invisible
│ - normalisation chemins │
│ - matching globs │
│ - table outils │
│ - filtre résultats (tree, commits, …) │
│ - préfixes path pour list_commits │
├──────────────────────────────────────────────┤
│ Upstream GitHub (Octokit) │ token + appels API
└──────────────────────────────────────────────┘
Règle : le Core ne sait pas comment le périmètre / la branche ont été choisis. Il reçoit (perimeter, tool, args) (éventuellement déjà bound) et rend un verdict. Zéro I/O réseau dans le Core.
Client ──► /mcp/infra ──► Session(perimeter=infra, branch=main)
Client ──► /mcp/infra/feat-aws ──► Session(perimeter=infra, branch=feat-aws)
Client ──► /mcp/root ──► Ops non filtré (pas pour agents)
Client ──► /mcp ──► Health only (pas d’outils)
| Élément | Cardinalité |
|---|---|
| Process git-blinder-mcp | 1 par token (local) |
| Token GitHub | 1 par process |
| Périmètres | N (registry en mémoire + option YAML au boot) |
| Connexions MCP actives | N (une URL = une session) |
| Agents | N, en parallèle |
git-blinder serve --token "$GITHUB_TOKEN" [--port 7400] [--config config.yaml]
git-blinder tools
git-blinder repos list [--url …] [--token …]
git-blinder branches list --repo owner/repo [--url …] [--token …]
git-blinder perimeter create <id> --repo owner/repo --read <glob> [--write <glob>] [--tools a,b,c] [--url …] [--token …]
git-blinder perimeter list|show|delete <id> [--url …] [--token …]
git-blinder perimeter branch list <id>
git-blinder perimeter branch add <id> <branch> # vérifie existence GitHub
git-blinder perimeter branch remove <id> <branch> # refuse de retirer default_branch
git-blinder perimeter commits list <id> [--branch <name>]
git-blinder perimeter commits show <id> <sha> [--branch <name>]Notes :
- Les commandes
perimeter*/repos/branches/commitsparlent au serveur déjà up (--url, défauthttp://127.0.0.1:7400). - Elles authentifient
/api/*avec--tokenouGITHUB_TOKEN(même secret queserve). branches listetrepos listsont des outils opérateur (découverte GitHub), pas la surface agent.perimeter commits *applique le même filtrage que les tools MCPlist_commits/get_commit.- Id
rootrefusé à la création (réservé à/mcp/root).
YAML optionnel au boot (--config) pour précharger des périmètres. Le token n’y figure pas.
| Surface | Auth | Public cible |
|---|---|---|
GET /mcp |
aucune | status opérateur (ids de périmètres) |
POST /mcp |
— | interdit (405) — ne sert plus d’outils |
/mcp/root |
aucune* | ops / debug — catalogue non filtré |
/mcp/<id>[/<branch>] |
aucune* | agent — scoped |
/api/* |
Authorization: Bearer <token serve> |
CLI / ops uniquement |
* Capability-by-path : connaître l’URL suffit. Un agent avec shell sur la même machine peut encore sonder /mcp/root ou d’autres ids — ce n’est pas un sandbox OS. V0 ferme le piège « POST /mcp = tout voir » et protège la surface d’administration /api. Suites possibles : secret de session, URL opaque par périmètre, Host/Origin hardening.
Hors périmètre en lecture :
- l’outil non autorisé / non exposé → absent de
tools/list/ « Not found » à l’appel ; - le chemin non lisible → « not found », pas « access denied » ;
- la branche hors allowlist → 404 sur l’URL de binding (l’agent ne la voit jamais).
Écriture hors write → erreur claire. Aucun appel GitHub partiel.
Si un appel touche plusieurs chemins (ex. push_files) et qu’un seul sort du write, l’appel entier est rejeté avant GitHub.
Un outil n’est exposable que si git-blinder-mcp sait extraire / borner les chemins et filtrer la sortie. Sinon : non listé.
Avant tout matching, normalisation stricte (rejet si non normalisable) : . / .., pas d’absolu, casse sensible, etc. Logique unique et pure.
Sur /mcp/<id>[/<branch>] :
owner/repo/ref/branch(écriture) sont forcés par le binding ;- les schémas agent omettent ces champs quand c’est pertinent ;
- l’agent ne peut pas pivoter vers une autre branche via les arguments d’outil.
Principe : l’opérateur autorise les branches ; l’orchestrateur en choisit une à la connexion ; l’agent ne voit que du contenu path-filtré sur cette branche.
Pour un périmètre P :
- À la création :
branches = [default_branch du repo]. branch add <name>: échoue si la branche n’existe pas sur GitHub (message clair : la créer d’abord) ; sinon l’ajoute à l’allowlist.branch remove <name>: interdit pourdefault_branch; sinon retire de l’allowlist.
La découverte des branches GitHub se fait via la CLI globale branches list --repo …, pas via l’agent.
/mcp/<id>→ branche =default_branch/mcp/<id>/<branch>→ autorisé ssibranch ∈ branches; sinon 404 « not allowed »
Sur une connexion périmètre, jamais exposés :
list_branchescompare
Même s’ils figurent dans le grant tools du YAML, ils sont retirés de la surface agent.
Outils agent typiques : get_tree, get_file, get_files, push_files, list_commits, get_commit.
/mcp/root (non filtré) expose le catalogue complet pour le debug opérateur — ne pas y brancher un agent. POST /mcp ne sert pas d’outils.
Quelle que soit la branche liée :
| Surface | Règle |
|---|---|
| Contenu / tree / blob | seuls les paths dans read |
Écriture (push_files) |
seuls les paths dans write ; tout-ou-rien ; commit atomique GitHub |
Diff d’un commit (get_commit) |
seuls les fichiers in-scope + omitted_count |
Liste de commits (list_commits) |
commits touchant un préfixe dérivé de read (voir 7.5) |
Deux outils :
-
list_commits— sur la branche liée.
Préfixe(s) GitHubpath=dérivés des globsread(ex.infra/**→path=infra).
Plusieurs globs disjoints → un appel par préfixe, fusion dédupliquée.
Pas d’enrichissement N+1 de chaque commit. -
get_commit— un SHA.
Charge ce commit ; filtre les fichiers auread; renvoieomitted_countopaque.
Commit purement hors-scope → not found (pas de fantôme vide).
Ne nomme jamais les chemins omis.
La CLI perimeter commits list|show applique les mêmes règles (branche allowlistée via --branch, défaut = default).
Pas d’outil commit séparé. push_files crée un commit GitHub atomique (blobs + tree + commit + avance de la ref de la branche liée).
| Tool | Ops /mcp/root |
Perimeter /mcp/<id>[/<branch>] |
POST /mcp |
|---|---|---|---|
get_tree |
oui | oui (si grant) | — |
get_file |
oui | oui | — |
get_files |
oui | oui | — |
push_files |
oui | oui | — |
list_commits |
oui | oui (filtré path + branche liée) | — |
get_commit |
oui | oui (filtré fichiers) | — |
list_branches |
oui | non | — |
compare |
oui | non | — |
POST /mcp → 405 (message pointant vers /mcp/root ou /mcp/<id>).
tools/list→ outils du périmètre ∩ outils agent (sanslist_branches/compare) ;tools/call→ revalidation (decide) + binding repo/branche + filtre de sortie ;- résultats → élagués au
read.
- tree / fichiers → hors
readretirés ; - commits purement hors-scope → absents / not found ;
- commits mixtes → fichiers in-scope +
omitted_count; - compteurs recalculés sur le sous-ensemble visible.
Plusieurs sessions sans mélange : pas d’état mutable partagé non synchronisé entre sessions ; token amont partagé, verdicts non.
| Méthode | Path | Rôle |
|---|---|---|
| GET | /mcp |
Status + liste des ids de périmètres (pas d’outils) |
| POST | /mcp |
405 — outils absents |
| GET/POST | /mcp/root |
MCP ops non filtré (root id réservé) |
| GET/POST | /mcp/<id> |
Status / MCP bound à default_branch |
| GET/POST | /mcp/<id>/<branch> |
Status / MCP bound à la branche allowlistée |
| * | /api/* |
401 sans Bearer ops (= token de serve) |
| GET/POST | /api/perimeters |
List / create (root → 400 reserved) |
| GET/DELETE | /api/perimeters/:id |
Show / delete |
| GET/POST/DELETE | /api/perimeters/:id/branches[/:name] |
Allowlist |
| GET | /api/perimeters/:id/commits[/:sha] |
Historique filtré |
| GET | /api/repos |
Repos visibles du token |
| GET | /api/repos/:owner/:repo/branches |
Branches GitHub (ops) |
1. Opérateur: git-blinder serve --token …
2. Opérateur: git-blinder repos list / branches list --repo …
3. Opérateur: git-blinder perimeter create infra --repo … --read 'infra/**' --write 'infra/aws/**'
4. Opérateur: crée la branche sur GitHub si besoin, puis
git-blinder perimeter branch add infra feat-aws
5. Orchestrateur: branche l’agent sur http://127.0.0.1:7400/mcp/infra/feat-aws
6. Agent: tools/list → pas de list_branches / compare
7. Agent: list_commits → historique path-filtré sur feat-aws
8. Agent: get_commit <sha> → fichiers in-scope + omitted_count
9. Agent: push_files → commit atomique sur feat-aws, borné par write
10. Agent B sur /mcp/landing en parallèle, zéro croisement
- UI web d’administration
- Multi-tokens dans un même process
- Multi-dépôts dans un même périmètre
- Quotas / rate-limit produit (hors limites GitHub)
- Mode distant / multi-hôte
- Autres backends que GitHub
- Binding Option A (multiplex par call)
- Approche C (visibilité branches par intersection de diff) pour les agents — abandonnée au profit de l’allowlist + binding URL
- Outils PR dédiés
- Création de branche par l’agent
| # | Décision | Motif |
|---|---|---|
| 1 | 1 process MCP local = 1 token GitHub | Surface de contrôle unique |
| 2 | N périmètres par process | Exigence produit |
| 3 | Agents simultanés sur le même process | Exigence produit |
| 4 | Option B : URL /mcp/<id>[/<branch>] |
Isolation simple, clients MCP HTTP |
| 5 | Core isolé du binding | Évolutivité |
| 6 | read / write indépendants |
Périmètre par verbe |
| 7 | Absence en lecture, refus explicite en écriture | Ne rien révéler / ne pas mentir |
| 8 | Branches = allowlist opérateur + binding URL | L’agent ne gère pas Git refs |
| 9 | Agents : pas de list_branches / compare |
Pas de surface découverte de branches |
| 10 | list_commits via path= dérivé du read |
Filtre côté GitHub, coût maîtrisé |
| 11 | get_commit + omitted_count opaque |
Honnêteté sans élargir le contexte |
| 12 | push_files = commit atomique |
Pas de commit local séparé |
| 13 | CLI d’abord, pas d’UI | Simplicité opératoire |
| 14 | Token hors config versionnable | Ne jamais committer le secret |
| 15 | Globs relatifs à la racine du dépôt | repo est déjà un champ du périmètre |
| 16 | Outils non filtrés sur /mcp/root, pas /mcp |
Évite le piège agent → POST /mcp |
| 17 | Id root réservé |
Pas de collision avec la surface ops |
| 18 | /api/* protégé par Bearer (= token serve) |
CLI / admin hors portée agent naïve |
| 19 | MCP périmètre sans Bearer (V0) | Simplicité clients MCP HTTP ; risque ids devinables accepté |
- PRs filtrées — même modèle que
get_commit/ list, non livré. - Stdio spawner — shim par périmètre devant le même daemon ; Core inchangé.
- Persistance allowlist — aujourd’hui registry mémoire (+ YAML boot) ; persistance runtime à préciser.
- Globs sans préfixe statique (
**,*.md) —list_commitsrenvoie liste vide (pas depath=utilisable) ; politique à affiner si besoin. - Auth MCP plus forte — secret de session, URL opaque par périmètre, validation
Host/Origin(voir AUDIT C1). /mcp/rootopt-in — éventuellement derrière un flag si on veut le couper en prod locale.
Un incrément n’est « bon » que si, à la main :
- Deux périmètres disjoints existent.
- Deux agents y sont branchés en même temps (URLs distinctes).
- Chacun ne liste que ses outils agent (sans
list_branches/compare). /mcp/<id>/<branch>hors allowlist → 404 ; branche allowlistée → binding OK.list_commits/perimeter commits listn’expose pas les commits purement horsread.get_commitmixte → fichiers in-scope +omitted_count; hors-scope pur → not found.- Une écriture hors
writeéchoue clairement et n’atteint pas GitHub. - Aucune réponse de A n’arrive chez B (et inversement).
POST /mcp→ 405 ;POST /mcp/rootliste les 8 outils (ops)./api/perimeterssans Bearer → 401 ; avec le token deserve→ OK.perimeter create root …→ rejeté (id réservé).
Contrats exécutables : features/cli/*.feature, features/support/01_api_perimeters.feature.
| Terme | Sens |
|---|---|
| Token | Credential GitHub du process (= Bearer ops V0) |
| Ops token | Même secret, présenté en Authorization: Bearer sur /api/* |
| Périmètre | Fenêtre outils + paths + allowlist branches |
| Connexion / binding | Canal MCP lié à un périmètre et une branche |
| Session | État runtime d’un agent sur une connexion |
| Core | Moteur de décision pur |
| Allowlist | Branches explicitement autorisées sur un périmètre |
| Marqueur opaque | omitted_count sans nommer les paths hors scope |
/mcp/root |
Surface non filtrée (ops) — pas pour agents |
| Capability-by-path | Connaître /mcp/<id> suffit pour s’y connecter (V0) |
GitBlinder : des blinders (œillères) pour un agent GitHub via MCP.
| Usage | Nom |
|---|---|
| Projet | GitBlinder |
| Repository | git-blinder-mcp |
| Package npm / CLI | git-blinder |
Accroche : Blinders for GitHub agents — one MCP, many perimeters.