
Build Mcpb
Harden local MCP server tool handlers against path traversal and untrusted model-driven inputs when packaging MCPB plugins for Claude.
Overview
build-mcpb is an agent skill most often used in Build (also Ship) that teaches how to secure local MCP/MCPB tool handlers against path traversal and untrusted tool inputs.
Install
npx skills add https://github.com/anthropics/claude-plugins-official --skill build-mcpbWhat is this skill?
- Documents that MCPB provides no platform sandbox—handlers must enforce limits with the user’s full OS privileges
- Path-traversal defense with resolve/relative containment checks in TypeScript and Python (not naive .. string checks)
- Treats every tool argument as untrusted because prompt injection can steer Claude into dangerous calls
- copy-paste-safe safeJoin patterns for filesystem-scoped MCP tools
- Documents path traversal as the #1 bug in local MCP servers
- Includes TypeScript and Python safe_join examples with resolve/relative containment
Adoption & trust: 2.5k installs on skills.sh; 29.6k GitHub stars; 3/3 security scanners passed (skills.sh audits).
What problem does it solve?
You are shipping an MCP server that runs with full user privileges, but tool parameters from the model can escape intended directories or trigger unintended actions.
Who is it for?
Solo builders writing filesystem or shell-adjacent MCP tools on Claude Code who need copy-paste containment checks before publishing an MCPB manifest.
Skip if: Teams that only consume hosted MCP with platform-enforced sandboxes and no custom tool code, or builders who want a checklist for unrelated OWASP web-app topics.
When should I use this skill?
You are building or reviewing local MCP/MCPB servers that expose filesystem, process, or network tools to Claude.
What do I get? / Deliverables
You implement containment-checked path joins and a clear untrusted-input mindset so MCP tools fail closed instead of reading or writing outside their roots.
- Containment-safe path join helpers in tool code
- Threat-model notes treating model-provided tool args as untrusted
Recommended Skills
Journey fit
Spans multiple journey phases - primary shelf plus alternate fits below.
MCP servers are authored and wired during Build; this skill is the canonical shelf for integration-time security patterns before those tools ship to users. Covers MCP tool implementation and proxy boundaries—the same surface as agent integrations and custom MCP endpoints.
Where it fits
Implement a read_file MCP tool and wrap user paths with resolve/relative checks before opening descriptors.
Pre-release review of every tool that accepts paths or spawns processes after adding new MCP capabilities.
Patch a reported escape bug in an existing MCP handler using the documented safeJoin pattern.
How it compares
Use as handler-level hardening guidance—not a hosted MCP permission manifest you can rely on for sandboxing.
Common Questions / FAQ
Who is build-mcpb for?
Indie and solo developers implementing custom MCP tools for Claude plugins where the server process inherits the user’s OS permissions.
When should I use build-mcpb?
During Build integrations when coding path-based tools; again in Ship security review before you ship MCPB; whenever you add a new tool that accepts paths, commands, or URLs from the model.
Is build-mcpb safe to install?
Review the Security Audits panel on this Prism page and treat the skill as documentation—you still validate your own server code and deployment surface.
SKILL.md
READMESKILL.md - Build Mcpb
# 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**. ```typescript 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:** ```python 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. ```typescript 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()]; } }; ``` ```python # 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**. ```typescript // ❌ 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