This guide walks you through building a community plugin for mcp-me. Plugins extend the MCP server with dynamic data from external services.
- Built-in plugins — Shipped with mcp-me (GitHub, Spotify, LinkedIn)
- npm plugins — Community packages named
mcp-me-plugin-<name> - Local plugins — Custom
.ts/.jsfiles referenced by path
Every plugin must implement the McpMePlugin interface:
import type { McpMePlugin, PluginResource, PluginTool, PluginPrompt } from "mcp-me";
export interface McpMePlugin {
// Unique identifier, e.g. "goodreads"
name: string;
// Human-readable description
description: string;
// Semver version string
version: string;
// Called once with user config from .mcp-me.yaml
initialize(config: Record<string, unknown>): Promise<void>;
// MCP Resources this plugin exposes
getResources(): PluginResource[];
// MCP Tools this plugin provides
getTools(): PluginTool[];
// Optional: MCP Prompts
getPrompts?(): PluginPrompt[];
// Optional: cleanup on server shutdown
destroy?(): Promise<void>;
}mkdir mcp-me-plugin-goodreads
cd mcp-me-plugin-goodreads
npm init -y
npm install mcp-me zod
npm install -D typescript @types/node tsupUpdate package.json:
{
"name": "mcp-me-plugin-goodreads",
"version": "0.1.0",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"keywords": ["mcp-me", "mcp-me-plugin", "goodreads"],
"peerDependencies": {
"mcp-me": ">=0.1.0"
}
}Important: Use
mcp-meas apeerDependency, not a regular dependency.
// src/schema.ts
import { z } from "zod";
export const goodreadsConfigSchema = z.object({
enabled: z.boolean().optional().default(true),
user_id: z.string().describe("Your Goodreads user ID"),
api_key_env: z.string().optional().describe("Env var for Goodreads API key"),
});
export type GoodreadsConfig = z.infer<typeof goodreadsConfigSchema>;// src/index.ts
import { z } from "zod";
import type { McpMePlugin, PluginResource, PluginTool } from "mcp-me";
import { goodreadsConfigSchema, type GoodreadsConfig } from "./schema.js";
class GoodreadsPlugin implements McpMePlugin {
name = "goodreads";
description = "Shows reading list and book reviews from Goodreads.";
version = "0.1.0";
private config!: GoodreadsConfig;
async initialize(rawConfig: Record<string, unknown>): Promise<void> {
this.config = goodreadsConfigSchema.parse(rawConfig);
}
getResources(): PluginResource[] {
return [
{
name: "goodreads-reading-list",
uri: "me://goodreads/reading-list",
title: "Reading List",
description: "Current reading list from Goodreads",
read: async () => {
// Fetch from Goodreads API or scrape
const books = await this.fetchReadingList();
return JSON.stringify(books, null, 2);
},
},
];
}
getTools(): PluginTool[] {
return [
{
name: "get_goodreads_books",
title: "Get Books",
description: "Get books from a specific shelf",
inputSchema: z.object({
shelf: z.string().optional().describe("Shelf name, e.g. 'read', 'to-read'"),
}),
annotations: { readOnlyHint: true },
execute: async (input) => {
const shelf = (input.shelf as string) ?? "read";
const books = await this.fetchShelf(shelf);
return JSON.stringify(books, null, 2);
},
},
];
}
private async fetchReadingList() {
// Your implementation here
return [];
}
private async fetchShelf(_shelf: string) {
// Your implementation here
return [];
}
}
// IMPORTANT: Export a default factory function
export default function createPlugin(): McpMePlugin {
return new GoodreadsPlugin();
}Your plugin's main entry point must export a default function (or named createPlugin) that returns a McpMePlugin instance:
// ✅ Correct
export default function createPlugin(): McpMePlugin {
return new MyPlugin();
}
// ✅ Also correct
export function createPlugin(): McpMePlugin {
return new MyPlugin();
}
// ❌ Wrong — don't export a class or instance directly
export default new MyPlugin();npx tsup src/index.ts --format esm --dts
npm publishnpm install mcp-me-plugin-goodreads# .mcp-me.yaml
plugins:
goodreads:
enabled: true
user_id: "12345"The plugin is automatically discovered via the mcp-me-plugin- naming convention.
Resources expose static or semi-static data. They should not perform heavy computation.
interface PluginResource {
name: string; // Unique name, e.g. "goodreads-reading-list"
uri: string; // URI pattern: "me://<plugin>/<resource>"
title: string; // Human-readable title
description: string; // What this resource provides
mimeType?: string; // Default: "application/json"
read: () => Promise<string>; // Returns the resource content
}URI convention: Always use me://<plugin-name>/<resource-name>.
Tools perform actions or queries. They accept input and return text.
interface PluginTool {
name: string; // Unique name, e.g. "get_goodreads_books"
title: string; // Human-readable title
description: string; // What this tool does
inputSchema: z.ZodType; // Zod schema for input validation
annotations?: { // Behavioral hints
readOnlyHint?: boolean;
destructiveHint?: boolean;
idempotentHint?: boolean;
};
execute: (input: Record<string, unknown>) => Promise<string>;
}Naming convention: Use snake_case prefixed with your plugin context, e.g. get_goodreads_books.
Prompts are optional reusable templates.
interface PluginPrompt {
name: string;
title: string;
description: string;
argsSchema?: z.ZodType;
generate: (args: Record<string, unknown>) => { role: "user" | "assistant"; content: string }[];
}- Validate config with Zod — Always parse user config through a Zod schema in
initialize() - Use
_envsuffix for secrets — Reference environment variables, never store secrets in YAML - Handle errors gracefully — Return meaningful error messages, don't crash the server
- Cache when possible — Avoid excessive API calls; cache responses with a reasonable TTL
- Be read-only — Set
readOnlyHint: trueon tools that don't modify external state - Document everything — Include a
README.mdwith config options, required permissions, and examples - Use semantic versioning — Follow semver for your plugin versions
- Add the
mcp-me-pluginkeyword — Helps with npm discoverability
The fastest way to start is with the built-in scaffolding command:
mcp-me create plugin myserviceThis creates src/plugins/myservice/schema.ts and src/plugins/myservice/index.ts with ready-to-edit templates. Then:
- Edit the schema and index files — implement your API calls
- Register in
src/plugin-engine/loader.ts(one import + one registry entry) - Run
npm test— the plugin harness validates it automatically
Every registered built-in plugin is automatically tested by tests/plugins/plugin-harness.test.ts:
- Validates factory returns a valid
McpMePlugin(name, description, version) - Initializes with minimal config
- Verifies
getResources()andgetTools()return arrays with required fields - Checks all resource URIs follow
me://convention - Verifies every plugin directory in
src/plugins/is registered - No manual test setup needed — just register and it's covered
| Plugin | Resources | Tools | Auth |
|---|---|---|---|
| github | profile, repos, activity, languages | get_github_repos |
Optional token |
| spotify | now playing, top artists, top tracks, recently played | get_spotify_top, get_spotify_now_playing |
OAuth (refresh token) |
| profile, experience, education, skills | search_linkedin_data |
Data export JSON | |
| wakatime | stats, languages, activity | get_wakatime_stats |
Optional API key |
| devto | profile, articles | get_devto_articles |
Optional API key |
| bluesky | profile, feed | get_bluesky_posts |
None |
| hackernews | profile, stories | get_hn_stories |
None |
| profile, posts | get_reddit_karma |
None | |
| gitlab | profile, projects, activity | get_gitlab_projects |
Optional token |
| mastodon | profile, toots | get_mastodon_posts |
None |
| youtube | channel, videos | get_youtube_videos |
Optional API key |
| lastfm | profile, recent, top artists | get_lastfm_now_playing, get_lastfm_top |
Optional API key |
| steam | profile, games | get_steam_recent_games, get_steam_playtime |
Optional API key |
You can test your plugin locally without publishing:
# .mcp-me.yaml
plugins:
my-plugin:
enabled: true
path: "/absolute/path/to/my-plugin/dist/index.js"
# ... your config options- Open an issue with the
plugin-requestlabel - Check existing built-in plugins for reference
- Join the discussion in GitHub Discussions