Skip to content

Your first plugin

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.

my-filer-plugins/
├── filer-plugins.json
└── plugins/
└── word-count/
├── filer-plugin.json
└── tools/
├── count-words.schema.json
└── count-words.js

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.

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.

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.

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.

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.

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.