Skip to content

Latest commit

 

History

History
542 lines (442 loc) · 9.94 KB

File metadata and controls

542 lines (442 loc) · 9.94 KB

Plugin Development Guide

Creating a Plugin

A plugin is a simple JavaScript module that exports tools. Each tool has:

  • Description
  • Input schema (JSON Schema)
  • Execute function

Minimal Plugin

// 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}`;
      }
    }
  }
};

With Initialization

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}`;
      }
    }
  }
};

Input Schema

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
}

Execute Function

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' };
}

Error Handling

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}`);
  }
}

Using Config

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}`;
}

Real-World Example: Weather Plugin

// 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
        }
      ]
    }
  ]
}

Plugin Types

Builtin Plugin

Located in plugins/ directory:

{
  "name": "my-plugin",
  "type": "builtin",
  "module": "my-plugin"  // Loads plugins/my-plugin.js
}

External Plugin

Located anywhere:

{
  "name": "external",
  "type": "external",
  "module": "/absolute/path/to/plugin.js"
}

Best Practices

1. Validate Inputs

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');
  }
  // ...
}

2. Security

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');
  }
}

3. Descriptive Errors

async execute(args, config) {
  try {
    // ...
  } catch (err) {
    throw new Error(`Failed to process ${args.filename}: ${err.message}`);
  }
}

4. Timeouts

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)
    )
  ]);
}

5. Return Structured Data

// 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?

Testing Your Plugin

1. Unit Test

// 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.js

2. Integration Test

Add 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.js

3. With MCP Inspector

mcp-inspector node server.js

Plugin Ideas

  • 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

Common Patterns

Pattern 1: Resource Access

{
  read_resource: {
    inputSchema: {
      type: 'object',
      properties: {
        id: { type: 'string' }
      }
    },
    async execute(args, config) {
      // Fetch resource by ID
      return resource;
    }
  }
}

Pattern 2: CRUD Operations

{
  create: { /* ... */ },
  read: { /* ... */ },
  update: { /* ... */ },
  delete: { /* ... */ }
}

Pattern 3: List + Detail

{
  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 */ };
    }
  }
}

Troubleshooting

Plugin Not Loading

Check:

  • File path is correct in config
  • Plugin exports default object
  • enabled: true in config
  • No syntax errors (check logs)

Tool Not Available

Check:

  • Tool is in plugin's tools object
  • Tool name matches config
  • enabled: true for the tool
  • Plugin loaded successfully

Execution Errors

  • Check input schema matches arguments
  • Add logging: console.error() goes to stderr
  • Test tool in isolation first
  • Validate config options

Advanced: Stateful Plugins

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
        };
      }
    }
  }
};

Resources

Happy plugin development!