# MCP server

## 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 `/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](/docs/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

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.

## Tools

```ts
// 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
```

## Project layout and what writes do

| Path | Is | A write or delete |
|---|---|---|
| `skin/` | The skin ([Skin format](/docs/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](/docs/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

`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:

```ts
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.

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