# viaSocket skill · the product's own MCP server > Part of the viaSocket skill: `https://flow.viasocket.com/documentation/skill.md`. **Why:** a product built with this skill uses the ecosystem through its users' apps and its AI; this is how it joins it. With an MCP URL the product itself becomes a tool for every AI client and for every other product's agent, so work can start in Claude, ChatGPT, Cursor or another agent and finish inside the product, with the user's own login. Without it the product is a destination only a human can reach. The product's own AI can use it too. The product gets an MCP URL its users paste into Claude, ChatGPT, Cursor or any MCP client. The tools behind it are the product's own API endpoints, with the product's instructions, skills and prompts beside them; users sign in with the product's own login, or with a key the product issues; the server carries the product's name and icon, a stable `/connector/` address, and, if wanted, the product's own domain. viaSocket hosts it at `mcp.viasocket.com`; the product hosts nothing but its API. **One server per product.** The developer creates it once; every user of the product uses the same URL and signs in with the product's login, so each tool call reaches the product's API as that user. Users never create servers of their own, and nothing per user is set up on viaSocket. **One command does all of it.** The server is described in one file, `viasocket.mcp.json`, and one command creates or updates it, keeps the ids of what it made, and tests the URL: ```bash curl -fsSL https://flow.viasocket.com/viasocket-login.mjs -o viasocket-login.mjs && node viasocket-login.mjs mcp ``` With `viasocket-apps` installed, `npx viasocket-apps mcp` is the same command. It signs the developer in when `.env` has no `VIASOCKET_AGENT_TOKEN` (the same one click as the setup command; tell the developer: "Sign in to viaSocket in the tab that opened, pick the workspace, and click **Allow**"), mints `VIASOCKET_MCP_TOKEN` from it and keeps it in `.env`, and never prints a secret. Signed in through the viaSocket MCP server instead: mint the token with its `call_any_read_api_of_viasocket` tool (`GET https://flow-api.viasocket.com/mcp/agent-token`, `Authorization` empty), write `data.token` to `.env` as `VIASOCKET_MCP_TOKEN` without echoing it, and run the command: it uses that token while it works (24 hours). The connector cannot call `mcp.viasocket.com` itself. ## 1. Decide, then get a yes Post these as the plan and wait for the developer's yes before the command runs. ### The tools - One per endpoint the product's users should reach from an AI client. Start with the few that matter. - Each has a name (`snake_case`, at most 45 characters), one sentence on when the AI should use it, and its inputs as a JSON Schema with a description per field. `readOnlyHint: true` for reads, `destructiveHint: true` for deletes. - **A tool call has 30 seconds.** Work that can take longer (a job, a build, a question to another agent) is two tools: one that starts it and answers an id at once, one that reads the result by that id. The product pushes the result to whoever waits for it; the AI polls or is told. ### Who signs in - **The product's login**: the user signs in to the product from the AI client (the product's OAuth); each tool call reaches the product's API as that user, with their token in the header the table below says. - **A key the product issues**: an API key, or a token minted per job or per session. The user (or the product, for an agent it starts) puts it in the client's MCP headers; the product's `validationUrl` says whether it is still good. This is the mode for machine-to-machine use: another product's agent, a CI job, a Cursor cloud agent. - **Nobody**: only for tools that read public data. How the token travels, in every mode: | hop | what is sent | | --------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | the AI client → viaSocket | `Authorization: Bearer ` on every request; viaSocket takes the token as what follows `Bearer `. Claude and ChatGPT send the token their sign-in got; Cursor, Claude Code, Windsurf and any client with a `headers` setting send what the user put there, so that entry is `"Authorization": "Bearer "`: another header name, or the bare token, is refused. | | viaSocket → the product's `validationUrl`, on every request while the login is on | the token under `headerName`: as `Bearer ` when `headerName` is `Authorization`, bare under any other name. A 200 means valid; anything else becomes a 401 to the client, so a token that expires stops working at once. With a key, that 401 carries no sign-in, and the client shows the server as failed until the user fixes the header. | | viaSocket → each tool's API, in every mode, whenever the client sent a token | the bare token under the tool's own `tokenHeader` (default `Authorization`), with the tool's `tokenPrefix` and a space in front when set: set `"tokenPrefix": "Bearer"` on each tool whose API reads `Authorization: Bearer `. A tool whose `headers` already sets that header keeps its own value and never gets the user's token. | So the product's API receives, for a tool with `"tokenPrefix": "Bearer"`, exactly what it receives from its own frontend: `Authorization: Bearer `, plus the tool's arguments in the body or the query string. Which clients send what: Cursor, Claude Code, Windsurf and VS Code send the `headers` of their config on every request and can also run the product's OAuth; Claude.ai and ChatGPT connectors have no header setting, so they need the product's OAuth or no login. Whether a client sends a header is confirmed by the command's test with `--token`, not assumed. ### The address and the words - The slug of `https://mcp.viasocket.com/connector/`: lowercase, 2 to 63 characters, the product's name. - **Instructions**: one paragraph every client reads at connect: what the product is, which tool to start with. - **Skills**: a markdown guide per job the product's users do with the tools, naming the tools in order; the model loads it when it decides it needs it. - **Prompts**: ready-made requests the user picks from the client's menu (a slash command in Claude Desktop, Claude Code and Cursor), with arguments the client asks for, such as "Weekly report for {{project}}". ## 2. Build it 1. **Write `viasocket.mcp.json`** in the product's repo (committed; a secret goes in as `"${ENV_NAME}"`, read from `.env` when the command runs, never written to the file): ```json { "name": "TaskPad", "slug": "taskpad", "instructions": "TaskPad is a task tracker. Start with list_tasks; use create_task and complete_task only when the user asks.", "branding": { "title": "TaskPad", "websiteUrl": "https://taskpad.example", "iconUrl": "https://taskpad.example/icon.png" }, "login": { "mode": "api-key", "validationUrl": "https://api.taskpad.example/me", "headerName": "Authorization", "method": "GET" }, "tools": [ { "name": "list_tasks", "description": "Lists one user's tasks. Use it before suggesting or changing anything.", "api": { "url": "https://api.taskpad.example/todos", "method": "GET", "tokenPrefix": "Bearer" }, "inputSchema": { "type": "object", "properties": { "completed": { "type": "boolean", "description": "true for done tasks only; leave out for all" } } }, "annotations": { "readOnlyHint": true } }, { "name": "complete_task", "description": "Marks one task as done. Only when the user says they finished it.", "api": { "url": "https://api.taskpad.example/todos/{{task_id}}", "method": "PATCH", "tokenPrefix": "Bearer" }, "inputSchema": { "type": "object", "properties": { "task_id": { "type": "number" } }, "required": ["task_id"] } } ], "skills": [ { "name": "daily_review", "description": "How to plan a user's day from their open tasks.", "file": "mcp/daily_review.md" } ], "prompts": [ { "name": "plan_my_day", "title": "Plan my day", "description": "Suggests the three tasks to do first.", "arguments": [{ "name": "focus", "description": "What matters today", "required": false }], "content": "List my open tasks with list_tasks and suggest the three to do first, keeping {{focus}} in mind." } ] } ``` - `login.mode` is `none`, `api-key` or `oauth`. For `oauth` add `loginUrl` (the product's authorize endpoint), `tokenUrl` (must accept a JSON body), `clientId` and `"clientSecret": "${TASKPAD_OAUTH_SECRET}"`, from an OAuth app registered in the product; then the developer registers the redirect URI `https://mcp.viasocket.com/oauth//callback` in that app (the id is in the ids file). `"when": "needed"` makes an OAuth server open to connect, with the sign-in asked only when a tool's API answers 401. - `api.url` and `api.headers` take `{{argument}}` placeholders; a GET sends the other arguments as the query string, the other methods as the JSON body (`"queryParams": ["page"]` moves some to the query string). `tokenHeader` names another header for the user's token. `outputSchema` when the answer has a fixed shape. - A skill's `content` can be inline markdown or a `file` next to the definition. 2. **Run the command.** It creates the server, its tools, skills and prompts, or updates them in place on a rerun, and removes what the file no longer lists. The ids of everything it made go to `viasocket.mcp.ids.json`: commit it, it holds no secret, and it is what makes a rerun update instead of duplicate. Then it calls `tools/list` and `prompts/list` on the URL as a client would and prints what it found; on a login server it reports the 401 that is right without a token. 3. **Test one tool as a client:** `node viasocket-login.mjs mcp --call list_tasks --args '{"completed":false}'`, with `--token ` on a login server (a key, or a token the product issued for a test user). The answer is what the product's API returned; a tool that fails answers with a text that starts with `Error:`. For the product's OAuth, ask the developer to add the URL to Claude or Cursor once, sign in, and call each tool. A tool that was never called is UNTESTED. 4. **A custom domain**, when wanted: `PUT https://mcp.viasocket.com/api/mcp//domain` with `{ "domain" }` and `Authorization: Bearer `; it answers the DNS records the developer adds and goes `pending_dns` → `issuing_cert` → `active` on its own (an hour at most). Everything else the management API can do is in `https://mcp.viasocket.com/api-reference.md`; the command covers what a product needs. 5. **Give it to the product's own AI, if the product has one:** the AI feature's send call takes `mcpServers` (`mcp_servers` over HTTP): `[{ "name": "", "url": "", "headers": { "Authorization": "Bearer " } }]`, next to its `tools`. The assistant then calls the same tools, as that user, with no second tool list to maintain. Any other MCP server goes in the same list. 6. **Hand it over:** the URL, and how a user adds it (Claude and ChatGPT: add a custom connector with the URL; Cursor and Claude Code: `{ "mcpServers": { "": { "url": "" } } }`, plus `"headers": { "Authorization": "Bearer " }` with a key). For another product's agent: the product mints a token for that job, hands it with the URL, and expires it when the job ends. ## Finished when The report says: the URL and the slug; each tool's name, endpoint and who may call it; which login mode and, for OAuth, that the redirect URI is registered; the instructions, each skill and each prompt; the test of step 3 for each tool, or UNTESTED with why; that `viasocket.mcp.json` and `viasocket.mcp.ids.json` are committed. ## When it goes wrong | symptom | cause | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | The command says `viasocket.mcp.json: …` | The definition is wrong at that key; nothing was created. Fix the key and rerun. | | The command says `viasocket.mcp.json holds the ids …` | The ids file of this page's earlier layout; the command moved it to `viasocket.mcp.ids.json`. Write the definition with the same slug and tool names, and rerun. | | `401 Unauthorized` from `/api/...` | `VIASOCKET_MCP_TOKEN` expired (24 hours); the command mints a new one itself, a hand-made call gets one from `GET https://flow-api.viasocket.com/mcp/agent-token`. | | `409` on the slug | Another server has it; pick another slug. | | Two tools with the same name on the URL | The ids file was deleted or not committed, so a rerun created anew; delete the extras with `DELETE /api/tool/id/<_id>` and keep the file. | | Sign-in fails after the product's login | The redirect URI is not registered in the OAuth app, or `tokenUrl` does not accept JSON. | | Every tool call answers `Error: Tool API responded 401` | The token went bare: set `"tokenPrefix": "Bearer"` on the tool, or `tokenHeader` for another header. | | Cursor never asks to sign in | `"when": "needed"` is set: Cursor signs in only when connecting itself answers 401. Drop it. | | A tool answers `Error:` after 30 seconds | The product's endpoint took longer than a tool call may; split it into start and read, as "The tools" says. | | The custom domain stays `pending_dns` | The records are not there yet; `POST /api/mcp//domain/check` runs the check now and `lastError` says what is missing. | END OF PAGE