loom-fs
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@loom-fslist the files in the loom-docs root and read its README"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
loom-fs — serveur MCP filesystem à rôles
Serveur MCP qui expose une vue restreinte du système de fichiers à un agent. Chaque répertoire accessible est déclaré explicitement, avec un rôle qui détermine ce qui peut y être fait.
Écrit pour remplacer @modelcontextprotocol/server-filesystem, qui accorde
lecture, écriture et suppression sur toute racine passée en argument, sans
granularité ni filtrage des fichiers sensibles.
Modèle de sécurité
Le rôle est un plafond
Un rôle définit le maximum de droits accordables. Une racine peut restreindre en
dessous via permissions, jamais élargir. Une tentative d'élargissement échoue au
chargement de la configuration, pas à l'exécution.
Rôle | Plafond | Usage visé |
|
| documentation, specs, données à ne jamais modifier |
|
| un dépôt de code — modifie, ne supprime pas |
|
| dossier de travail jetable |
Le point de passage unique
security.resolve() est le seul endroit qui transforme une chaîne fournie par le
modèle en chemin réel. Tous les outils y passent. Il applique trois contrôles dans
cet ordre, et le message d'erreur indique toujours lequel a mordu :
Permission — l'opération est-elle dans les droits de la racine ? →
root 'x' (role reference) grants [read], not writeConfinement —
os.path.realpathest appliqué à la cible et à la racine, puis on vérifie l'inclusion sur les chemins résolus. →path escapes the rootMotifs de refus — appliqués à chaque composant du chemin relatif. →
matches deny pattern '.env'
Résoudre les symlinks avant le contrôle d'inclusion est ce qui fait échouer un lien
sortant ici plutôt qu'au open(). Une racine atteinte via un symlink fonctionne
normalement : elle est résolue une fois au chargement.
Les cibles inexistantes résolvent correctement — realpath résout le préfixe
existant et concatène le reste — donc la création de fichier ne bute pas sur le
contrôle de confinement.
Motifs de refus
Trois niveaux qui s'additionnent, jamais ne se soustraient :
BASE_DENY, en dur dans le code, non désactivable ;denyau niveau de la configuration, appliqué à toutes les racines ;denyd'une racine particulière.
.env .env.* *.pem *.key *.p12 *.pfx
id_rsa* id_ecdsa* id_ed25519*
.ssh .aws .gnupg .netrc .npmrc .pypirc
.git .venv node_modules __pycache__ .mcp-trashLa liste de base reste dans le code délibérément. Si elle passait en configuration,
elle deviendrait retirable — et c'est exactement le genre de ligne qu'on commente
« juste pour ce test » un soir. Une garantie qu'un fichier de config peut désactiver
n'est plus une garantie. extra = "forbid" sur Config fait qu'une tentative de la
redéfinir depuis le TOML échoue au chargement au lieu d'être ignorée en silence.
Ces motifs filtrent aussi les résultats : un fichier refusé n'apparaît ni dans
list_directory ni dans search_files. Le modèle ne sait pas qu'il existe.
Ce que ce filtrage n'est pas
Le filtrage porte sur des noms. Un secret codé en dur dans settings.py est lu
sans obstacle. C'est une heuristique qui attrape les cas usuels, pas une frontière.
La frontière, c'est le choix des racines. Le reste est du confort.
Related MCP server: Filesystem MCP
Configuration
TOML, chemin passé via --config.
audit_log = "/home/[user]/.local/state/loom-fs-mcp/audit.log"
audit_log_max_bytes = 8388608 # 8 Mio par fichier
audit_log_backups = 3 # audit.log.1 … .3, soit 32 Mio au total
max_search_results = 200
max_search_bytes = 67108864 # budget total scanné par recherche
# Motifs refusés sur toutes les racines, en plus de la liste de base du code.
deny = ["*.sqlite3", "*.dump"]
[[roots]]
name = "loom"
path = "/home/[user]/dev/loom/src"
role = "sourcecode"
[[roots]]
name = "loom-docs"
path = "/home/[user]/dev/docs"
role = "reference"
[[roots]]
name = "scratch"
path = "/home/[user]/temp"
role = "workspace"
deny = ["*.tmp"]Champs d'une racine
Champ | Obligatoire | Détail |
| oui |
|
| oui | doit exister et être un répertoire ; |
| oui |
|
| non | sous-ensemble du plafond du rôle ; défaut = plafond |
| non | motifs supplémentaires |
| non | défaut : 1 Mio / 2 Mio / 8 Mio selon le rôle |
Règles de validation
Toute incohérence lève au chargement — le serveur ne démarre pas en mode dégradé.
Un répertoire inexistant est rejeté.
Deux racines dont l'une contient l'autre sont rejetées : la permission effective serait ambiguë.
Deux racines de même nom sont rejetées.
Une racine sans aucune permission est rejetée.
Outils
La surface d'outils s'adapte à la configuration : les outils d'écriture ne sont
enregistrés que si au moins une racine accorde write, ceux de suppression que si
une racine accorde delete. Avec uniquement des racines reference, le modèle voit
quatre outils de lecture et rien d'autre — pas un outil désactivé qu'il pourrait
tenter d'appeler.
Outil | Permission | Détail |
| — | racines, rôles, permissions effectives. À appeler en premier. |
|
| une seule profondeur, entrées filtrées |
|
| UTF-8 ; refuse le binaire (octet nul dans les 4 premiers Kio) et le hors-limite |
|
| voir ci-dessous |
|
| refuse d'écraser sans |
|
| remplace une occurrence exacte et unique ; échoue si 0 ou ≥2 |
|
| source et destination dans la même racine |
|
| déplace vers la corbeille — n'efface rien |
Corbeille
delete déplace la cible vers <racine>/.mcp-trash/<horodatage UTC>/. Rien n'est
effacé, la restauration est manuelle. La corbeille est elle-même dans les motifs de
refus, donc inaccessible via les outils — impossible de vider la corbeille par
l'agent. La racine elle-même ne peut pas être supprimée.
Recherche
search_files(root, pattern, contains=None, max_results=None)
Parcours
security.iter_files() élague avant de descendre : un .venv ou un
node_modules sous la racine coûte un fnmatch sur son nom, pas un parcours
complet. C'est tout le gain — le reste est du détail.
Mesuré sur un arbre de 24 300 fichiers dont 300 utiles, le reste étant du .venv et
du node_modules : 1224 ms → 3,2 ms, soit un facteur 380. La version précédente
utilisait Path.glob() puis filtrait, donc traversait le bruit à chaque appel.
Parcours en largeur, tri par répertoire : l'ordre est déterministe, et un résultat tronqué contient les correspondances les plus proches de la racine plutôt qu'une tranche arbitraire.
Les symlinks sont ignorés en bloc. Ça règle d'un coup le confinement par entrée et les cycles — un lien circulaire ne fait plus tourner la boucle indéfiniment. Un répertoire illisible est sauté, jamais fatal au parcours.
Motifs
** traverse les répertoires, * et ? restent dans un segment, [...] définit
une classe, [!...] sa négation.
Un écart assumé avec glob standard : un motif sans / matche le nom de base à
n'importe quelle profondeur. *.py trouve donc tous les fichiers Python de la
racine, ce que les gens attendent — et ça évite qu'un agent conclue à tort qu'il n'y
en a aucun. Pour la sémantique stricte, préciser pkg/*.py.
Sont rejetés : motif vide, absolu, contenant .., ou avec un [ non fermé. Les
motifs compilés sont mémoïsés.
Filtre contains
Recherche en octets, sans décodage. Avant lecture, trois gardes : la taille est
comparée au max_read_bytes de la racine, les 4 premiers Kio servent à détecter le
binaire, et un budget global max_search_bytes arrête le scan.
Chaque correspondance rend le chemin, le numéro de ligne et un extrait — ce qui évite
un read_file de suivi.
Résultat
{
"matches": [{"path": "loom_ia/agent.py", "line": 483, "preview": "…"}],
"truncated": false,
"examined_files": 36,
"scanned_bytes": 222742,
"skipped_too_large": 0,
"skipped_binary": 0,
"note": null
}truncated et note existent pour une raison précise : une liste tronquée qui se
présente comme complète, c'est un agent qui conclut « ce symbole n'existe nulle
part » alors qu'il a vu les 200 premiers fichiers sur 3 000.
Installation
cd ~/dev/loom-fs-mcp
uv sync
cp roots.example.toml roots.toml # adapter les chemins
uv run pytest # 47 testsVérifier le démarrage en direct avant de brancher le client — ça évite de debugger à travers l'application :
uv run loom-fs-mcp --config roots.tomlLe serveur doit lister ses racines puis annoncer le transport stdio.
Branchement dans Claude Desktop
~/.config/Claude/claude_desktop_config.json :
{
"mcpServers": {
"loom-fs": {
"command": "/home/[user]/dev/loom-fs-mcp/.venv/bin/loom-fs-mcp",
"args": ["--config", "/home/[user]/dev/loom-fs-mcp/roots.toml"],
"env": {
"FASTMCP_SHOW_SERVER_BANNER": "false",
"FASTMCP_CHECK_FOR_UPDATES": "off"
}
}
}
}Fermer complètement l'application avant d'éditer ce fichier : elle le réécrit à la
fermeture et le bloc mcpServers serait perdu.
Journal d'audit
Une ligne ALLOW ou DENY par résolution, avec la racine, le chemin relatif et
l'opération. Rotation par RotatingFileHandler, bornée par audit_log_max_bytes et
audit_log_backups.
Pour purger à chaud, tronquer, ne pas supprimer : le serveur garde le descripteur
ouvert, et après un rm il continue d'écrire dans l'inode supprimé, invisible, sans
rendre l'espace avant l'arrêt du processus.
truncate -s 0 ~/.local/state/loom-fs-mcp/audit.logMême raison si tu passes par logrotate : il faut copytruncate.
La rotation se fait dans le processus qui écrit. Plusieurs instances du serveur sur
le même fichier se marcheraient dessus — un fichier par instance, ou
WatchedFileHandler avec logrotate externe.
Dépannage
L'application reste bloquée au lancement. FastMCP interroge PyPI au démarrage
(check_for_updates, défaut stable) et affiche une bannière. L'appel réseau
s'intercale entre l'exec() et la réponse au initialize ; si le réseau est lent ou
filtré, le client attend. Les deux variables d'environnement ci-dessus les coupent ;
main() les pose aussi par setdefault, donc le serveur est correct même lancé sans
environnement particulier.
FASTMCP_CHECK_FOR_UPDATES attend off — c'est un Literal['stable', 'prerelease', 'off']. Un booléen fait échouer la validation des réglages au démarrage.
uv run comme command. À éviter : uv résout les dépendances, et peut
installer voire télécharger un interpréteur, sans rien écrire sur stdout pendant ce
temps. Pointer directement .venv/bin/loom-fs-mcp supprime toute résolution entre
l'exec() et le handshake. Si uv est indispensable, ajouter --frozen --no-sync.
Le serveur sort immédiatement en erreur. Presque toujours roots.toml : un
répertoire déclaré n'existe pas, ou deux racines sont imbriquées. Le message le dit
explicitement — le lancer en direct pour le lire.
Un refus inattendu. Le message nomme la règle qui a mordu. Le journal d'audit donne le détail.
Limites connues
Pas de quota ni de limitation de débit
Un agent en boucle peut lire des milliers de fichiers.
À noter avant d'en ajouter un : ce n'est pas un contrôle de sécurité. Si une injection détourne l'agent, un plafond à 200 fichiers laisse fuir 200 fichiers. Ce qu'un quota borne, c'est le coût et le rayon d'un emballement.
Et « par session » n'existe pas côté serveur : en stdio, le processus survit à toutes les conversations. Un compteur cumulatif finirait par se déclencher et bloquerait jusqu'au redémarrage de l'application, sans explication. S'il en faut un, ce serait un seau à jetons — régénéré dans le temps — comptant des octets plutôt que des appels : 10 000 petits fichiers ne coûtent rien, 50 fichiers de 8 Mio pèsent 400 Mio.
Pertinent le jour où ce serveur alimente un agent non surveillé. Peu utile en usage interactif.
.git est refusé
Donc pas d'historique : ni « pourquoi ce code est écrit comme ça », ni « qu'est-ce qui a changé dans ce commit ».
L'ouvrir en accès fichier ne rendrait presque rien : les objets sont compressés, donc
le contrôle binaire refuse l'essentiel. Ce qui resterait lisible, c'est .git/config
— qui contient des identifiants quand une remote est en HTTPS — et les hooks, qui sont
du code exécuté à la prochaine opération git.
Un outil git dédié en lecture seule serait la bonne réponse, mais il est plus
coûteux qu'il n'y paraît :
il travaille au niveau du dépôt, donc sur un périmètre plus large que les racines ;
l'historique contourne la liste de refus — un
.envcommité il y a deux ans est toujours dans les objets — il faudrait donc dériver des pathspecs:(exclude,glob)de la même liste, soit une seconde source de vérité à maintenir ;lancer
gitdans un dépôt exécute la configuration de ce dépôt : alias,core.pager,diff.external, filtrestextconvsont autant de commandes shell. Sur un dépôt cloné, c'est une exécution de code arbitraire déclenchée par un outil « lecture seule ». Neutralisation explicite obligatoire.
Écarté pour l'instant : l'archéologie git se fait en session Claude Code, dans le dépôt, où git fonctionne nativement. À reconsidérer si le besoin apparaît en Chat.
Le processus tourne hors VM
Avec les droits de l'utilisateur. Les racines sont la seule barrière réelle : leur choix est la décision de sécurité, tout le reste est de la défense en profondeur.
This server cannot be deployed
Maintenance
Related MCP Connectors
The trust harness for AI agents. Set what an agent can do before it acts.
Securely search and manage workspace context files for AI agents and teams.
Multiple Gmail accounts, editable Google Sheets & Docs for AI agents. Deny-by-default access rules.
Security gateway for AI agents: policy, approval, and audited execution, no secrets shared.
Related MCP Servers
- AlicenseNot gradedqualityCmaintenanceProvides secure, sandboxed file system access for AI assistants to read, write, and manage project files with controlled command execution capabilities, all confined to a designated workspace directory.MIT
- FlicenseNot gradedqualityDmaintenanceProvides secure filesystem access for AI assistants with optimizations like file reading limits and depth-limited traversal to improve token efficiency. It enables AI models to read, write, and search files within explicitly allowed directories while automatically skipping large system folders.3-
- AlicenseNot gradedqualityCmaintenanceProvides a secure, constrained filesystem workspace for LLM agents to manage files, notes, and code artifacts via stdio or remote HTTP. It features granular access controls, including extension whitelisting, storage quotas, and immutable paths for safe automated file operations.BSD 3-Clause
- AlicenseNot gradedqualityBmaintenanceProvides safe filesystem access for AI clients with root confinement, read-only mode, and file operations like read, write, search, copy, move, delete.MIT