Complete guide for building and distributing custom MCP (Model Context Protocol) connectors as Claude Desktop Extension bundles (.mcpb files). Use this skill whenever a user wants to: create a custom MCP server, package an MCP for Claude Desktop, build a connector for an API or internal tool, distribute an MCP to their team or company, create or fix a manifest.json for a Claude extension, package a .mcpb or .dxt file, set up user_config for API keys, or publish a connector. Also triggers for: "how do I build a Claude connector", "how do I share my MCP with my team", "how do I package my MCP server", or any variation of creating distributable Claude Desktop extensions. ALWAYS use this skill when building or packaging MCPs — it contains the correct spec version, CLI commands, and distribution steps.
npx claudepluginhub p3nj/p3nj-market --plugin mcp-connector-builderThis skill uses the workspace's default tool permissions.
This skill guides you through the full workflow: write an MCP server → package it as a Claude Desktop Extension bundle (`.mcpb`) → distribute to your team.
Guides Next.js Cache Components and Partial Prerendering (PPR) with cacheComponents enabled. Implements 'use cache', cacheLife(), cacheTag(), revalidateTag(), static/dynamic optimization, and cache debugging.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Generates original PNG/PDF visual art via design philosophy manifestos for posters, graphics, and static designs on user request.
This skill guides you through the full workflow: write an MCP server → package it as a Claude Desktop Extension bundle (.mcpb) → distribute to your team.
Key terminology:
.mcpb. Formerly called DXT (.dxt). Both extensions install in Claude Desktop, but .mcpb + manifest_version: "0.3" is the current standard.Use Node.js. It ships with Claude Desktop so end users need zero additional setup. Python requires them to have Python installed — adds friction.
my-connector/
├── manifest.json ← Required. Bundle metadata & config.
├── package.json
├── icon.png ← 512×512px PNG recommended (256×256 minimum)
└── servers/
└── server.mjs ← MCP server entry point
mkdir my-connector && cd my-connector
npm init -y
npm install @modelcontextprotocol/sdk zod
In package.json, set "type": "module" so ES module imports work.
Use McpServer + registerTool (the modern API). Validate all inputs with Zod .strict().
#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
// Read credentials from env (injected by manifest at runtime)
const API_KEY = process.env.MY_API_KEY || '';
// Validate required credentials at startup
if (!API_KEY) {
console.error('ERROR: MY_API_KEY is required. Configure it in connector settings.');
process.exit(1);
}
const server = new McpServer({ name: 'my-connector', version: '1.0.0' });
server.registerTool('tool_name', {
title: 'Human-readable title',
description: `What this tool does and when Claude should use it.
Include examples of good inputs so Claude uses it correctly.`,
inputSchema: z.object({
query: z.string().describe('What to search for. Example: "error logs from last hour"'),
limit: z.number().int().min(1).max(100).default(20).describe('Max results to return'),
}).strict(),
annotations: {
readOnlyHint: true, // true = no side effects (reads only)
destructiveHint: false, // false = won't delete/modify data
idempotentHint: true, // true = safe to retry
openWorldHint: true, // true = calls external APIs
},
}, async (params) => {
try {
const result = await callYourApi(params.query, params.limit);
return { content: [{ type: 'text', text: JSON.stringify(result) }] };
} catch (err) {
return {
content: [{ type: 'text', text: `Error: ${err.message}` }],
isError: true, // tells Claude to surface this as an error
};
}
});
const transport = new StdioServerTransport();
await server.connect(transport);
Good tool design:
This is the most critical file. Use manifest_version: "0.3" — not dxt_version.
{
"manifest_version": "0.3",
"name": "my-connector",
"display_name": "My Connector",
"version": "1.0.0",
"description": "One-line description shown in Claude Desktop.",
"long_description": "Detailed description in markdown. Explain what APIs this connects to and what problems it solves.",
"author": {
"name": "Your Name",
"url": "https://yoursite.com"
},
"icon": "icon.png",
"homepage": "https://yoursite.com",
"license": "MIT",
"keywords": ["keyword1", "keyword2"],
"tools": [
{
"name": "tool_name",
"description": "What this tool does."
}
],
"server": {
"type": "node",
"entry_point": "servers/server.mjs",
"mcp_config": {
"command": "node",
"args": ["${__dirname}/servers/server.mjs"],
"env": {
"MY_API_KEY": "${user_config.api_key}",
"MY_SITE": "${user_config.site}"
}
}
},
"user_config": {
"api_key": {
"type": "string",
"title": "API Key",
"description": "Your API key. Found at Organization Settings → API Keys.",
"sensitive": true,
"required": true
},
"site": {
"type": "string",
"title": "Site",
"description": "Your site URL, e.g. app.example.com",
"sensitive": false,
"required": false,
"default": "app.example.com"
}
},
"compatibility": {
"runtimes": { "node": ">=18" }
}
}
| Field | Required | Notes |
|---|---|---|
manifest_version | Yes | Must be "0.3" |
name | Yes | Machine-readable, no spaces |
display_name | No | Human-friendly name for the UI |
version | Yes | Semver, e.g. "1.0.0" |
description | Yes | Shown in Claude Desktop UI |
author.name | Yes | — |
server | Yes | Runtime config (see below) |
tools | Recommended | Declare all tools statically. Required for directory submission. |
user_config | Recommended | Declare all user-supplied values (API keys, sites, etc.) |
server.type options: "node" (recommended), "python", "binary", "uv" (experimental)
Template variables in args / env:
${__dirname} — extension install directory${user_config.FIELD} — value the user typed in settings${HOME}, ${USER} — system environment variablesuser_config field types: string, number, boolean, directory, file, enum
sensitive: true → stored in OS keychain, masked in UI (use for API keys/passwords)required: true → extension won't enable until the user fills this indefault → pre-fills the field in settings UI# Install the mcpb CLI (one-time global install)
npm install -g @anthropic-ai/mcpb
# Always validate first — catches spec errors before packaging
mcpb validate manifest.json
# Package into a .mcpb bundle
mcpb pack
Output file: <name>-<version>.mcpb
The bundle is a zip of your project files + node_modules (production deps). Dev dependencies and files matching .mcpbignore / .npmignore are excluded automatically.
Important:
mcpbv2.1.2+ is required formanifest_version: "0.3". The olderdxtCLI (v0.2.6) only supports the olddxt_versionkey and will reject the current spec. Always usemcpb.
.mcpb file, oruser_config fields when promptedCheck Claude Desktop logs if something doesn't work:
~/Library/Logs/Claude/Share the .mcpb file via Slack, email, or shared drive. Team members double-click to install. Each person installs individually on their own machine.
Available on Team and Enterprise plans.
.mcpb as a private/custom extensionWarning: Enabling the allowlist removes all previously-installed extensions from team members' Claude Desktop. Give everyone a heads-up before enabling.
macOS MDM profiles and Windows Group Policy can pre-install approved extensions automatically. Requires Claude Desktop v0.13.91+.
Increment version in both manifest.json and package.json, repack, and redistribute. For Option B (allowlist), upload the new .mcpb and existing users can update from their connector settings.
| Symptom | Cause | Fix |
|---|---|---|
dxt_version: Required + manifest_version: Unrecognized | Old dxt CLI | npm install -g @anthropic-ai/mcpb |
| Server starts but tools aren't available | Wrong transport | Use StdioServerTransport, not HTTP |
| Tools fail silently | Env vars not injected | Check user_config → env mappings in manifest |
| Bundle is very large (>20 MB) | Dev deps or test files included | Add .mcpbignore, run npm install --production before packing |
| Credentials not saved between restarts | Hardcoded in server file | Read from process.env — manifest injects them at startup |
mcpb validate passes but install fails | entry_point path wrong | Verify the path in server.entry_point matches your actual file |
The smallest possible connector — one tool, reads one env var:
servers/server.mjs
#!/usr/bin/env node
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
const server = new McpServer({ name: 'hello-connector', version: '1.0.0' });
server.registerTool('say_hello', {
title: 'Say Hello',
description: 'Returns a greeting.',
inputSchema: z.object({ name: z.string().describe('Name to greet') }).strict(),
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
}, async ({ name }) => ({
content: [{ type: 'text', text: `Hello, ${name}!` }],
}));
await server.connect(new StdioServerTransport());
manifest.json
{
"manifest_version": "0.3",
"name": "hello-connector",
"display_name": "Hello Connector",
"version": "1.0.0",
"description": "A minimal example connector.",
"author": { "name": "Your Name" },
"server": {
"type": "node",
"entry_point": "servers/server.mjs",
"mcp_config": {
"command": "node",
"args": ["${__dirname}/servers/server.mjs"]
}
},
"tools": [{ "name": "say_hello", "description": "Returns a greeting." }]
}
Then: mcpb pack → hello-connector-1.0.0.mcpb → double-click to install.