Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
ac1bc47
feat(limitless-plugin): complete variable/token authoring verbs in wr…
tknatwork Jul 14, 2026
2af9a0d
feat(limitless-plugin): add Slots suite — get_slots, reset_slot, appe…
tknatwork Jul 14, 2026
90acdbf
feat(limitless-plugin): enable FigJam/Slides/Buzz surfaces + Dev-Mode…
tknatwork Jul 14, 2026
67b915b
feat(limitless-plugin): FigJam authoring adapter — 7 tools (Wave 4)
tknatwork Jul 14, 2026
a88b443
feat(limitless-plugin): Figma Slides authoring adapter — 7 tools (Wav…
tknatwork Jul 14, 2026
3a03d97
feat(limitless-plugin): Figma Buzz authoring adapter — 5 tools (Wave 6)
tknatwork Jul 14, 2026
0d13da0
fix(limitless-plugin): move Dev-Mode companion to dev-mode/manifest.json
tknatwork Jul 14, 2026
14692aa
fix(limitless-plugin): make Dev-Mode companion self-contained (Figma …
tknatwork Jul 14, 2026
6115ba3
refactor(limitless-plugin): make canvas + Dev Mode the flagship manifest
tknatwork Jul 14, 2026
3f7ea88
fix(limitless-plugin): extras manifest must include "figma" (Figma rule)
tknatwork Jul 14, 2026
2d73678
feat(skills): bundle offline design-system knowledge layer (Wave 1a)
tknatwork Jul 14, 2026
a7d25ec
feat(skills): serving tools — list_skills / read_skill / get_build_re…
tknatwork Jul 14, 2026
ead0781
feat(lint): lint_run plugin channel + lint_design_system orchestrator…
tknatwork Jul 14, 2026
ae734e5
feat(lint): token-tier detectors — 9 variable-graph rules (Wave 3a)
tknatwork Jul 14, 2026
ff47102
feat(lint): scope-discipline detectors — 7 rules (Wave 3b)
tknatwork Jul 14, 2026
dc086e5
fix(lint): guard variant components in lint_run snapshot (live-test bug)
tknatwork Jul 14, 2026
778cda8
fix(lint): calibrate to advise, not dictate — real-file tuning (Wave 3d)
tknatwork Jul 14, 2026
87c7f5f
feat(lint): node-binding scan + no-node-binds-primitive (Wave 3c)
tknatwork Jul 14, 2026
28514a2
feat(lint): mode/theming + naming detectors — 7 rules (Wave 4)
tknatwork Jul 14, 2026
2e951d4
feat(lint): codegen-contract detectors — 3 rules (Wave 5)
tknatwork Jul 14, 2026
97719ff
feat(lint): component/variant-structure detectors — 4 rules (Wave 6)
tknatwork Jul 14, 2026
7ef3b0c
feat(lint): accessibility contrast detectors — 2 rules (Wave 7)
tknatwork Jul 14, 2026
2b28820
fix(lint): a11y contrast pairs only via on-<X> convention + regressio…
tknatwork Jul 14, 2026
9d166c9
test(lint): golden clean-file fixture — 0 findings across the full suite
tknatwork Jul 14, 2026
4b8c75a
feat(lint): actionable, self-healing lint gate in get_build_recipe + …
tknatwork Jul 14, 2026
5f44eb7
feat(lint): 4 offline-verifiable detectors + dirty-fixture true-posit…
tknatwork Jul 14, 2026
cbe42b0
feat(lint): opt-in config surface + 4 config-driven rules (37→41/57)
tknatwork Jul 14, 2026
070a8cc
fix(lint): address 3 confirmed findings from the adversarial review
tknatwork Jul 14, 2026
406e1cc
feat(lint): 5 pending rules that need no new plugin data (41→46/57)
tknatwork Jul 14, 2026
ba9e63a
feat(lint): plugin snapshot enrichment + 5 node/variant-data rules (4…
tknatwork Jul 14, 2026
fe47cc5
fix(lint): stop component walk at nested-instance boundaries (review …
tknatwork Jul 14, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions figma-limitless-mcp/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,11 @@ binaries/
# Server build output
server/dist/

# Extras companion (FigJam/Slides/Buzz): copies of the built bundle (kept in
# sync by plugin/scripts/sync-extras.mjs). The manifest.json itself IS tracked.
plugin/extras/code.js
plugin/extras/index.html

# Editor/IDE
.idea/
.vscode/
Expand Down
87 changes: 79 additions & 8 deletions figma-limitless-mcp/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,67 @@ Agent identity defaults to MCP clientInfo (`node.agentSupplier`); tools accept a
APIs are beta ("subject to change" per Plugin API Update 127, June 2026). Verified live 2026-07-13:
Motion styles enumerate; shaders API present (0 shaders in file).

## Design-system skills + structure linter (closed loop)

Two offline halves that answer "how should the tokens be derived?" and "is the
structure right?" — no network, no Figma AI credits.

- **Knowledge** — Markdown skills bundled in `server/skills/`, copied to
`dist/skills/` at build (`scripts/copy-skills.mjs`) and served by
`server/src/skills.ts`: `list_skills` (catalog), `read_skill(slug)` (full doc,
whitelisted slug — no path traversal), `get_build_recipe(step?)` (the canonical
Primitive→Semantic→Component order + the step's **actionable lint gate**).
- **Linter** — `lint_design_system` runs 51/57 detectors over a `LintSnapshot`. The
plugin's `lint_run` gathers the snapshot (variable graph + styles + components +
node bindings, after `loadAllPagesAsync()`) — plus **per-component enrichment**
from one bounded DFS (20k-node budget + `componentScanTruncated`): TEXT-style
`fontSize`, and per-component AGGREGATES (raw-paint/untyped-text booleans, min
font size, referenced-property-key union, variant tuples) — never raw node
trees, so the payload stays small. The DFS treats nested instances/components as
boundaries (their layers belong to their own root), so defects aren't
mis-attributed. The server runs the detectors, which are **pure functions**
`(snap, config?) => PartialFinding[]` in `server/src/lint/detectors/<tier>.ts`,
registered into the `DETECTORS` map via the `detectors/register.ts` side-effect
that `lint/index.ts` imports. Enrichment-driven detectors degrade to silent when
their fields are absent (old plugin build). Remaining 6 rules are deferred
(instance-override / frame-fingerprint / exportAsync / Code-Connect data).

**The closed loop:** `get_build_recipe(step)` → build that tier → run the gate's
`run` call (`lint_design_system {only:[…]}`) → fix `severity:error` findings →
advance to `next_step`. `buildGate` splits each gate into `enforced_now` vs
`forward_declared` from the live registry at runtime, so a gate never asks the
lint tool for a rule whose detector hasn't landed.

**Philosophy — advise, don't dictate (load-bearing).** Everyone's DS structure is
their own choice. Only *objectively broken* things are `severity:error`
(alias-target-resolves, scope-legal-for-resolved-type); every opinionated rule is
`warn`. The linter is **read-only — it never mutates the design system.** Rules
must only assert what they can prove: e.g. a11y contrast pairs fg↔bg *only* via
the explicit `on-<X>` naming convention, never by guessing from suffixes (loose
matching fabricated 53 findings on a real 3-tier DS that just doesn't use `on-X`).

**Opt-in config surface.** House-style / config-required rules (`semantic-role-allowlist`,
`top-segment-in-tier-vocabulary`, `numeric-scale-zero-padded`, `codesyntax-web-matches-name`)
are marked `defaultOn:false` in the registry and run ONLY when explicitly enabled
— so the default lint stays advisory-but-precise. `runLint`/`lint_design_system`
take `enable[]` / `disable[]` / `config{}` (per-rule, keyed by rule_id). Config is
validated in `lint/config.ts` (`resolveRuleConfig` → `LintConfigError`); bad/missing
config for an enabled rule is reported under `config_errors` and the rule skipped —
never a crash (detectors also run inside per-rule try/catch → `rule_failures`). The
report's `available_optin[]` advertises every off-by-default rule with its
`config_shape` + an `enable_hint`, so the AI can discover and turn them on.
`analyze(snap)` is WeakMap-memoized so the whole suite classifies the graph once.

**Adding a detector** touches 3 places: the pure fn in
`server/src/lint/detectors/<tier>.ts` (signature `(snap, config?)`) + its bundle
export (already `Object.assign`ed in `detectors/register.ts`), and the rule entry
in `server/src/lint/registry.ts` (objectively-broken → `error`, else `warn`; add
`defaultOn:false` for house-style/config rules). For a **configurable/opt-in**
rule also add a `RULE_CONFIG` entry in `lint/config.ts` (shape + defaults +
validation) and read the resolved `config` arg in the detector. Then add a fixture
case to `server/test/` and, if it belongs to a build step, wire it into
`STEP_GATES` in `skills.ts`.

## Conventions & invariants

- **pnpm only** (workspace rule). Build: `pnpm install && pnpm run build` in each package.
Expand All @@ -49,14 +110,24 @@ Motion styles enumerate; shaders API present (0 shaders in file).

## Testing

`scripts/e2e-live-test.mjs` is a live end-to-end suite that exercises ~50 tool
paths against a real Figma file. Prereq: Figma Desktop open with the plugin
running in a scratch file. Run `node scripts/e2e-live-test.mjs` from the repo
root (or `FILE_KEY=<key> node …` to target one of several connected files). It
creates everything inside a dedicated test page and removes all artifacts
(page, styles, variables, collections, temp state) on exit — the file is left
as found. Only one MCP server may hold `:1994`; kill stragglers first
(`lsof -i :1994`) so the test runs as leader, not follower.
Two layers:

- **Unit** — `cd server && pnpm test` (builds, then `node --test test/*.test.mjs`).
Runs against compiled `dist/`, no Figma needed — the lint detectors are pure
functions. Covers the a11y contrast regression (on-`X` pairing, same-colour
skip, suffix-only non-pairing) and the **golden clean-file fixture** (a correct
3-tier DS that must produce 0 findings across the full suite — the anti-noise
ratchet). **Server/detector code cannot be hot-reloaded into a running MCP
session** (stdio, spawned once at session start), so verify detector changes
here offline; a live re-lint only reflects new code after a fresh session.
- **Live E2E** — `scripts/e2e-live-test.mjs` exercises ~50 tool paths against a
real Figma file. Prereq: Figma Desktop open with the plugin running in a scratch
file. Run `node scripts/e2e-live-test.mjs` from the repo root (or
`FILE_KEY=<key> node …` to target one of several connected files). It creates
everything inside a dedicated test page and removes all artifacts (page, styles,
variables, collections, temp state) on exit — the file is left as found. Only
one MCP server may hold `:1994`; kill stragglers first (`lsof -i :1994`) so the
test runs as leader, not follower.

## Registration

Expand Down
23 changes: 23 additions & 0 deletions figma-limitless-mcp/plugin/extras/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"name": "Figma Limitless MCP — Extras (FigJam / Slides / Buzz)",
"id": "figma-limitless-mcp-extras",
"api": "1.0.0",
"main": "code.js",
"ui": "index.html",
"permissions": [
"teamlibrary"
],
"networkAccess": {
"allowedDomains": [
"ws://localhost:1994"
],
"reasoning": "Connects to the local figma-limitless-mcp server via WebSocket to stream Figma document data to AI tools"
},
"documentAccess": "dynamic-page",
"editorType": [
"figma",
"figjam",
"slides",
"buzz"
]
}
2 changes: 1 addition & 1 deletion figma-limitless-mcp/plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "concurrently \"vite build --watch\" \"vite build --watch -c vite.config.main.ts\"",
"build": "vite build && vite build -c vite.config.main.ts",
"build": "vite build && vite build -c vite.config.main.ts && node scripts/sync-extras.mjs",
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
Expand Down
22 changes: 22 additions & 0 deletions figma-limitless-mcp/plugin/scripts/sync-extras.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copies the built plugin bundle into extras/ so the companion manifest
// (FigJam / Slides / Buzz) can reference its code/ui in the SAME directory —
// Figma forbids a manifest's "main"/"ui" from pointing at a parent dir
// (../dist). The two manifests (plugin/manifest.json = figma+dev, and
// plugin/extras/manifest.json = figjam/slides/buzz) share one codebase; this
// keeps extras/ in lockstep with dist/ on every build.
import { copyFileSync, mkdirSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const pluginDir = join(dirname(fileURLToPath(import.meta.url)), "..");
const extrasDir = join(pluginDir, "extras");
mkdirSync(extrasDir, { recursive: true });

for (const [from, to] of [
["dist/code.js", "code.js"],
["dist/index.html", "index.html"],
]) {
copyFileSync(join(pluginDir, from), join(extrasDir, to));
}

console.log("synced dist/ -> extras/ (code.js, index.html)");
Loading