@linktr.ee/arbor-mcp
v0.5.0
Published
Model Context Protocol server exposing Arbor design system tools: Playroom snippets, Figma→code sync health, UX-writing checks, decision-log lookup, and federated component/version metadata.
Downloads
1,829
Maintainers
Readme
@linktr.ee/arbor-mcp
A Model Context Protocol server that exposes Arbor design system tools to MCP-connected AI agents (Claude Code, Claude Desktop, Cursor, Zed, etc.).
What it does
Registers a set of read-only Arbor design system tools. The component-resolution tool is documented in detail below; the DS-as-data metadata tools (component inventory, single-component lookup, version history) are described under DS-as-data tools.
arbor_open_in_playroom
The legacy name
arbor.open_in_playroomremains registered as a deprecated alias (it routes to the same handler) and will be removed in the next major. Prefer the snake_case name.
Resolves an Arbor PascalCase component name into a Playroom share URL with the canonical default snippet pre-loaded.
Input (validated by a strict Zod schema — unknown keys are rejected):
{ "componentName": "Button" }Output: structuredContent matching the tool's outputSchema (the machine
payload), plus a concise one-line text summary (the human mirror — it does
not duplicate the full JSON):
{
"coverage": "mapped",
"arborComponentName": "Button",
"snippet": "<Button>Save changes</Button>",
"url": "https://arbor.linktr.ee/playroom/?slug=flowering-sapling-glen-5",
"source": "minted",
"slug": "flowering-sapling-glen-5"
}A mapped result carries the pretty ?slug= URL when slug minting succeeds
(source: "minted" or "cached"). If minting is unavailable it degrades to a
self-contained lz-string #?code=… URL with degraded: true and a reason,
and no slug.
coverage (for this by-name tool) is one of:
mapped— the component appears infigma-mappings.tsAND has a canonical entry incompositions.ts. High confidence.not_found— the URL points at the Playroom homepage; agents should surface this to the human rather than silently following the link.
A third coverage,
fallback(best-effort fuzzy name matching), exists only on the Figma plugin's selection-driven node-ID path. This by-name MCP tool never returns it — itsoutputSchemaismapped | not_found.
DS-as-data tools
Three read-only metadata tools (shipping in a forthcoming release) expose the
design system as queryable data. They answer "what components / versions exist"
— not token values. (For token values, read the compiled CSS in
packages/design-tokens/dist/web/ or the shadcn registry.json, which are
fresher.)
arbor_list_versions— design system version history: each entry is{ id, name, createdAt, readOnly }.arbor_list_components— component inventory:name,id, and a shortdescriptionper component. Returns top-level components only (Figma variant instances are filtered out). Accepts an optional case-insensitivequerystring that filters by name or description.arbor_get_component— one component's metadata, looked up byname(case-insensitive) orid. Returns{ found, component }.
(A fourth tool, arbor_get_docs, is deliberately not built yet — deferred
until a docs page-content route exists.)
Runtime config + graceful degradation
These tools read the design system's metadata from an internal Lambda
federation proxy. The proxy URL is built in (override with
ARBOR_MCP_SUPERNOVA_PROXY_URL). While the route is access-gated, also set
ARBOR_MCP_SUPERNOVA_PROXY_TOKEN — one shared internal value provisioned by
the Arbor team (not a per-engineer token, and never bundled in this package).
If the route is open, the tools work with no configuration at all:
| Variable | Purpose |
| --------------------------------- | -------------------------------------------------------- |
| ARBOR_MCP_SUPERNOVA_PROXY_URL | Full proxy URL, including the /supernova/query path |
| ARBOR_MCP_SUPERNOVA_PROXY_TOKEN | Shared inbound bearer token presented to the proxy |
When either var is unset — or the proxy is unreachable — the tools degrade
gracefully: they return a successful result with source: "unavailable" and
a detail string explaining why. They never throw and never crash the server.
An agent should treat "unavailable" as "couldn't reach the data" rather than
"the design system has no components/versions."
Metadata, not token values. These tools surface Supernova metadata (component names, ids, descriptions, version history) only. They deliberately do not return token values — those live in the compiled CSS (
packages/design-tokens/dist/web/) and the shadcnregistry.json, which are the fresher source of truth.
Install
The package is published publicly to npm as @linktr.ee/arbor-mcp. The
expected use is npx-style spawning by an MCP client — see the wiring
section below. No global install required.
# Smoke-check the latest release. The server speaks JSON-RPC over stdio (there
# is no --help flag); pipe a handshake in so stdin closes and the process
# exits. Expect a tools/list response naming arbor_open_in_playroom.
printf '%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"smoke","version":"0"}}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/list"}' \
| npx -y @linktr.ee/arbor-mcpIf you're working inside the Arbor monorepo, build the local source instead so you're testing your changes:
yarn install
yarn workspace @linktr.ee/arbor-mcp buildThat produces a single bundled ESM file at
packages/arbor-mcp/dist/index.js (with shebang) ready to be spawned by
any MCP client.
Wire it into an MCP client
Claude Code / Claude Desktop / Cursor (.mcp.json or equivalent)
The recommended setup uses the public npm package — no clone required:
{
"mcpServers": {
"arbor": {
"command": "npx",
"args": ["-y", "@linktr.ee/arbor-mcp"]
}
}
}If you're hacking on the server inside the Arbor workspace, point at the local build instead so changes pick up without republishing:
{
"mcpServers": {
"arbor": {
"command": "node",
"args": ["./packages/arbor-mcp/dist/index.js"]
}
}
}For portability across clients that don't substitute path variables
(Claude Desktop, most CLI MCP clients), prefer absolute paths over
${workspaceFolder}.
Verify the connection
After restarting your MCP client, the arbor_open_in_playroom tool should
appear in the available-tools list. Ask the agent something like:
Use the arbor MCP server to open Button in Playroom.
The agent will call the tool and return the URL.
Develop
# Run the server against tsx directly (no build step) — useful for
# iterating on tool handlers.
yarn workspace @linktr.ee/arbor-mcp dev
# Run the tests: the in-memory Client handshake + protocol contract
# (server.test.ts) plus tool metadata, input validation, lookup
# happy/not-found paths, and the figma-mappings coverage gate.
yarn workspace @linktr.ee/arbor-mcp test
# Smoke-test the full JSON-RPC handshake end-to-end.
printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"manual","version":"0.0.0"}}}\n{"jsonrpc":"2.0","id":2,"method":"tools/list"}\n' | node dist/index.jsArchitecture
packages/arbor-mcp/
├── src/
│ ├── index.ts # entrypoint: connects stdio transport
│ ├── server.ts # creates the McpServer + registers ARBOR_TOOLS
│ └── tools/
│ ├── registry.ts # ArborTool convention + registerArborTool
│ └── open-in-playroom.ts # tool: schemas, annotations, handler
├── tests/
│ ├── server.test.ts # in-memory Client handshake + contract
│ └── open-in-playroom.test.ts # node --test, validation + coverage
├── build.mjs # esbuild bundle to dist/index.js
└── tsconfig.json # rootDir: ../.. for cross-package importsThe arbor_open_in_playroom handler delegates to lookupByArborName in
apps/playroom/src/canonical-snippet-lookup.ts — the same module the
Figma plugin's "Open in Playroom" relaunch button uses. This keeps the
agent-facing surface and the designer-facing surface in lockstep.
The server is built on the SDK's high-level McpServer + registerTool
(the modern, non-deprecated API). Every tool is an ArborTool registry
object (tools/registry.ts): a .strict() Zod inputSchema, a Zod
outputSchema (the handler returns matching structuredContent),
annotation hints, and a total handler. Input-validation and handler errors
surface as isError: true results, never protocol rejections.
Why bundle vs. emit tsc files?
tsc would emit one .js per .ts and preserve cross-package directory
structure under dist/. Node's ESM resolver doesn't add .js extensions
to extensionless imports (the workspace TS source uses extensionless
imports because moduleResolution: bundler), so the multi-file output
fails to resolve at runtime. Bundling with esbuild flattens the
dependency graph into a single self-contained file. tsc --noEmit still
runs in the build script for type checking.
Future tools
When adding a second tool, follow the registry convention:
- New file under
src/tools/<name>.tsexporting anArborToolobject (seetools/registry.ts):name, optional deprecatedaliases, aconfigwithtitle/description/.strict()ZodinputSchema/ ZodoutputSchema/annotations, and a totalhandlerreturning{ content, structuredContent }. - Add it to the
ARBOR_TOOLSarray insrc/server.ts. - Mirror the test file shape under
tests/(drive it through the in-memoryClientas inserver.test.ts, plus pure-handler unit tests).
Pass inputSchema as a z.object({...}).strict() (a ZodObject, not a
raw shape) — the SDK only preserves .strict() rejection of unknown keys
for a ZodObject; a raw shape is rebuilt non-strict and silently strips them.
