tmcpro/framer-mcp
If you are the rightful owner of framer-mcp and would like to certify it and/or have it hosted online, please leave a comment on the right or send an email to dayong@mcphub.com.
The Framer Mission Control Protocol (MCP) server is a local development tool designed to enable programmatic interaction with the Framer canvas through a secure API.
Framer Mission Control Protocol (MCP) - Local MVP
Project Overview
This project combines a local MCP server with a companion Framer plugin so developers and external clients can interact with the Framer canvas programmatically. By exposing a secure Tool API, Framer users can automate page generation, update content, and manipulate components directly from scripts or other services.
This MVP is built with Node.js and targets local development and testing.
Architecture
The system consists of three main components:
-
Framer Plugin:
- A React-based plugin that runs directly inside the Framer application.
- It is responsible for executing commands using Framer's official Plugin API.
- It establishes and maintains a secure WebSocket connection to the MCP Server.
-
MCP Server:
- A Node.js server built with Express and WebSocket support.
- It acts as a secure bridge between API clients (CLI tools, services) and the Framer Plugin.
- It exposes a REST API (
/v1/invoke) for sending commands and manages a per-user request queue to ensure operations are executed sequentially.
-
Shared Logic:
- A
shareddirectory contains common code for both the server and plugin, including type definitions, message protocols, and Zod schemas for validation, ensuring consistency across the system.
- A
Data Flow
- The user opens the plugin in Framer.
- The plugin requests a unique, hashed User ID from the server.
- The plugin generates a secret and establishes a WebSocket connection to the server using the User ID and secret.
- The plugin UI displays the full MCP URL, including the User ID and secret, for the user to copy.
- An API client makes a
POSTrequest to the server's/v1/invokeendpoint with the URL, a tool name, and parameters. - The server authenticates the request, validates the parameters, and forwards the command to the correct plugin over the WebSocket connection.
- The plugin executes the command using the Framer API and sends the result back to the server.
- The server relays the result back to the API client as the HTTP response.
Getting Started
Prerequisites
Steps
-
Clone the repository:
git clone https://github.com/tmcpro/framer-mcp cd framer-mcp -
Install dependencies:
npm install -
Run the plugin:
npm run dev --prefix plugin -
Run the server:
npm run dev --prefix serverThe server starts on http://localhost:3000 and the plugin dev server prints an install URL.
To run both simultaneously, use
npm run devfrom the project root.
Auth Modes
The server supports configurable auth for local vs. cloud environments via AUTH_PROVIDER:
legacy(default): requires?id=<USER_ID>&secret=<SECRET>on requests and the plugin WS query. Compatible with existing plugin flows.none: disables server-side auth for local development. Protect with your local network boundary.cf-access: assumes Cloudflare Access is enforcing auth in front of the server. No custom auth logic runs in the app.
Set in server/.env:
AUTH_PROVIDER=none # local dev
# AUTH_PROVIDER=cf-access # when deployed behind Cloudflare Access
Configuration
Environment variables control both the plugin and server. The server uses dotenv to load settings from a local .env file (server/.env) during development. For production, configure variables via your hosting platform's secret manager and never commit .env files. Rotate SERVER_SALT regularly in deployed environments.
Plugin
| Variable | Purpose | Default | Security Notes |
|---|---|---|---|
VITE_SERVER_BASE_URL | Base URL of the MCP server that the plugin connects to. | http://localhost:3000 | Exposed in browser; do not put secrets here. |
- Development: create
plugin/.envwithVITE_SERVER_BASE_URL=http://localhost:3000to match the default server started bynpm run dev. - Production: set
VITE_SERVER_BASE_URLto your deployed server's URL before building the plugin. For example:VITE_SERVER_BASE_URL=https://mcp.example.com npm run build --prefix plugin
Server
| Variable | Purpose | Default | Security Notes |
|---|---|---|---|
PORT | Port for the HTTP server to listen on. | 3000 | Non-sensitive. |
REQUEST_TIMEOUT_MS | Time to wait for plugin responses before timing out. | 5000 | Non-sensitive. |
QUEUE_MAX | Maximum number of pending requests allowed per user. | 16 | Non-sensitive. |
SERVER_SALT | Salt used to hash user IDs. | dev-salt-change-me-in-prod | Treat as secret; rotate in production. |
IDEMPOTENCY_TTL_MS | Duration to cache responses for idempotency. | 600000 (10 minutes) | Non-sensitive. |
NODE_ENV | Node.js environment mode. | development | Set to production in deployments; avoid test. |
Create server/.env to override these defaults. For example:
PORT=4000
SERVER_SALT=prod-secret-salt
Install the plugin in Framer:
- Open the printed Vite URL in your browser and follow the prompt to add the plugin to Framer.
- In your Framer project, open the plugin from the **Plugins** panel. The small UI should appear in the bottom-right corner.
Verify the connection
- When the plugin successfully connects to the server, the UI shows `Status: online`.
- In legacy mode, the UI may display a URL containing a user ID and secret for convenience. In non-legacy modes, you can call the API directly without query parameters.
Send a test request
Confirm everything is working with one of the following, depending on auth mode:
- Legacy:
```bash
curl -X POST "http://localhost:3000/v1/invoke?id=<USER_ID>&secret=<SECRET>" \
-H 'Content-Type: application/json' \
-d '{"tool":"project.getInfo","params":{}}'
```
- None / Cloudflare Access:
```bash
curl -X POST "http://localhost:3000/v1/invoke" \
-H 'Content-Type: application/json' \
-d '{"tool":"project.getInfo","params":{}}'
```
A JSON response with project details means the server and plugin are communicating correctly.
Running Tests
This project includes a test suite for the server to ensure its core logic is working correctly.
To run the tests, use the following command:
npm test --prefix server
This will execute all Jest tests located in the server/src/__tests__ directory.
API Reference
The MCP API exposes a single POST /v1/invoke?id=<USER_ID>&secret=<SECRET> endpoint used to run tools in a connected Framer project.
Core capabilities include:
- Creating, updating, and removing nodes on the canvas.
- Managing selection and interacting with the editor UI.
- Inserting components and working with project files and code.
For the formal specification, see the .
Additional REST and WebSocket details live in .
curl -X POST "<MCP_URL>/v1/invoke?id=USER_ID&secret=SECRET" -H 'Content-Type: application/json' -d '{"tool":"project.getInfo","params":{}}'
Tool Manifest
GET /v1/tools returns metadata for every available tool. The response begins
with a version string that increments whenever tool schemas change so clients
know when to refresh their cached manifest.
Each tool object contains:
-
name– unique identifier for the tool."name": "nodes.createFrame" -
description– natural-language summary of the tool."description": "Creates a new frame node." -
paramsSchema– JSON Schema describing required parameters. This structure helps clients construct valid requests."paramsSchema": { "type": "object", "properties": { "attrs": { "type": "object", "properties": { "name": { "type": "string" } } } } } -
resultSchema– JSON Schema describing the result shape."resultSchema": { "type": "object", "properties": { "node": { "type": "object", "properties": { "id": { "type": "string" }, "type": { "type": "string" } } } } } -
examples– Array of sample{params, result}pairs showing real usage."examples": [ { "params": { "attrs": { "name": "Landing" } }, "result": { "node": { "id": "123", "type": "FrameNode" } } } ] -
errors– Array of objects describing possible failures."errors": [ { "code": "BAD_REQUEST", "message": "Invalid parameters" }, { "code": "INTERNAL", "message": "Unexpected server error" } ]
Schemas and examples provide explicit contracts for generating requests and interpreting responses.
Tool Surface
All API calls are made via POST /v1/invoke?id=<USER_ID>&secret=<SECRET>. The body of the request is a JSON object with tool and params keys.
Nodes
nodes.createFrame: Creates a new Frame.params:{ parentId?: string, attrs?: NodeAttributes }
nodes.addText: Adds a new Text layer.params:{ parentId?: string, text: string, attrs?: NodeAttributes }
nodes.setText: Updates the content of a Text layer.params:{ nodeId: string, text: string }
nodes.addImageFromUrl: Fetches an image from a URL and adds it to the canvas.params:{ parentId?: string, url: string, attrs?: NodeAttributes, fit?: 'cover'|'contain'|'fill' }
nodes.addSvg: Adds an SVG as a VectorNode.params:{ parentId?: string, svg: string, attrs?: NodeAttributes }
nodes.setAttributes: Applies a set of attributes to any node.params:{ nodeId: string, attrs: NodeAttributes }
nodes.getNode: Retrieves the properties of a single node.params:{ nodeId: string }
nodes.remove: Deletes a node.params:{ nodeId: string }
nodes.duplicate: Duplicates a node.params:{ nodeId: string, parentId?: string }
Selection
selection.get: Gets the currently selected node IDs.params:{}
selection.set: Sets the current selection.params:{ nodeIds: string[] }
Editor
editor.zoomIntoView: Zooms the editor viewport to fit a specific node.params:{ nodeId: string }
editor.notify: Shows a notification toast in the Framer UI.params:{ message: string, durationMs?: number }
Components
components.list: Lists all available components in the project.params:{}
components.insertByUrl: Inserts a component using its import URL.params:{ insertUrl: string, parentId?: string, attrs?: NodeAttributes, props?: object }
components.insertByName: Inserts a component by its name.params:{ name: string, parentId?: string, attrs?: NodeAttributes, props?: object }
Project & Code
project.getInfo: Retrieves the current project's ID and name.params:{}
code.createFile: Creates a new code file (e.g., a React component).params:{ path: string, template?: 'component'|'override'|'empty' }
code.readFile: Reads the content of a code file.params:{ fileId?: string, path?: string }
code.updateFile: Updates the content of a code file.params:{ fileId?: string, path?: string, code: string }
Project Structure
framer-mcp/
├─ .gitignore
├─ package.json # Root dependencies and scripts
├─ README.md # This file
├─ shared/ # Shared types, schemas, and protocols
│ ├─ protocol.ts
│ ├─ schemas.ts
│ └─ tools.ts
├─ server/ # The Node.js MCP Server
│ ├─ package.json
│ ├─ jest.config.js
│ ├─ tsconfig.json
│ └─ src/
│ ├─ __tests__/ # Server-side tests
│ ├─ index.ts # Express app setup and server entrypoint
│ ├─ router.invoke.ts # Handles the main /invoke endpoint
│ └─ ws.hub.ts # Manages WebSocket connections
└─ plugin/ # The Framer Plugin
├─ package.json
├─ vite.config.ts
├─ tsconfig.json
└─ src/
├─ main.tsx # Plugin entrypoint, connects to server
├─ ui/App.tsx # The React component for the plugin UI
└─ handlers/ # Logic for executing Tool API commands
Local Usage Examples
- Start plugin + server:
npm run dev - With
AUTH_PROVIDER=none, you can invoke without id/secret once the plugin shows “online”:
curl -X POST "http://localhost:3000/v1/invoke" \
-H 'Content-Type: application/json' \
-d '{"tool":"project.getInfo","params":{}}'
- JSON-RPC (MCP-style) over HTTP (useful for adapters):
curl -X POST "http://localhost:3000/v1/mcp" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":"1","method":"tool/list","params":{}}'
Cloudflare Agents (Remote MCP)
Recommended for production: deploy behind Cloudflare and use Cloudflare Access instead of custom auth.
- Protect the origin with Cloudflare Access (Application type: Web → include WS). Set
AUTH_PROVIDER=cf-accesson the server. - Point the plugin
VITE_SERVER_BASE_URLto your Access-protected origin. - For Cloudflare Agents: use the Remote MCP Server guide to register an MCP server that proxies to this API so external agents can call tools. Implement a small Worker that translates MCP JSON-RPC methods (
tool/list,tool_code/invoke) to this server’s REST endpoints.
Local dev remains unchanged (AUTH_PROVIDER=none).
Configuration Summary
- Server: see
server/.env.examplefor all supported settings. Copy toserver/.envand adjust as needed. - Plugin: copy
plugin/.env.exampletoplugin/.envand setVITE_SERVER_BASE_URL. - Typical local setup:
AUTH_PROVIDER=none,VITE_SERVER_BASE_URL=http://localhost:3000.