diff --git a/.mcp.json b/.mcp.json index f834812..618e7d9 100644 --- a/.mcp.json +++ b/.mcp.json @@ -1,6 +1,6 @@ { "mcpServers": { - "claude-plugins": { + "provectus-claude-plugins-finder": { "command": "node", "args": ["dist/index.js"] } diff --git a/README.md b/README.md index 6af41df..6706d14 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Claude Plugins -A shared repository of plugins for Claude Code. Each plugin packages reusable expertise — agents, skills, prompts, and MCP server configs — into a standard format that can be discovered and consumed through the built-in MCP server. +A shared repository of plugins for Claude Code. Each plugin packages reusable expertise — agents, skills, prompts, and MCP server configs — into a standard format that can be discovered and consumed through the built-in MCP server. Available as an [npm package](https://www.npmjs.com/package/@provectusinc/claude-plugins) for easy integration via `npx`. ## Philosophy @@ -21,6 +21,53 @@ A few principles guide the design: | `kotlin-expert` | Agent | Kotlin 2.0+, coroutines, Spring Boot, domain modeling | | `react-expert` | Agent | React 19+, concurrent rendering, Tailwind, accessibility | +## Installation & Usage + +### Add to Claude Code + +Add it to your project's `.mcp.json` (project-level) or `~/.claude/claude_mcp_settings.json` (global): + +```json +{ + "mcpServers": { + "provectus-claude-plugins-finder": { + "command": "npx", + "args": ["-y", "@provectusinc/claude-plugins@latest"] + } + } +} +``` + +### Using the plugins + +Once connected, three tools become available in your Claude Code sessions: + +| Tool | What it does | Example prompt | +|------------------|-------------------------------------------|-----------------------------------------------| +| `list_plugins` | Browse all plugins, filter by type or tag | *"List all available plugins"* | +| `get_plugin` | Retrieve a plugin's full content | *"Get the python-expert agent prompt"* | +| `search_plugins` | Search plugins by keyword | *"Search for plugins related to code review"* | + +You can ask Claude naturally and it will call the right tool: + +- *"What plugins are available for Python?"* +- *"Show me the react-expert agent"* +- *"Find plugins tagged with 'git'"* + +Claude will use the retrieved plugin content (agent prompts, skills, etc.) to enhance its responses with specialized expertise. + +### Add to other MCP clients + +Any MCP-compatible client can connect to the server via stdio. The server package is `@provectusinc/claude-plugins` (binary name: `claude-plugins`): + +```bash +npx -y @provectusinc/claude-plugins +``` + +Configure your client to spawn this command and communicate over stdin/stdout using the [MCP protocol](https://modelcontextprotocol.io). + +--- + ## Current Repo This repository is both a collection of plugins and an MCP server. The server exposes tools that allow any MCP-compatible client — including other Claude Code sessions — to discover, search, and retrieve plugins programmatically. @@ -63,95 +110,72 @@ Other commands: ```bash pnpm run build # Compile TypeScript (only needed if modifying src/) pnpm run start # Run the compiled MCP server (stdio) -pnpm run serve # Start HTTP server on port 3000 +pnpm run test # Run the test suite ``` If you are only adding or editing plugins (not modifying the MCP server source in `src/`), you do not need to run `pnpm run build`. -### Claude Code Commands +### Testing Locally -These slash commands are available when working in this repo with Claude Code: +#### Run the test suite -| Command | Purpose | -|------------------|--------------------------------------------------------------------------| -| `/update-readme` | Regenerate the Current Plugins table in the README from plugin manifests | - -### MCP Server - -Any agentic tooling that supports the [Model Context Protocol](https://modelcontextprotocol.io) can connect to this server and programmatically list, search, and retrieve plugins — including their full markdown content. +```bash +pnpm run test +``` -#### Available Tools +This runs integration tests that verify tool registration, plugin filtering, search, and error handling using an in-memory MCP transport. -| Tool | Description | Parameters | -|------------------|--------------------------------------------------------------|------------------------------------------------------------| -| `list_plugins` | List all plugins with optional filters | `type?` (`skill`, `agent`, `prompt`), `tag?` (string) | -| `get_plugin` | Get a plugin's full details or a single component's markdown | `name` (string), `component?` (`skill`, `agent`, `prompt`) | -| `search_plugins` | Full-text search across name, description, and tags | `query` (string), `type?` (`skill`, `agent`, `prompt`) | +#### Test the server manually -#### Connect from Claude Code Locally +You can test the stdio server by piping JSON-RPC messages directly: -Add the server to your project's `.mcp.json` (or `~/.claude/claude_mcp_settings.json` for global access): +```bash +# Build first (or use tsx for dev mode) +pnpm run build -```json -{ - "mcpServers": { - "claude-plugins": { - "command": "node", - "args": ["/path/to/claude-plugins/dist/index.js"] - } - } -} +# Send an initialize + list_plugins request +echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}} +{"jsonrpc":"2.0","method":"notifications/initialized"} +{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_plugins","arguments":{}}}' | node dist/index.js ``` -Then in any Claude Code session the three tools above become available automatically. - -#### Connect from other MCP clients +#### Test with Claude Code -The server supports both **stdio** and **HTTP** transports. +The local server is already configured in `.mcp.json`. Start a Claude Code session and the `list_plugins`, `get_plugin`, and `search_plugins` tools will be available. Try asking Claude to "list all available plugins" to verify the connection. -**stdio** (default) — for local MCP clients: +#### Test the npm package before publishing ```bash -node /path/to/claude-plugins/dist/index.js -``` - -Or in dev mode (no build step required): +# Simulate an npm install locally +pnpm pack -```bash -npx tsx /path/to/claude-plugins/src/index.ts +# Test the packed tarball works as a CLI +npx ./provectusinc-claude-plugins-0.0.0-develop.tgz ``` -**HTTP** — for remote access (defaults to port 3000): - -```bash -pnpm run serve # port 3000 (all platforms) +### Claude Code Commands -# Custom port: -# macOS/Linux (bash, zsh, etc.): -PORT=8080 pnpm run serve +These slash commands are available when working in this repo with Claude Code: -# Windows PowerShell: -$env:PORT=8080; pnpm run serve +| Command | Purpose | +|------------------|--------------------------------------------------------------------------| +| `/update-readme` | Regenerate the Current Plugins table in the README from plugin manifests | -# Windows cmd.exe: -set PORT=8080&& pnpm run serve -``` +### MCP Server -This starts an HTTP server with the MCP Streamable HTTP transport at `POST /mcp`. Clients can connect using any MCP-compatible HTTP client: +Any agentic tooling that supports the [Model Context Protocol](https://modelcontextprotocol.io) can connect to this server and programmatically list, search, and retrieve plugins — including their full markdown content. -```bash -curl -X POST http://localhost:3000/mcp \ - -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"1.0.0"}}}' -``` +#### Available Tools -#### Example workflow +| Tool | Description | Parameters | +|------------------|--------------------------------------------------------------|------------------------------------------------------------| +| `list_plugins` | List all plugins with optional filters | `type?` (`skill`, `agent`, `prompt`), `tag?` (string) | +| `get_plugin` | Get a plugin's full details or a single component's markdown | `name` (string), `component?` (`skill`, `agent`, `prompt`) | +| `search_plugins` | Full-text search across name, description, and tags | `query` (string), `type?` (`skill`, `agent`, `prompt`) | -A typical agentic integration pattern: +#### Connect from a local clone -1. Call `list_plugins` or `search_plugins` to discover relevant plugins -2. Call `get_plugin` with `component: "agent"` to retrieve the full markdown prompt -3. Use the returned markdown as a system prompt, agent instruction, or context injection +The repo includes a `.mcp.json` that configures the local MCP server automatically. Just clone, install, build, and start a Claude Code session. ### Contributing diff --git a/package.json b/package.json index a5f4d16..3778f74 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,37 @@ { - "name": "claude-plugins", - "packageManager": "pnpm@10.6.2", - "version": "1.0.0", + "name": "@provectusinc/claude-plugins", + "version": "0.0.0-develop", + "description": "MCP server for discovering and serving Claude Code plugins", + "repository": { + "type": "git", + "url": "https://github.com/provectus/claude-plugins.git" + }, "type": "module", "main": "dist/index.js", + "bin": { + "claude-plugins": "dist/index.js" + }, + "files": [ + "dist", + "plugins" + ], + "keywords": [ + "mcp", + "claude", + "plugins", + "model-context-protocol" + ], + "license": "MIT", + "packageManager": "pnpm@10.6.2", "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "tsx src/index.ts", - "serve": "PORT=3000 node dist/index.js", - "test": "vitest run" + "test": "vitest run", + "prepublishOnly": "pnpm run build" }, "dependencies": { - "@modelcontextprotocol/sdk": "^1.12.1", + "@modelcontextprotocol/sdk": "^1.26.0", "glob": "^11.0.1", "zod": "^3.24.2" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e68934b..8eccebd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,7 +9,7 @@ importers: .: dependencies: '@modelcontextprotocol/sdk': - specifier: ^1.12.1 + specifier: ^1.26.0 version: 1.26.0(zod@3.25.76) glob: specifier: ^11.0.1 diff --git a/src/index.ts b/src/index.ts index fc2aa7b..3923c30 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,83 +1,17 @@ +#!/usr/bin/env node + import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; -import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import { createServer as createHttpServer } from "http"; -import { join } from "path"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; import { loadPlugins } from "./loader.js"; import { createServer } from "./server.js"; -const pluginsDir = join(process.cwd(), "plugins"); +const __dirname = dirname(fileURLToPath(import.meta.url)); +const pluginsDir = join(__dirname, "..", "plugins"); const plugins = loadPlugins(pluginsDir); console.error(`Loaded ${plugins.length} plugins`); -const MAX_BODY_SIZE = 1024 * 1024; // 1 MB - -const portEnv = process.env.PORT; -let port: number | undefined; - -if (portEnv !== undefined) { - const parsed = Number(portEnv); - if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 0) { - console.error(`Invalid PORT value "${portEnv}". Expected a non-negative integer.`); - process.exit(1); - } - port = parsed; -} - -if (port !== undefined) { - const httpServer = createHttpServer(async (req, res) => { - if (req.url === "/mcp" && req.method === "POST") { - const server = createServer(plugins); - const transport = new StreamableHTTPServerTransport({ - sessionIdGenerator: undefined, - }); - - try { - await server.connect(transport); - - const body = await new Promise((resolve, reject) => { - let data = ""; - let size = 0; - req.on("data", (chunk: Buffer) => { - size += chunk.length; - if (size > MAX_BODY_SIZE) { - req.destroy(); - reject(new Error("Request body too large")); - return; - } - data += chunk; - }); - req.on("end", () => resolve(data)); - req.on("error", reject); - }); - - await transport.handleRequest(req, res, JSON.parse(body)); - } catch (err) { - if (!res.headersSent) { - const status = err instanceof Error && err.message === "Request body too large" ? 413 : 400; - res.writeHead(status, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: status === 413 ? "Request body too large" : "Bad request" })); - } - } finally { - transport.close(); - server.close(); - } - } else { - res.writeHead(404, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ error: "Not found" })); - } - }); - - httpServer.listen(port, () => { - console.error(`MCP HTTP server listening on port ${port}`); - }); - - process.on("SIGINT", () => { - httpServer.close(); - process.exit(0); - }); -} else { - const server = createServer(plugins); - const transport = new StdioServerTransport(); - await server.connect(transport); -} +const server = createServer(plugins); +const transport = new StdioServerTransport(); +await server.connect(transport); diff --git a/src/server.ts b/src/server.ts index e7e7d2a..b4e0e54 100644 --- a/src/server.ts +++ b/src/server.ts @@ -19,18 +19,20 @@ export function createServer(plugins: Plugin[]): McpServer { version: "1.0.0", }); - server.tool( + server.registerTool( "list_plugins", - "List available plugins, optionally filtered by component type or tag", { - type: z - .enum(["skill", "agent", "prompt"]) - .optional() - .describe("Filter to plugins that have this component type"), - tag: z - .string() - .optional() - .describe("Filter to plugins that have this tag"), + description: "List available plugins, optionally filtered by component type or tag", + inputSchema: z.object({ + type: z + .enum(["skill", "agent", "prompt"]) + .optional() + .describe("Filter to plugins that have this component type"), + tag: z + .string() + .optional() + .describe("Filter to plugins that have this tag"), + }), }, async ({ type, tag }) => { let results = plugins; @@ -54,17 +56,19 @@ export function createServer(plugins: Plugin[]): McpServer { } ); - server.tool( + server.registerTool( "get_plugin", - "Get a plugin's full details or a specific component's content", { - name: z.string().describe("Plugin name"), - component: z - .enum(["skill", "agent", "prompt"]) - .optional() - .describe( - "If specified, return only this component's content" - ), + description: "Get a plugin's full details or a specific component's content", + inputSchema: z.object({ + name: z.string().describe("Plugin name"), + component: z + .enum(["skill", "agent", "prompt"]) + .optional() + .describe( + "If specified, return only this component's content" + ), + }), }, async ({ name, component }) => { const plugin = plugins.find( @@ -100,15 +104,17 @@ export function createServer(plugins: Plugin[]): McpServer { } ); - server.tool( + server.registerTool( "search_plugins", - "Search plugins by keyword across name, description, and tags", { - query: z.string().describe("Search query"), - type: z - .enum(["skill", "agent", "prompt"]) - .optional() - .describe("Filter to plugins that have this component type"), + description: "Search plugins by keyword across name, description, and tags", + inputSchema: z.object({ + query: z.string().describe("Search query"), + type: z + .enum(["skill", "agent", "prompt"]) + .optional() + .describe("Filter to plugins that have this component type"), + }), }, async ({ query, type }) => { const q = query.toLowerCase();