MCP server
Connect
Section titled “Connect”Endpoint https://skins.dev/mcp · Streamable HTTP · OAuth 2.1 with PKCE and dynamic client
registration. The first time, your client opens the browser to sign in with your Skins account.
- Claude Code:
claude mcp add --transport http skins https://skins.dev/mcp, then/mcpto sign in. - Claude.ai and Claude Desktop: add a custom connector with the endpoint.
Tools answer with JSON and use the platform’s names: a project is a gadget, a template is a blueprint.
To try the tools first, open the MCP Playground: the official MCP Inspector in
your browser, connected to the endpoint. Or run it locally with
npx @modelcontextprotocol/inspector --server-url https://skins.dev/mcp --transport http.
The skill
Section titled “The skill”The server ships the how-to as an agent skill, skins: the workflow, pins, recordings, network
traffic, snapshots and the skin’s files, written for an agent working through these tools. It is
always the deployed version, and clients get it two ways:
- MCP resources:
skill://skins/SKILL.md, plusskill://skins/references/*.md, which it names. Any client that reads resources can load it; the server’s instructions point there. - The Skills extension (
io.modelcontextprotocol/skills):skills/listandskills/getreturn the skill’s name, description and file manifest with SHA-256 digests, for clients that load skills from MCP servers.
// Account and projectsgetMyProfile(): { email: string; displayName: string; userId: string }listGadgets(): { id: string; title: string; created: string; lastActive: string; /* … */ }[]createGadget(title: string, blueprintId?: string): { id: string; title: string; bindingName: string } // a Skins project: blueprintId "skins.layer-studio"applyTemplate(gadgetId: string, blueprintId: string): { applied: string } // makes it a Skins project, or updates it; idempotentdeleteGadget(gadgetId: string): { deleted: string } // permanent, owner only
// Sharing (roles: see Privacy and security)shareGadget(gadgetId: string, username: string, role: "build" | "use", note?: string) // username: an emaillistCollaborators(gadgetId: string)
// FileslistGadgetFiles(gadgetId: string) // code, plus data files under data/readGadgetFiles(gadgetId: string, paths: string[]) // published code, or data files, as textwriteGadgetFile(gadgetId: string, path: string, content: string) // creates or replaces the whole filedeleteGadgetFiles(gadgetId: string, paths: string[])getGadgetSkin(gadgetId: string): { // the published skin; fails without a valid skin/skin.json name: string; description: string; activeWhen: string; trigger: string; html?: string; css?: string; js?: string;}
// CoderunCode(gadgetId: string, code: string): string // console output and return valueProject layout and what writes do
Section titled “Project layout and what writes do”| Path | Is | A write or delete |
|---|---|---|
skin/ |
The skin (Skin format) | Joins the project’s draft, for the person to publish. The result has the draft’s url. An invalid skin/skin.json is rejected with the reasons. |
data/ |
Recordings (Recording format) and other data | Takes effect at once, up to 64 MB per file. No history: a delete can’t be undone. |
app/, AGENTS.md |
Studio itself, from the template. Read AGENTS.md first. |
Don’t edit: template updates replace them. |
runCode and the Studio methods
Section titled “runCode and the Studio methods”code is a module exporting one async function. env holds the project’s binding (bindingName
from createGadget; return Object.keys(env) lists everything there). The person’s requests in
Studio, called pins here, live in the project’s own database. These methods read and answer
them:
interface LayerStudio { // Recording: recording.json, see Recording format listRecordings(): Promise<(Recording & { annotationCount: number; openCount: number })[]>; listAllThreads(): Promise<{ recordingId: string; pins: Pin[]; snapshots: object[] }[]>; // snapshots: see the skill listThreads(recordingId: string): Promise<Pin[]>; listAnnotations(recordingId: string): Promise<object[]>; // each pin's element: nodeId, selector, html, bbox, … replyToAnnotation(annotationId: string, text: string, author: "AI", resolve?: boolean): Promise<{ ok: true }>; setAnnotationResolved(annotationId: string, resolved: boolean, author: "AI"): Promise<{ ok: true }>;}
interface Pin { annotationId: string; pin: number; // its number in the recording kind: "element" | "spot"; element: string; // a short label for what it's on atMs: number; // when in the recording status: "open" | "resolved"; thread: { author: { id: string; name: string; kind: "person" | "ai" }; text: string; at: number }[];}An agent signs as "AI", shown as bryo. Replies are cut at 4,000 characters.
export default async function (self, env) { const studio = env.GADGET; // the project's bindingName const open = []; for (const { recordingId, pins } of await studio.listAllThreads()) for (const pin of pins) if (pin.status === "open") open.push({ recordingId, ...pin }); if (open.length) await studio.replyToAnnotation(open[0].annotationId, "Done: try the draft.", "AI", true); return `${open.length} open`;}