# viaSocket MCP — Complete API Reference

> **For AI agents:** this document is the complete integration spec for the viaSocket MCP service. Every endpoint, header, request key, response key, validation rule and side effect is listed here. When integrating, follow the types and rules exactly. Each section stands on its own, so you can jump straight to the one you need. Section 12 has step-by-step integration recipes.

- **Source of truth:** `Walkover-Web-Solution/viasocket-mcp`, branch `main` (commit `59da8da`)
- **Production base URL:** `https://mcp.viasocket.com`
- **Content type:** `application/json` for every management API body unless stated otherwise
- **MCP protocol SDK:** `@modelcontextprotocol/sdk` ^1.12.1

---

## Table of contents

1. [Concepts](#1-concepts)
2. [Endpoint quick reference](#2-endpoint-quick-reference)
3. [Authentication](#3-authentication)
4. [Data models](#4-data-models)
5. [Management API: MCP servers](#5-management-api-mcp-servers)
6. [Management API: tools](#6-management-api-tools)
7. [Management API: skills](#7-management-api-skills) (7B prompts, 7C resources)
8. [MCP protocol endpoints (for AI clients)](#8-mcp-protocol-endpoints-for-ai-clients)
9. [How tools execute at runtime](#9-how-tools-execute-at-runtime)
10. [OAuth 2.1 proxy (per-server login)](#10-oauth-21-proxy-per-server-login)
11. [Discovery and health endpoints](#11-discovery-and-health-endpoints)
12. [Integration recipes](#12-integration-recipes)
13. [Errors](#13-errors)
14. [Limits, rules and gotchas](#14-limits-rules-and-gotchas)

---

## 1. Concepts

| Concept | What it is |
|---|---|
| **Org** | A viaSocket company/workspace. Every resource belongs to one org. Every member of the org can see and manage all of the org's MCP servers, tools and skills. |
| **MCP server** | A named container that AI clients (Claude, ChatGPT, Cursor, …) connect to over the Model Context Protocol. It has its own URL, its own tool list and optional auth. |
| **MCP URL** | `https://mcp.viasocket.com/mcp/{mcpServerId}-{orgId}`. This is the URL the user pastes into their AI client. |
| **Tool** | One callable action on an MCP server. There are two types: **`flow`** runs a viaSocket flow (`https://flow.sokt.io/func/{flowId}`), and **`custom`** calls any HTTP API you configure. |
| **Skill** | A Markdown instruction document stored at org level. Skills are connected to MCP servers, where each one appears as a zero-argument MCP tool that returns the Markdown: instructions the **model** loads when it decides it needs them. |
| **Prompt** | A ready-made instruction stored at org level, optionally with arguments (`{{placeholders}}` in its content). Prompts are connected to MCP servers, where each one is an MCP prompt (`prompts/list`, `prompts/get`): what the **user** picks from the client's prompt menu, a slash command in Claude Desktop, Claude Code and Cursor, after filling in the arguments. |
| **Resource** | Read-only context stored at org level: a style guide, a schema, a price list. Resources are connected to MCP servers, where each one is an MCP resource (`resources/list`, `resources/read`) at its own URI: what the **user** (or the client) attaches to the conversation, the @ / "Add context" menu in Claude Desktop, Claude Code and Cursor. |
| **Widget** | Optional HTML attached to a tool. ChatGPT (Apps SDK) renders it as an iframe instead of plain text. |
| **External auth** | Optional per-server setting. AI clients must present a token that your own API validates. It can also run a full OAuth login against your platform (section 10). |
| **Branding** | Optional per-server identity (name, title, icon, instructions) that the server reports to AI clients in the MCP `initialize` handshake. |

---

## 2. Endpoint quick reference

### 2.1 Management API (for building integrations and dashboards)

All of these require auth (section 3). Base path: `https://mcp.viasocket.com/api`.

| Method | Path | Purpose | Success status |
|---|---|---|---|
| POST | `/api/mcp` | Create an MCP server | 201 |
| GET | `/api/mcp` | List the org's MCP servers | **201** |
| PUT | `/api/mcp/:id` | Update an MCP server (name, client, auth, branding) | 200 |
| DELETE | `/api/mcp/:id` | Delete an MCP server and all its tools | 200 |
| POST | `/api/mcp/:id/clone` | Clone an MCP server together with its tools | 201 |
| PUT | `/api/mcp/:id/domain` | Set the server's custom domain (white-label) | 200 |
| POST | `/api/mcp/:id/domain/check` | Check the custom domain now (DNS, certificate) | 200 |
| DELETE | `/api/mcp/:id/domain` | Remove the custom domain and its certificate | 200 |
| GET | `/api/mcp/:id/well-known` | List the server's `/.well-known` files | 200 |
| PUT | `/api/mcp/:id/well-known/:name` | Add or replace a `/.well-known` file (domain verification) | 200 |
| DELETE | `/api/mcp/:id/well-known/:name` | Remove a `/.well-known` file | 200 |
| POST | `/api/tool` | Create or update a tool (upsert) | 201 |
| GET | `/api/tools/:mcpServerId` | List the tools of an MCP server | 200 |
| DELETE | `/api/tool/:flowId` | Delete a flow tool by its `flowId` | **201** |
| DELETE | `/api/tool/id/:toolId` | Delete any tool by its `_id` | **201** |
| POST | `/api/skill` | Create a skill | 201 |
| GET | `/api/skill` | List the org's skills (without `content`) | 200 |
| GET | `/api/skill/:skillId` | Get one skill (with `content`) | 200 |
| PUT | `/api/skill/:skillId` | Update a skill (partial) | 200 |
| DELETE | `/api/skill/:skillId` | Delete a skill and remove it from every server | 200 |
| POST | `/api/skill/:skillId/run` | Return the skill's instructions | 200 |
| GET | `/api/skills/:mcpServerId` | List the skills connected to an MCP server (with `content`) | 200 |
| POST | `/api/mcp/:mcpServerId/skill/:skillId` | Connect a skill to an MCP server | 200 |
| DELETE | `/api/mcp/:mcpServerId/skill/:skillId` | Disconnect a skill from an MCP server | 200 |
| POST | `/api/prompt` | Create a prompt | 201 |
| GET | `/api/prompt` | List the org's prompts (without `content`) | 200 |
| GET | `/api/prompt/:promptId` | Get one prompt (with `content`) | 200 |
| PUT | `/api/prompt/:promptId` | Update a prompt (partial) | 200 |
| DELETE | `/api/prompt/:promptId` | Delete a prompt and remove it from every server | 200 |
| POST | `/api/prompt/:promptId/run` | Return the prompt's content filled with `arguments` | 200 |
| GET | `/api/prompts/:mcpServerId` | List the prompts connected to an MCP server (with `content`) | 200 |
| POST | `/api/mcp/:mcpServerId/prompt/:promptId` | Connect a prompt to an MCP server | 200 |
| DELETE | `/api/mcp/:mcpServerId/prompt/:promptId` | Disconnect a prompt from an MCP server | 200 |
| POST | `/api/resource` | Create a resource | 201 |
| GET | `/api/resource` | List the org's resources (without `content`) | 200 |
| GET | `/api/resource/:resourceId` | Get one resource (with `content`) | 200 |
| PUT | `/api/resource/:resourceId` | Update a resource (partial) | 200 |
| DELETE | `/api/resource/:resourceId` | Delete a resource and remove it from every server | 200 |
| GET | `/api/resources/:mcpServerId` | List the resources connected to an MCP server (with `content`) | 200 |
| POST | `/api/mcp/:mcpServerId/resource/:resourceId` | Connect a resource to an MCP server | 200 |
| DELETE | `/api/mcp/:mcpServerId/resource/:resourceId` | Disconnect a resource from an MCP server | 200 |

> In the management API, `:id` and `:mcpServerId` are always the **bare** Mongo `_id`. Do not include the `-{orgId}` suffix.

### 2.2 MCP protocol (for AI clients)

| Method | Path | Transport |
|---|---|---|
| POST | `/mcp/:id` | Streamable HTTP (stateless, recommended) |
| GET | `/mcp/:id` | Opens a legacy SSE stream |
| POST | `/connector/:slug` | Same as `/mcp/:id`, addressed by slug |
| GET | `/connector/:slug` | Same as `/mcp/:id`, addressed by slug |
| GET | `/:id/sse` | Legacy SSE: open the stream |
| POST | `/:id/messages?sessionId=…` | Legacy SSE: send a JSON-RPC message |
| POST | `/:id/sse` | Streamable HTTP served on the legacy path |

In these routes, `:id` can be `{mcpServerId}-{orgId}`, a bare `{mcpServerId}`, or a slug (section 8.1).

### 2.3 OAuth and discovery

| Method | Path |
|---|---|
| GET | `/.well-known/oauth-protected-resource/mcp/:id` |
| GET | `/.well-known/oauth-protected-resource/connector/:id` |
| GET | `/.well-known/oauth-authorization-server/oauth/:id` |
| POST | `/oauth/:id/register` |
| GET | `/oauth/:id/authorize` |
| GET | `/oauth/:id/callback` |
| POST | `/oauth/:id/token` |
| GET | `/.well-known/mcp-server` |
| GET | `/healthcheck` |
| GET | `/postman-collection.json` |
| GET | `/api-reference.md` (this document) |

---

## 3. Authentication

### 3.1 Management API (`/api/*`)

Send **one** of these headers:

| Header | Value | Notes |
|---|---|---|
| `proxy_auth_token` | A viaSocket user session token | The standard way. The token is resolved through viaSocket's identity service, and the user's **current company** becomes the org. |
| `Authorization` | `Bearer <internal JWT>` | Used by viaSocket's internal services. The JWT is signed with viaSocket's internal secret and its payload holds `userId`, `orgId` and optionally `userName`, `orgName`, `meta`. External integrators normally do not have this. |
| `Authorization` | `Bearer <MCP agent token>` | The same internal JWT, minted for **you** by viaSocket's API: `GET https://flow-api.viasocket.com/mcp/agent-token` with your viaSocket agent token (`VIASOCKET_AGENT_TOKEN`, bare in `Authorization`) answers `{ data: { token, token_type: "Bearer", expires_in: 86400, api, reference } }`. Valid 24 hours; store it with your other secrets (the viaSocket skill keeps it in `.env` as `VIASOCKET_MCP_TOKEN`) and fetch a new one on a 401. This is the way for coding agents and integrators. |

Rules:
- If both headers are present, `Authorization: Bearer` is used and `proxy_auth_token` is ignored.
- A missing or invalid credential returns **401** `{ "success": false, "message": "Unauthorized" }`.
- Every resource is scoped to the caller's org. Accessing another org's resource returns **404**, not 403.

### 3.2 MCP endpoints (`/mcp/:id`, `/connector/:slug`, `/:id/sse`, `/:id/messages`)

Each MCP server decides this through its `isAuthenticationEnabled` and `externalAuth` settings:

| Server config | What the AI client must send | How it is checked |
|---|---|---|
| `isAuthenticationEnabled: false` (default) | Nothing | Open access |
| `isAuthenticationEnabled: true`, with `externalAuth.validationUrl` | `Authorization: Bearer <token>` | The server calls `externalAuth.validationUrl` with `externalAuth.method` (GET/POST), passing the token in the header `externalAuth.headerName`. **HTTP 200 = valid**; anything else, or a network error, = 401. If `headerName` is `Authorization`, the full `Bearer <token>` value is forwarded; for any other header name, only the raw token is sent. Timeout: 5 s. |
| `isAuthenticationEnabled: true`, without `externalAuth` | `Authorization: Bearer <jwt>` | The JWT is verified with a per-server secret issued by viaSocket. |

When a request is rejected:
- If `externalAuth.loginUrl` is set, the response is **401** with JSON `{ "error": "unauthorized", "message": "Authentication required" }` and this header:
  ```
  WWW-Authenticate: Bearer error="invalid_token", error_description="Authentication required", resource_metadata="https://mcp.viasocket.com/.well-known/oauth-protected-resource/mcp/{rawId}"
  ```
  AI clients that support MCP OAuth (Claude, ChatGPT) follow `resource_metadata` and start the login automatically (section 10).
- Otherwise the response is **401** with plain text `Unauthorized`.

Token forwarding: whatever bearer token the AI client sends is also forwarded to the tool's backend when a tool runs (section 9).

---

## 4. Data models

TypeScript notation. `?` = may be absent. All documents also carry `_id: string` (Mongo ObjectId) and `__v: number`. Dates are ISO-8601 strings in JSON.

### 4.1 `McpServer`

```ts
interface McpServer {
  _id: string;
  userId: string;                    // creator's user id
  orgId: string;                     // owning org
  createdAt: string;                 // ISO date
  name?: string;
  isAuthenticationEnabled: boolean;  // default false
  client?: string;                   // free-form label, e.g. "claude", "cursor", "chatgpt"
  skillIds: string[];                // connected skill _ids, default []
  promptIds: string[];               // connected prompt _ids, default []
  resourceIds: string[];             // connected resource _ids, default []
  wellKnownFiles?: Array<{ name: string; content: string; updatedAt: string }>;  // served at /.well-known/<name> (section 5.7)
  slug?: string;                     // unique alias for /connector/:slug (set with `slug` on create or update)
  customDomain?: CustomDomain;       // the customer's own domain (section 5.6); listed as its settings-page view
  branding?: McpBranding;
  externalAuth?: ExternalAuth;
  url?: string;                      // only in API responses: "https://mcp.viasocket.com/mcp/{_id}-{orgId}"
  __v: number;
}

interface McpBranding {
  name?: string;          // serverInfo.name in the MCP initialize response
  title?: string;         // serverInfo.title
  websiteUrl?: string;    // serverInfo.websiteUrl
  iconUrl?: string;       // serverInfo.icons = [{ src: iconUrl }]
  instructions?: string;  // "instructions" in the MCP initialize response
}

interface ExternalAuth {
  validationUrl: string;            // REQUIRED whenever externalAuth is sent. Your endpoint; must return 200 for a valid token.
  loginUrl?: string;                // your OAuth authorization endpoint; turns on the OAuth proxy (section 10)
  tokenUrl?: string;                // your OAuth token endpoint; required if loginUrl is set
  clientId?: string;                // your OAuth app's client id; required if loginUrl is set
  clientSecret?: string;            // your OAuth app's client secret; required if loginUrl is set
  headerName?: string;              // header used to send the token to validationUrl; default "Authorization"
  method?: "GET" | "POST";          // HTTP method for validationUrl; default "GET"
}
```

### 4.2 `Tool`

```ts
interface Tool {
  _id: string;
  name: string;                       // MCP tool name (sanitized at runtime; see section 14)
  mcpServerId: string;
  type: "flow" | "custom";            // default "flow"
  flowId?: string;                    // required when type = "flow"
  apiConfig?: ApiConfig;              // required (with url) when type = "custom"
  description: string;                // required
  inputSchema?: JsonSchemaObject;     // modern input definition (preferred)
  inputParameters?: LegacyParam[];    // legacy input definition (generated from payload.body)
  outputSchema?: JsonSchemaObject;    // optional; makes the tool return structuredContent
  annotations?: ToolAnnotations;
  widget?: ToolWidget;
  createdAt: string;
  updatedAt: string;
  __v: number;
}

interface ApiConfig {
  url: string;                                      // the HTTP endpoint to call
  method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE"; // default "POST"
  headers: Record<string, string>;                  // default {}; values may contain {{paramName}} placeholders
  multipart?: {
    fileField: string;        // REQUIRED: form-data field name for the file
    fileParam?: string;       // input param holding a URL; the server downloads it
    contentParam?: string;    // input param holding base64 file bytes (wins over fileParam)
  };                          // at least one of fileParam / contentParam is required
}

interface ToolAnnotations {        // MCP behavioural hints
  title?: string;
  readOnlyHint?: boolean;
  destructiveHint?: boolean;
  idempotentHint?: boolean;
  openWorldHint?: boolean;
}

interface ToolWidget {
  html: string;                       // required to enable the widget
  csp?: {
    connectDomains?: string[];        // -> openai/widgetCSP.connect_domains
    resourceDomains?: string[];       // -> openai/widgetCSP.resource_domains
  };
  domain?: string;                    // -> openai/widgetDomain
  sensitiveFields?: string[];         // top-level response keys hidden from the model (section 9.5)
}

interface LegacyParam {
  name: string;                       // dotted path, e.g. "customer.email"
  type: "string" | "number" | "boolean" | "enum";
  description: string;
  required: boolean;
  enumValues?: string[];              // when type = "enum"
}

type JsonSchemaObject = {
  type: "object";
  properties: Record<string, JsonSchema>;
  required?: string[];
};
```

### 4.3 `Skill`

```ts
interface Skill {
  _id: string;
  orgId: string;
  createdBy?: string;      // user id
  name: string;            // /^[a-zA-Z0-9_-]{1,45}$/, unique per org; becomes the MCP tool name
  description: string;     // non-empty
  content: string;         // non-empty Markdown instructions
  createdAt: string;
  updatedAt: string;
  __v: number;
}
```

### 4.3B `Prompt`

```ts
interface Prompt {
  _id: string;
  orgId: string;
  createdBy?: string;
  name: string;              // the MCP prompt name: ^[a-zA-Z0-9_-]{1,45}$, unique per org
  title?: string;            // shown in the client's menu; default: the name with _ and - as spaces, title-cased
  description: string;       // one sentence on when a user would pick it
  arguments: Array<{ name: string; description?: string; required: boolean }>;  // what the client asks the user for
  content: string;           // the instruction, with {{argument}} placeholders
  createdAt: string;
  updatedAt: string;
}
```

On a server, `mcpServer.promptIds` lists the connected prompts.

### 4.3C `Resource`

```ts
interface Resource {
  _id: string;
  orgId: string;
  createdBy?: string;
  name: string;              // the MCP resource name: ^[a-zA-Z0-9_-]{1,45}$, unique per org
  title?: string;            // shown in the client's menu; default: the name with _ and - as spaces, title-cased
  description?: string;
  uri: string;               // what clients read it by; default resource://<name>; unique per org; never ui:// (widgets)
  mimeType: string;          // default "text/markdown"; e.g. "application/json", "text/csv"
  content: string;           // the text resources/read returns
  createdAt: string;
  updatedAt: string;
}
```

On a server, `mcpServer.resourceIds` lists the connected resources.

### 4.4 Standard response envelope (management API)

```ts
// success
{ success: true, message: string, data: any }
// failure
{ success: false, message: string, error?: string }
```

---

## 5. Management API: MCP servers

### 5.1 Create an MCP server

`POST /api/mcp`

**Body**

| Key | Type | Required | Description |
|---|---|---|---|
| `name` | string | no | Display name |
| `client` | string | no | Free-form client label (`"claude"`, `"cursor"`, `"chatgpt"`, …) |
| `isAuthenticationEnabled` | boolean | no | Default `false`. Must be `true` for `externalAuth` to be enforced. |
| `externalAuth` | `ExternalAuth` | no | See 4.1. If present: `validationUrl` is required; `method` must be `GET` or `POST`; if `loginUrl` is set, then `tokenUrl`, `clientId` and `clientSecret` are all required. |

`branding` is **not** accepted on create. Set it with PUT.

**Example**

```json
{
  "name": "My MCP Server",
  "client": "claude",
  "isAuthenticationEnabled": true,
  "externalAuth": {
    "validationUrl": "https://api.example.com/me",
    "loginUrl": "https://example.com/oauth/authorize",
    "tokenUrl": "https://api.example.com/oauth/token",
    "clientId": "abc",
    "clientSecret": "xyz",
    "headerName": "Authorization",
    "method": "GET"
  }
}
```

**Response 201**

```json
{
  "success": true,
  "message": "Tool successfully created or updated",
  "data": {
    "_id": "665f1c2e9b1e8a0012345678",
    "userId": "103028",
    "orgId": "62818",
    "name": "My MCP Server",
    "client": "claude",
    "isAuthenticationEnabled": true,
    "skillIds": [],
    "externalAuth": { "...": "..." },
    "createdAt": "2026-10-08T10:00:00.000Z",
    "__v": 0,
    "url": "https://mcp.viasocket.com/mcp/665f1c2e9b1e8a0012345678-62818"
  }
}
```

**Errors:** 400 (validation failure; see section 13).

### 5.2 List MCP servers

`GET /api/mcp`

Returns every MCP server in the caller's org, including ones created by other members.

**Response 201** (note: 201, not 200)

```json
{
  "success": true,
  "message": "Mcp server list successfully fetched",
  "data": [ { "...McpServer fields...": "", "url": "https://mcp.viasocket.com/mcp/{_id}-{orgId}" } ],
  "chatbotEmbedToken": "<internal; ignore>"
}
```

### 5.3 Update an MCP server

`PUT /api/mcp/:id`

| Key | Type | Required | Description |
|---|---|---|---|
| `name` | string | **yes** | Always required, even if you are only changing another field. Send the current name to keep it. |
| `client` | string | no | Updated only if sent |
| `isAuthenticationEnabled` | boolean | no | Updated only if sent |
| `externalAuth` | `ExternalAuth` | no | If sent, **replaces the whole object** (same validation as create) |
| `branding` | `McpBranding` | no | If sent, **replaces the whole object** |
| `slug` | string \| null | no | The server's alias at `/connector/<slug>`: 2-63 lowercase letters, digits, `_` and `-` (starting and ending with a letter or digit), not shaped like a server id. `null` removes it. Also accepted on create (`POST /api/mcp`), checked before the server is made. |

**Response 200:** `{ "success": true, "message": "MCP updated successfully", "data": McpServer }` (the response has no `url` field).

**Errors:** 400 `{ "success": false, "message": "Name is required" }` or an invalid `slug`; 404 `MCP server not found`; 409 the `slug` is used by another server; 500 `{ "success": false, "message": "Failed to update MCP", "error": "..." }` (also returned for externalAuth validation errors on this endpoint).

### 5.4 Delete an MCP server

`DELETE /api/mcp/:id`

Deletes the server **and all of its tools**. Skills remain in the org library.

**Response 200:** `{ "success": true, "message": "MCP server deleted successfully", "data": McpServer }`

### 5.6 Custom domain (white-label)

A server can be served on the customer's own domain, e.g. `https://mcp.client.com/mcp` (or a root domain, `https://client.com/mcp`). One domain per server; a domain belongs to one server.

- `PUT /api/mcp/:id/domain` `{ "domain": "mcp.client.com" }` sets it (replacing any other) with status `pending_dns`, and returns the DNS records the customer adds:
  - root domain: `A @ 8.232.228.180`; subdomain: `CNAME <subdomain> mcp.docstar.io`;
  - always: `TXT _docstar-verify[.<subdomain>] <verifyToken>` (proves ownership).
- A background check (every 90 s; `POST /api/mcp/:id/domain/check` runs it now) moves it along: once the TXT matches and the domain resolves only to the white-label address (public DNS, no AAAA), it requests a Google-managed certificate and map entry (`issuing_cert`), then `active` once Google issues it (15-60 min). Active domains are re-checked daily; one whose DNS stops pointing here for 3 days is removed.
- `DELETE /api/mcp/:id/domain` removes the map entry, the certificate and the domain. Deleting the server does the same.

`CustomDomain` (as listed): `{ domain, status: "pending_dns" | "issuing_cert" | "active" | "failed", url: "https://<domain>/mcp", records: [{ type, name, value }], lastError, lastCheckedAt, createdAt }`. `lastError` starts with a code: `TXT_MISSING`, `DNS_NOT_POINTING`, `AAAA_PRESENT`, `RATE_LIMITED`, `CHECK_FAILED`, or Google's reason (e.g. `RESOLVED_TO_NOT_SERVING`, `CAA`).

Errors: 400 invalid domain (IP, wildcard, one of ours, not ownable); 409 used by another server; 503 custom domains not configured.

On the custom domain, `/mcp` is the server; its own `/mcp/<id>`, `/connector/<slug>`, `/oauth/<id>/*` and OAuth discovery paths work too; everything else (other servers, `/api`, legacy SSE paths) is 404. OAuth discovery for `/mcp` is at `/.well-known/oauth-protected-resource/mcp` with `resource: https://<domain>/mcp`. Sign-in goes through `https://<domain>/oauth/<id>/callback`, so the customer's OAuth app must allow that redirect URI too.

Configuration: `WHITELABEL_GCP_PROJECT` (turns the feature on, e.g. `via-socket-prod`), `WHITELABEL_CERT_MAP` (default `whitelabel-certmap`), `WHITELABEL_IP` (default `8.232.228.180`), `WHITELABEL_CNAME` (default `mcp.docstar.io`), `GOOGLE_APPLICATION_CREDENTIALS` (the onboarding service account's key file, mounted from a Kubernetes Secret).

### 5.7 `/.well-known` files (domain verification)

Directories and app stores check that a host is yours by reading a file under `/.well-known`: Glama reads `glama.json`, OpenAI reads `openai-apps-challenge`. A server can hold its own such files, served at `https://<host>/.well-known/<name>` on its custom domain (section 5.6), and on one of our hosts whose `/mcp` short URL is that server (e.g. `mcp.docstar.io`).

| Call | Body / notes |
|---|---|
| `GET /api/mcp/:id/well-known` | `[{ name, contentType, content, updatedAt, urls }]`; `urls` are where each file is served (empty when the server has no host of its own yet). |
| `PUT /api/mcp/:id/well-known/:name` | `{ content }`: a string, or a JSON object for a `.json` name. Adds the file or replaces the one with that name. |
| `DELETE /api/mcp/:id/well-known/:name` | Removes it. |

Rules: `name` is one file name (`^[A-Za-z0-9][A-Za-z0-9._-]{0,99}$`, no `..`); `oauth-protected-resource`, `oauth-authorization-server`, `openid-configuration` and `mcp-server` are taken. A `.json` name must hold valid JSON and is served as `application/json`; every other name as `text/plain`. At most 16 KB per file and 20 files per server. A name the server hasn't set falls through to the shared files.

```json
PUT /api/mcp/:id/well-known/glama.json
{ "content": { "$schema": "https://glama.ai/mcp/schemas/connector.json", "claim": "glama_claim_..." } }
```

### 5.5 Clone an MCP server

`POST /api/mcp/:id/clone`

| Key | Type | Required | Description |
|---|---|---|---|
| `name` | string | no | Default: `"{source name} (Copy)"` (or `"MCP Server (Copy)"`) |

What gets copied: `client`, `isAuthenticationEnabled`, `externalAuth`, and every tool (name, description, type, inputParameters, inputSchema, outputSchema, annotations, widget, apiConfig). **Flow tools get a new, independent flow** (the flow is duplicated on viaSocket, so the clone has a new `flowId`).
What does **not** get copied: `skillIds`, `promptIds`, `resourceIds`, `wellKnownFiles`, `branding`, `slug`.

**Response 201**

```json
{
  "success": true,
  "message": "MCP server cloned successfully",
  "data": {
    "mcpServer": { "...McpServer...": "", "url": "https://mcp.viasocket.com/mcp/{newId}-{orgId}" },
    "tools": [ "...Tool[]..." ]
  }
}
```

---

## 6. Management API: tools

### 6.1 Create or update a tool (upsert)

`POST /api/tool`

**Body**

| Key | Type | Required | Description |
|---|---|---|---|
| `mcpServerId` | string | **yes** | Must be a server in your org (bare `_id`) |
| `type` | `"flow"` \| `"custom"` | no | Default `"flow"`. Any value other than `"custom"` is treated as `"flow"`. |
| `flowId` | string | yes if flow | The viaSocket flow id. **This is also the upsert key for flow tools** (see below). |
| `apiConfig` | `ApiConfig` | yes if custom | `url` required; `method` defaults to `POST` and must be one of `GET, POST, PUT, PATCH, DELETE` (case-insensitive); `headers` defaults to `{}`; `multipart` is optional (see 4.2). |
| `toolId` | string | no | Custom tools only: the `_id` of an existing tool to update. If omitted, a new custom tool is always created. |
| `title` | string | conditional | Tool name. If omitted, falls back to `payload.name`. One of the two is required. |
| `desc` | string | conditional | Tool description. If omitted, falls back to `payload.description`. One of the two is required. |
| `payload` | object | no | Input/output definitions (below) |
| `payload.name` | string | no | Fallback for `title` |
| `payload.description` | string | no | Fallback for `desc` |
| `payload.inputSchema` | JSON Schema object | no | **Modern format (preferred).** If this key is present, `payload.body` is ignored. |
| `payload.body` | object | no | **Legacy format.** Used only when `payload.inputSchema` is absent (see 6.1.2). |
| `payload.outputSchema` | JSON Schema object | no | Declares structured output (see 9.4) |
| `payload.annotations` | `ToolAnnotations` | no | Used if the top-level `annotations` is absent |
| `annotations` | `ToolAnnotations` | no | Wins over `payload.annotations`. Unknown keys are dropped; `"true"`/`"false"` strings are converted to booleans. **If neither is sent, the stored annotations are left unchanged.** Sending `{}` clears them. |
| `widget` | `ToolWidget` | no | Saved only if `widget.html` is a non-empty string |
| `status` | string | no | Accepted but **not stored** |

**Upsert behaviour**
- **Flow tools:** the server looks up an existing tool by `flowId` **across all servers**. If one exists, it is updated, and `mcpServerId` is overwritten with the one you sent, so the tool **moves** to that server. One flowId can back only one tool.
- **Custom tools:** pass `toolId` to update; omit it to create.
- On update, send the **full** tool definition every time.

#### 6.1.1 Modern input format: `payload.inputSchema` (JSON Schema)

Supported JSON Schema features (anything else becomes "any value"):

| Schema | Becomes |
|---|---|
| `{ "type": "string" }` | string (keeps `description`) |
| `{ "type": "number" }` / `{ "type": "integer" }` | number (integers are not enforced) |
| `{ "type": "boolean" }` | boolean |
| `{ "type": "object", "properties": {...}, "required": [...] }` | nested object; keys not in `required` are optional |
| `{ "type": "array", "items": {...} }` | array (items `any` if `items` is missing) |
| `{ "enum": ["a","b"] }` | string enum (on a non-object, non-array schema) |
| `{ "anyOf": [ X, { "type": "null" } ] }` | X, nullable and optional |
| object without `properties`, or any other type | any |

Top-level properties not listed in `required` are optional. `description` is kept for primitive types only.

```json
{
  "flowId": "scri12345",
  "mcpServerId": "665f1c2e9b1e8a0012345678",
  "title": "send_email",
  "desc": "Send an email to a recipient",
  "payload": {
    "inputSchema": {
      "type": "object",
      "properties": {
        "to":      { "type": "string", "description": "Recipient email" },
        "subject": { "type": "string" },
        "cc":      { "type": "array", "items": { "type": "string" } },
        "priority":{ "enum": ["low", "high"] }
      },
      "required": ["to"]
    }
  },
  "annotations": { "title": "Send email", "readOnlyHint": false, "destructiveHint": false, "openWorldHint": true }
}
```

#### 6.1.2 Legacy input format: `payload.body`

A nested object in which any node that has **both** `type` and `required` keys is a parameter. Its dotted path becomes the parameter name. Other object nodes are walked recursively.

| Leaf key | Meaning |
|---|---|
| `type` | `"string"`, `"number"` or `"boolean"`; any other value becomes `"string"` |
| `required` | boolean |
| `help` / `description` | description (`help` wins) |
| `children` | `[{ "value": "low" }, …]`; makes the parameter an enum of those values |

```json
{
  "flowId": "scri12345",
  "mcpServerId": "665f1c2e9b1e8a0012345678",
  "title": "create_ticket",
  "desc": "Create a support ticket",
  "payload": {
    "body": {
      "subject": { "type": "string", "required": true, "help": "Ticket subject" },
      "customer": {
        "email": { "type": "string", "required": true, "description": "Customer email" }
      },
      "priority": {
        "type": "string", "required": false, "description": "Ticket priority",
        "children": [{ "value": "low" }, { "value": "high" }]
      }
    }
  }
}
```

This becomes `inputParameters` = `subject`, `customer.email`, `priority (enum)`. AI clients see the parameter names `subject`, `customer__email`, `priority` (dots become `__`; see section 14). Your flow receives them nested again: `{ "subject": "...", "customer": { "email": "..." }, "priority": "low" }`.

#### 6.1.3 Custom (HTTP API) tool example

```json
{
  "type": "custom",
  "mcpServerId": "665f1c2e9b1e8a0012345678",
  "title": "deploy_app",
  "desc": "Deploy a zipped app",
  "apiConfig": {
    "url": "https://api.example.com/deploy",
    "method": "POST",
    "headers": { "X-App-Name": "{{app_name}}" },
    "multipart": { "fileField": "bundle", "fileParam": "bundle_url", "contentParam": "bundle_base64" }
  },
  "payload": {
    "inputSchema": {
      "type": "object",
      "properties": {
        "app_name":      { "type": "string" },
        "bundle_url":    { "type": "string", "description": "Public URL of the zip" },
        "bundle_base64": { "type": "string", "description": "Base64 zip content" }
      },
      "required": ["app_name"]
    },
    "outputSchema": {
      "type": "object",
      "properties": { "deploymentId": { "type": "string" }, "url": { "type": "string" } },
      "required": ["deploymentId"]
    }
  },
  "widget": {
    "html": "<div id=root></div><script>/* reads window.openai.toolOutput */</script>",
    "csp": { "connectDomains": ["https://api.example.com"], "resourceDomains": ["https://cdn.example.com"] },
    "domain": "https://example.com",
    "sensitiveFields": ["pollToken"]
  }
}
```

**Response 201:** `{ "success": true, "message": "Tool successfully created or updated", "data": Tool }`

**Errors (400):** `flowId is required for flow tools`, `apiConfig.url is required for custom tools`, `apiConfig.method must be one of GET, POST, PUT, PATCH, DELETE`, `apiConfig.multipart.fileField is required`, `apiConfig.multipart needs fileParam (a URL to fetch) and/or contentParam (base64 file content)`, plus Mongo validation errors (for example a missing name or description). **404** `MCP server not found`.

### 6.2 List tools of an MCP server

`GET /api/tools/:mcpServerId`

**Response 200:** `{ "success": true, "message": "Tools fetched successfully", "data": Tool[] }`

Errors: 404 `MCP server not found`; 500 `An error occurred while fetching tools`.

### 6.3 Delete a flow tool by flowId

`DELETE /api/tool/:flowId`

**Response 201:** `{ "success": true, "message": "Tool deleted successfully", "data": Tool }` · 404 `Tool not found`

### 6.4 Delete any tool by _id

`DELETE /api/tool/id/:toolId`

Use this for custom tools, which have no flowId.

**Response 201:** `{ "success": true, "message": "Tool deleted successfully", "data": Tool }` · 404 `Tool not found`

---

## 7. Management API: skills

A skill is an org-level Markdown instruction document. When a skill is connected to an MCP server, AI clients see it as a tool with **no arguments** that returns the skill's `content`.

### 7.1 Create a skill

`POST /api/skill`

| Key | Type | Required | Rule |
|---|---|---|---|
| `name` | string | yes | `^[a-zA-Z0-9_-]{1,45}$`, unique within the org |
| `description` | string | yes | non-empty after trim |
| `content` | string | yes | non-empty after trim; Markdown |

```json
{
  "name": "pdf_processing",
  "description": "Extract text and fill forms in PDF files",
  "content": "# PDF Processing\n\n## When to use\nUse when the user asks to read or fill a PDF.\n\n## Steps\n1. ...\n2. ..."
}
```

**Response 201:** `{ "success": true, "message": "Skill created successfully", "data": Skill }`

**Errors (400):** `name is required`, `name must be 1-45 characters using only letters, numbers, underscores or hyphens`, `description is required`, `description must be a non-empty string`, `content is required`, `content must be a non-empty string`, `A skill named "<name>" already exists in this org`.

### 7.2 List skills (org library)

`GET /api/skill`

Sorted newest first. **`content` is omitted.**

**Response 200:** `{ "success": true, "message": "Skills fetched successfully", "data": Omit<Skill,"content">[] }`

### 7.3 Get a skill

`GET /api/skill/:skillId`

**Response 200:** `{ "success": true, "message": "Skill fetched successfully", "data": Skill }` · 404 `Skill not found`

### 7.4 Update a skill

`PUT /api/skill/:skillId`

Partial update: send any subset of `name`, `description`, `content` (same rules as create). Sending none of them returns 400 `Nothing to update - provide name, description or content`. `updatedAt` is refreshed.

**Response 200:** `{ "success": true, "message": "Skill updated successfully", "data": Skill }`

### 7.5 Delete a skill

`DELETE /api/skill/:skillId`

The skill is first removed from every MCP server's `skillIds`, then deleted.

**Response 200:** `{ "success": true, "message": "Skill deleted successfully", "data": Skill }`

### 7.6 Run a skill

`POST /api/skill/:skillId/run` (no body)

Returns the same content an MCP client gets when it calls the skill tool.

**Response 200**

```json
{ "success": true, "message": "Skill executed successfully", "data": { "name": "pdf_processing", "description": "...", "content": "# PDF Processing ..." } }
```

### 7.7 Connect a skill to an MCP server

`POST /api/mcp/:mcpServerId/skill/:skillId` (no body)

Idempotent: connecting the same skill twice does not duplicate it.

**Response 200:** `{ "success": true, "message": "Skill connected to MCP server", "data": McpServer }` · 404 `MCP server not found` / `Skill not found`

### 7.8 Disconnect a skill from an MCP server

`DELETE /api/mcp/:mcpServerId/skill/:skillId`

**Response 200:** `{ "success": true, "message": "Skill disconnected from MCP server", "data": McpServer }`

### 7.9 List skills connected to an MCP server

`GET /api/skills/:mcpServerId`

Includes `content`.

**Response 200:** `{ "success": true, "message": "Skills fetched successfully", "data": Skill[] }`

---

## 7B. Management API: prompts

Same shape as skills: an org-level library, connected to servers. Every route needs auth (section 3).

| Call | Body / notes |
|---|---|
| `POST /api/prompt` | `{ name, description, content, title?, arguments? }`. `arguments` is a list of `{ name, description?, required? }`; each `name` matches `^[a-zA-Z0-9_-]{1,45}$`. Errors: 400 (validation, or a name already used in the org). |
| `GET /api/prompt` | The org's prompts without `content`. |
| `GET /api/prompt/:promptId` | One prompt with `content`. 404 when not in your org. |
| `PUT /api/prompt/:promptId` | Partial: any of `name`, `title`, `description`, `arguments`, `content`. `arguments` replaces the whole list. |
| `DELETE /api/prompt/:promptId` | Deletes it and removes it from every server. |
| `POST /api/prompt/:promptId/run` | `{ arguments?: { <name>: <value> } }` → `{ name, description, content }` with the placeholders filled; 400 when a required argument is missing. The same text an MCP client gets from `prompts/get`. |
| `GET /api/prompts/:mcpServerId` | The prompts connected to a server, with `content`. |
| `POST` / `DELETE /api/mcp/:mcpServerId/prompt/:promptId` | Connect / disconnect. Idempotent. |

**Example**

```json
{
  "name": "weekly_report",
  "title": "Weekly report",
  "description": "Write the weekly report for one project from its latest numbers.",
  "arguments": [
    { "name": "project", "description": "The project's name as it appears in Acme", "required": true },
    { "name": "audience", "description": "Who reads it: team or customer", "required": false }
  ],
  "content": "Use list_projects to find {{project}}, then get_metrics for the last 7 days. Write a report for {{audience}} ..."
}
```

## 7C. Management API: resources

Same shape as prompts: an org-level library, connected to servers. Every route needs auth (section 3).

| Call | Body / notes |
|---|---|
| `POST /api/resource` | `{ name, content, title?, description?, uri?, mimeType? }`. `uri` defaults to `resource://<name>`; any absolute URI works (`docs://pricing/plans`) except `ui://`, which widgets use. `mimeType` defaults to `text/markdown`. Errors: 400 (validation, or a name or URI already used in the org). |
| `GET /api/resource` | The org's resources without `content`. |
| `GET /api/resource/:resourceId` | One resource with `content`: the same text an MCP client gets from `resources/read`. 404 when not in your org. |
| `PUT /api/resource/:resourceId` | Partial: any of `name`, `title`, `description`, `uri`, `mimeType`, `content`. Renaming keeps the URI, so clients that saved it keep finding it; change `uri` only on purpose. |
| `DELETE /api/resource/:resourceId` | Deletes it and removes it from every server. |
| `GET /api/resources/:mcpServerId` | The resources connected to a server, with `content`. |
| `POST` / `DELETE /api/mcp/:mcpServerId/resource/:resourceId` | Connect / disconnect. Idempotent. |

**Example**

```json
{
  "name": "style_guide",
  "title": "Style guide",
  "description": "How Acme writes customer-facing text.",
  "mimeType": "text/markdown",
  "content": "# Acme style\n- Short sentences.\n- No jargon."
}
```

On the server this is `resources/list` → `{ name: "style_guide", uri: "resource://style_guide", title: "Style guide", mimeType: "text/markdown" }` and `resources/read { uri: "resource://style_guide" }` → `{ contents: [{ uri, mimeType, text }] }`. A resource whose URI is already registered on the server (a widget's, say) is skipped there.

## 8. MCP protocol endpoints (for AI clients)

These are standard **Model Context Protocol** endpoints. Any MCP client library (for example `@modelcontextprotocol/sdk`) can connect to them directly. Each MCP server exposes its tools, connected skills (as tools), connected prompts (`prompts/list`, `prompts/get`) and connected resources (`resources/list`, `resources/read`), plus its tools' widgets as `ui://` resources.

### 8.1 The `:id` segment

1. If `:id` exactly matches a server's `slug`, that server is used.
2. Otherwise the server id is everything before the first `-`. So `665f…5678-62818`, `665f…5678` and `665f…5678-anything` all resolve to `665f…5678`.

The canonical URL returned by the API is `https://mcp.viasocket.com/mcp/{mcpServerId}-{orgId}`. A server with a slug (set with `slug` on create or update) is also at `https://mcp.viasocket.com/connector/{slug}` (and on every host that points here, e.g. `https://mcp.docstar.io/connector/{slug}`). A server with an active custom domain is at `https://<domain>/mcp` (section 5.6).

### 8.2 Streamable HTTP (recommended): `POST /mcp/:id`

- **Stateless:** a fresh server is created for every request, and no `Mcp-Session-Id` is issued. Every request must carry its own auth.
- **Required header:** `Accept: application/json, text/event-stream`. Both values must be present, or the server returns **406**:
  ```json
  { "jsonrpc": "2.0", "error": { "code": -32603, "message": "Accept header must include both application/json and text/event-stream for POST requests" }, "id": null }
  ```
- `Content-Type: application/json`
- `Authorization: Bearer <token>`, if the server has auth enabled (section 3.2)
- Body: one JSON-RPC 2.0 message.

Supported methods: `initialize`, `notifications/initialized`, `ping`, `tools/list`, `tools/call`, `resources/list`, `resources/read` (widget HTML), plus everything else the MCP SDK provides by default. There are **no prompts**.

**`initialize`**

```http
POST /mcp/665f1c2e9b1e8a0012345678-62818
Accept: application/json, text/event-stream
Content-Type: application/json

{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"my-app","version":"1.0.0"}}}
```

The result's `serverInfo` is `{ "name": "viasocket-mcp", "version": "1.0.0" }` unless the server has `branding`, in which case it becomes `{ name: branding.name, version: "1.0.0", title?, websiteUrl?, icons?: [{src}] }` and `instructions` is set to `branding.instructions`. Capabilities: `tools`, `resources`, `prompts` (prompts is empty).

**`tools/list`**

```json
{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}
```

Each tool in the result has: `name`, `description`, `inputSchema` (JSON Schema), optionally `outputSchema`, `annotations`, and `_meta: { "openai/outputTemplate": "ui://widget/{toolId}-{hash}.html" }` for widget tools. Skill tools have an empty `inputSchema` and the annotations `{ title, readOnlyHint: true, destructiveHint: false, openWorldHint: false }`.

**`tools/call`**

```json
{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"send_email","arguments":{"to":"a@b.com","subject":"Hi"}}}
```

The result format is described in 9.4.

Server errors are returned as HTTP 500 `{ "jsonrpc": "2.0", "error": { "code": -32603, "message": "Internal server error" }, "id": null }`.

### 8.3 `GET /mcp/:id`

Requires `Accept: text/event-stream` (otherwise 406). This opens a **legacy SSE session** (same as 8.4), not a Streamable HTTP notification stream.

### 8.4 Legacy SSE transport

1. `GET /:id/sse`. The server responds with `text/event-stream` and first sends an `endpoint` event whose data is `/{mcpServerId}/messages?sessionId=<uuid>`. A keep-alive comment (`:`) is sent every 15 s.
2. `POST /{mcpServerId}/messages?sessionId=<uuid>` with a JSON-RPC body. The server replies `202 Accepted`, and the JSON-RPC response arrives on the SSE stream.
3. The `Authorization: Bearer` token sent on the GET is remembered for the session. Every POST may send a newer token, which replaces it (token rotation).
4. If the session id is unknown, the POST returns 500.

`POST /:id/sse` behaves exactly like `POST /mcp/:id` (8.2). It exists for clients that only know the `/sse` URL.

### 8.5 Client configuration examples

Claude Desktop / Cursor (`mcp.json`):

```json
{
  "mcpServers": {
    "viasocket": { "url": "https://mcp.viasocket.com/mcp/665f1c2e9b1e8a0012345678-62818" }
  }
}
```

With a static token:

```json
{
  "mcpServers": {
    "viasocket": {
      "url": "https://mcp.viasocket.com/mcp/665f1c2e9b1e8a0012345678-62818",
      "headers": { "Authorization": "Bearer <token>" }
    }
  }
}
```

Node.js SDK:

```ts
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";

const transport = new StreamableHTTPClientTransport(
  new URL("https://mcp.viasocket.com/mcp/665f1c2e9b1e8a0012345678-62818"),
  { requestInit: { headers: { Authorization: "Bearer <token>" } } } // omit if auth is off
);
const client = new Client({ name: "my-app", version: "1.0.0" });
await client.connect(transport);
const { tools } = await client.listTools();
const result = await client.callTool({ name: "send_email", arguments: { to: "a@b.com" } });
```

---

## 9. How tools execute at runtime

### 9.1 Arguments

- `inputSchema` tools: the arguments are passed through unchanged.
- Legacy `inputParameters` tools: the MCP parameter names are mapped back to their original dotted names (for example `customer__email` → `customer.email`).
- In both cases, keys that contain dots are then expanded into nested objects (`{"a.b": 1}` → `{"a": {"b": 1}}`).

### 9.2 Flow tools (`type: "flow"`)

```
POST https://flow.sokt.io/func/{flowId}
Content-Type: application/json
Authorization: <raw token>        <- only if the MCP client sent one; no "Bearer " prefix is added
<body = arguments>
```

The flow's JSON response is returned to the AI client as a JSON string. On any error, the tool returns the text `Error: Tool not recognized or unable to perform the requested task.`

### 9.3 Custom tools (`type: "custom"`)

```
{apiConfig.method} {apiConfig.url, with {{param}} placeholders filled from top-level arguments}
<apiConfig.headers, with {{param}} placeholders filled from top-level arguments>
<apiConfig.tokenHeader, default Authorization>: <the token, bare; "<tokenPrefix> <token>" when apiConfig.tokenPrefix is set>   <- only if the client sent a token and that header is not already in apiConfig.headers
```

- **Placeholders** `{{param}}` work in `apiConfig.url` (for a path such as `https://api.example.com/todos/{{task_id}}`) and in `apiConfig.headers`. An argument used by a placeholder is not sent again in the query or body. A placeholder with no matching argument is left as-is.
- **GET:** the remaining arguments go in the **query string** (`/todos?userId=1&completed=false`).
- **POST, PUT, PATCH, DELETE:** the remaining arguments are the **JSON body**, except those named in `apiConfig.queryParams` (a list of argument names), which go in the query string.
- **The user's token** goes in `apiConfig.tokenHeader` (default `Authorization`), **bare**. Set `apiConfig.tokenPrefix: "Bearer"` for an API that expects `Authorization: Bearer <token>`, which most OAuth APIs do. A header already set in `apiConfig.headers` is never replaced.
- **Multipart mode** (`apiConfig.multipart` set): the body is `multipart/form-data`. The file comes from `arguments[contentParam]` (base64), or else is downloaded from the URL in `arguments[fileParam]` (30 s timeout). It is uploaded under the field `fileField` with filename `fileField`. Every other argument is sent as a string form field.
- Timeout: 30 s.
- Results:
  - HTTP < 400: the response body is returned as a JSON string.
  - HTTP ≥ 400: `Error: Tool API responded with status {status}: {body}`
  - Network failure: `Error: Unable to reach tool API ({message})`
  - Missing file: `Error: missing "<contentParam>" (base64 file content) or "<fileParam>" (a URL to fetch) (expected one of these, to upload as "<fileField>")`

Errors come back as ordinary text content. `isError` is **not** set, so check whether the text starts with `Error:`.

### 9.4 Tool result format (MCP `tools/call` result)

```ts
{
  content: [{ type: "text", text: string }],   // JSON string of the backend response (sensitive fields removed)
  structuredContent?: object,                  // only if the tool has a widget or an outputSchema:
                                               //   the parsed JSON of `text`, or { result: text } if it is not JSON
  _meta?: { toolResponseMetadata: object }     // only if widget.sensitiveFields matched (see 9.5)
}
```

If an `outputSchema` is declared, the MCP SDK validates `structuredContent` against it. Make sure your backend returns a JSON object that matches it.

### 9.5 Widgets (ChatGPT Apps SDK)

For a tool with `widget.html`:
- An MCP resource is registered at the URI `ui://widget/{toolId}-{first 8 hex of sha256(html)}.html` with mimeType `text/html+skybridge`. Editing the HTML changes the URI, which busts ChatGPT's cache.
- The tool's `_meta["openai/outputTemplate"]` points to that URI.
- The resource's `_meta` includes `openai/widgetCSP: { connect_domains, resource_domains }` and `openai/widgetDomain` when they are configured.
- The widget reads the tool result through `window.openai.toolOutput` (= `structuredContent`).
- `sensitiveFields`: these top-level keys of a JSON-object response are **removed** from `content` and `structuredContent` (so the model never sees them) and moved into `_meta.toolResponseMetadata`. The widget reads them as `window.openai.toolResponseMetadata`.

### 9.6B Prompts

Each connected prompt is registered as an MCP prompt with its `name`, `title`, `description` and one string argument per entry of `arguments` (optional unless `required`). `prompts/get` answers one `user` message whose text is `content` with every `{{name}}` replaced by the argument given; a placeholder with no value is left as it is. Prompt names are their own namespace: a prompt and a tool may share a name. Two connected prompts with the same name get `_1`, `_2`, … suffixes.

### 9.6 Skill tools

These take no arguments. The description is `"{skill.description} Call this to load the full instructions for this skill before doing the task."`. The title is derived from the name (`pdf_processing` → `Pdf Processing`). The result is `{ content: [{ type: "text", text: skill.content }] }`. Skills are registered after tools, so if a skill and a tool share a name, the skill gets a `_1` suffix.

---

## 10. OAuth 2.1 proxy (per-server login)

Turn this on to make AI clients (Claude, ChatGPT) log users in to **your** platform before they use the MCP server. viaSocket acts as the OAuth authorization server towards the AI client, and forwards the real login to your OAuth provider.

### 10.1 Setup

Create or update the MCP server with `isAuthenticationEnabled: true` and:

```json
"externalAuth": {
  "validationUrl": "https://api.yourapp.com/me",
  "loginUrl":      "https://yourapp.com/oauth/authorize",
  "tokenUrl":      "https://api.yourapp.com/oauth/token",
  "clientId":      "<your OAuth app client id>",
  "clientSecret":  "<your OAuth app client secret>",
  "headerName":    "Authorization",
  "method":        "GET"
}
```

In your OAuth app, register this **redirect URI**: `https://mcp.viasocket.com/oauth/{mcpServerId}/callback` (bare `_id`, no org suffix).

**Requirements for your platform:**
- `loginUrl` must accept `client_id`, `redirect_uri`, `response_type=code`, `state` and optionally `scope`, then redirect back with `?code=…&state=…` (or `?error=…`).
- `tokenUrl` must accept a **JSON** body (`Content-Type: application/json`):
  - `{ client_id, client_secret, grant_type: "authorization_code", code, redirect_uri }`
  - `{ client_id, client_secret, grant_type: "refresh_token", refresh_token }`

  It must return `{ access_token, expires_in?, refresh_token? }`. Timeout: 10 s.
- `validationUrl` must return **HTTP 200** for a valid access token sent in `headerName`.

### 10.2 Flow

```
AI client                     mcp.viasocket.com                       your platform
   |-- POST /mcp/{id} (no token) ->|
   |<- 401 + WWW-Authenticate resource_metadata=… --|
   |-- GET /.well-known/oauth-protected-resource/mcp/{id} ->|
   |<- { resource, authorization_servers:[.../oauth/{mcpServerId}] }
   |-- GET /.well-known/oauth-authorization-server/oauth/{mcpServerId} ->|
   |-- POST /oauth/{mcpServerId}/register (DCR) or use a CIMD URL client_id ->|
   |-- GET /oauth/{mcpServerId}/authorize?...PKCE... ->|-- 302 loginUrl?client_id&redirect_uri=.../callback&state -->|
   |                                                   |<-- 302 /oauth/{mcpServerId}/callback?code&state ----------|
   |                                                   |-- POST tokenUrl (JSON, authorization_code) ------------->|
   |<- 302 client redirect_uri?code=<viaSocket code>&state |
   |-- POST /oauth/{mcpServerId}/token (code + code_verifier) ->|
   |<- { access_token: <YOUR platform's token>, refresh_token, expires_in? }
   |-- POST /mcp/{id}  Authorization: Bearer <token> ->|-- GET validationUrl (headerName: token) -->|
```

The access token the AI client receives **is your platform's own access token**, and it is forwarded to your tool backends as-is.

### 10.3 Endpoints

**`GET /.well-known/oauth-protected-resource/mcp/:id`** (also `/connector/:id`)

```json
{ "resource": "https://mcp.viasocket.com/mcp/{rawId}", "authorization_servers": ["https://mcp.viasocket.com/oauth/{mcpServerId}"] }
```

Returns 404 `{ "error": "not_found", "message": "No OAuth configuration for this MCP server" }` if `externalAuth.loginUrl` is not set.

**`GET /.well-known/oauth-authorization-server/oauth/:id`** (RFC 8414)

```json
{
  "issuer": "https://mcp.viasocket.com/oauth/{mcpServerId}",
  "authorization_endpoint": "https://mcp.viasocket.com/oauth/{mcpServerId}/authorize",
  "token_endpoint": "https://mcp.viasocket.com/oauth/{mcpServerId}/token",
  "registration_endpoint": "https://mcp.viasocket.com/oauth/{mcpServerId}/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code", "refresh_token"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["client_secret_post", "none"],
  "client_id_metadata_document_supported": true
}
```

**`POST /oauth/:id/register`** (RFC 7591 Dynamic Client Registration, JSON)

| Key | Type | Required |
|---|---|---|
| `redirect_uris` | string[] | yes, non-empty |
| `client_name` | string | no |

Response 201:

```json
{
  "client_id": "<uuid>", "client_secret": "<64 hex>", "client_id_issued_at": 1760000000, "client_secret_expires_at": 0,
  "redirect_uris": ["..."], "client_name": "...", "token_endpoint_auth_method": "client_secret_post",
  "grant_types": ["authorization_code", "refresh_token"], "response_types": ["code"]
}
```

Error 400: `{ "error": "invalid_client_metadata", "error_description": "redirect_uris is required" }`

**Client ID Metadata Documents (CIMD):** instead of registering, a client can use an **HTTPS URL** as its `client_id`. The server fetches that URL (5 s timeout, 64 KB max, cached for 5 min). The document must be JSON with `client_id` exactly equal to the URL and a non-empty `redirect_uris`. CIMD clients are public (no secret); PKCE is their proof of possession.

**`GET /oauth/:id/authorize`** (query parameters)

| Param | Required | Value |
|---|---|---|
| `response_type` | yes | `code` |
| `client_id` | yes | A DCR client id or a CIMD URL |
| `redirect_uri` | yes | Must be in the client's `redirect_uris` |
| `code_challenge` | yes | PKCE challenge |
| `code_challenge_method` | yes | `S256` (the only supported method) |
| `state` | no | Echoed back to you |
| `scope` | no | Forwarded to your `loginUrl` |

Response: 302 to your `loginUrl`. Errors: 400 `invalid_request` / `invalid_client`; 500 `server_error` (OAuth not configured).

**`GET /oauth/:id/callback`** is internal (your platform redirects here). It redirects to the AI client's `redirect_uri` with `?code=…&state=…`, or with `?error=access_denied|server_error&state=…`. An unknown or expired state returns 400 with plain text.

**`POST /oauth/:id/token`** (`application/x-www-form-urlencoded` or JSON)

Authorization code grant:

| Key | Required |
|---|---|
| `grant_type` | `authorization_code` |
| `client_id` | yes |
| `client_secret` | yes for DCR clients; omit for CIMD clients |
| `code` | yes (valid for 5 minutes, single use) |
| `redirect_uri` | yes; must match the one used in /authorize |
| `code_verifier` | yes (PKCE) |

Refresh grant:

| Key | Required |
|---|---|
| `grant_type` | `refresh_token` |
| `client_id` | yes (must be the client the token was issued to) |
| `client_secret` | yes for DCR clients |
| `refresh_token` | yes |

Success:

```json
{ "access_token": "<your platform token>", "token_type": "bearer", "refresh_token": "<token>", "expires_in": 3600 }
```

`expires_in` is present only if your platform returned it.

Refresh behaviour:
- If your platform issued a real `refresh_token`, a real refresh is performed against your `tokenUrl`. A rotated refresh token is honoured.
- If it did not, the access token doubles as the refresh token. Refreshing re-checks it against `validationUrl` and returns the same token.

Errors:

| Status | Body | Meaning |
|---|---|---|
| 400 | `{ "error": "unsupported_grant_type" }` | |
| 401 | `{ "error": "invalid_client" }` | Unknown client or wrong secret |
| 400 | `{ "error": "invalid_grant" }` | Bad, expired or reused code; redirect_uri mismatch; unknown refresh token |
| 400 | `{ "error": "invalid_grant", "error_description": "PKCE verification failed" }` | |
| 400 | `{ "error": "invalid_grant", "error_description": "Upstream refresh token was rejected" }` | Your platform rejected the refresh token |
| 400 | `{ "error": "invalid_grant", "error_description": "The underlying token is no longer valid" }` | |
| 503 | `{ "error": "temporarily_unavailable", ... }` | Your platform was unreachable or returned 5xx; retry |

> Client registrations, pending logins, codes and refresh-token mappings are kept **in memory**. After a viaSocket deploy or restart, clients get `invalid_client` / `invalid_grant` and must re-register and log in again. Well-behaved MCP clients do this automatically.

---

## 11. Discovery and health endpoints

| Endpoint | Response |
|---|---|
| `GET /healthcheck` | Plain text `Welcome to Viasocket Mcp!.` |
| `GET /.well-known/mcp-server` | Static descriptor: `{ name: "Viasocket MCP Server", version: "1.0.0", description, capabilities: { tools: true, resources: false, prompts: false }, authentication: { required: false, type: "none" }, endpoints: { sse: ".../{id}/sse", messages: ".../{id}/messages", mcp: ".../mcp/{id}" }, transport: ["sse","http"] }`. This is generic; per-server auth is described in section 3.2. |
| `GET /.well-known/oauth-authorization-server`, `GET /.well-known/openid-configuration` | Legacy static files that advertise "no auth". Use the per-server endpoints from section 10 instead. |
| `GET /postman-collection.json` | Postman collection of the API |
| Any other path | 404 `{ "error": "Route Not Found", "message": "Cannot {METHOD} {path}" }` |

---

## 12. Integration recipes

### Recipe A: Publish your own APIs as an MCP server

1. `POST /api/mcp` with `{ "name": "Acme Tools", "client": "claude" }`, and save `data._id` and `data.url`.
2. For each API endpoint, call `POST /api/tool` with `type: "custom"`, `mcpServerId`, `title` (snake_case, at most 45 chars, `[a-zA-Z0-9_-]`), `desc` (explain clearly when the AI should use it), `apiConfig`, `payload.inputSchema`, and optionally `annotations` (`readOnlyHint: true` for GETs, `destructiveHint: true` for deletes).
3. Give `data.url` to the end user to paste into Claude, ChatGPT or Cursor.
4. Check the result with `POST {url}` and the body `tools/list` (section 8.2).

### Recipe B: Require login with your platform

1. Do Recipe A.
2. `PUT /api/mcp/{id}` with `name` (required), `isAuthenticationEnabled: true` and a full `externalAuth` (section 10.1).
3. Register `https://mcp.viasocket.com/oauth/{id}/callback` in your OAuth app.
4. Leave `Authorization` out of `apiConfig.headers` and set `apiConfig.tokenPrefix: "Bearer"` on each custom tool: the user's token is then sent as `Authorization: Bearer <token>`. Without `tokenPrefix` it is sent bare.

### Recipe C: Static API key auth (no OAuth)

1. Set `isAuthenticationEnabled: true` and `externalAuth: { validationUrl, headerName, method }` **without** `loginUrl`.
2. The end user configures the MCP client with `Authorization: Bearer <key>`. Every request is validated against your `validationUrl`.

### Recipe D: Brand the server

`PUT /api/mcp/{id}` with `{ "name": "...", "branding": { "name": "acme", "title": "Acme", "websiteUrl": "https://acme.com", "iconUrl": "https://acme.com/icon.png", "instructions": "Use these tools to manage Acme projects." } }`

### Recipe E: Ship instructions with skills

1. `POST /api/skill` with `name`, `description` and Markdown `content`.
2. `POST /api/mcp/{mcpServerId}/skill/{skillId}`.
3. The AI client now sees a zero-argument tool named after the skill. Calling it returns the Markdown.

### Recipe E2: Give users ready-made prompts

1. `POST /api/prompt` with `name`, `description`, `content` (use `{{argument}}` placeholders) and `arguments`.
2. `POST /api/mcp/{mcpServerId}/prompt/{promptId}`.
3. The user now sees it in their client's prompt menu (a slash command in Claude Desktop, Claude Code and Cursor), fills in the arguments, and the filled text becomes their message. Use a skill (Recipe E) instead when the **model** should load instructions on its own.

### Recipe F: Keep a remote tool list in sync

1. `GET /api/tools/{mcpServerId}` to read the current tools.
2. For each desired tool: `POST /api/tool` (pass `toolId` for existing custom tools, or reuse the same `flowId` for flow tools).
3. For each tool you no longer want: `DELETE /api/tool/id/{toolId}`.

---

## 13. Errors

| Status | Where | Body |
|---|---|---|
| 400 | Management API validation or processing failure | `{ "success": false, "message": "An error occurred while processing your request", "error": "<reason>" }` |
| 401 | Management API auth | `{ "success": false, "message": "Unauthorized" }` |
| 401 | MCP endpoint auth | JSON `{ "error": "unauthorized", "message": "Authentication required" }` + `WWW-Authenticate` (OAuth servers), or plain text `Unauthorized` |
| 404 | Resource not in your org or not found | `{ "success": false, "message": "MCP server not found" \| "Tool not found" \| "Skill not found" }` |
| 406 | MCP endpoint, wrong `Accept` header | JSON-RPC error, code `-32603` |
| 500 | MCP endpoint internal error | JSON-RPC error `{ code: -32603, message: "Internal server error" }` |
| 500 | Unhandled | Plain text `Something broke!` |

Tool-level failures are **not** HTTP errors. They come back as a successful `tools/call` whose text starts with `Error:` (section 9.3).

---

## 14. Limits, rules and gotchas

1. **Tool names** shown to AI clients: every character outside `[a-zA-Z0-9_-]` becomes `_`, and names are truncated to **45** characters. Duplicate names on one server get `_1`, `_2`, … suffixes, in registration order (tools first, then skills).
2. **Legacy parameter names**: `.` becomes `__`, and any other character outside `[a-zA-Z0-9_-]` becomes `-`. They are mapped back before execution.
3. **Skill names** must already match `^[a-zA-Z0-9_-]{1,45}$` and be unique within the org.
4. **A flowId belongs to exactly one tool.** Posting the same `flowId` with a different `mcpServerId` moves the tool.
5. **Custom tools without `toolId` always create a new tool.** Store the returned `_id` so you can update it later.
6. `PUT /api/mcp/:id` **always requires `name`**. `externalAuth` and `branding` replace the whole object when sent.
7. `slug` can be set on create (`POST /api/mcp`) and on update (`PUT /api/mcp/:id`; `null` removes it). `branding` is set on update only.
8. Clone does **not** copy skills, branding or slug.
9. Deleting a server deletes its tools. Deleting a skill disconnects it everywhere.
10. Some success responses use **201** where you would expect 200: `GET /api/mcp`, and both tool `DELETE` routes. Treat any 2xx as success.
11. `status` on `POST /api/tool` is accepted but not stored.
12. Streamable HTTP is **stateless**: no session id, and auth is required on every request.
13. Every tool receives the client's token **bare** (what followed `Bearer ` in the client's `Authorization`): flow tools always in `Authorization`; custom tools in `apiConfig.tokenHeader` (default `Authorization`), with `apiConfig.tokenPrefix` in front when the tool sets it (`"Bearer"` for an API that reads `Authorization: Bearer <token>`). A custom tool that sets that header itself in `apiConfig.headers` keeps its own value and never gets the user's token. Section 9.3 shows the exact request.
14. Upstream timeouts: custom tool calls 30 s, file download 30 s, validationUrl 5 s, OAuth tokenUrl 10 s, CIMD fetch 5 s.
15. MCP `prompts` are the connected prompts (section 7B). `resources` are used only for widgets.
16. OAuth state is held in memory and is reset when viaSocket redeploys (section 10.3).
17. The upstream OAuth `tokenUrl` call sends **JSON**, not form-encoded data. Your token endpoint must accept JSON.
