A plugin is a simple JavaScript module that exports tools. Each tool has:
- Description
- Input schema (JSON Schema)
- Execute function
// plugins/my-plugin.js
export default {
tools: {
my_tool: {
description: 'A simple tool',
inputSchema: {
type: 'object',
properties: {
message: {
type: 'string',
description: 'A message'
}
},
required: ['message']
},
async execute(args, config) {
return `You said: ${args.message}`;
}
}
}
};export default {
// Called once when plugin loads
async init(pluginConfig) {
this.apiKey = pluginConfig.apiKey;
console.log('Plugin initialized with config:', pluginConfig);
},
tools: {
api_call: {
description: 'Call an API',
inputSchema: {
type: 'object',
properties: {
endpoint: { type: 'string' }
},
required: ['endpoint']
},
async execute(args, toolConfig) {
// Use this.apiKey from init
// Use toolConfig for tool-specific settings
return `Called ${args.endpoint}`;
}
}
}
};Use JSON Schema to define inputs:
inputSchema: {
type: 'object',
properties: {
// String input
name: {
type: 'string',
description: 'User name'
},
// Number input
age: {
type: 'number',
description: 'User age',
minimum: 0,
maximum: 150
},
// Boolean input
active: {
type: 'boolean',
description: 'Is active?'
},
// Array input
tags: {
type: 'array',
items: { type: 'string' },
description: 'List of tags'
},
// Object input
settings: {
type: 'object',
properties: {
theme: { type: 'string' }
}
},
// Enum (choices)
status: {
type: 'string',
enum: ['active', 'inactive', 'pending']
}
},
required: ['name', 'age'] // Required fields
}The execute function receives two arguments:
async execute(args, config) {
// args: User-provided arguments (validated against inputSchema)
// config: Tool-specific config from config.json
// Return a string or object
return { success: true, data: 'result' };
}async execute(args, config) {
if (!args.path) {
throw new Error('Path is required');
}
try {
// Do something
return 'Success';
} catch (err) {
throw new Error(`Operation failed: ${err.message}`);
}
}Tool-specific config from config.json:
{
"tools": [
{
"name": "my_tool",
"enabled": true,
"customOption": "value",
"apiKey": "secret"
}
]
}Access in execute:
async execute(args, config) {
const apiKey = config.apiKey;
const option = config.customOption;
// Use them
return `Called with ${option}`;
}// plugins/weather.js
import { request } from 'https';
function fetchWeather(city, apiKey) {
return new Promise((resolve, reject) => {
const url = `https://api.weatherapi.com/v1/current.json?key=${apiKey}&q=${city}`;
request(url, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
if (res.statusCode === 200) {
resolve(JSON.parse(data));
} else {
reject(new Error(`API returned ${res.statusCode}`));
}
});
}).on('error', reject).end();
});
}
export default {
async init(config) {
if (!config.apiKey) {
throw new Error('Weather plugin requires apiKey in config');
}
this.apiKey = config.apiKey;
},
tools: {
get_weather: {
description: 'Get current weather for a city',
inputSchema: {
type: 'object',
properties: {
city: {
type: 'string',
description: 'City name (e.g., "London", "New York")'
},
units: {
type: 'string',
enum: ['metric', 'imperial'],
description: 'Temperature units (optional, default: metric)'
}
},
required: ['city']
},
async execute(args, config) {
const { city, units = 'metric' } = args;
const data = await fetchWeather(city, this.apiKey);
return {
city: data.location.name,
country: data.location.country,
temperature: data.current.temp_c,
condition: data.current.condition.text,
humidity: data.current.humidity,
windSpeed: data.current.wind_kph
};
}
}
}
};Config:
{
"plugins": [
{
"name": "weather",
"type": "builtin",
"module": "weather",
"enabled": true,
"apiKey": "your-api-key-here",
"tools": [
{
"name": "get_weather",
"enabled": true
}
]
}
]
}Located in plugins/ directory:
{
"name": "my-plugin",
"type": "builtin",
"module": "my-plugin" // Loads plugins/my-plugin.js
}Located anywhere:
{
"name": "external",
"type": "external",
"module": "/absolute/path/to/plugin.js"
}Even though JSON Schema validates types, add business logic validation:
async execute(args, config) {
if (args.age < 0) {
throw new Error('Age cannot be negative');
}
// ...
}Sanitize user inputs:
async execute(args, config) {
// Don't allow path traversal
const safePath = args.path.replace(/\.\./g, '');
// Check allowed paths
if (config.allowedPaths && !config.allowedPaths.some(p => safePath.startsWith(p))) {
throw new Error('Path not allowed');
}
}async execute(args, config) {
try {
// ...
} catch (err) {
throw new Error(`Failed to process ${args.filename}: ${err.message}`);
}
}For long operations:
async execute(args, config) {
const timeout = config.timeout || 30000;
return Promise.race([
doLongOperation(args),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
)
]);
}// Good: Structured response
return {
status: 'success',
data: { ... },
timestamp: Date.now()
};
// Also good: Simple string
return 'Operation completed successfully';
// Avoid: Unstructured data
return result; // What is result?// test-plugin.js
import plugin from './plugins/my-plugin.js';
const args = { message: 'Hello' };
const config = {};
const result = await plugin.tools.my_tool.execute(args, config);
console.log('Result:', result);node test-plugin.jsAdd to config.json and test via MCP:
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"my_tool","arguments":{"message":"test"}}}' | node server.jsmcp-inspector node server.js- Database: Query SQL databases
- API Clients: GitHub, Slack, Discord, etc.
- File Formats: Parse CSV, XML, YAML
- Image Processing: Resize, convert, metadata
- Crypto: Hashing, encryption, signing
- Date/Time: Parse, format, timezone conversions
- Text Processing: Regex, markdown, templates
- System: Process management, system info
- Network: Ping, traceroute, DNS lookup
- Git: Repository operations
{
read_resource: {
inputSchema: {
type: 'object',
properties: {
id: { type: 'string' }
}
},
async execute(args, config) {
// Fetch resource by ID
return resource;
}
}
}{
create: { /* ... */ },
read: { /* ... */ },
update: { /* ... */ },
delete: { /* ... */ }
}{
list_items: {
async execute(args, config) {
return [{ id: 1, name: 'Item 1' }, /* ... */];
}
},
get_item: {
inputSchema: {
properties: {
id: { type: 'number' }
}
},
async execute(args, config) {
return { id: args.id, /* full details */ };
}
}
}Check:
- File path is correct in config
- Plugin exports default object
enabled: truein config- No syntax errors (check logs)
Check:
- Tool is in plugin's
toolsobject - Tool name matches config
enabled: truefor the tool- Plugin loaded successfully
- Check input schema matches arguments
- Add logging:
console.error()goes to stderr - Test tool in isolation first
- Validate config options
Maintain state across tool calls:
export default {
async init(config) {
this.cache = new Map();
this.requestCount = 0;
},
tools: {
cached_fetch: {
async execute(args, config) {
this.requestCount++;
if (this.cache.has(args.key)) {
return this.cache.get(args.key);
}
const result = await fetch(args.url);
this.cache.set(args.key, result);
return result;
}
},
get_stats: {
async execute(args, config) {
return {
requests: this.requestCount,
cacheSize: this.cache.size
};
}
}
}
};Happy plugin development!