Skip to content
React 19No peer dependencies

MCP server

Serve the component catalogue to Claude Code, Codex and Cursor over MCP (Model Context Protocol), and build a screen from a Figma design: read the frame, install the components, write the JSX, check it against the real API.

Found something wrong, or missing? Open an issue on the bug tracker.

On this page

GramproKit ships an MCP server. Model Context Protocol is how a coding agent calls tools, and this one answers questions about the component library: what exists, what a component's props really are, what a Figma frame contains, and whether the JSX the agent just wrote is valid.

It is the difference between an agent recalling an API and reading one.

Beta. The tools work and are tested, but their names and shapes may change before this settles.

It answers on demand, which is why it beats pasting documentation into a prompt: the passports come to 280 KB and a page needs two or three components. The agent skill is the other half - a document an agent reads up front, telling it the library exists and how it is meant to be used. Either is useful alone; together an agent knows to reach for the library and can ask what it actually takes.

Setup

npx @grampro/kit mcp install

That writes the configuration for every agent it knows — Claude Code, Codex and Cursor — and names the files it touched. Narrow it with --for codex, or a comma-separated list.

agentfilekey
Claude Code.mcp.jsonmcpServers.grampro
Codex.codex/config.toml[mcp_servers.grampro]
Cursor.cursor/mcp.jsonmcpServers.grampro

Use it rather than editing those files by hand. It writes UTF-8 without a byte-order mark and with LF endings, and a BOM is the one thing that reliably breaks them: PowerShell's Out-File -Encoding utf8 adds one, and JSON parsers then reject the file with an error that names the first character and explains nothing. Running it again merges, leaves other servers alone, and reports unchanged when there is nothing to do.

The Codex config is project-scoped. For every project, put the same [mcp_servers.grampro] block in ~/.codex/config.toml.

An MCP server is not invoked by name. There is no /grampro; the model calls the tools itself when they are relevant. /mcp shows whether the server is connected.

It writes to the project root - the nearest folder above you holding a .git or a package.json - and prints which one it picked, because an agent only reads these files from the folder it has open.

If the server does not show up

Writing the file is not the same as loading the server, and in Claude Code two things sit between them.

It needs approving. A .mcp.json server is project-scoped, and Claude Code asks before trusting one. Until you agree it stays at Pending approval, and nothing asks a second time; claude mcp reset-project-choices brings the prompt back.

It needs a fresh session. The configuration is read at session start, so an editor window that was already open sees nothing. In the VS Code extension, run Developer: Reload Window.

/mcp lists the servers with their state, which separates "not configured" from "configured, not approved".

Both are skippable by installing it for yourself instead of for the project:

bash
claude mcp add -s user grampro -- npx -y @grampro/kit mcp

User scope lives in ~/.claude.json, loads in every project, and needs no approval because it belongs to you rather than to the repository. Project scope is still the better choice when the team should get it from a commit.

The tools

toolwhat it answers
list_componentsWhat exists, and what each one is for
get_componentProps with types and allowed values, which are required, handler signatures, slots, states, composition rules
get_exampleWorking usage from the component's README
get_guidanceAuthored notes on forms, overlays, the grid, pickers, styling, accessibility
figma_frameA design's structure, text, repeated regions, and the theme variables its colours map onto. Takes a designFile path, a pasted design, or a Figma url
install_componentCopies components into the project being worked in
validate_usageWhether the JSX an agent just wrote matches the passports

validate_usage is the point. A page written from passports alone compiles on the first attempt — every type, union value and handler signature in a passport is accurate, because it was generated from the source. What a passport cannot stop is a model reaching for a prop it remembers from somewhere else, so the server checks:

code
<Alert variant="info" title="Hi" dismissible onDismiss={...} />

  dismissible - not a prop of this component. Did you mean: onDismiss

It starts in about the time an empty Node process takes. An agent spawns the server once per session, so the mcp command is routed before the CLI's own dependencies load; they are no use to a JSON-RPC loop and cost around 70ms.

From a whole design to a whole application

One frame at a time is the wrong unit for a product. Export every screen at once and the theme is read across all of them, which matters more than the convenience: a screen that is mostly white names a different background from one that is mostly grey, and derived per frame they disagree.

1. Export every screen

Get the plugin on Figma Community

In Figma: Plugins -> Grampro Kit -> Export all screens for an agent. It walks every page, takes each top-level frame, and derives one set of theme values from the whole design. Component sets are skipped - the pages the plugin generates are the library, not the product.

Press Save .json, and put the file in your project.

2. Split it into a folder

npx @grampro/kit design ./design.json
text
Wrote 3 screen(s) to design/
  purchase-orders.json         Purchase orders (Product) - 22 nodes
  suppliers.json               Suppliers (Product) - 16 nodes
  settings.json                Settings (Admin) - 12 nodes
  index.json                   the plan: every screen, shared theme

A browser cannot write a directory, and a Figma plugin is an iframe - so the plugin hands over one file and the CLI does the filesystem work. Each screen is written in the same shape a single-frame export has, so figma_frame reads one with no special case. Pass a second argument to put the folder somewhere other than design/.

3. Have the mapping checked before anything is built

index.json is the plan: every screen with its size, node count and the regions whose names repeat, plus the one shared theme. Start there.

text
Read design/index.json. For each screen, tell me which kit component you
would use for each repeated region, and which ones you are unsure about.
Do not write any code yet.

This is the cheap place to be wrong. A design drawn without the component library names nothing directly, so every component is inferred - and a guess that Action is a Card rather than a Button costs one sentence here and five screens of rework later.

4. Build one screen at a time

text
Now build design/purchase-orders.json.
 
1. figma_frame with designFile set to that path.
2. get_component for each component before you write it.
3. install_component for anything this project does not have.
4. Write the screen.
5. validate_usage on the JSX, and fix what it reports.
 
Use the --gbs-* variables from index.json rather than hard-coded colours.
Stop after this screen and I will check it before you start the next.

One screen per pass, validated each time. Building all of them in a single run is faster right up until an inference is wrong, at which point the same mistake is in every screen before you have seen any of them.

Why a file rather than a paste

designFile takes a path; design takes the JSON itself. Prefer the path for anything but a small frame. A chat input caps at around fifty thousand characters and a whole screen passes that, and a pasted export sits in the conversation twice - once as the argument, again inside the tool call - where a file appears only in the answer.

From one Figma frame to one screen

For a single screen, or to try the run before committing to a whole design. The agent reads the frame, finds out what the components actually take, installs what it needs, writes the screen, and has its own work checked before it reports back.

Before you start

npm i -D @grampro/kitnpx grampro-kit mcp install

Reload your editor window, approve the server when asked, and check /mcp shows it connected. For the Figma URL route, store a token once:

npx grampro-kit secret set figma

In Figma, right-click the frame you want and choose Copy link to selection. That link carries a node-id, which takes the read down to a single API call instead of listing the file first.

The prompt

Paste this, with your own link and screen name. The order is the point: every step after the first depends on the agent having read something rather than assumed it.

text
Build the "Purchase orders" screen from this Figma frame:
https://www.figma.com/design/<file-key>/<name>?node-id=2003-3731
 
Work in this order, using the grampro MCP server:
 
1. figma_frame on that URL, to read the design.
2. list_components, then get_component for each component you plan to use.
   Do not write a prop you have not read there.
3. install_component for anything this project does not already have.
4. Write the screen.
5. validate_usage on the JSX you wrote, before telling me you are done,
   and fix whatever it reports.
 
Use the --gbs-* theme variables figma_frame returns rather than hard-coded
colours. If a region of the design has no matching component, tell me
instead of approximating it.

What each step is for

figma_frame returns the structure, the text, the regions whose names repeat, and the theme variables the design's own colours map onto. It does not decide which component each region is - see below.

get_component is the step people skip, and skipping it is what produces code that looks right and does not compile. The passport carries every prop with its type, which values a union accepts, which props are required, and handler signatures, all generated from the source. An agent that reads it does not have to remember.

validate_usage is the one that earns the server its place. A page written from passports alone compiles on the first attempt; what a passport cannot prevent is a model reaching for a prop it remembers from a different library. That is what this catches:

text
<Alert variant="info" title="Hi" dismissible onDismiss={...} />
 
  dismissible - not a prop of this component. Did you mean: onDismiss

No token, or a rate-limited one

Use the plugin instead. It reads the scene graph from inside Figma, so it needs no token, works on any file you can open, and never calls the REST API - which means it cannot be rate limited.

Getting it.

Install Grampro Kit

Using it. Select the frame and run Plugins → Grampro Kit → Export selection for an agent. Press Save .json and pass the path as designFile, or Copy JSON and paste it with this:

text
Build this screen from the design JSON below. Pass it to figma_frame as the
`design` argument, then follow the same order: get_component before writing
any component, install_component for what is missing, validate_usage at the
end.
 
<paste the plugin's JSON here>

The plugin reads the scene graph from inside Figma, so it needs no token, works on any file you can open, and cannot be rate limited.

The two routes

By URL - pass a figma.com/design/... link. With just the URL it lists the frames in the file; called again with a frame it returns that screen. A link that already carries a node-id, which is what Figma copies when you right-click a frame, goes straight there in one call. This route needs a token.

By plugin export - in Figma, run Grampro Kit -> Export selection for an agent and hand the JSON over as design. The plugin reads the scene graph from inside Figma, so it works on any file you can open, asks for nothing, and cannot be rate limited.

Either way you get the same answer: the structure, the text, the regions whose names repeat - eight Status cell nodes are a column - and the --gbs-* variables the design's own colours and radii map onto.

Colours are given roles rather than ranked, because the most-used colour in a design is the body text and a near-black navy scores high on saturation - so "most common" and "most saturated" both pick the wrong one. Lightness and saturation together find the accent.

What it will not do

It does not decide which component each region of the design is. A design drawn without the component library has no instances to look up, so that call is inference - and an agent holding the catalogue and the designer's own names for things infers better than string matching inside a tool would. Anything that is not a kit component is reported as such rather than approximated.

Rate limits

Figma charges for file reads by the size of the response, so asking for a whole file to answer a question about one frame is what earns a 429 - and the lockout is long. A Retry-After of 395274 has been seen on one token.

Nothing here fetches a whole file any more. The frame listing uses depth=1, which returns the document's outline rather than its contents, and the frame itself is fetched by id. Responses are held for five minutes, so the natural flow of list-then-read costs one outline and one frame. A URL with a node-id skips the listing entirely.

If a token is already locked out, the plugin export still works.

Credentials

The URL route needs a Figma personal access token, and it goes where the operating system keeps credentials:

npx @grampro/kit secret set figma

It prompts with the echo off and never takes the value as an argument - arguments land in shell history and in every process listing on the machine. For a script, pipe it: echo "$TOKEN" | npx @grampro/kit secret set figma.

platformwhere it goes
WindowsData Protection API, tied to your Windows account
macOSthe keychain
LinuxSecret Service (GNOME Keyring, KWallet)
anything elsea 0600 file in your config directory, and it says so

secret list shows what is stored, masked, and names the backend in use; secret remove figma deletes it. Give the token File content: read and nothing else.

The lookup order is FIGMA_TOKEN in the environment, then the store, then a .env - the last only so an upgrade does not break a project set up the old way, and it prints a note saying to move it. A credential in a file inside a working tree is one git add -A away from being published, which is why this moved.