Skip to navigation

MCP servers

Each Taktoria app has a Model Context Protocol 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, 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:

AppServer
LaunchCalhttps://www.taktoria.com/mcp/acme/launchcal
OKR Hubhttps://www.taktoria.com/mcp/acme/okrhub
Doryhttps://www.taktoria.com/mcp/acme/dory
Go Linkshttps://www.taktoria.com/mcp/acme/golinks
Memegenhttps://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.

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:

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

Cursor then shows the server as needing sign-in.

Claude Code

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

ScopeGrants
<app>:readThe app’s read tools: everything the person can see in it
<app>:writeIts write tools as well, within what the person may change
offline_accessA 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. 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).

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.

Skills

Each server also has skills: short guides, in the Agent Skills 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:

Statusdata.codeMeaning
401unauthorizedNo token: connect the server
401invalid_tokenExpired, revoked, for another server, or the person has left the workspace: connect again
402subscription_requiredThe workspace has no active subscription to the app
403seat_requiredThe person holds no seat on the app
403mcp_disabledA workspace admin has turned off Allow MCP connections
403google_sign_in_requiredThe workspace requires Google, and the connection was not made signed in with Google
403insufficient_scopeThe connection was not granted this app
429rate_limitedA 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).

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:

{
"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:

EndpointScopeDoes
GET /mcp-connectionsmcpConnections:readLists connections: the person, the client, its scopes, when it was used
DELETE /mcp-connections/{id}mcpConnections:writeRevokes one, and every token it holds, at once
GET /mcp-connections/settingsmcpConnections:readWhether the workspace allows MCP connections
PATCH /mcp-connections/settingsmcpConnections:writeTurns 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 and DELETE /slack-connections/{id} with the slackConnections scopes, which no MCP scope stands for either.