mirror of
https://github.com/anthropics/claude-plugins-official.git
synced 2026-03-20 11:33:08 +00:00
fix(plugin): mcp-server-dev — correct APIs against spec, add missing primitives
Corrects fabricated/deprecated APIs: ext-apps App class model (not embedded resources), real MCPB v0.4 manifest (no permissions block exists), registerTool (not server.tool), @anthropic-ai/mcpb package name, CIMD preferred over DCR. Adds missing spec coverage: resources, prompts, elicitation (with capability check + fallback), sampling, roots, tool annotations, structured output, instructions field, progress/cancellation.
This commit is contained in:
@@ -16,15 +16,14 @@ MCPB is a local MCP server **packaged with its runtime**. The user installs one
|
||||
|
||||
```
|
||||
my-server.mcpb (zip archive)
|
||||
├── manifest.json ← identity, entry point, permissions, config schema
|
||||
├── manifest.json ← identity, entry point, config schema, compatibility
|
||||
├── server/ ← your MCP server code
|
||||
│ ├── index.js
|
||||
│ └── node_modules/ ← bundled dependencies (or vendored)
|
||||
├── runtime/ ← optional: pinned Node/Python if not using host's
|
||||
└── icon.png
|
||||
```
|
||||
|
||||
The host reads `manifest.json`, launches the entry point 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.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -32,32 +31,44 @@ The host reads `manifest.json`, launches the entry point as a **stdio** MCP serv
|
||||
|
||||
```json
|
||||
{
|
||||
"$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.",
|
||||
"entry": {
|
||||
"author": { "name": "Your Name" },
|
||||
"server": {
|
||||
"type": "node",
|
||||
"main": "server/index.js"
|
||||
},
|
||||
"permissions": {
|
||||
"filesystem": { "read": true, "write": false },
|
||||
"network": false
|
||||
},
|
||||
"config": {
|
||||
"rootDir": {
|
||||
"type": "string",
|
||||
"description": "Directory to expose. Defaults to ~/Documents.",
|
||||
"default": "~/Documents"
|
||||
"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"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`entry.type`** — `node`, `python`, or `binary`. Determines which bundled/host runtime launches `main`.
|
||||
**`server.type`** — `node`, `python`, or `binary`. Informational; the actual launch comes from `mcp_config`.
|
||||
|
||||
**`permissions`** — declared upfront and shown to the user at install. Request the minimum. Broad permissions (`filesystem.write: true`, `network: true`) trigger scarier consent UI and more scrutiny.
|
||||
**`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`.
|
||||
|
||||
**`config`** — user-settable values surfaced in the host's settings UI. Your server reads them from env vars (`MCPB_CONFIG_<KEY>`).
|
||||
**`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.
|
||||
|
||||
---
|
||||
|
||||
@@ -73,15 +84,18 @@ import { readFile, readdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { homedir } from "node:os";
|
||||
|
||||
const ROOT = (process.env.MCPB_CONFIG_ROOTDIR ?? "~/Documents")
|
||||
.replace(/^~/, homedir());
|
||||
// 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.tool(
|
||||
server.registerTool(
|
||||
"list_files",
|
||||
"List files in a directory under the configured root.",
|
||||
{ path: z.string().default(".") },
|
||||
{
|
||||
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() }));
|
||||
@@ -89,10 +103,13 @@ server.tool(
|
||||
},
|
||||
);
|
||||
|
||||
server.tool(
|
||||
server.registerTool(
|
||||
"read_file",
|
||||
"Read a file's contents. Path is relative to the configured root.",
|
||||
{ path: z.string() },
|
||||
{
|
||||
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 }] };
|
||||
@@ -103,7 +120,9 @@ const transport = new StdioServerTransport();
|
||||
await server.connect(transport);
|
||||
```
|
||||
|
||||
**Sandboxing is your job.** The manifest permissions gate what the *host* allows the process to do, but don't rely on that alone — validate paths, refuse to escape `ROOT`, etc. See `references/local-security.md`.
|
||||
**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.
|
||||
|
||||
---
|
||||
|
||||
@@ -115,35 +134,29 @@ await server.connect(transport);
|
||||
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 @modelcontextprotocol/mcpb pack . -o my-server.mcpb
|
||||
npx @anthropic-ai/mcpb pack
|
||||
```
|
||||
|
||||
`mcpb pack` zips the directory, validates `manifest.json`, and optionally pulls a pinned Node runtime into `runtime/`.
|
||||
`mcpb pack` zips the directory and validates `manifest.json` against the schema.
|
||||
|
||||
### Python
|
||||
|
||||
```bash
|
||||
pip install -t server/vendor -r requirements.txt
|
||||
npx @modelcontextprotocol/mcpb pack . -o my-server.mcpb --runtime python3.12
|
||||
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 — `mcpb pack --multiarch` cross-builds, but it's slow; avoid native deps if you can.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## Permissions: request the minimum
|
||||
## MCPB has no sandbox — security is on you
|
||||
|
||||
The install prompt shows what you ask for. Every extra permission is friction.
|
||||
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.
|
||||
|
||||
| Need | Request |
|
||||
|---|---|
|
||||
| Read files in one directory | `filesystem.read: true` + enforce root in code |
|
||||
| Write files | `filesystem.write: true` — justify in description |
|
||||
| Call a local HTTP service | `network: { "allow": ["localhost:*"] }` |
|
||||
| Call the internet | `network: true` — but ask yourself why this isn't a remote server |
|
||||
| Spawn processes | `process.spawn: true` — highest scrutiny |
|
||||
If you came here expecting filesystem/network scoping from the manifest: it doesn't exist. Build it yourself in tool handlers.
|
||||
|
||||
If you find yourself requesting `network: true` to hit a cloud API, stop — that's a remote server wearing an MCPB costume. The user gains nothing from running it locally.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -158,15 +171,20 @@ Widget authoring is covered in the **`build-mcp-app`** skill; it works the same
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
# 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
|
||||
|
||||
# Pack and validate
|
||||
npx @modelcontextprotocol/mcpb pack . -o test.mcpb
|
||||
npx @modelcontextprotocol/mcpb validate test.mcpb
|
||||
# Validate manifest against schema, then pack
|
||||
npx @anthropic-ai/mcpb validate
|
||||
npx @anthropic-ai/mcpb pack
|
||||
|
||||
# Install into Claude desktop for end-to-end
|
||||
npx @modelcontextprotocol/mcpb install test.mcpb
|
||||
# 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.
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
# Local MCP Security
|
||||
|
||||
An MCPB server runs as the user, with whatever permissions the manifest was granted. 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.
|
||||
**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.
|
||||
|
||||
Defense in depth. Manifest permissions are the outer wall; validation in your tool handlers is the inner wall.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -39,6 +41,41 @@ def safe_join(root: Path, user_path: str) -> Path:
|
||||
|
||||
---
|
||||
|
||||
## 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**.
|
||||
@@ -62,11 +99,13 @@ Split read and write into separate tools. Most workflows only need read. A tool
|
||||
```
|
||||
list_files ← safe to call freely
|
||||
read_file ← safe to call freely
|
||||
write_file ← separate tool, separate permission, separate scrutiny
|
||||
write_file ← separate tool, separate scrutiny
|
||||
delete_file ← consider not shipping this at all
|
||||
```
|
||||
|
||||
If you ship write/delete, consider requiring a confirmation widget (see `build-mcp-app`) so the user explicitly approves each destructive call.
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
@@ -94,7 +133,7 @@ Same for directory listings (cap entry count), search results (cap matches), and
|
||||
|
||||
## Secrets
|
||||
|
||||
- **Config secrets** (`secret: true` in manifest): host stores in OS keychain, delivers via env var. Don't log them. Don't include them in tool results.
|
||||
- **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.
|
||||
|
||||
@@ -104,8 +143,7 @@ Same for directory listings (cap entry count), search results (cap matches), and
|
||||
|
||||
- [ ] Every path parameter goes through containment check
|
||||
- [ ] No `exec()` / `shell=True` — `execFile` / array-argv only
|
||||
- [ ] Write/delete split from read tools
|
||||
- [ ] Write/delete split from read tools; `readOnlyHint`/`destructiveHint` annotations set
|
||||
- [ ] Size caps on file reads, listing lengths, search results
|
||||
- [ ] Manifest permissions match actual code behavior (no over-requesting)
|
||||
- [ ] Secrets never logged or returned in tool results
|
||||
- [ ] Tested with adversarial inputs: `../../etc/passwd`, `; rm -rf ~`, 10GB file
|
||||
|
||||
@@ -1,119 +1,127 @@
|
||||
# MCPB Manifest Schema
|
||||
# MCPB Manifest Schema (v0.4)
|
||||
|
||||
Every `.mcpb` bundle has a `manifest.json` at its root. The host validates it before install.
|
||||
Validated against `github.com/anthropics/mcpb/schemas/mcpb-manifest-v0.4.schema.json`. The schema uses `additionalProperties: false` — unknown keys are rejected. Add `"$schema"` to your manifest for editor validation.
|
||||
|
||||
---
|
||||
|
||||
## Top-level fields
|
||||
|
||||
| Field | Type | Required | Notes |
|
||||
|---|---|---|---|
|
||||
| `name` | string | ✅ | Unique identifier. Lowercase, hyphens only. Shown in settings. |
|
||||
| `version` | string | ✅ | Semver. Host compares for update prompts. |
|
||||
| `description` | string | ✅ | One line. Shown in the install prompt. |
|
||||
| `entry` | object | ✅ | How to launch the server — see below. |
|
||||
| `permissions` | object | ✅ | What the server needs — see below. |
|
||||
| `config` | object | — | User-settable values surfaced in settings UI. |
|
||||
| `icon` | string | — | Path to PNG inside the bundle. 256×256 recommended. |
|
||||
| `homepage` | string | — | URL shown in settings. |
|
||||
| `minHostVersion` | string | — | Refuse install on older hosts. |
|
||||
|
||||
---
|
||||
|
||||
## `entry`
|
||||
|
||||
```json
|
||||
{ "type": "node", "main": "server/index.js" }
|
||||
```
|
||||
|
||||
| `type` | `main` points at | Runtime resolution |
|
||||
| Field | Required | Description |
|
||||
|---|---|---|
|
||||
| `node` | `.js` or `.mjs` file | `runtime/node` if present, else host-bundled Node |
|
||||
| `python` | `.py` file | `runtime/python` if present, else host-bundled Python |
|
||||
| `binary` | executable | Run directly. Must be built per-platform. |
|
||||
|
||||
**`args`** (optional array) — extra argv passed to the entry. Rarely needed.
|
||||
|
||||
**`env`** (optional object) — static env vars set at launch. For user-configurable values use `config` instead.
|
||||
| `manifest_version` | ✅ | Schema version. Use `"0.4"`. |
|
||||
| `name` | ✅ | Package identifier (lowercase, hyphens). Must be unique. |
|
||||
| `version` | ✅ | Semver version of YOUR package. |
|
||||
| `description` | ✅ | One-line summary. Shown in marketplace. |
|
||||
| `author` | ✅ | `{name, email?, url?}` |
|
||||
| `server` | ✅ | Entry point and launch config. See below. |
|
||||
| `display_name` | | Human-friendly name. Falls back to `name`. |
|
||||
| `long_description` | | Markdown. Shown on detail page. |
|
||||
| `icon` / `icons` | | Path(s) to icon file(s) in the bundle. |
|
||||
| `homepage` / `repository` / `documentation` / `support` | | URLs. |
|
||||
| `license` | | SPDX identifier. |
|
||||
| `keywords` | | String array for search. |
|
||||
| `user_config` | | Install-time config fields. See below. |
|
||||
| `compatibility` | | Host/platform/runtime requirements. See below. |
|
||||
| `tools` / `prompts` | | Optional declarative list for marketplace display. Not enforced at runtime. |
|
||||
| `tools_generated` / `prompts_generated` | | `true` if tools/prompts are dynamic (can't list statically). |
|
||||
| `screenshots` | | Array of image paths. |
|
||||
| `localization` | | i18n bundles. |
|
||||
| `privacy_policies` | | URLs. |
|
||||
|
||||
---
|
||||
|
||||
## `permissions`
|
||||
## `server` — launch configuration
|
||||
|
||||
```json
|
||||
{
|
||||
"filesystem": { "read": true, "write": false },
|
||||
"network": { "allow": ["localhost:*", "127.0.0.1:*"] },
|
||||
"process": { "spawn": false }
|
||||
}
|
||||
```
|
||||
|
||||
### `filesystem`
|
||||
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| `false` or omitted | No filesystem access beyond the bundle itself |
|
||||
| `{ "read": true }` | Read anywhere the OS user can |
|
||||
| `{ "read": true, "write": true }` | Read and write |
|
||||
|
||||
There's no path scoping at the manifest level — scope in your code. The manifest permission is a coarse consent gate, not a sandbox.
|
||||
|
||||
### `network`
|
||||
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| `false` or omitted | No network (most local-first servers) |
|
||||
| `{ "allow": ["host:port", ...] }` | Allowlisted destinations. `*` wildcards ports. |
|
||||
| `true` | Unrestricted. Heavy scrutiny — explain why in `description`. |
|
||||
|
||||
### `process`
|
||||
|
||||
| Value | Meaning |
|
||||
|---|---|
|
||||
| `false` or omitted | Can't spawn child processes |
|
||||
| `{ "spawn": true }` | Can spawn. Needed for wrapping CLIs. |
|
||||
| `{ "spawn": true, "allow": ["git", "ffmpeg"] }` | Spawn only allowlisted binaries |
|
||||
|
||||
---
|
||||
|
||||
## `config`
|
||||
|
||||
User-editable settings, surfaced in the host UI. Each key becomes an env var: `MCPB_CONFIG_<UPPERCASE_KEY>`.
|
||||
|
||||
```json
|
||||
{
|
||||
"config": {
|
||||
"rootDir": {
|
||||
"type": "string",
|
||||
"description": "Directory to expose",
|
||||
"default": "~/Documents"
|
||||
},
|
||||
"maxFileSize": {
|
||||
"type": "number",
|
||||
"description": "Skip files larger than this (MB)",
|
||||
"default": 10,
|
||||
"min": 1,
|
||||
"max": 500
|
||||
},
|
||||
"includeHidden": {
|
||||
"type": "boolean",
|
||||
"description": "Include dotfiles in listings",
|
||||
"default": false
|
||||
},
|
||||
"apiKey": {
|
||||
"type": "string",
|
||||
"description": "Optional API key for the sync feature",
|
||||
"secret": true
|
||||
"server": {
|
||||
"type": "node",
|
||||
"entry_point": "server/index.js",
|
||||
"mcp_config": {
|
||||
"command": "node",
|
||||
"args": ["${__dirname}/server/index.js"],
|
||||
"env": {
|
||||
"API_KEY": "${user_config.apiKey}",
|
||||
"ROOT_DIR": "${user_config.rootDir}"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`type`** — `string`, `number`, `boolean`. Enums: use `string` with `"enum": ["a", "b", "c"]`.
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `type` | `"node"`, `"python"`, or `"binary"` |
|
||||
| `entry_point` | Relative path to main file. Informational. |
|
||||
| `mcp_config.command` | Executable to launch. |
|
||||
| `mcp_config.args` | Argv array. Use `${__dirname}` for bundle-relative paths. |
|
||||
| `mcp_config.env` | Environment variables. Use `${user_config.KEY}` to substitute user config. |
|
||||
|
||||
**`secret: true`** — host masks the value in UI and stores it in the OS keychain instead of a plain config file.
|
||||
**Substitution variables** (in `args` and `env` only):
|
||||
- `${__dirname}` — absolute path to the unpacked bundle directory
|
||||
- `${user_config.<key>}` — value the user entered at install time
|
||||
- `${HOME}` — user's home directory
|
||||
|
||||
**`required: true`** — host blocks server launch until the user sets it. Use sparingly — a server that won't start until configured is a bad first-run experience.
|
||||
**There are no auto-prefixed env vars.** The env var names your server reads are exactly what you declare in `mcp_config.env`. If you write `"ROOT_DIR": "${user_config.rootDir}"`, your server reads `process.env.ROOT_DIR`.
|
||||
|
||||
---
|
||||
|
||||
## `user_config` — install-time settings
|
||||
|
||||
```json
|
||||
"user_config": {
|
||||
"apiKey": {
|
||||
"type": "string",
|
||||
"title": "API Key",
|
||||
"description": "Your service API key. Stored encrypted.",
|
||||
"sensitive": true,
|
||||
"required": true
|
||||
},
|
||||
"rootDir": {
|
||||
"type": "directory",
|
||||
"title": "Root directory",
|
||||
"description": "Directory to expose to the server.",
|
||||
"default": "${HOME}/Documents"
|
||||
},
|
||||
"maxResults": {
|
||||
"type": "number",
|
||||
"title": "Max results",
|
||||
"description": "Maximum items returned per query.",
|
||||
"default": 50,
|
||||
"min": 1,
|
||||
"max": 500
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Required | Description |
|
||||
|---|---|---|
|
||||
| `type` | ✅ | `"string"`, `"number"`, `"boolean"`, `"directory"`, `"file"` |
|
||||
| `title` | ✅ | Form label. |
|
||||
| `description` | ✅ | Help text under the input. |
|
||||
| `default` | | Pre-filled value. Supports `${HOME}`. |
|
||||
| `required` | | If `true`, install blocks until filled. |
|
||||
| `sensitive` | | If `true`, stored in OS keychain + masked in UI. **NOT `secret`** — that field doesn't exist. |
|
||||
| `multiple` | | If `true`, user can enter multiple values (array). |
|
||||
| `min` / `max` | | Numeric bounds (for `type: "number"`). |
|
||||
|
||||
`directory` and `file` types render native OS pickers — prefer these over free-text paths for UX and validation.
|
||||
|
||||
---
|
||||
|
||||
## `compatibility` — gate installs
|
||||
|
||||
```json
|
||||
"compatibility": {
|
||||
"claude_desktop": ">=1.0.0",
|
||||
"platforms": ["darwin", "win32", "linux"],
|
||||
"runtimes": { "node": ">=20" }
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Description |
|
||||
|---|---|
|
||||
| `claude_desktop` | Semver range. Install blocked if host is older. |
|
||||
| `platforms` | OS allowlist. Subset of `["darwin", "win32", "linux"]`. |
|
||||
| `runtimes` | Required runtime versions, e.g. `{"node": ">=20"}` or `{"python": ">=3.11"}`. |
|
||||
|
||||
---
|
||||
|
||||
@@ -121,12 +129,28 @@ User-editable settings, surfaced in the host UI. Each key becomes an env var: `M
|
||||
|
||||
```json
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/anthropics/mcpb/main/schemas/mcpb-manifest-v0.4.schema.json",
|
||||
"manifest_version": "0.4",
|
||||
"name": "hello",
|
||||
"version": "0.1.0",
|
||||
"description": "Minimal MCPB server.",
|
||||
"entry": { "type": "node", "main": "server/index.js" },
|
||||
"permissions": {}
|
||||
"author": { "name": "Your Name" },
|
||||
"server": {
|
||||
"type": "node",
|
||||
"entry_point": "server/index.js",
|
||||
"mcp_config": {
|
||||
"command": "node",
|
||||
"args": ["${__dirname}/server/index.js"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Empty `permissions` means no filesystem, no network, no spawn — pure computation only. Valid, if unusual.
|
||||
---
|
||||
|
||||
## What MCPB does NOT have
|
||||
|
||||
- **No `permissions` block.** There is no manifest-level filesystem/network/process scoping. The server runs with full user privileges. Enforce boundaries in your tool handlers — see `local-security.md`.
|
||||
- **No auto env var prefix.** No `MCPB_CONFIG_*` convention. You wire config → env explicitly in `server.mcp_config.env`.
|
||||
- **No `entry` field.** It's `server` with `entry_point` inside.
|
||||
- **No `minHostVersion`.** It's `compatibility.claude_desktop`.
|
||||
|
||||
Reference in New Issue
Block a user