Generated banner for Build Mcpb
Generated logo for Build Mcpb

Build Mcpb

AnthropicOfficialPublished by Anthropic

  • Skill
  • Tool integration
  • Automation and integrations
  • Apache-2.0
  • Low risk

From the author

This skill should be used when the user wants to "package an MCP server", "bundle an MCP", "make an MCPB", "ship a local MCP server", "distribute a local MCP", discusses ".mcpb files", mentions bundling a Node or Python runtime with their MCP server, or needs an MCP server that interacts with the local filesystem, desktop apps, or OS and must be installable without the user having Node/Python set up.
— the description by Anthropic

AutoGineer summary

Written by AutoGineer from the skill’s own files, not by the author

MCPB is the standard format for distributing local MCP servers that require direct access to a user's machine, such as filesystem operations, desktop application control, or local hardware interaction. By bundling the server code with its necessary runtime (Node or Python), MCPB allows users to install and run these servers without needing to manage local toolchains or environment configurations. The process involves creating a manifest file, bundling the server logic, and using the MCPB CLI to package the final archive.

What it does

  • Packages MCP server code and runtimes into a single .mcpb archive.
  • Defines install-time configuration schemas for user-provided settings.
  • Provides a manifest-driven approach to managing server identity and compatibility.
  • Enables distribution of local-first MCP tools that do not rely on remote HTTP endpoints.
  • Supports native OS file and directory pickers for user configuration.
  • Validates server manifests against official schema requirements.

When to use it

  • When your MCP server needs to interact with local files, OS APIs, or desktop applications.
  • When you want to distribute a local MCP server that works out-of-the-box without requiring the user to install Node or Python.
  • When you need to provide install-time configuration options to the end user via a GUI.
  • When you are building a tool that requires local-first execution rather than a remote HTTP-based architecture.

Advantages

  • Simplifies distribution by eliminating local environment setup for the end user.
  • Provides a consistent, standardized packaging format for local MCP servers.
  • Allows for secure, user-approved configuration of local paths and credentials.
  • Enables native integration with host UI components like file pickers.

Usage examples

“How do I package my local Node-based MCP server into an .mcpb file?”
The agent guides you through creating a manifest.json, bundling your dependencies with esbuild, and running the 'npx @anthropic-ai/mcpb pack' command.
“I need to add a directory picker to my MCP server's installation settings.”
The agent explains how to define a 'user_config' field with 'type: directory' in your manifest.json, which renders a native folder picker for the user.

Install

  1. Download the skill as a ZIP:

    https://autogineer.com/agent-skills/anthropics/build-mcpb/download
  2. Unzip it into the cross-client skills folder of your project:

    mkdir -p .agents/skills && unzip build-mcpb.zip -d .agents/skills

Documentation by Anthropic

Build an MCPB (Bundled Local MCP Server)

MCPB is a local MCP server packaged with its runtime. The user installs one file; it runs without needing Node, Python, or any toolchain on their machine. It's the sanctioned way to distribute local MCP servers.

MCPB is the secondary distribution path. Anthropic recommends remote MCP servers for directory listing — see https://claude.com/docs/connectors/building/what-to-build.

Use MCPB when the server must run on the user's machine — reading local files, driving a desktop app, talking to localhost services, OS-level APIs. If your server only hits cloud APIs, you almost certainly want a remote HTTP server instead (see build-mcp-server). Don't pay the MCPB packaging tax for something that could be a URL.


What an MCPB bundle contains

my-server.mcpb              (zip archive)
├── manifest.json           ← identity, entry point, config schema, compatibility
├── server/                 ← your MCP server code
│   ├── index.js
│   └── node_modules/       ← bundled dependencies (or vendored)
└── icon.png

The host reads manifest.json, launches server.mcp_config.command as a stdio MCP server, and pipes messages. From your code's perspective it's identical to a local stdio server — the only difference is packaging.


Manifest

{
  "$schema": "https://raw.githubusercontent.com/anthropics/mcpb/main/schemas/mcpb-manifest-v0.4.schema.json",
  "manifest_version": "0.4",
  "name": "local-files",
  "version": "0.1.0",
  "description": "Read, search, and watch files on the local filesystem.",
  "author": { "name": "Your Name" },
  "server": {
    "type": "node",
    "entry_point": "server/index.js",
    "mcp_config": {
      "command": "node",
      "args": ["${__dirname}/server/index.js"],
      "env": {
        "ROOT_DIR": "${user_config.rootDir}"
      }
    }
  },
  "user_config": {
    "rootDir": {
      "type": "directory",
      "title": "Root directory",
      "description": "Directory to expose. Defaults to ~/Documents.",
      "default": "${HOME}/Documents",
      "required": true
    }
  },
  "compatibility": {
    "claude_desktop": ">=1.0.0",
    "platforms": ["darwin", "win32", "linux"]
  }
}

server.type — node, python, or binary. Informational; the actual launch comes from mcp_config.

server.mcp_config — the literal command/args/env to spawn. Use ${__dirname} for bundle-relative paths and ${user_config.<key>} to substitute install-time config. There's no auto-prefix — the env var names your server reads are exactly what you put in env.

user_config — install-time settings surfaced in the host's UI. type: "directory" renders a native folder picker. sensitive: true stores in OS keychain. See references/manifest-schema.md for all fields.


Server code: same as local stdio

The server itself is a standard stdio MCP server. Nothing MCPB-specific in the tool logic.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { readFile, readdir } from "node:fs/promises";
import { join } from "node:path";
import { homedir } from "node:os";

// ROOT_DIR comes from what you put in manifest's server.mcp_config.env — no auto-prefix
const ROOT = (process.env.ROOT_DIR ?? join(homedir(), "Documents"));

const server = new McpServer({ name: "local-files", version: "0.1.0" });

server.registerTool(
  "list_files",
  {
    description: "List files in a directory under the configured root.",
    inputSchema: { path: z.string().default(".") },
    annotations: { readOnlyHint: true },
  },
  async ({ path }) => {
    const entries = await readdir(join(ROOT, path), { withFileTypes: true });
    const list = entries.map(e => ({ name: e.name, dir: e.isDirectory() }));
    return { content: [{ type: "text", text: JSON.stringify(list, null, 2) }] };
  },
);

server.registerTool(
  "read_file",
  {
    description: "Read a file's contents. Path is relative to the configured root.",
    inputSchema: { path: z.string() },
    annotations: { readOnlyHint: true },
  },
  async ({ path }) => {
    const text = await readFile(join(ROOT, path), "utf8");
    return { content: [{ type: "text", text }] };
  },
);

const transport = new StdioServerTransport();
await server.connect(transport);

Sandboxing is entirely your job. There is no manifest-level sandbox — the process runs with full user privileges. Validate paths, refuse to escape ROOT, allowlist spawns. See references/local-security.md.

Before hardcoding ROOT from a config env var, check if the host supports roots/list — the spec-native way to get user-approved directories. See references/local-security.md for the pattern.


Build pipeline

Node
npm install
npx esbuild src/index.ts --bundle --platform=node --outfile=server/index.js
# or: copy node_modules wholesale if native deps resist bundling
npx @anthropic-ai/mcpb pack

mcpb pack zips the directory and validates manifest.json against the schema.

Python
pip install -t server/vendor -r requirements.txt
npx @anthropic-ai/mcpb pack

Vendor dependencies into a subdirectory and prepend it to sys.path in your entry script. Native extensions (numpy, etc.) must be built for each target platform — avoid native deps if you can.


MCPB has no sandbox — security is on you

Unlike mobile app stores, MCPB does NOT enforce permissions. The manifest has no permissions block — the server runs with full user privileges. references/local-security.md is mandatory reading, not optional. Every path must be validated, every spawn must be allowlisted, because nothing stops you at the platform level.

If you came here expecting filesystem/network scoping from the manifest: it doesn't exist. Build it yourself in tool handlers.

If your server's only job is hitting a cloud API, stop — that's a remote server wearing an MCPB costume. The user gains nothing from running it locally, and you're taking on local-security burden for no reason.


MCPB + UI widgets

MCPB servers can serve UI resources exactly like remote MCP apps — the widget mechanism is transport-agnostic. A local file picker that browses the actual disk, a dialog that controls a native app, etc.

Widget authoring is covered in the build-mcp-app skill; it works the same here. The only difference is where the server runs.


Testing

# Interactive manifest creation (first time)
npx @anthropic-ai/mcpb init

# Run the server directly over stdio, poke it with the inspector
npx @modelcontextprotocol/inspector node server/index.js

# Validate manifest against schema, then pack
npx @anthropic-ai/mcpb validate
npx @anthropic-ai/mcpb pack

# Sign for distribution
npx @anthropic-ai/mcpb sign dist/local-files.mcpb

# Install: drag the .mcpb file onto Claude Desktop

Test on a machine without your dev toolchain before shipping. "Works on my machine" failures in MCPB almost always trace to a dependency that wasn't actually bundled.


Reference files

  • references/manifest-schema.md — full manifest.json field reference

  • references/local-security.md — path traversal, sandboxing, least privilege

Files

references/local-security.md

Local MCP Security

MCPB provides no sandbox. There's no permissions block in the manifest, no filesystem scoping, no network allowlist enforced by the platform. The server process runs with the user's full privileges — it can read any file the user can, spawn any process, hit any network endpoint.

Claude drives it. That combination means: tool inputs are untrusted, even though they come from an AI the user trusts. A prompt-injected web page can make Claude call your delete_file tool with a path you didn't intend.

Your tool handlers are the only defense. Everything below is about building that defense yourself.


Path traversal

The #1 bug in local MCP servers. If you take a path parameter and join it to a root, resolve and check containment.

import { resolve, relative, isAbsolute } from "node:path";

function safeJoin(root: string, userPath: string): string {
  const full = resolve(root, userPath);
  const rel = relative(root, full);
  if (rel.startsWith("..") || isAbsolute(rel)) {
    throw new Error(`Path escapes root: ${userPath}`);
  }
  return full;
}

resolve normalizes .., symlink segments, etc. relative tells you if the result left the root. Don't just String.includes("..") — that misses encoded and symlink-based escapes.

Python equivalent:

from pathlib import Path

def safe_join(root: Path, user_path: str) -> Path:
    full = (root / user_path).resolve()
    if not full.is_relative_to(root.resolve()):
        raise ValueError(f"Path escapes root: {user_path}")
    return full

Roots — ask the host, don't hardcode

Before hardcoding ROOT from a config env var, check if the host supports roots/list. This is the spec-native way to get user-approved workspace boundaries.

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

const server = new McpServer({ name: "...", version: "..." });

let allowedRoots: string[] = [];
server.server.oninitialized = async () => {
  const caps = server.getClientCapabilities();
  if (caps?.roots) {
    const { roots } = await server.server.listRoots();
    allowedRoots = roots.map(r => new URL(r.uri).pathname);
  } else {
    allowedRoots = [process.env.ROOT_DIR ?? process.cwd()];
  }
};
# fastmcp — inside a tool handler
async def my_tool(ctx: Context) -> str:
    try:
        roots = await ctx.list_roots()
        allowed = [urlparse(r.uri).path for r in roots]
    except Exception:
        allowed = [os.environ.get("ROOT_DIR", os.getcwd())]

If roots are available, use them. If not, fall back to config. Either way, validate every path against the allowed set.


Command injection

If you spawn processes, never pass user input through a shell.

// ❌ catastrophic
exec(`git log ${branch}`);

// ✅ array-args, no shell
execFile("git", ["log", branch]);

If you're wrapping a CLI, build the full argv as an array. Validate each flag against an allowlist if the tool accepts flags at all.


Read-only by default

Split read and write into separate tools. Most workflows only need read. A tool that's read-only can't be weaponized into data loss no matter what Claude is tricked into calling it with.

list_files   ← safe to call freely
read_file    ← safe to call freely
write_file   ← separate tool, separate scrutiny
delete_file  ← consider not shipping this at all

Pair this with tool annotations — readOnlyHint: true on every read tool, destructiveHint: true on delete/overwrite tools. Hosts surface these in permission UI (auto-approve reads, confirm-dialog destructive). See ../build-mcp-server/references/tool-design.md.

If you ship write/delete, consider requiring explicit confirmation via elicitation (see ../build-mcp-server/references/elicitation.md) or a confirmation widget (see build-mcp-app) so the user approves each destructive call.


Resource limits

Claude will happily ask to read a 4GB log file. Cap everything:

const MAX_BYTES = 1_000_000;
const buf = await readFile(path);
if (buf.length > MAX_BYTES) {
  return {
    content: [{
      type: "text",
      text: `File is ${buf.length} bytes — too large. Showing first ${MAX_BYTES}:\n\n`
            + buf.subarray(0, MAX_BYTES).toString("utf8"),
    }],
  };
}

Same for directory listings (cap entry count), search results (cap matches), and anything else unbounded.


Secrets

  • Config secrets (sensitive: true in manifest user_config): host stores in OS keychain, delivers via env var. Don't log them. Don't include them in tool results.

  • Never store secrets in plaintext files. If the host's keychain integration isn't enough, use keytar (Node) / keyring (Python) yourself.

  • Tool results flow into the chat transcript. Anything you return, the user (and any log export) can see. Redact before returning.


Checklist before shipping

  • [ ] Every path parameter goes through containment check

  • [ ] No exec() / shell=True — execFile / array-argv only

  • [ ] Write/delete split from read tools; readOnlyHint/destructiveHint annotations set

  • [ ] Size caps on file reads, listing lengths, search results

  • [ ] Secrets never logged or returned in tool results

  • [ ] Tested with adversarial inputs: ../../etc/passwd, ; rm -rf ~, 10GB file

Requirements and compatibility

Written for: Claude Code, Claude apps

Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta

“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.

Needs: Needs an MCP server, Needs Python, Needs Node.js

Security

What our scanner noticed· Low risk

  • Instructions that use the network

A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.

Attribution and licence

Published by
Anthropic
Publisher
Anthropic (Official)
Source
anthropics/claude-plugins-official · plugins/mcp-server-dev/skills/build-mcpb · commit 0412be7
Licence
Apache License 2.0 (redistribution allowed)
  • LICENSE (added by AutoGineer from plugins/mcp-server-dev/LICENSE)
  • ATTRIBUTION.md (written by AutoGineer)
Synced

AutoGineer is not affiliated with or endorsed by Anthropic or Anthropic.

Download ZIP

The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download build-mcpb.zip