---
title: "TSLink as an MCP Server"
description: "44 owner tools for sharing and managing local services over stdio or a tailnet-only HTTP control plane"
url: "https://tslink.md/docs/mcp-server"
locale: "en"
product_version: "0.1.1"
source: "https://github.com/anydoor7/tslink/blob/v0.1.1/docs/agents.md"
---

> Documentation index: https://tslink.md/llms.txt · Installed binary is authoritative: `tslink manifest`.

Scenario guides: [Use your coding agent's web UI from your phone](https://tslink.md/docs/agent-ui-phone.md).

## What `tslink mcp` is

TSLink exposes 44 owner tools from one registry over two transports: a local stdio child process (`tslink mcp`) and an opt-in tailnet-only Streamable HTTP endpoint (`tslink serve --mcp`). This page describes controlling TSLink itself. To proxy an existing third-party MCP server, see [MCP Server Hosting](https://tslink.md/docs/mcp-hosting.md).

## Register with a local MCP client

```json
{"mcpServers":{"tslink":{"command":"tslink","args":["mcp"]}}}
```

Use the installed binary's absolute path if the client has a different `PATH`. Do not add a credential to this client configuration. The default share flow requires no stored credential: TSLink returns `needs_login` for browser enrollment when needed.

The MCP process itself opens no network listener. Calling `share`, `add`, or `template_apply` may install/start the separate background gateway and its tsnet services. Installation announcements go to stderr.

## The stdio contract

| Item                   | Behavior                                                                                                                                                                                                                                 |
| ---------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| stdout                 | JSON-RPC frames only, one JSON object per line                                                                                                                                                                                           |
| stderr                 | Diagnostics, logs, and normal installation announcements; successful sessions need not have empty stderr                                                                                                                                 |
| `--json`               | `tslink mcp --json` is rejected with exit `2`; stdout is reserved for protocol frames                                                                                                                                                    |
| Versions               | Current `2026-07-28`; older `2025-11-25`, `2025-06-18`, `2025-03-26`, `2024-11-05` are supported                                                                                                                                         |
| Negotiation            | Current clients put their version in each request's `_meta` and need no `initialize`. Older clients use `initialize` then `notifications/initialized`. The deprecated handshake echoes a supported version or falls back to `2025-11-25` |
| EOF                    | Already-read requests drain their answers before exit. Keep reading stdout while closing stdin                                                                                                                                           |
| EOF watchdog           | A call still running 6m after stdin closes is cancelled; after 5s handler grace the command exits `1`. A blocked stdout response gets at most a further 5s before it is abandoned                                                        |
| Signals                | SIGINT/SIGTERM cancels in-flight calls, rolls back a `share` still waiting for its URL, and exits `1`. A second signal terminates immediately                                                                                            |
| Session-ending input   | Malformed JSON, a value that is not a JSON-RPC message, a JSON-RPC batch, or a record over 1,048,576 bytes ends the session, drops in-flight answers, and prevents reading later records                                                 |
| Duplicate in-flight id | Receives no answer; do not reuse an id while its call is outstanding                                                                                                                                                                     |
| URL polling            | `url.wait` is a Go duration capped at `5m`; an excessive wait is a usage error                                                                                                                                                           |

Do not assume malformed input receives a recoverable JSON-RPC error. Reopen the child process after session termination. The SDK owns protocol negotiation and error handling. `serverInfo.version` identifies the installed build (or `dev` when unstamped).

For an older client, the handshake begins with:

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

```json
{"jsonrpc":"2.0","method":"notifications/initialized"}
```

## Tools and complete input fields

Owner `tools/list` publishes 44 tools; reduced sessions advertise their permitted subset, each with `inputSchema` and `outputSchema`. Inputs are strict: unknown fields and invalid types are refused. The tables below list every input field; live schemas from your installed build remain authoritative. Daemon lifecycle, installation, login/logout, and configuration are CLI-only.

### Session tool index

Local owner exposes 44 tools; viewer 12, people-manager 18. Use `tools/list` in the actual session. Required parameters are explicit below; optional fields retain their live schema constraints. Roles never widen through arguments. V = viewer, A = app-operator, P = people-manager; every row is available to owner. Reduced roles also enforce app and duration scope, and request tools add portal-owner checks.

| Tool               | Required parameters                         | Optional parameters                                                                                                                                                                                                                                                                                                                              | Reduced roles |
| ------------------ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- |
| `access_explain`   | `service`: string                           | None                                                                                                                                                                                                                                                                                                                                             | V/A/P         |
| `access_log`       | None                                        | `app`: string, `decision`: string, `limit`: integer, `since`: string, `until`: string, `who`: string                                                                                                                                                                                                                                             | V/A/P         |
| `access_summary`   | None                                        | `app`: string, `decision`: string, `limit`: integer, `since`: string, `until`: string, `who`: string                                                                                                                                                                                                                                             | V/A/P         |
| `add`              | `name`: string, `type`: string              | `allow`: array, `control_url`: string, `dir`: string, `ephemeral`: boolean, `funnel`: boolean, `funnel_ttl`: string, `health`: object, `no_auto_provision`: boolean, `no_daemon_install`: boolean, `preserve_host`: boolean, `public_ack`: boolean, `request_limits`: object, `requestable`: boolean, `tags`: array, `target`: string            | Owner only    |
| `app_restart`      | `app`: string                               | None                                                                                                                                                                                                                                                                                                                                             | A             |
| `apps_detect`      | None                                        | None                                                                                                                                                                                                                                                                                                                                             | Owner only    |
| `doctor`           | None                                        | `probe_external`: boolean                                                                                                                                                                                                                                                                                                                        | V/A/P         |
| `extend`           | `service`: string                           | `ack_never`: boolean, `for`: string, `regrant`: boolean, `until`: string, `who`: string                                                                                                                                                                                                                                                          | A/P           |
| `guest_create`     | `app`: string, `for`: string                | `label`: string, `pin`: string, `print_link`: boolean, `public`: boolean                                                                                                                                                                                                                                                                         | Owner only    |
| `guest_list`       | None                                        | None                                                                                                                                                                                                                                                                                                                                             | Owner only    |
| `guest_revoke`     | `id`: string                                | None                                                                                                                                                                                                                                                                                                                                             | Owner only    |
| `guest_show`       | `id`: string                                | None                                                                                                                                                                                                                                                                                                                                             | Owner only    |
| `health`           | None                                        | None                                                                                                                                                                                                                                                                                                                                             | V/A/P         |
| `invite_device`    | `email`: string, `service`: string          | `allow_exit_node`: boolean, `multi_use`: boolean, `print_link`: boolean                                                                                                                                                                                                                                                                          | Owner only    |
| `invite_list`      | None                                        | `show_urls`: boolean                                                                                                                                                                                                                                                                                                                             | Owner only    |
| `invite_resend`    | `invite_id`: string, `kind`: string         | None                                                                                                                                                                                                                                                                                                                                             | Owner only    |
| `invite_revoke`    | `invite_id`: string, `kind`: string         | None                                                                                                                                                                                                                                                                                                                                             | Owner only    |
| `invite_user`      | `email`: string                             | `print_link`: boolean, `role`: string                                                                                                                                                                                                                                                                                                            | Owner only    |
| `list`             | None                                        | None                                                                                                                                                                                                                                                                                                                                             | V/A/P         |
| `logs`             | None                                        | `last`: integer, `level`: string, `since`: string, `source`: string                                                                                                                                                                                                                                                                              | Owner only    |
| `mcp_audit`        | None                                        | None                                                                                                                                                                                                                                                                                                                                             | Owner only    |
| `people_add`       | `apps`: array, `who`: string                | `ack_never`: boolean, `for`: string, `invite`: boolean, `print_links`: boolean, `qr`: boolean, `qr_invite`: string, `until`: string                                                                                                                                                                                                              | Owner only    |
| `people_grant`     | `app`: string, `for`: string, `who`: string | None                                                                                                                                                                                                                                                                                                                                             | A/P           |
| `people_list`      | None                                        | None                                                                                                                                                                                                                                                                                                                                             | V/A/P         |
| `people_remove`    | `who`: string                               | `reconcile_invites`: object                                                                                                                                                                                                                                                                                                                      | Owner only    |
| `people_revoke`    | `app`: string, `who`: string                | None                                                                                                                                                                                                                                                                                                                                             | A/P           |
| `people_update`    | `who`: string                               | `ack_never`: boolean, `apps`: array, `for`: string, `invite`: boolean, `print_links`: boolean, `qr`: boolean, `qr_invite`: string, `reconcile_invites`: object, `replace_invites`: object, `until`: string                                                                                                                                       | Owner only    |
| `portal_disable`   | None                                        | None                                                                                                                                                                                                                                                                                                                                             | Owner only    |
| `portal_enable`    | `owner`: string                             | `admins`: array, `funnel`: boolean, `hostname`: string                                                                                                                                                                                                                                                                                           | Owner only    |
| `recipe_apply`     | `recipe_id`: string                         | `allow`: string, `control_url`: string, `ephemeral`: boolean, `force_unsafe_public`: boolean, `funnel`: boolean, `funnel_ttl`: string, `health`: object, `name`: string, `no_auto_provision`: boolean, `no_daemon_install`: boolean, `preserve_host`: boolean, `public_ack`: boolean, `request_limits`: object, `tags`: string, `target`: string | Owner only    |
| `recipe_list`      | None                                        | None                                                                                                                                                                                                                                                                                                                                             | V/A/P         |
| `recipe_plan`      | `recipe_id`: string                         | `allow`: string, `control_url`: string, `ephemeral`: boolean, `force_unsafe_public`: boolean, `funnel`: boolean, `funnel_ttl`: string, `health`: object, `name`: string, `no_auto_provision`: boolean, `no_daemon_install`: boolean, `preserve_host`: boolean, `public_ack`: boolean, `request_limits`: object, `tags`: string, `target`: string | Owner only    |
| `requests_approve` | `for`: string, `id`: string                 | `ack_never`: boolean                                                                                                                                                                                                                                                                                                                             | P             |
| `requests_deny`    | `id`: string                                | `reason`: string                                                                                                                                                                                                                                                                                                                                 | P             |
| `requests_list`    | None                                        | None                                                                                                                                                                                                                                                                                                                                             | P             |
| `share`            | `target`: string                            | `allow`: array, `ephemeral`: boolean, `funnel`: boolean, `funnel_ttl`: string, `name`: string, `no_daemon_install`: boolean, `preserve_host`: boolean, `public_ack`: boolean, `request_limits`: object, `tags`: array                                                                                                                            | Owner only    |
| `status`           | None                                        | None                                                                                                                                                                                                                                                                                                                                             | V/A/P         |
| `tags_list`        | None                                        | None                                                                                                                                                                                                                                                                                                                                             | V/A/P         |
| `tags_set`         | `service`: string, `tag`: string            | None                                                                                                                                                                                                                                                                                                                                             | Owner only    |
| `template_apply`   | `name`: string                              | `no_daemon_install`: boolean                                                                                                                                                                                                                                                                                                                     | Owner only    |
| `template_list`    | None                                        | None                                                                                                                                                                                                                                                                                                                                             | V/A/P         |
| `template_plan`    | `name`: string                              | None                                                                                                                                                                                                                                                                                                                                             | Owner only    |
| `unshare`          | `name`: string                              | None                                                                                                                                                                                                                                                                                                                                             | Owner only    |
| `url`              | `name`: string                              | `wait`: string                                                                                                                                                                                                                                                                                                                                   | V/A/P         |

For `extend`, give exactly one of `for`/`until`; reduced roles must select `who`. Guest creation requires `for` and `public: true` when first enabling the gate. People permanent member grants require `ack_never: true`; new guest/public never is refused. Normal `qr` returns payload text; MCP has no `qr_png` argument. `portal_enable.funnel: true` is refused.

### `share`

Share an existing file, directory, or HTTP port. Directory targets expose browsable contents; regular-file targets expose only that file. Existing target/name matches can be reused; unrelated name collisions receive a numeric suffix.

| Field               | Type / required  | Behavior                                                                                                                            |
| ------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `target`            | string, required | Existing path, port `1..65535`, or `host:port` HTTP target                                                                          |
| `name`              | string           | Optional DNS label, at most 63 characters, `^[a-z0-9]([a-z0-9-]*[a-z0-9])?$`                                                        |
| `ephemeral`         | boolean          | Default `true`; tailnet node lifetime, not registry auto-deletion                                                                   |
| `no_daemon_install` | boolean          | Require an already-running gateway; do not install it automatically                                                                 |
| `allow`             | string array     | Login emails or `tag:` entries; Omitted means no extra allow rule; people policy still applies. Conflicts with Funnel               |
| `tags`              | string array     | Each begins with `tag:`; defaults to the configured tag in credential-backed mode                                                   |
| `funnel`            | boolean          | Default `false`; public exposure requires an HTTP port target, `public_ack: true`, and no `allow`                                   |
| `public_ack`        | boolean          | Default `false`; explicit acknowledgement of public internet exposure                                                               |
| `funnel_ttl`        | string           | Shared relative/absolute lifetime grammar; minimum 1h, default 24h, default public maximum 7d; new public never is refused.         |
| `preserve_host`     | boolean          | Proxy only; forward the node’s canonical external Host. Default false rewrites upstream Host; Origin is unchanged                   |
| `request_limits`    | object           | `max_body`, `unlimited_ack`, `header_timeout`, `read_timeout`, `idle_timeout`; upload size/inactivity limits, not request frequency |

`share` waits up to 30s for readiness or enrollment. `status` is always present: `ready` carries `url` and `name`; `needs_login` carries `auth_url`. Public shares may include `funnel_expires_at` and `funnel_rearmed`. A reused active share keeps its existing deadline; an expired Funnel can be rearmed with the requested lifetime.

### `add`

Write or replace a named service registry entry. It ensures the gateway exists by default and returns current URL/enrollment evidence without an additional URL wait; call `url` to poll.

| Field               | Type / required  | Behavior                                                                                                                            |
| ------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `name`              | string, required | DNS label, at most 63 characters; same pattern as `share.name`. Existing name is replaced                                           |
| `type`              | string, required | `proxy`, `file`, or `tcp`                                                                                                           |
| `target`            | string           | Required for proxy/TCP, rejected for file. Proxy accepts `host:port` or URL; TCP accepts `host:port`                                |
| `dir`               | string           | Absolute directory path required for file; rejected for proxy/TCP                                                                   |
| `allow`             | string array     | HTTP login emails or `tag:` entries; rejected for TCP and with Funnel                                                               |
| `tags`              | string array     | `tag:` entries; configured default in credential-backed mode                                                                        |
| `ephemeral`         | boolean          | Default `false`                                                                                                                     |
| `funnel`            | boolean          | Default `false`; requires proxy, `public_ack: true`, no `allow`, and no `control_url`                                               |
| `public_ack`        | boolean          | Default `false`; explicit public acknowledgement                                                                                    |
| `funnel_ttl`        | string           | Shared relative/absolute lifetime grammar; minimum 1h, default 24h, default public maximum 7d; new public never is refused.         |
| `no_daemon_install` | boolean          | Save configuration without installing the background service                                                                        |
| `no_auto_provision` | boolean          | Default `false`; disable automatic Funnel policy provisioning, valid only with Funnel                                               |
| `control_url`       | string           | Custom control server, such as Headscale; rejected with Funnel                                                                      |
| `preserve_host`     | boolean          | Proxy only; forward the node’s canonical external Host. Default false rewrites upstream Host; Origin is unchanged                   |
| `request_limits`    | object           | `max_body`, `unlimited_ack`, `header_timeout`, `read_timeout`, `idle_timeout`; upload size/inactivity limits, not request frequency |
| `health`            | object           | `path`, `status_min`, `status_max`, `body_contains`, `timeout`, `interval`; path/status/body are proxy only                         |
| `requestable`       | boolean          | Opt in to name discovery in private portal requests; default false, private HTTP/file only.                                         |

Read `created`, `url`, `url_pending`, `endpoint`, `exposure`, `funnel_rearmed`, and any `auth_url` / `next` / `warnings` to distinguish registration from readiness. Target validation refuses literal link-local, unspecified, and cloud-metadata IPs and `metadata.google.internal`, but does not resolve other hostnames or recheck destinations at connection time. An authorized agent can select other addresses reachable by the daemon.

### Remaining 17 service tools

An empty object `{}` is the complete input for a tool listed as having no fields.

| Tool             | Complete input fields                                                                                                                                                                                      | Result / semantics                                                                                                                                                                   |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `list`           | None                                                                                                                                                                                                       | `services`: exact or pending URL and state, plus `funnel_requested`, `funnel_active`, `funnel_state`, optional deadline/remaining/error                                              |
| `unshare`        | `name`: required DNS-label string                                                                                                                                                                          | `ok`, `name`, `removed`, `device_cleaned`, `device_cleanup_skipped`, optional `device_skip_reason` / `device_warning`                                                                |
| `status`         | None                                                                                                                                                                                                       | `authenticated` aliases `node_authorized`; separately read `credential_stored`, authorization/service counts, `daemon_running`, `supervision`, optional `status`, `auth_url`, `next` |
| `url`            | `name`: required DNS-label string; `wait`: optional Go duration, maximum `5m`, empty/nonpositive means no polling                                                                                          | Exact `name`, `url`, `state`; `url_not_ready` while pending, never a guessed hostname                                                                                                |
| `tags_list`      | None                                                                                                                                                                                                       | Registered service names and locally recorded tags                                                                                                                                   |
| `tags_set`       | `service`: required string; `tag`: required string beginning `tag:`                                                                                                                                        | Replace a service's local tags with that one tag; result `service`, `tags`                                                                                                           |
| `access_explain` | `service`: required string                                                                                                                                                                                 | Local exposure/enforcement evidence, unknown external policy, and backend-auth assumptions; not effective tailnet authorization proof                                                |
| `doctor`         | `probe_external`: optional boolean, default `false`                                                                                                                                                        | Findings, counts, supervision, runtime/credential evidence. `health_exit_code` reports CLI health severity; warnings/critical findings do not themselves make the MCP tool fail      |
| `logs`           | `source`: `err` or `out`, default `err`; `last`: integer `1..1000`, default `100`; `level`: optional `debug`, `info`, `warn`, `error` minimum; `since`: positive Go duration, default `1h`, maximum `168h` | Read-only bounded log window; see below                                                                                                                                              |
| `invite_user`    | `email`: required string; `role`: optional `member`, `admin`, `billing-admin`, `it-admin`, `network-admin`, `auditor`, default `member`; `print_link`: boolean, default `false`                            | Real tailnet invitation; confirm recipient and role. `print_link` returns a bearer URL instead of sending email                                                                      |
| `invite_device`  | `service`, `email`: required strings; `print_link`, `multi_use`, `allow_exit_node`: booleans, default `false`                                                                                              | Real invitation to share one owned service device with a person outside the tailnet                                                                                                  |
| `invite_list`    | `show_urls`: boolean, default `false`                                                                                                                                                                      | Reads open invitations; bearer URLs hidden by default. `complete: false` means some owned devices could not be checked                                                               |
| `invite_revoke`  | `kind`: required `user` or `device`; `invite_id`: required string                                                                                                                                          | Cancels a real invitation; result includes `revoked` and a remote-side-effect plan                                                                                                   |
| `invite_resend`  | `kind`: required `user` or `device`; `invite_id`: required string                                                                                                                                          | Resends real email to original recipient; not supported for a `print_link` invitation                                                                                                |
| `template_list`  | None                                                                                                                                                                                                       | Built-in templates and their service counts; installs no third-party applications                                                                                                    |
| `template_plan`  | `name`: required string                                                                                                                                                                                    | Read-only plan; `dry_run: true`, `applied: false`                                                                                                                                    |
| `template_apply` | `name`: required string; `no_daemon_install`: optional boolean                                                                                                                                             | Write missing template services; preserve existing names, install gateway if needed unless disabled. Read `applied`, `created`, `skipped`                                            |

Invitation URLs are bearer credentials. Public Funnel and invitation mutations have real external effects; an agent should confirm the exact action with its user before calling them. MCP user invitations with a role other than `member`, and device invitations permitting exit-node use, additionally require owner opt-in via `mcp.allow_elevated_invites`; otherwise the result is `mcp_elevated_invite_refused`. The `unshare` tool is also annotated destructive: it performs the same exact-ownership device and node-state deletion as `tslink remove`, and the server instructions ask clients to confirm it with the user.

### `logs` and identity boundaries

Application/access logs go to stderr (`tslink.err.log`); `out` is daemon stdout. MCP returns `source`, `file`, optional `level`, `since`, `since_at`, `lines`, `count`, `matched`, `truncated`, optional `truncated_reason` (`line_limit` or `byte_limit`), and `redacted: true`. Returned text is also capped at 256 KiB after redaction.

Credential material, recognized Tailscale login/invitation URLs, and email addresses are redacted. This narrows accidental model context; it does not guarantee removal of every secret shape. `tslink logs` CLI reads the same file verbatim, so redaction is not an access boundary for an agent with a shell. Use `access_log` / `access_summary` for the bounded retained history and receipts described in [access history](https://tslink.md/docs/access-history.md); identity may be absent and drops/crash gaps remain possible. Proxy header propagation retains its separate best-effort cache boundary.

## Worked example: report to URL

After the appropriate version negotiation, call:

```json
{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"share","arguments":{"target":"/absolute/path/report.html","name":"report"}}}
```

A successful handoff contains the same payload as JSON in `content[0].text` and as a parsed object in `structuredContent`:

```json
{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"{\"status\":\"needs_login\",\"auth_url\":\"https://login.tailscale.com/a/...\"}"}],"structuredContent":{"status":"needs_login","auth_url":"https://login.tailscale.com/a/..."}}}
```

Show `auth_url` to the user. Once authorized, call `url` with `{"name":"report","wait":"30s"}` and hand back `structuredContent.url`. Use `list` to inspect readiness without reissuing the share. When done, call `unshare` with `{"name":"report"}`.

`unshare` is idempotent: `ok: true` can accompany `removed: false` for an absent name. Local removal is separate from remote device cleanup, which requires an API client and exact ownership proof. Default zero-credential operation can remove the registry entry while leaving the remote device behind.

## Errors and cancellation

Tool/business failures return `isError: true`, human error text in `content[0].text`, and `structuredContent` containing `ok: false`, numeric CLI `code`, and `error` with a stable code/message and possible `next` actions. `needs_login` is a successful tool result. Protocol errors such as an unknown method (`-32601`) or invalid parameters (`-32602`) belong to JSON-RPC, not the CLI envelope. Session-ending inputs follow the termination rules above; do not treat them as recoverable per-line errors.

Use unique request ids and MCP cancellation notifications when abandoning a request. Closing stdin drains already-read requests; it does not immediately cancel them. Use signals to cancel the process, while continuing to drain stdout until it exits.

## Remote MCP control plane

`tslink serve --mcp` exposes the same 44 owner tools at `https://<node>.<tailnet>.ts.net/mcp` on a dedicated TLS tsnet node. Default name: `tslink-mcp`; override with `mcp.node_name`. Its state is `~/.config/tslink/mcp-node/`. It is not a registry service and cannot be published through Funnel.

### Enabling it

The control plane is off by default. Set `--mcp` or `mcp.enabled: true`; both require a nonempty owner `mcp.allow` or explicit `mcp.bindings` in `config.json`. MCP configuration is edited directly; `tslink config set` also manages supported access-log keys:

```json
{"mcp":{"enabled":true,"allow":["you@example.com","tag:ops"],"node_name":"tslink-mcp","events_keepalive":"20s"}}
```

Use a deliberate daemon restart or foreground `serve --mcp` workflow after configuration; do not start a second gateway while one is already running.

### Boundaries

* The listener is tailnet-only TLS on its own tsnet node, never a host-interface or Funnel listener. Authorized peers have high-privilege control, including public shares and real invitations.
* `mcp.allow` matches WhoIs login emails / `tag:` entries. Without valid owner entries or bindings, startup is refused. Scope identity denials use HTTP 403 with `mcp_scope_denied`; see [roles and app scopes](https://tslink.md/docs/mcp-scopes.md).
* An `Origin` header must exactly match the endpoint's own HTTPS origin; other origins receive `403` before authorization. Requests without `Origin` can proceed to authorization.
* Streamable HTTP is stateless, with a 1 MiB request-body limit. Client requests must originate on a device able to reach your tailnet; cloud-hosted connectors cannot reach a private address merely because your laptop can.
* Credential-backed control-plane nodes are ephemeral; zero-credential browser-enrolled nodes are persistent and may need explicit tailnet device removal when disabled. Crashes can leave devices pending Tailscale cleanup.

## Event stream at `/events`

The same remote node mounts owner-only server-sent events with the same Origin and caller checks. Reduced roles receive 404 and poll tools instead. It emits only named `snapshot`, `update`, and `keepalive` events, not default `message` events. Register handlers for those names; `onmessage` alone receives nothing.

Every payload includes its `type`. SSE `id:` carries instance-global `event_id`, distinct from the per-stream `sequence`; deduplicate with `event_id`. Drop cached state when `instance` changes because the daemon restarted. There is no `retry:` field or `Last-Event-ID` replay: every reconnect receives a full snapshot.

`mcp.events_keepalive` is a Go duration, defaults to `20s`, and must be between `5s` and `5m`. This event endpoint is part of the remote control plane, not the roadmap admin REST/dashboard API.

## Related

* [Command Reference](https://tslink.md/docs/commands.md)
* [Task recipes](https://tslink.md/docs/use-cases.md)
* [MCP Server Hosting](https://tslink.md/docs/mcp-hosting.md)

## People and app recipe tools

Local owner remains default; shipped [MCP roles and app scopes](https://tslink.md/docs/mcp-scopes.md) restrict operations. They do not sandbox shell/filesystem access or grant app access. Mutation receipts are bounded; see [access history](https://tslink.md/docs/access-history.md).

| Tool            | Complete input fields                                                                                                                                                                           | Semantics                                                                                                                                                                                    |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `people_add`    | `who`: required login; `apps`: required nonempty string array; `for`: duration or `never`; `invite`, `print_links`: booleans ; `until`: string; `ack_never`, `qr`: boolean; `qr_invite`: string | Save private HTTP/file grants. First grant scopes an app. Invite creation needs a user-owned API token; explicit print reveals bearer links                                                  |
| `people_list`   | None                                                                                                                                                                                            | Read people, grants, deadlines and revocation records                                                                                                                                        |
| `people_update` | `who`: required; `apps`, `for`, `invite`, `print_links`; `reconcile_invites`, `replace_invites`: app-to-ID objects ; `until`: string; `ack_never`, `qr`: boolean; `qr_invite`: string           | Supply app scope, lifetime or `invite: true`. Invite-only retries reuse completed IDs. Reconciliation needs owner verification; replacement needs invite-only and preserves grants/deadlines |
| `people_remove` | `who`: required; `reconcile_invites`: app-to-ID object                                                                                                                                          | Save denial before pending-invite cleanup. Read `complete` and `cleanup`; accepted network shares may remain                                                                                 |
| `apps_detect`   | None                                                                                                                                                                                            | Bounded loopback GET detection; `complete: false` means partial, not proof of app security                                                                                                   |
| `recipe_list`   | None                                                                                                                                                                                            | Versioned catalog with ports, app-side advice, health paths and safety levels                                                                                                                |
| `recipe_plan`   | Fields listed below                                                                                                                                                                             | Read-only preview                                                                                                                                                                            |
| `recipe_apply`  | Same fields as `recipe_plan`                                                                                                                                                                    | Apply the reviewed plan. Existing names remain unchanged; does not install or configure the backend                                                                                          |

Both recipe tools accept these fields:

| Field                                       | Type / behavior                                                                                                             |
| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `recipe_id`                                 | Required string from `recipe_list`                                                                                          |
| `name`, `target`                            | Optional strings; name override and loopback HTTP(S) host-port override                                                     |
| `allow`, `tags`                             | Optional comma-separated strings, unlike the service tools' arrays                                                          |
| `health`, `request_limits`                  | Objects with the same fields as `add` above                                                                                 |
| `preserve_host`                             | Boolean override; omission uses the recipe's Host default                                                                   |
| `ephemeral`, `no_daemon_install`            | Optional booleans                                                                                                           |
| `funnel`, `public_ack`, `no_auto_provision` | Optional booleans; public exposure requires `funnel: true`, `public_ack: true`, and suitable app authentication             |
| `funnel_ttl`                                | Shared relative/absolute lifetime grammar; minimum 1h, default 24h, default public maximum 7d; new public never is refused. |
| `control_url`                               | Optional string; incompatible with Funnel                                                                                   |
| `force_unsafe_public`                       | Explicit dangerous boolean override of a never-public recipe; does not establish app authentication                         |

Call `recipe_plan` before `recipe_apply` with the same options. People mutations ask for confirmation of person, apps and deadline because they can narrow existing exposure. They cannot scope TCP or Funnel. Revocation blocks new requests, not already accepted streams. See [people sharing](https://tslink.md/docs/people-sharing.md).

Example tool arguments for a seven-day grant:

```json
{"who":"alice@example.com","apps":["preview"],"for":"7d"}
```

Use this with `people_add` after registering and enrolling `preview`. Read `person`, `complete`, `invites`, `message` and `invite_requirement`; a successful command can still have incomplete invitations.

Sources: [TSLink MCP service registry](https://github.com/anydoor7/tslink/blob/v0.1.1/cmd/mcp.go), [people tools](https://github.com/anydoor7/tslink/blob/v0.1.1/cmd/mcp_people.go), [recipe tools](https://github.com/anydoor7/tslink/blob/v0.1.1/cmd/apps_mcp.go).

## New access tools in practice

`health` reads backend observations; `access_log`/`access_summary` query retained scoped history. `people_grant`/`people_revoke` change one pre-existing person’s app grant. `extend` sets now + duration, with explicit regrant for expired state. `app_restart` queues a gateway node restart, not a backend restart or completed result: poll status/health. Reduced operators cannot restart an existing public Funnel app.

`guest_create/list/show/revoke` remain owner-only and support one HTTP proxy app; link/PIN can be forwarded and never proves a person’s identity. `portal_enable/disable` manages a private per-host node; remote enable requires the current portal owner, and first bootstrap/recovery is local. `requests_list/approve/deny` is owner/people-manager only; remote callers also need the current human portal-owner identity, not merely an admin designation. Reduced roles cannot create unknown people or modify protected portal-owner/admin records.

`mcp_audit` is owner-only. A persisted intent precedes mutation; missing completion means unknown outcome. Access history and receipts have independent bounds and possible gaps; they are not complete-audit or compliance guarantees. See [guest links](https://tslink.md/docs/guest-links.md), [portal/requests](https://tslink.md/docs/portal-requests.md), [roles](https://tslink.md/docs/mcp-scopes.md), [durations](https://tslink.md/docs/durations.md) and [access history](https://tslink.md/docs/access-history.md).
