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.
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 installThat 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.
| agent | file | key |
|---|---|---|
| Claude Code | .mcp.json | mcpServers.grampro |
| Codex | .codex/config.toml | [mcp_servers.grampro] |
| Cursor | .cursor/mcp.json | mcpServers.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:
claude mcp add -s user grampro -- npx -y @grampro/kit mcpUser 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
| tool | what it answers |
|---|---|
list_components | What exists, and what each one is for |
get_component | Props with types and allowed values, which are required, handler signatures, slots, states, composition rules |
get_example | Working usage from the component's README |
get_guidance | Authored notes on forms, overlays, the grid, pickers, styling, accessibility |
figma_frame | A 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_component | Copies components into the project being worked in |
validate_usage | Whether 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:
<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
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.jsonWrote 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 themeA 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.
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
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 installReload 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 figmaIn 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.
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:
<Alert variant="info" title="Hi" dismissible onDismiss={...} />
dismissible - not a prop of this component. Did you mean: onDismissNo 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. A Figma Community listing is pending review. Until that lands, install it from source - you need the Figma desktop app, since development plugins cannot be imported in the browser:
git clone https://github.com/gramproindia/kit.gitThen Plugins -> Development -> Import plugin from manifest... and
choose figma-plugin/manifest.json from the clone. The build is
committed, so there is nothing to compile.
Using it. Select the frame and run Plugins -> Development -> Grampro Kit -> Export selection for an agent. Copy the JSON it shows, and paste it with this:
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 figmaIt 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.
| platform | where it goes |
|---|---|
| Windows | Data Protection API, tied to your Windows account |
| macOS | the keychain |
| Linux | Secret Service (GNOME Keyring, KWallet) |
| anything else | a 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.