Your first plugin
What you will build
Section titled “What you will build”A tool that counts the words in a text file. Once it’s installed, the folder agent calls it when you ask something like “how long is this document?” — you never call it directly.
Create the repository layout
Section titled “Create the repository layout”my-filer-plugins/├── filer-plugins.json└── plugins/ └── word-count/ ├── filer-plugin.json └── tools/ ├── count-words.schema.json └── count-words.jsThe manifest
Section titled “The manifest”plugins/word-count/filer-plugin.json:
{ "name": "word-count", "displayName": "Word Count", "version": "0.1.0", "description": "Counts the words in a text file.", "author": { "name": "you", "url": "https://github.com/you" }, "license": "MIT", "runtime": "node", "engines": { "filer": ">=0.6.0" }, "contributes": { "tools": [ { "id": "count_words", "title": "Count words", "description": "Counts the words in a text file. Use when the user asks how long a document is.", "schema": "./tools/count-words.schema.json", "handler": "./tools/count-words.js" } ] }, "permissions": { "fs": { "read": { "parameter": true, "deny": ["**/.env", "**/.env.*"] } }, "log": true }}id is the MCP tool name the agent calls, so it can’t contain dots. description is what the model reads when deciding whether to call the tool — write it for the model, not for a person browsing a list. permissions.fs.read.parameter: true means the path this tool reads comes from the call’s own arguments, and is checked against the calling agent’s folder at dispatch time; the deny list keeps secret-like files out even then.
The schema
Section titled “The schema”plugins/word-count/tools/count-words.schema.json:
{ "type": "object", "properties": { "path": { "type": "string", "format": "file-path", "description": "Absolute path of the text file to count" } }, "required": ["path"]}format: "file-path" marks path as the argument the app’s permission checks apply to — it’s what ties this schema field back to the fs.read permission above.
The handler
Section titled “The handler”plugins/word-count/tools/count-words.js:
export default async function countWords(params, ctx) { const text = (await ctx.fs.read(params.path)).toString('utf8'); const words = text.split(/\s+/).filter(Boolean).length; ctx.log.info(`counted ${words} words in ${params.path}`); return `${words} words in ${params.path}`;}A string return reaches the agent verbatim; anything else is JSON-stringified first. Either export default or module.exports works as the handler’s export.
The catalog
Section titled “The catalog”filer-plugins.json, at the root of the repository:
{ "name": "my-filer-plugins", "displayName": "My Filer Plugins", "owner": { "name": "you", "url": "https://github.com/you" }, "version": "0.1.0", "plugins": [ { "name": "word-count", "source": "./plugins/word-count", "description": "Counts the words in a text file.", "version": "0.1.0", "category": "utility", "tags": ["files"] } ]}source is a path relative to the repository root — that’s how the catalog points at the plugin folder above.
Install it
Section titled “Install it”Push the repository to a public GitHub repo. Then, in Filer, open Connect and click the Manage sources button at the bottom of the plugin list — the Plugin Sources panel opens. Paste the repository URL into the URL box and click Add. Your plugin now appears in the list (the All filter); select it and click Install. Open a chat in a folder with a text file, and ask how many words a document has.
Change it
Section titled “Change it”Edit a handler, commit, and push again. Back in Filer, click Check for updates at the bottom of the plugin list, switch to the Updates filter, select your plugin, and click Update. See Testing locally for the full loop while you iterate.
- Guides › Tools — result envelopes, returning images
- Guides › Actions — a right-click entry that calls this same tool
- Publishing — sharing the repository beyond yourself
