Skip to content

OpenPencil — native canvas and MCP

OpenPencil gives a human member and an MCP client the same editable web canvas. The editor runs in your browser. A resident editor on the VPS maintains the project when you close the browser. Native changes synchronize through the project’s authenticated Yjs connection.

ConnectionAddress or behavior
Web editoropenpencil.devfellowship.com
MCP endpointhttps://openpencil.devfellowship.com/mcp
TransportStreamable HTTP
AuthenticationYour current DFL Supabase user JWT, as a bearer token
Project accessThe owner, or a current global IAM level of at least 80
Tools23 tools, served by dfl-services/services/dfl-openpencil
Resident capacityTwo projects at once across the service
Durable project limits10 projects per owner; 100 projects across the service

This endpoint lives with the OpenPencil service. It has its own address rather than the <host>.mcp.devfellowship.com address used by this repository’s 14 package hosts. Its tools are discovered through its own MCP tools/list.

Prerequisite: the shared DFL fleet connection

Section titled “Prerequisite: the shared DFL fleet connection”

Complete Auth & security before the first tool call. It explains the shared DFL identity and the global MCP client connection used across projects. OpenPencil reuses that DFL bearer JWT. It does not require a separate account.

Use your own DFL login in the web editor. OpenPencil Desktop is optional. One OpenPencil MCP connection is sufficient. No extra skill or Playwright MCP is required for your first design change. Your assistant still needs its own model access.

The same current global DFL credential authenticates this MCP endpoint. The shared auth guide covers that credential and its renewal. Keep that setup in the client’s global user scope, then use this page for the native design tool calls.

A project is a durable document owned by a human DevFellowship member. Projects have separate documents, resident contexts and synchronization rooms. For parallel work, use a separate project or version for each person. A global administrator can access another owner’s project, so choose the intended project explicitly before each edit.

The public editor URL is stable:

https://openpencil.devfellowship.com/projects/PROJECT_UUID

Use that URL to return to the editor. Normal browser entry uses central DFL SSO. get_attach_url is a separate private, one-use entry mechanism with a 60-second lifetime. Consume it immediately when needed. Its fragment is a capability; keep the full attach URL out of messages, logs, documents and screenshots.

Closing the browser leaves the resident available for MCP work. To release one of the two resident slots, call close_project for your project. This preserves the canonical FIG checkpoint and project metadata. Reopen the project when you need it again. delete_project permanently removes the FIG and metadata; use close_project to free resident capacity.

These limits count different resources. The two-resident limit is global and counts active resident contexts. The durable document limits are 10 projects per owner and 100 projects across the service. close_project preserves a document, so it does not reduce those durable counts. At a document limit, review existing projects with their owner and preserve the designs you need before any planned permanent removal.

Tool calls: create, inspect, edit and save

Section titled “Tool calls: create, inspect, edit and save”

The following examples show MCP tools/call parameters. An MCP client handles initialization, the bearer header and the mcp-session-id returned by initialization. A full JSON-RPC request wraps each parameter block like this:

{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": { "name": "list_projects", "arguments": {} }
}

A successful response contains result.content text with a JSON payload. Parse that payload to read the project’s id, version and runtime.generation, or the native tool’s returned node data. HTTP success alone does not prove a canvas change. Read the node back and inspect the native canvas.

{ "name": "list_projects", "arguments": {} }

Create a new project only when you need a separate document:

{
"name": "create_project",
"arguments": { "name": "My first OpenPencil design" }
}

name is required and has a 120-character limit. The caller must be an active human member. owner_id is optional; only a global administrator with IAM at least 80 can assign a new project to another existing active human member. Omit it to create your own project. Keep the id returned by the tool.

Replace PROJECT_UUID in these examples with your project’s returned id:

{
"name": "get_project",
"arguments": { "project_id": "PROJECT_UUID" }
}
{
"name": "get_page_tree",
"arguments": { "project_id": "PROJECT_UUID", "depth": 2 }
}

get_project reads durable metadata. get_page_tree ensures a resident exists and reads its current scene. If the resident was absent, call get_project again after get_page_tree to obtain the current runtime.generation.

get_page_tree also accepts page_id, root_id, node_types and depth. Use get_node for a specific node. Use find_nodes with a unique layer name to find it again after a resident restart.

Every document mutation requires these fields in addition to the native operation’s arguments:

FieldRequirement
project_idThe project UUID returned by create_project or list_projects.
operation_idA new identifier for each new operation: 8–100 letters, digits or hyphens. A UUID works.
base_versionThe current integer version returned by get_project.
generationOptional in the schema; supply the current runtime.generation to reject an obsolete resident.

Read get_project before each new mutation. Human edits and MCP edits both advance the checkpoint version. A version from an earlier call can be stale.

The mutation examples below use 42, 43, 44, 45 and 46 only as illustrative versions. Replace each with the latest returned version. Replace CURRENT_GENERATION, RECTANGLE_NODE_ID and TEXT_NODE_ID with current returned values. Generate a new operation ID for each new call.

{
"name": "create_shape",
"arguments": {
"project_id": "PROJECT_UUID",
"operation_id": "first-rectangle-20261007",
"base_version": 42,
"generation": "CURRENT_GENERATION",
"type": "RECTANGLE",
"name": "FIRST_TEST_RECTANGLE",
"x": 100,
"y": 100,
"width": 320,
"height": 180
}
}

Keep the returned node ID. Re-read get_project, then set the fill:

{
"name": "set_fill",
"arguments": {
"project_id": "PROJECT_UUID",
"operation_id": "first-fill-20261007",
"base_version": 43,
"generation": "CURRENT_GENERATION",
"id": "RECTANGLE_NODE_ID",
"color": "#2563EB"
}
}

create_shape requires type, x, y, width and height. Width and height must be at least 1. Optional fields include name, parent_id and page_id. Supported types are FRAME, RECTANGLE, ELLIPSE, TEXT, LINE, STAR, POLYGON and SECTION.

set_fill requires id and color. For a gradient, add color_end and gradient: top-bottom, bottom-top, left-right or right-left.

Re-read get_project before this call:

{
"name": "create_shape",
"arguments": {
"project_id": "PROJECT_UUID",
"operation_id": "first-title-20261007",
"base_version": 44,
"generation": "CURRENT_GENERATION",
"type": "TEXT",
"name": "FIRST_TEST_TITLE",
"x": 120,
"y": 130,
"width": 280,
"height": 48
}
}

Keep its returned node ID. Re-read get_project, then set the text:

{
"name": "set_text",
"arguments": {
"project_id": "PROJECT_UUID",
"operation_id": "first-text-20261007",
"base_version": 45,
"generation": "CURRENT_GENERATION",
"id": "TEXT_NODE_ID",
"text": "FIRST LIVE CANVAS TEST"
}
}

set_text requires id and text. To adjust typography, use set_font after another metadata read:

{
"name": "set_font",
"arguments": {
"project_id": "PROJECT_UUID",
"operation_id": "first-font-20261007",
"base_version": 46,
"generation": "CURRENT_GENERATION",
"id": "TEXT_NODE_ID",
"size": 24
}
}

Read get_node for both returned IDs, or get_page_tree for the project. Check the actual editor: the rectangle and title must appear without a reload. The human can select, drag and edit those native elements. Wait for the editor’s save acknowledgment before the human closes the tab.

{
"name": "save_checkpoint",
"arguments": { "project_id": "PROJECT_UUID" }
}

This call writes actual FIG bytes atomically and returns checkpoint metadata, including a version and SHA-256 hash. Each successful document mutation also checkpoints its result. save_checkpoint creates a new checkpoint version; it does not use the replay-idempotent mutation contract.

{
"name": "close_project",
"arguments": { "project_id": "PROJECT_UUID" }
}

The result reports closed: true and deleted: false. The document survives. Return through the stable project URL to restore it from the canonical FIG.

After a lost mutation response, retry the same operation ID and exactly the same arguments, including the original base version and generation. The service stores the latest 32 completed operation results and returns a matching record without repeating the mutation. Do not change the arguments under the same operation ID.

An older retry outside that journal is refused by its stale base version. A genuine new edit requires new metadata and a new operation ID. On version_conflict, read the current state and decide whether the intended change still applies. On generation_conflict, reopen the project and rediscover its nodes by unique names. Native node IDs can change during FIG import into a new resident.

A failed mutation or checkpoint can quarantine a resident. Close and reopen that project to recover from its last canonical checkpoint. Coordinate with any human who is currently editing it.

Every project-specific tool requires project_id. Native document mutation tools also require the controls described above. Optional native arguments are available through this endpoint’s tools/list schemas.

ToolPurposeOther required arguments
create_projectCreate a human-owned durable project.name; no project_id on creation.
list_projectsList projects the caller can access.None; no project_id.
get_projectRead canonical metadata and resident state.None.
save_checkpointWrite a canonical FIG checkpoint.None.
get_attach_urlIssue a private, one-use attach URL.None.
close_projectRelease the resident and preserve the document.None.
delete_projectPermanently remove the document and metadata.None.
get_nodeInspect a native node.id.
find_nodesFind nodes by optional name or type filters.None.
get_page_treeRead the current scene graph.None.
list_documentsInspect the resident’s native document.None.
select_nodesSet the resident selection.ids.
viewport_zoom_to_fitFit the resident viewport to nodes.ids.
create_shapeCreate a native element.type, x, y, width, height, mutation controls.
update_nodeChange native node properties.id, mutation controls.
set_fillApply a solid fill or gradient.id, color, mutation controls.
set_strokeApply an outline.id, color, mutation controls.
set_textReplace text content.id, text, mutation controls.
set_fontChange typography.id, mutation controls.
set_layoutChange frame layout.id, mutation controls.
set_opacitySet opacity.id, value, mutation controls.
clone_nodeDuplicate a native node.id, mutation controls.
delete_nodeRemove a native node.id, mutation controls.

The resident selection and viewport are separate from a human browser’s local selection and viewport. The shared scene graph carries document changes.

SymptomNext step
Browser shows Falha 401 with an old session.Sign out of DFL, sign in again, then reopen the project. A private window can confirm a fresh session.
MCP returns 401.Obtain your current DFL JWT. Restart the client with the updated environment variable.
MCP returns foreign_session.Reconnect the MCP transport after a service restart or session expiry.
Project access is refused.Confirm the caller and intended project. Access requires ownership or current global IAM at least 80.
The service reports resident capacity.Two residents are shared across the service. Coordinate with another user or close your own idle project with close_project.
The service reports a durable project limit.Review existing projects with the owner: 10 per owner, 100 across the service. Closing a resident does not reduce document counts.
version_conflict or generation_conflict.Re-read metadata and rediscover current node IDs as described above.
An open editor disconnects.Reopen its stable project URL before you continue. Disconnected input is blocked.
A canvas change is missing.Read the target nodes back. Check the correct project, native canvas and save acknowledgment.

Report the failed step, operating system, client version and error in #chapter-web3. Include a screenshot with private tokens and attach fragments removed.

The native editor acceptance report contains three staged screenshots and real public MCP/Playwright checks: native input, live MCP changes, and work without a human tab followed by return after a restart. Tainan accepted the human review on 7 October 2026.

This curated external-service guide uses the deployed 23-tool schema and dfl-openpencil source at commit 50786b1. It is separate from this repository’s generated package manifest. Use the live endpoint’s tools/list as the argument authority when the service changes.