Skip to content

MCP server

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 /mcp to 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 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, plus skill://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/list and skills/get return the skill’s name, description and file manifest with SHA-256 digests, for clients that load skills from MCP servers.
// Account and projects
getMyProfile(): { 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; idempotent
deleteGadget(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 email
listCollaborators(gadgetId: string)
// Files
listGadgetFiles(gadgetId: string) // code, plus data files under data/
readGadgetFiles(gadgetId: string, paths: string[]) // published code, or data files, as text
writeGadgetFile(gadgetId: string, path: string, content: string) // creates or replaces the whole file
deleteGadgetFiles(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;
}
// Code
runCode(gadgetId: string, code: string): string // console output and return value
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.

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`;
}