> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.taktoria.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.taktoria.com/_mcp/server.

# MCP servers

> Connect Claude, ChatGPT, Cursor or another AI assistant to a Taktoria app through its MCP server: addresses, OAuth, scopes, tools, skills and errors.

Each Taktoria app has a [Model Context Protocol](https://modelcontextprotocol.io)
server in every workspace, so an AI assistant such as Claude, ChatGPT or
Cursor can work in it. An assistant connects with OAuth, as a person: it gets
their seats, sees what they can see and changes only what they could change,
in the one workspace whose server address it was given. Every change it makes
is in the workspace's audit log under their name, with the assistant named
beside it.

This is different from an [API key](/getting-started/authentication), which
speaks for the whole workspace. Use a key for automation; use MCP for an
assistant working on somebody's behalf.

## Server URLs

Every workspace has its own server for each app, with the workspace's slug
(the `acme` in `https://www.taktoria.com/app/acme`) in the address:

```
https://www.taktoria.com/mcp/<workspace>/<app>
```

For the `acme` workspace:

| App       | Server                                        |
| --------- | --------------------------------------------- |
| LaunchCal | `https://www.taktoria.com/mcp/acme/launchcal` |
| OKR Hub   | `https://www.taktoria.com/mcp/acme/okrhub`    |
| Dory      | `https://www.taktoria.com/mcp/acme/dory`      |
| Go Links  | `https://www.taktoria.com/mcp/acme/golinks`   |
| Memegen   | `https://www.taktoria.com/mcp/acme/memegen`   |

People find their workspaces' addresses, with a button to copy each, under
**Account → Connected apps** in Taktoria, and workspace admins under **Settings → MCP
connections**. Because the address names the workspace, the sign-in step knows
it before anybody signs in: see [Authorization](#authorization).

Each is a Streamable HTTP server, stateless: every request is a `POST`
carrying a bearer token. It speaks MCP **2026-07-28** to a client that asks for
it, and the 2025 revisions (**2025-11-25** and **2025-06-18**) to any other.
They answer on Taktoria's own domain only. A workspace whose app has a domain
of its own (`dory.acme.com`, say) still connects assistants at
`https://www.taktoria.com/mcp/<workspace>/<app>`: the app's domain serves its
pages, not its MCP server. The addresses tools return do use it: a page of an
app with a domain of its own, or a Dory join link, is on that domain, and
anything else (another app, settings) on Taktoria's.

## Connecting

### Claude

In Claude's settings, under connectors, add a custom connector with the app's
server URL for your workspace, then connect it. Claude opens Taktoria to sign
in and choose what it may do. Add one connector per app. On Team and
Enterprise plans an owner of the Claude organization may have to add it first.

### ChatGPT

In ChatGPT's settings, add a custom connector (developer mode may need to be
on), paste the server URL, choose OAuth, and create it.

### Cursor

In `~/.cursor/mcp.json`, or `.cursor/mcp.json` in a project:

```json
{
  "mcpServers": {
    "taktoria-launchcal": { "url": "https://www.taktoria.com/mcp/acme/launchcal" }
  }
}
```

Cursor then shows the server as needing sign-in.

### Claude Code

```bash
claude mcp add --transport http taktoria-launchcal https://www.taktoria.com/mcp/acme/launchcal
```

Then run `/mcp` in Claude Code and authenticate the server.

## Authorization

Clients discover everything themselves, as the MCP specification describes:

1. A request without a token gets `401` and
   `WWW-Authenticate: Bearer resource_metadata="https://www.taktoria.com/.well-known/oauth-protected-resource/mcp/<workspace>/<app>"`.
2. That protected resource metadata (RFC 9728) gives the server's address as
   its `resource` and names the authorization server,
   `https://www.taktoria.com/api/auth`, whose metadata (RFC 8414) is at
   `https://www.taktoria.com/.well-known/oauth-authorization-server/api/auth`.
3. The client identifies itself in one of two ways. On protocol 2026-07-28 it
   may use a Client ID Metadata Document: its `client_id` is the HTTPS
   address of a JSON document describing it, which Taktoria fetches and
   checks (the authorization server's metadata says
   `client_id_metadata_document_supported: true`). Such a client is shown on
   the consent page and in the connection lists by that document's host. Or
   it registers itself (RFC 7591, `POST /api/auth/oauth2/register`), as
   Claude, ChatGPT and Cursor still do. It then sends the person through the
   authorization code flow with PKCE
   (`S256` only), naming the server as its `resource` (RFC 8707). A request
   that names no server, or servers of two workspaces, cannot be granted.
4. The person signs in the way the server's workspace signs people in: with
   Google straight away if it requires Google, through single sign-on if it
   has it, and otherwise on the workspace's own sign-in page. Somebody who is
   already signed in but is not a member is told so, and can sign in as
   somebody else.
5. On the consent page, which shows the workspace (there is nothing to pick),
   they choose **No access**, **Read only** or **Read and write** for the app.
   Somebody who is a Viewer of the app is offered only **Read only**.
   They can narrow what was asked for, never widen it.
6. The client exchanges the code at `POST /api/auth/oauth2/token` for an
   access token (an hour) and, if it asked for `offline_access`, a refresh
   token (30 days, rotated on every use). Tokens are revoked at
   `POST /api/auth/oauth2/revoke`.

A token is bound to the server it was issued for, in its workspace: one for
`/mcp/acme/launchcal` is refused at `/mcp/acme/okrhub` and at
`/mcp/globex/launchcal`. If a workspace's slug changes, so do its addresses,
and assistants connect again at the new ones.

## Scopes

| Scope            | Grants                                                         |
| ---------------- | -------------------------------------------------------------- |
| `<app>:read`     | The app's read tools: everything the person can see in it      |
| `<app>:write`    | Its write tools as well, within what the person may change     |
| `offline_access` | A refresh token, so the connection lasts beyond the first hour |

`<app>` is `launchcal`, `okrhub`, `dory`, `golinks` or `memegen`. A server
lists its write tools only to a token with `<app>:write`, and refuses them to
any other. A Viewer of the app gets a read-only connection whatever it was
granted: its scopes are cut to `<app>:read`, so no write tool or skill is
listed, and one called anyway is refused (`viewer_only`). Nothing is stored on
the token, so a Viewer made a Member writes again with the same connection.
See [App roles](/getting-started/authentication#app-roles). Each tool carries MCP's `readOnlyHint`, `destructiveHint` and
`idempotentHint` annotations.

Underneath, `<app>:read` stands for every read scope of that app's API
resources, plus `members:read` and `teams:read` (who is in the workspace, and
its teams, which every app's pickers show), and `<app>:write` adds the app's
own write scopes: `launchcal:write`, for
instance, is `launches:write`, `steps:write`, `insights:write`,
`artifacts:write` and `templates:write`. They are a ceiling on top of the
person's own permissions, never a grant of more. No MCP scope stands for
`mcpConnections`: a connection can never see or revoke connections, nor turn
them off (see [Managing connections](#managing-connections)).

## Tools

Every server has:

* `whoami`: the person, their role, the workspace, and this connection's
  access to the app.
* `search`: everything in the app the person can see that matches some words,
  grouped by kind with totals, each match with the address of its page.
* `find_people`: members of the workspace whose name or email address
  matches some words, A to Z, with each one's user id, email address, picture,
  role, the teams they are on (directly, or through a team inside one), and
  `hasSeat`, whether they hold a seat on this server's app. On LaunchCal's
  server, also the functional teams each is on. Paged with `cursor`, up to 50
  at a time (`limit`, 20 unless asked).
* `find_teams`: the workspace's organizational teams whose name matches, A to
  Z, with each one's id, slug, how many people are on it (through the teams
  inside it too, and directly), and the teams it sits in and holds. On
  LaunchCal's server, the first page also has the matching functional teams.
  Paged as `find_people` is.

Tools that name people or teams (a go link's editors, a Dory event's hosts, a
launch's owners, stakeholders and reviewers, an objective's owner) take ids;
`find_people` and `find_teams` are how an assistant gets them from a name.
Both find only current members and the workspace's own teams, the same people
and teams the app's own pickers offer. Words match as `search`'s do: every
word somewhere in the name (or the email address), case and accents ignored.
Each app's read scope is enough to use them, since every member sees these
in the app; underneath, `<app>:read` carries the API's `members:read` and
`teams:read`.

More tools arrive app by app.

* Go Links: [every Go Links tool](/ai-assistants/go-links-tools).
* LaunchCal: [every LaunchCal tool](/ai-assistants/launchcal-tools).
* OKR Hub: [every OKR Hub tool](/ai-assistants/okr-hub-tools).
* Dory: [every Dory tool](/ai-assistants/dory-tools).
* Memegen: [every Memegen tool](/ai-assistants/memegen-tools).

## Skills

Each server also has skills: short guides, in the
[Agent Skills](https://agentskills.io) `SKILL.md` format, that teach an
assistant how to do something in the app with its tools. Every server starts
with `find-anything`, on searching it well. A skill that changes things is
offered only to a connection allowed to write in that app.

A skill's address is `skill://<app>/<name>/SKILL.md`, and any files it refers
to sit beside it, such as
`skill://okrhub/draft-quarterly-okrs/references/writing-good-key-results.md`.
Clients reach skills in whichever way they support:

* **The Skills extension** (SEP-2640, `io.modelcontextprotocol/skills`), on
  protocol 2026-07-28. The server declares it in its capabilities and answers
  `skills/list` and `skills/get`. Each skill comes with a manifest listing
  every file's URI, `sha256:` digest and size, and the files are read with
  `resources/read`. The extension is new; check your client's documentation
  for whether it uses it.
* **Resources.** Every `SKILL.md` is listed in `resources/list` and read with
  `resources/read`, for clients that let the model read a server's resources.
* **Prompts.** Every skill is also a prompt of the same name, with its
  arguments. Some clients, such as Claude Code, offer a server's prompts as
  slash commands.
* **Instructions.** The server's `instructions` name the skills a connection
  may use, with their addresses, and ask the model to read the one that fits
  before multi-step work.

## Errors

A problem with the connection itself is answered before MCP begins, with an
HTTP status and a JSON-RPC error whose `data.code` says which:

| Status | `data.code`               | Meaning                                                                                   |
| ------ | ------------------------- | ----------------------------------------------------------------------------------------- |
| 401    | `unauthorized`            | No token: connect the server                                                              |
| 401    | `invalid_token`           | Expired, revoked, for another server, or the person has left the workspace: connect again |
| 402    | `subscription_required`   | The workspace has no active subscription to the app                                       |
| 403    | `seat_required`           | The person holds no seat on the app                                                       |
| 403    | `mcp_disabled`            | A workspace admin has turned off **Allow MCP connections**                                |
| 403    | `google_sign_in_required` | The workspace requires Google, and the connection was not made signed in with Google      |
| 403    | `insufficient_scope`      | The connection was not granted this app                                                   |
| 429    | `rate_limited`            | A rate limit is used up: wait `Retry-After` seconds and send it again                     |

### Rate limits

Each connection (one person, one client, one workspace) may make 600 reads
and 120 writes a minute, and every connection and API key in a workspace
together 3,000 and 600. A tool call that changes something is a write;
listing, reading and skills are reads. A JSON-RPC batch counts each call in
it. `search`, `export_questions`, `get_meme_image`, `preview_meme`,
`search_template_images`, `bulk_score`, `bulk_create_objectives`,
`link_ticketing_project`, `sync_ticketing_project`, `export_artifact` and
`list_docs_destinations` also have a budget of their own, shared with the
REST API (see [Rate limits](/getting-started/errors-and-pagination#rate-limits)).

Every answer carries the IETF `RateLimit-Policy` and `RateLimit` headers.
Over a limit, the request is refused before any of it runs, with HTTP `429`,
`Retry-After` in seconds, and a JSON-RPC error:

```json
{
  "jsonrpc": "2.0",
  "error": {
    "code": -32001,
    "message": "Too many requests: the mcp-read limit is 600 per minute. Try again in 7 seconds.",
    "data": { "code": "rate_limited", "policy": "mcp-read", "retryAfter": 7 }
  },
  "id": null
}
```

Nothing in a refused request was done, so it is safe to send again once
`Retry-After` has passed. Requests sent while refused are counted too.

Inside a working connection, a tool that cannot do what was asked returns a
tool error (`isError: true`) with a plain English explanation, such as a
write tool called on a read-only connection or by a Viewer of the app
(`viewer_only`), or something the person may not see or change.

Every request is checked afresh, so revoking a connection (the person, in
Account → Connected apps, or a workspace admin, in Settings → MCP connections),
removing the person, or turning MCP connections off takes effect at once.

## Managing connections

What Settings → MCP connections and Account → Connected apps do is in the
REST API too, with an API key holding the `mcpConnections` scopes, or a
signed-in session:

| Endpoint                                                                            | Scope                  | Does                                                                    |
| ----------------------------------------------------------------------------------- | ---------------------- | ----------------------------------------------------------------------- |
| [`GET /mcp-connections`](/api-reference/mcp-connections/list)                       | `mcpConnections:read`  | Lists connections: the person, the client, its scopes, when it was used |
| [`DELETE /mcp-connections/{id}`](/api-reference/mcp-connections/revoke)             | `mcpConnections:write` | Revokes one, and every token it holds, at once                          |
| [`GET /mcp-connections/settings`](/api-reference/mcp-connections/get-settings)      | `mcpConnections:read`  | Whether the workspace allows MCP connections                            |
| [`PATCH /mcp-connections/settings`](/api-reference/mcp-connections/update-settings) | `mcpConnections:write` | Turns **Allow MCP connections** on or off                               |

A key, like a workspace admin, sees and revokes every connection in the workspace and
turns the switch. Anybody else's session sees and revokes only their own, and
is refused the switch (`403 admin_required`). No token is ever returned, and
revoking and the switch are in the audit log as they are from the settings
pages (`mcp_connection.deleted`, `organization.updated`).

These are the platform's own scopes, never an app's: no MCP scope stands for
them, so an assistant connected through MCP cannot list, revoke or turn off
connections, its own included.

Slack's commands follow the same rule as MCP where a workspace requires
Google: a person's Slack account acts for them only once they have connected
it signed in with Google, unless the workspace's admins allow Slack commands
without it. Those connections are managed the same way, at
[`GET /slack-connections`](/api-reference/slack-connections/list) and
[`DELETE /slack-connections/{id}`](/api-reference/slack-connections/disconnect)
with the `slackConnections` scopes, which no MCP scope stands for either.