Build MCP servers end-to-end. Scaffolds a production-ready Python or TypeScript server from API documentation, implements tools, validates the MCPB bundle, creates an embedded skill resource, and guides release to the mpak registry. Covers the full lifecycle from API analysis to published bundle. Use when building a new MCP server, wrapping an API, or creating an integration. Triggers include "build an MCP server", "create a server for X", "/build-mcpb".
Resources
3Install
npx skillscat add nimblebraininc/contributor-toolkit/build-mcpb Install via the SkillsCat registry.
Build MCPB
Build MCP servers end-to-end: scaffold from API docs, implement tools, validate the bundle, create an embedded skill resource, and release to the mpak registry. Supports Python (FastMCP) and TypeScript (@modelcontextprotocol/sdk).
Quick Start
> /build-mcpbThe skill bootstraps everything needed: language, service name, repo creation, template setup — then proceeds through the build pipeline.
Pipeline Overview
Phase 0: Bootstrap Language, service name, repo creation, template setup
Phase 1: API Analysis Fetch docs, identify resources, propose tools
Phase 2: Scaffold Verify project structure from template
Phase 3: Implement Write tool logic, models, client
Phase 4: Verify Lint, typecheck, test
Phase 5: Validate Bundle Manifest, build, bundle, MTF scan, runtime
Phase 6: Embed Skill Create in-package skill resource
Phase 7: Release Commit, push, cut release, verify bundlePhase 0: Bootstrap
Set up language, service name, repo, and template customization — everything needed before the build pipeline begins.
0a: Entry Routing
Determine whether this is a warm start or cold start:
- Warm start: Invoked from
/nimblebrain-contributor(or after it) in the same session. The conversation already contains a service name, language choice, and the environment has been checked. Heuristic: if the conversation already contains a service name from prior nimblebrain-contributor interaction, treat as warm. - Cold start: Standalone
/build-mcpbinvocation with no prior context. Need to establish everything from scratch.
0b: Prerequisites (cold-start only)
If cold start, verify the basics before proceeding:
- gh CLI —
gh auth statussucceeds - Language toolchain — Python:
uv --version,ruff --version,ty --version; TypeScript:node --version,npm --version - mpak CLI —
mpak --versionsucceeds
If any check fails, tell the contributor what's missing and point them to ~/.claude/skills/nimblebrain-contributor/references/DEV_SETUP.md for setup instructions. Don't block on optional tools (e.g., mpak-scanner) — just note they're unavailable and skip the phases that need them.
0c: Language
- Warm start: Use the language from the conversation context.
- Cold start: Check the current working directory:
pyproject.tomlexists → Pythonpackage.jsonexists → TypeScript- Neither → ask the user which language they're using
- Both → ask the user (unusual — clarify which is primary)
0d: Service Name
- Warm start: Use the service name from the conversation context.
- Cold start:
- If
manifest.jsonexists, parse thenamefield and strip the scope:@nimblebraininc/<name>→<name> - Otherwise, derive from the directory name: strip
mcp-prefix (e.g.,mcp-stripe→stripe) - If neither works, ask the user
- If
0e: Naming Variables
Derive these values from <name> (the service name):
| Variable | Example (<name> = jsonplaceholder) |
|---|---|
<name> |
jsonplaceholder |
<Name> (PascalCase) |
Jsonplaceholder |
<NAME> (UPPER_SNAKE) |
JSONPLACEHOLDER |
<display> (human-readable) |
Ask the user, e.g. "JSONPlaceholder" |
Language-specific derived names:
| Variable | Python | TypeScript |
|---|---|---|
| Package scope | @nimblebraininc/<name> |
@nimblebraininc/<name> |
| Module / package | mcp_<name> (underscores) |
mcp-<name> (hyphens) |
| Source directory | src/mcp_<name>/ |
src/ |
| Env var | <NAME>_API_KEY |
<NAME>_API_KEY |
0f: Repo Creation + Template Customization
Skip this sub-section if manifest.json already exists with the correct service name — the repo is already set up.
Otherwise:
Make sure the user is outside of any existing git repo. If they aren't, help them navigate to a suitable directory first.
Confirm before running:
"This will create a public repo atNimbleBrainInc/mcp-<name>on GitHub. Ready to go?"Once confirmed:
Python:
gh repo create NimbleBrainInc/mcp-<name> \ --template NimbleBrainInc/mcp-server-template-python --public --cloneNode / TypeScript:
gh repo create NimbleBrainInc/mcp-<name> \ --template NimbleBrainInc/mcp-server-template-typescript --public --cloneImmediately after cloning,
cdintomcp-<name>and replace all template placeholders with the actual service name. Do not move on until this is done — downstream phases assume the project already has correct names everywhere.
Python template substitutions:
- Rename the package directory:
mv src/mcp_example src/mcp_<name> - Replace across all files (
*.py,*.toml,*.json,*.md,Makefile,.env.example):mcp_example→mcp_<name>(package name in imports, paths, logger)mcp-example→mcp-<name>(project/bundle name)@nimblebraininc/example→@nimblebraininc/<name>(registry identifier)ExampleClient→<Name>Client(class name)ExampleAPIError→<Name>APIError(class name)EXAMPLE_API_KEY→<NAME>_API_KEY(env var)https://api.example.com/v1→ leave as TODO for Phase 3 to fill with actual API URLhttps://example.com/settings/api→ leave as TODO for Phase 3mcp-server-example→mcp-server-<name>(User-Agent)FastMCP("Example")→FastMCP("<display>")(server display name)"example"inpyproject.tomlkeywords →"<name>"- Update
pyproject.tomlURLs to usemcp-<name>repo name - Update README title and description to reference
<display>instead of "Example"
TypeScript template substitutions:
Replace across all files (*.ts, *.json, *.md, Makefile, CLAUDE.md):
YOUR_SERVER_NAME→<name>YOUR_DISPLAY_NAME→<display>YOUR_REPO_NAME→mcp-<name>YOUR_API_KEY_ENV_VAR→<NAME>_API_KEYYOUR_API_HOST→ leave as TODO for Phase 3YOUR_API_BASE_URL→ leave as TODO for Phase 3YOUR_GITHUB_USERNAME→ look up viagh api user -q .loginYOUR_SERVICE→<display>
After substitutions, do a quick sanity check — grep for any remaining example/Example/EXAMPLE (Python) or YOUR_ (TypeScript) across the project. If any remain, fix them. Then confirm to the user: "Template customized — all placeholder names replaced with <name>."
0g: API Key Readiness
Ask the user to have their API key for the target service ready. They'll need it during Phase 3 implementation.
0h: Confirm
Show bootstrap summary and ask the user to confirm before proceeding. Skip this prompt only when warm-start with all values consistent.
=> Bootstrap complete:
Language: [Python/TypeScript]
Service: <name>
Module: <module>
Env var: <NAME>_API_KEY
Repo: NimbleBrainInc/mcp-<name>
=> Ready to start the build pipeline? [Y/n]Phase 1: API Analysis
Fetch and analyze the API documentation (use WebFetch)
Identify:
- Authentication method (Bearer token, API key, OAuth)
- Base URL for API calls
- Available resources and CRUD operations
- Pagination patterns
- Response schemas
Check for OpenAPI/Swagger spec at common paths:
/openapi.json,/swagger.json,/api/openapi.json
Present summary to user for approval:
=> Analyzing [Service] API... Base URL: https://api.example.com/v1 Auth: Bearer token Resources found: [list] => Proposed tools ([count]): [Resource]: list_X, get_X, create_X, update_X ... => Proceed? [Y/n]
Phase 2: Scaffold
Phase 0 created the repo from template and customized all placeholders. Verify it's intact.
Python — check that these exist:
src/mcp_<name>/server.py,api_client.py,api_models.py,__init__.pytests/manifest.json,server.json,pyproject.toml,Makefile.github/workflows/ci.yml,.github/workflows/build-bundle.yml.gitignore,.mcpbignore
TypeScript — check that these exist:
src/index.ts,config.ts,constants.ts,types.ts,schemas.ts,formatters.tssrc/handlers/,src/utils/apiClient.ts,src/utils/errorResponse.tstests/manifest.json,server.json,package.json,tsconfig.json,Makefile.github/workflows/ci.yml,.github/workflows/build-bundle.yml.gitignore,.mcpbignore
If any files are missing, create them following the patterns in references/PATTERNS.md.
Fallback: If
.gitignoreis missing (e.g., manual repo without template), generate it using the canonical content fromreferences/PATTERNS.md. This prevents build artifacts (bundle/,deps/,*.mcpb), scanner reports, and secrets from being committed.
See references/PATTERNS.md for full directory structure reference.
Phase 3: Implement
Concept Mapping
| Concept | Python | TypeScript |
|---|---|---|
| Response models | Pydantic BaseModel |
Zod z.object() in types.ts |
| Tool input validation | Python type hints + FastMCP | Zod schemas in schemas.ts |
| HTTP client | aiohttp.ClientSession |
fetch() built-in |
| Tool registration | @mcp.tool() decorator |
server.registerTool() call |
| Response formatting | Return Pydantic model directly | formatters.ts → JSON.stringify |
| Error handling | try/except APIError |
try/catch → errorResponse() |
If Python
Implement in this order:
api_models.py— Pydantic models for API responses. UseField(alias=...)for camelCase mapping.api_client.py— Async aiohttp client. Set BASE_URL, add one method per endpoint.server.py— FastMCP server with@mcp.tool()decorators. Global client with lazy init. Dual transport (http_app + stdio).manifest.json+server.json— Fill all placeholder fields. Seereferences/CONVENTIONS.mdfor the fullserver.jsonschema.
See references/PATTERNS.md → "Python Server Patterns" for complete code examples.
If TypeScript
Implement in this order:
src/types.ts— Zod schemas for API response shapessrc/utils/apiClient.ts— Rename class, set BASE_URL, add methods. Update import inerrorResponse.ts.src/schemas.ts— Zod input schema for each toolsrc/formatters.ts— One formatter per resource type (pure functions)src/handlers/<tool>.ts— One file per toolsrc/index.ts—server.registerTool()for each toolsrc/config.ts— Rename env varmanifest.json+server.json— Fill all TODO fields, thenmake sync
See references/PATTERNS.md → "TypeScript Server Patterns" for complete code examples.
Critical Notes (TypeScript)
- Use exact dependency versions (no
^or~) — range specifiers are L2 MTF findings - Use
.jsextensions in all imports (Node ESM requirement) - Never edit
src/constants.tsmanually — usemake sync - Never edit
.github/workflows/— shared infrastructure - The embedded SKILL.md lives in
src/and is copied tobuild/during compilation
Phase 4: Verify
Run all checks and fix any issues before proceeding.
Python:
uv sync --dev
uv run ruff format src/ tests/
uv run ruff check src/ tests/
uv run ty check src/
uv run pytest tests/ -vTypeScript:
make checkThis runs: format:check → lint → typecheck → test
Phase 5: Validate Bundle
5a: Manifest Validation
Check manifest.json against the mpak registry schema. See references/CONVENTIONS.md for the full manifest format per language.
Registry validation checklist (read manifest.json and verify each):
- Name format — must match
/^@[a-z0-9][a-z0-9-]{0,38}\/[a-z0-9][a-z0-9-]{0,213}$/(e.g.,@nimblebraininc/stripe) - Version — valid semver string (e.g.,
0.1.0) - server.type — must be one of:
python,node,binary - server.mcp_config — required object with:
command(string, required) — e.g.,"python"or"node"args(array of strings, required) — e.g.,["-m", "mcp_stripe.server"]env(object, optional) — maps env vars to${user_config.<field>}references
- user_config entries — each must have:
type(required) — e.g.,"string"sensitive: truefor secrets (API keys, tokens)- Referenced via
${user_config.<field>}inserver.mcp_config.env
- tools array — each entry needs
name(required) anddescription(recommended) - Python-specific:
server.type: "python",entry_pointis module path - TypeScript-specific:
server.type: "node",entry_point: "build/index.js",${__dirname}prefix in args
mpak.json check — verify mpak.json exists in repo root with:
{
"name": "@nimblebraininc/<name>",
"maintainers": ["<github-username>"]
}This file is required for package claiming on the registry. The name must match manifest.json.
5b: Build Validation
- Python:
uv syncsucceeds, entry point module is importable - TypeScript:
npm run buildsucceeds,build/index.jsexists
5c: Bundle Inspection
- Python:
make bundle(vendors deps intodeps/, packs withnpx @anthropic-ai/mcpb pack) - TypeScript:
make bundle(builds, prunes dev deps, packs) - Both: no accidental large files (.git, node_modules), manifest.json present in bundle root
5d: MTF Compliance (if mpak-scanner available)
mpak-scanner scan .5e: Runtime Validation
The MCP protocol requires an initialize handshake before any method calls. Send initialize, then notifications/initialized, then tools/list:
Python:
printf '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"0.1.0"}}}\n{"jsonrpc":"2.0","method":"notifications/initialized"}\n{"jsonrpc":"2.0","method":"tools/list","id":2}\n' | uv run python -m mcp_<name>.server 2>/dev/nullTypeScript:
printf '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"0.1.0"}}}\n{"jsonrpc":"2.0","method":"notifications/initialized"}\n{"jsonrpc":"2.0","method":"tools/list","id":2}\n' | node build/index.js --stdio 2>/dev/nullBoth: server responds with valid JSON-RPC initialize result followed by tools/list result. No garbage on stdout (logs go to stderr).
Phase 6: Embed Skill Resource
6a: Analyze Tools
Extract the server's tool surface:
- Python: Extract all
@mcp.tool()functions fromserver.py - TypeScript: Extract all
server.registerTool()calls fromsrc/index.ts
6b: Draft SKILL.md
Generate a draft embedded skill file:
- Python:
src/mcp_<name>/SKILL.md - TypeScript:
src/SKILL.md
No frontmatter — this is pure Markdown. The draft should contain:
- Tool selection table — each tool with a one-line description and when to use it
- Context reuse rules — which tool outputs feed into subsequent calls
- Multi-step workflow patterns — 2–3 composed workflows showing how to chain tools
Example structure:
# <Service> MCP Server — Skill Guide
## Tools
| Tool | Use when... |
|------|-------------|
| `list_items` | You need to browse or search items |
| `get_item` | You have an item ID and need full details |
| `create_item` | You need to create a new item |
## Context Reuse
- Use the `id` from `list_items` results when calling `get_item`
- Use the `id` from `create_item` response for follow-up `get_item` calls
## Workflows
### 1. Inventory Check
1. `list_items` with category filter
2. For each item: `get_item` to get full details
3. Summarize findings
### 2. Create and Verify
1. `create_item` with required fields
2. `get_item` with the returned ID to confirm creation6c: Review with Contributor
The embedded skill created so far is inherently opinionated — it shapes how an LLM uses the server. The contributor understands the target API's nuances better than this pipeline can: which tool combinations are most valuable, what context flows are non-obvious, and what real workflows users will care about.
Show the draft to the contributor and ask for their input. Present the full SKILL.md content and ask:
=> Here's the draft embedded skill for your server:
[show SKILL.md content]
=> This guides how an LLM will select and compose your tools. Consider:
- Are the right workflows represented?
- Are there tool combinations or sequencing patterns you'd add?
- Any context reuse rules that aren't obvious from the tool signatures?
- Should any workflows be removed or reframed?
=> Let me know what to change, or approve to continue.Iterate with the contributor until they approve the SKILL.md. This may involve multiple rounds — the contributor might add domain-specific workflows, adjust tool selection guidance, or refine context reuse rules that only someone familiar with the API would know.
Do not proceed to wiring (6d) until the contributor has explicitly approved the skill content.
6d: Wire the Resource
Python: Add to server.py:
from importlib.resources import filesimportSKILL_CONTENT = files("mcp_<name>").joinpath("SKILL.md").read_text()instructions=parameter onFastMCP(...)constructor@mcp.resource("skill://<name>/usage")decorated function returningSKILL_CONTENT
TypeScript: Add to src/index.ts:
import { readFileSync } from "fs"andimport { join } from "path"const SKILL_CONTENT = readFileSync(join(__dirname, "SKILL.md"), "utf-8")instructionsinMcpServerconstructorserver.resource("skill-usage", "skill://<name>/usage", ...)registration
TypeScript bundling: Since .mcpbignore excludes both src/ and *.md:
- Add to Makefile
buildtarget:cp src/SKILL.md build/SKILL.md - Add to
.mcpbignore:!build/SKILL.md
See references/PATTERNS.md for complete wiring examples in both languages.
6e: Verify
Run MCP runtime validation (same as Phase 5e) and confirm resources/list includes skill://<name>/usage:
Python:
printf '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"0.1.0"}}}\n{"jsonrpc":"2.0","method":"notifications/initialized"}\n{"jsonrpc":"2.0","method":"resources/list","id":2}\n' | uv run python -m mcp_<name>.server 2>/dev/nullTypeScript:
printf '{"jsonrpc":"2.0","method":"initialize","id":1,"params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"test","version":"0.1.0"}}}\n{"jsonrpc":"2.0","method":"notifications/initialized"}\n{"jsonrpc":"2.0","method":"resources/list","id":2}\n' | node build/index.js --stdio 2>/dev/nullThe response should include a resource with uri: "skill://<name>/usage".
Note: The embedded skill is encouraged but not mandatory per mpak spec. If no meaningful workflows exist yet (e.g., the server has only 1–2 tools), it's acceptable to skip this phase and add the skill later.
Phase 7: Release
The contributor created and owns the repo — there is no PR to open. The goal is: code on main → GitHub Release → build-bundle.yml triggers → bundles built and published to the mpak registry.
7a: Pre-flight Checks
Before committing, verify the release will succeed:
- Version consistency — Read the version from
manifest.jsonand confirm it matches across all version sources:- Python:
pyproject.toml,server.json,src/mcp_<name>/__init__.py - TypeScript: run
make syncand verify no diff
If anything is out of sync, runmake bump VERSION=<version>to fix.
- Python:
mpak.json— Must exist in the repo root withnamematchingmanifest.json. Without it, the registry announce fails silently after the bundle builds.- No secrets in working tree — Check for
.envfiles or anything containing real API keys. Warn the contributor if found; do not stage them. - Skill resource wired — Verify SKILL.md exists at the expected path (
src/mcp_<name>/SKILL.mdfor Python,src/SKILL.mdfor TypeScript) and the resource is registered in server code.
7b: Stage and Commit
Do not use git add -A. Review first, then stage explicitly:
- Run
git statusand show the contributor what will be committed. - Stage server source, tests, skills, manifests, config, and workflow files. Do NOT stage
.env, credentials, or debug artifacts. - Derive the version from
manifest.jsonfor the commit message:
VERSION=$(jq -r .version manifest.json)
git add <files...>
git commit -m "Add <service> MCP server v${VERSION}"7c: Push
Do NOT push on the contributor's behalf. Ask them:
"Push your changes to GitHub: git push origin main"
7d: Verify CI
After the push, actively monitor CI — don't just point to a URL:
gh run list --repo NimbleBrainInc/mcp-<name> --branch main --limit 1
gh run watch --repo NimbleBrainInc/mcp-<name>If CI fails: read the logs with gh run view <run-id> --log-failed, help the contributor diagnose and fix, then create a new commit (not amend).
7e: Cut Release
Once CI passes, derive the tag from manifest.json:
VERSION=$(jq -r .version manifest.json)
gh release create "v${VERSION}" --title "v${VERSION}" --generate-notes7f: Monitor Bundle Build
The release triggers build-bundle.yml on 3 runners (linux-amd64, linux-arm64, darwin-arm64). Track it:
gh run list --repo NimbleBrainInc/mcp-<name> --workflow=build-bundle.yml --limit 1
gh run watch --repo NimbleBrainInc/mcp-<name>If a runner fails, check logs with gh run view <run-id> --log-failed. Common issues:
- Dependency vendor fails — version conflicts in
pyproject.toml/package.json - mcpb pack fails —
manifest.jsonformat issues (re-check against Phase 5a checklist) - OIDC announce fails —
mpak.jsonmissing ornamedoesn't match manifest
7g: Wrap Up
Once all 3 runners succeed, the server bundle is announced to the mpak registry. Tell the contributor:
"Your <display> MCP server is built, bundled, and announced to the mpak registry. It may take several minutes to propagate. Once live, anyone can run it:
mpak run @nimblebraininc/<name>To check availability:
mpak search <name>References
See references/ in this skill for:
CONVENTIONS.md— Naming, manifest format, versioning, build system, entry points (Python + TypeScript)PATTERNS.md— Complete code patterns, directory structures, CI workflows (Python + TypeScript)SKILL_FORMAT.md— Embedded skill resource format and wiring patterns
The canonical project structure comes from the GitHub template repos (NimbleBrainInc/mcp-server-template-python and NimbleBrainInc/mcp-server-template-typescript), cloned and customized during Phase 0 Bootstrap.