mcp-server-dev
Skills for designing and building MCP servers that work seamlessly with Claude — guides you through deployment models (remote HTTP, MCPB, local), tool design patterns, auth, and interactive MCP apps.
Installation
/plugin marketplace add giginet/claude-plugins-official
/plugin install mcp-server-dev@claude-plugins-official
claude plugin marketplace add giginet/claude-plugins-official
claude plugin install mcp-server-dev@claude-plugins-official
Skills
| Name | Description | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
build-mcp-app |
This skill should be used when the user wants to build an "MCP app", add "interactive UI" or "widgets" to an MCP server, "render components in chat", build "MCP UI resources", make a tool that shows a "form", "picker", "dashboard" or "confirmation dialog" inline in the conversation, or mentions "apps SDK" in the context of MCP. Use AFTER the build-mcp-server skill has settled the deployment model, or when the user already knows they want UI widgets. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Build an MCP App (Interactive UI Widgets)An MCP app is a standard MCP server that also serves UI resources — interactive components rendered inline in the chat surface. Build once, runs in Claude and ChatGPT and any other host that implements the apps surface. The UI layer is additive. Under the hood it's still tools, resources, and the same wire protocol. If you haven't built a plain MCP server before, the When a widget beats plain textDon't add UI for its own sake — most tools are fine returning text or JSON. Add a widget when one of these is true:
If none apply, skip the widget. Text is faster to build and faster for the user. Widgets vs Elicitation — route correctlyBefore building a widget, check if elicitation covers it. Elicitation is spec-native, zero UI code, works in any compliant host.
If elicitation covers it, use it. See Architecture: two deployment shapesRemote MCP app (most common)Hosted streamable-HTTP server. Widget templates are served as resources; tool results reference them. The host fetches the resource, renders it in an iframe sandbox, and brokers messages between the widget and Claude.
MCPB-packaged MCP app (local + UI)Same widget mechanism, but the server runs locally inside an MCPB bundle. Use this when the widget needs to drive a local application — e.g., a file picker that browses the actual local disk, a dialog that controls a desktop app. For MCPB packaging mechanics, defer to the How widgets attach to toolsA widget-enabled tool has two separate registrations:
When Claude calls the tool, the host sees
The URI scheme Widget runtime — the
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Method | Direction | Use for |
|---|---|---|
app.ontoolresult = fn |
Host → widget | Receive the tool's return value |
app.ontoolinput = fn |
Host → widget | Receive the tool's input args (what Claude passed) |
app.sendMessage({...}) |
Widget → host | Inject a message into the conversation |
app.updateModelContext({...}) |
Widget → host | Update context silently (no visible message) |
app.callServerTool({name, arguments}) |
Widget → server | Call another tool on your server |
app.openLink({url}) |
Widget → host | Open a URL in a new tab (sandbox blocks window.open) |
app.getHostContext() / app.onhostcontextchanged |
Host → widget | Theme (light/dark), locale, etc. |
sendMessage is the typical "user picked something, tell Claude" path. updateModelContext is for state that Claude should know about but shouldn't clutter the chat. openLink is required for any outbound navigation — window.open and <a target="_blank"> are blocked by the sandbox attribute.
What widgets cannot do:
- Access the host page's DOM, cookies, or storage
- Make network calls to arbitrary origins (CSP-restricted — route through callServerTool)
- Open popups or navigate directly — use app.openLink({url})
- Load remote images reliably — inline as data: URLs server-side
Keep widgets small and single-purpose. A picker picks. A chart displays. Don't build a whole sub-app inside the iframe — split it into multiple tools with focused widgets.
Scaffold: minimal picker widget
Install:
npm install @modelcontextprotocol/sdk @modelcontextprotocol/ext-apps zod express
Server (src/server.ts):
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { registerAppTool, registerAppResource, RESOURCE_MIME_TYPE }
from "@modelcontextprotocol/ext-apps/server";
import express from "express";
import { readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { z } from "zod";
const require = createRequire(import.meta.url);
const server = new McpServer({ name: "contact-picker", version: "1.0.0" });
// Inline the ext-apps browser bundle into the widget HTML.
// The iframe CSP blocks CDN script fetches — bundling is mandatory.
const bundle = readFileSync(
require.resolve("@modelcontextprotocol/ext-apps/app-with-deps"), "utf8",
).replace(/export\{([^}]+)\};?\s*$/, (_, body) =>
"globalThis.ExtApps={" +
body.split(",").map((p) => {
const [local, exported] = p.split(" as ").map((s) => s.trim());
return `${exported ?? local}:${local}`;
}).join(",") + "};",
);
const pickerHtml = readFileSync("./widgets/picker.html", "utf8")
.replace("/*__EXT_APPS_BUNDLE__*/", () => bundle);
registerAppTool(server, "pick_contact", {
description: "Open an interactive contact picker. User selects one contact.",
inputSchema: { filter: z.string().optional().describe("Name/email prefix filter") },
_meta: { ui: { resourceUri: "ui://widgets/picker.html" } },
}, async ({ filter }) => {
const contacts = await db.contacts.search(filter ?? "");
return { content: [{ type: "text", text: JSON.stringify(contacts) }] };
});
registerAppResource(server, "Contact Picker", "ui://widgets/picker.html", {},
async () => ({
contents: [{ uri: "ui://widgets/picker.html", mimeType: RESOURCE_MIME_TYPE, text: pickerHtml }],
}),
);
const app = express();
app.use(express.json());
app.post("/mcp", async (req, res) => {
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
res.on("close", () => transport.close());
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
});
app.listen(process.env.PORT ?? 3000);
For local-only widget apps (driving a desktop app, reading local files), swap the transport to StdioServerTransport and package via the build-mcpb skill.
Widget (widgets/picker.html):
<!doctype html>
<meta charset="utf-8" />
<style>
body { font: 14px system-ui; margin: 0; }
ul { list-style: none; padding: 0; margin: 0; max-height: 300px; overflow-y: auto; }
li { padding: 10px 14px; cursor: pointer; border-bottom: 1px solid #eee; }
li:hover { background: #f5f5f5; }
.sub { color: #666; font-size: 12px; }
</style>
<ul id="list"></ul>
<script type="module">
/*__EXT_APPS_BUNDLE__*/
const { App } = globalThis.ExtApps;
(async () => {
const app = new App({ name: "ContactPicker", version: "1.0.0" }, {});
const ul = document.getElementById("list");
app.ontoolresult = ({ content }) => {
const contacts = JSON.parse(content[0].text);
ul.innerHTML = "";
for (const c of contacts) {
const li = document.createElement("li");
li.innerHTML = `<div>${c.name}</div><div class="sub">${c.email}</div>`;
li.addEventListener("click", () => {
app.sendMessage({
role: "user",
content: [{ type: "text", text: `Selected contact: ${c.id} (${c.name})` }],
});
});
ul.append(li);
}
};
await app.connect();
})();
</script>
See references/widget-templates.md for more widget shapes.
Design notes that save you a rewrite
One widget per tool. Resist the urge to build one mega-widget that does everything. One tool → one focused widget → one clear result shape. Claude reasons about these far better.
Tool description must mention the widget. Claude only sees the tool description when deciding what to call. "Opens an interactive picker" in the description is what makes Claude reach for it instead of guessing an ID.
Widgets are optional at runtime. Hosts that don't support the apps surface simply ignore _meta.ui and render the tool's text content normally. Since your tool handler already returns meaningful text/JSON (the widget's data), degradation is automatic — Claude sees the data directly instead of via the widget.
Don't block on widget results for read-only tools. A widget that just displays data (chart, preview) shouldn't require a user action to complete. Return the display widget and a text summary in the same result so Claude can continue reasoning without waiting.
Layout-fork by item count, not by tool count. If one use case is "show one result in detail" and another is "show many results side-by-side", don't make two tools — make one tool that accepts items[], and let the widget pick a layout: items.length === 1 → detail view, > 1 → carousel. Keeps the server schema simple and lets Claude decide count naturally.
Put Claude's reasoning in the payload. A short note field on each item (why Claude picked it) rendered as a callout on the card gives users the reasoning inline with the choice. Mention this field in the tool description so Claude populates it.
Normalize image shapes server-side. If your data source returns images with wildly varying aspect ratios, rewrite to a predictable variant (e.g. square-bounded) before fetching for the data-URL inline. Then give the widget's image container a fixed aspect-ratio + object-fit: contain so everything sits centered.
Follow host theme. app.getHostContext()?.theme (after connect()) plus app.onhostcontextchanged for live updates. Toggle a .dark class on <html>, keep colors in CSS custom props with a :root.dark {} override block, set color-scheme. Disable mix-blend-mode: multiply in dark — it makes images vanish.
Testing
Claude Desktop — current builds still require the command/args config shape (no native "type": "http"). Wrap with mcp-remote and force http-only transport so the SSE probe doesn't swallow widget-capability negotiation:
{
"mcpServers": {
"my-server": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://localhost:3000/mcp",
"--allow-http", "--transport", "http-only"]
}
}
}
Desktop caches UI resources aggressively. After editing widget HTML, fully quit (⌘Q / Alt+F4, not window-close) and relaunch to force a cold resource re-fetch.
Headless JSON-RPC loop — fast iteration without clicking through Desktop:
# test.jsonl — one JSON-RPC message per line
{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"t","version":"0"}}}
{"jsonrpc":"2.0","method":"notifications/initialized"}
{"jsonrpc":"2.0","id":2,"method":"tools/list"}
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"your_tool","arguments":{...}}}
(cat test.jsonl; sleep 10) | npx mcp-remote http://localhost:3000/mcp --allow-http
The sleep keeps stdin open long enough to collect all responses. Parse the jsonl output with jq or a Python one-liner.
Host fallback — use a host without the apps surface (or MCP Inspector) and confirm the tool's text content degrades gracefully.
CSP debugging — open the iframe's own devtools console. CSP violations are the #1 reason widgets silently fail (blank rectangle, no error in the main console). See references/iframe-sandbox.md.
Reference files
references/iframe-sandbox.md— CSP/sandbox constraints, the bundle-inlining pattern, image handlingreferences/widget-templates.md— reusable HTML scaffolds for picker / confirm / progress / displayreferences/apps-sdk-messages.md— theAppclass API: widget ↔ host ↔ server messaging
build-mcp-server| name | build-mcp-server |
|---|---|
| version | 0.1.0 |
Build an MCP Server
You are guiding a developer through designing and building an MCP server that works seamlessly with Claude. MCP servers come in many forms — picking the wrong shape early causes painful rewrites later. Your first job is discovery, not code.
Do not start scaffolding until you have answers to the questions in Phase 1. If the user's opening message already answers them, acknowledge that and skip straight to the recommendation.
Phase 1 — Interrogate the use case
Ask these questions conversationally (batch them into one message, don't interrogate one-at-a-time). Adapt wording to what the user has already told you.
1. What does it connect to?
| If it connects to… | Likely direction |
|---|---|
| A cloud API (SaaS, REST, GraphQL) | Remote HTTP server |
| A local process, filesystem, or desktop app | MCPB or local stdio |
| Hardware, OS-level APIs, or user-specific state | MCPB |
| Nothing external — pure logic / computation | Either — default to remote |
2. Who will use it?
- Just me / my team, on our machines → Local stdio is acceptable (easiest to prototype)
- Anyone who installs it → Remote HTTP (strongly preferred) or MCPB (if it must be local)
- Users of Claude desktop who want UI widgets → MCP app (remote or MCPB)
3. How many distinct actions does it expose?
This determines the tool-design pattern — see Phase 3.
- Under ~15 actions → one tool per action
- Dozens to hundreds of actions (e.g. wrapping a large API surface) → search + execute pattern
4. Does a tool need mid-call user input or rich display?
- Simple structured input (pick from list, enter a value, confirm) → Elicitation — spec-native, zero UI code. Host support is rolling out (Claude Code ≥2.1.76) — always pair with a capability check and fallback. See
references/elicitation.md. - Rich/visual UI (charts, custom pickers with search, live dashboards) → MCP app widgets — iframe-based, needs
@modelcontextprotocol/ext-apps. Seebuild-mcp-appskill. - Neither → plain tool returning text/JSON.
5. What auth does the upstream service use?
- None / API key → straightforward
- OAuth 2.0 → you'll need a remote server with CIMD (preferred) or DCR support; see
references/auth.md
Phase 2 — Recommend a deployment model
Based on the answers, recommend one path. Be opinionated. The ranked options:
⭐ Remote streamable-HTTP MCP server (default recommendation)
A hosted service speaking MCP over streamable HTTP. This is the recommended path for anything wrapping a cloud API.
Why it wins: - Zero install friction — users add a URL, done - One deployment serves all users; you control upgrades - OAuth flows work properly (the server can handle redirects, DCR, token storage) - Works across Claude desktop, Claude Code, Claude.ai, and third-party MCP hosts
Choose this unless the server must touch the user's local machine.
→ Fastest deploy: Cloudflare Workers — references/deploy-cloudflare-workers.md (zero to live URL in two commands)
→ Portable Node/Python: references/remote-http-scaffold.md (Express or FastMCP, runs on any host)
Elicitation (structured input, no UI build)
If a tool just needs the user to confirm, pick an option, or fill a short form, elicitation does it with zero UI code. The server sends a flat JSON schema; the host renders a native form. Spec-native, no extra packages.
Caveat: Host support is new (Claude Code shipped it in v2.1.76; Desktop unconfirmed). The SDK throws if the client doesn't advertise the capability. Always check clientCapabilities.elicitation first and have a fallback — see references/elicitation.md for the canonical pattern. This is the right spec-correct approach; host coverage will catch up.
Escalate to build-mcp-app widgets when you need: nested/complex data, scrollable/searchable lists, visual previews, live updates.
MCP app (remote HTTP + interactive UI)
Same as above, plus UI resources — interactive widgets rendered in chat. Rich pickers with search, charts, live dashboards, visual previews. Built once, renders in Claude and ChatGPT.
Choose this when elicitation's flat-form constraints don't fit — you need custom layout, large searchable lists, visual content, or live updates.
Usually remote, but can be shipped as MCPB if the UI needs to drive a local app.
→ Hand off to the build-mcp-app skill.
MCPB (bundled local server)
A local MCP server packaged with its runtime so users don't need Node/Python installed. The sanctioned way to ship local servers.
Choose this when the server must run on the user's machine — it reads local files, drives a desktop app, talks to localhost services, or needs OS-level access.
→ Hand off to the build-mcpb skill.
Local stdio (npx / uvx) — not recommended for distribution
A script launched via npx / uvx on the user's machine. Fine for personal tools and prototypes. Painful to distribute: users need the right runtime, you can't push updates, and the only distribution channel is Claude Code plugins.
Recommend this only as a stepping stone. If the user insists, scaffold it but note the MCPB upgrade path.
Phase 3 — Pick a tool-design pattern
Every MCP server exposes tools. How you carve them matters more than most people expect — tool schemas land directly in Claude's context window.
Pattern A: One tool per action (small surface)
When the action space is small (< ~15 operations), give each a dedicated tool with a tight description and schema.
create_issue — Create a new issue. Params: title, body, labels[]
update_issue — Update an existing issue. Params: id, title?, body?, state?
search_issues — Search issues by query string. Params: query, limit?
add_comment — Add a comment to an issue. Params: issue_id, body
Why it works: Claude reads the tool list once and knows exactly what's possible. No discovery round-trips. Each tool's schema validates inputs precisely.
Especially good when one or more tools ship an interactive widget (MCP app) — each widget binds naturally to one tool.
Pattern B: Search + execute (large surface)
When wrapping a large API (dozens to hundreds of endpoints), listing every operation as a tool floods the context window and degrades model performance. Instead, expose two tools:
search_actions — Given a natural-language intent, return matching actions
with their IDs, descriptions, and parameter schemas.
execute_action — Run an action by ID with a params object.
The server holds the full catalog internally. Claude searches, picks, executes. Context stays lean.
Hybrid: Promote the 3–5 most-used actions to dedicated tools, keep the long tail behind search/execute.
→ See references/tool-design.md for schema examples and description-writing guidance.
Phase 4 — Pick a framework
Recommend one of these two. Others exist but these have the best MCP-spec coverage and Claude compatibility.
| Framework | Language | Use when |
|---|---|---|
Official TypeScript SDK (@modelcontextprotocol/sdk) |
TS/JS | Default choice. Best spec coverage, first to get new features. |
FastMCP 3.x (fastmcp on PyPI) |
Python | User prefers Python, or wrapping a Python library. Decorator-based, very low boilerplate. This is jlowin's package — not the frozen FastMCP 1.0 bundled in the official mcp SDK. |
If the user already has a language/stack in mind, go with it — both produce identical wire protocol.
Phase 5 — Scaffold and hand off
Once you've settled the four decisions (deployment model, tool pattern, framework, auth), do one of:
- Remote HTTP, no UI → Scaffold inline using
references/remote-http-scaffold.md(portable) orreferences/deploy-cloudflare-workers.md(fastest deploy). This skill can finish the job. - MCP app (UI widgets) → Summarize the decisions so far, then load the
build-mcp-appskill. - MCPB (bundled local) → Summarize the decisions so far, then load the
build-mcpbskill. - Local stdio prototype → Scaffold inline (simplest case), flag the MCPB upgrade path.
When handing off, restate the design brief in one paragraph so the next skill doesn't re-ask.
Beyond tools — the other primitives
Tools are one of three server primitives. Most servers start with tools and never need the others, but knowing they exist prevents reinventing wheels:
| Primitive | Who triggers it | Use when |
|---|---|---|
| Resources | Host app (not Claude) | Exposing docs/files/data as browsable context |
| Prompts | User (slash command) | Canned workflows ("/summarize-thread") |
| Elicitation | Server, mid-tool | Asking user for input without building UI |
| Sampling | Server, mid-tool | Need LLM inference in your tool logic |
→ references/resources-and-prompts.md, references/elicitation.md, references/server-capabilities.md
Quick reference: decision matrix
| Scenario | Deployment | Tool pattern |
|---|---|---|
| Wrap a small SaaS API | Remote HTTP | One-per-action |
| Wrap a large SaaS API (50+ endpoints) | Remote HTTP | Search + execute |
| SaaS API with rich forms / pickers | MCP app (remote) | One-per-action |
| Drive a local desktop app | MCPB | One-per-action |
| Local desktop app with in-chat UI | MCP app (MCPB) | One-per-action |
| Read/write local filesystem | MCPB | Depends on surface |
| Personal prototype | Local stdio | Whatever's fastest |
Reference files
references/remote-http-scaffold.md— minimal remote server in TS SDK and FastMCPreferences/deploy-cloudflare-workers.md— fastest deploy path (Workers-native scaffold)references/tool-design.md— writing tool descriptions and schemas Claude understands wellreferences/auth.md— OAuth, CIMD, DCR, token storage patternsreferences/resources-and-prompts.md— the two non-tool primitivesreferences/elicitation.md— spec-native user input mid-tool (capability check + fallback)references/server-capabilities.md— instructions, sampling, roots, logging, progress, cancellationreferences/versions.md— version-sensitive claims ledger (check when updating)
build-mcpb| name | build-mcpb |
|---|---|
| version | 0.1.0 |
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.
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— fullmanifest.jsonfield referencereferences/local-security.md— path traversal, sandboxing, least privilege
README
mcp-server-dev
Skills for designing and building MCP servers that work seamlessly with Claude.
What's inside
Three skills that compose into a full build path:
| Skill | Purpose |
|---|---|
build-mcp-server |
Entry point. Interrogates the use case, picks deployment model (remote HTTP / MCPB / local stdio), picks tool-design pattern, routes to a specialized skill. |
build-mcp-app |
Adds interactive UI widgets (forms, pickers, confirm dialogs) rendered inline in chat. Works on remote servers and MCPB bundles. |
build-mcpb |
Packages a local stdio server with its runtime so users can install it without Node/Python. For servers that must touch the local machine. |
How it works
build-mcp-server is the front door. It asks what you're connecting to, who'll use it, how big the action surface is, and whether you need in-chat UI. From those answers it recommends one of four paths:
- Remote streamable-HTTP (the default recommendation for anything wrapping a cloud API) — scaffolded inline
- MCP app — hands off to
build-mcp-app - MCPB — hands off to
build-mcpb - Local stdio prototype — scaffolded inline with an MCPB upgrade note
Each skill ships reference files for the parts that don't fit in the main instructions: auth flows (DCR/CIMD), tool-description writing, widget templates, manifest schemas, security hardening.
Usage
Ask Claude to "help me build an MCP server" and the entry skill will trigger. Or invoke directly:
/mcp-server-dev:build-mcp-server
License
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
claude-plugins-official