Kwisple
Browse the docs

Integrations

MCP connectors and OAuth

Connect Claude or any MCP client to the platform with a personal token or OAuth 2.1, what every MCP tool may do, and why an AI connection only ever proposes.

The dashboard runs an MCP server. People connect their own AI assistant to it (Claude Code, Claude Desktop, claude.ai, or any client that speaks the Model Context Protocol) and work in their clients' accounts from there: read the brand, see what is waiting, draft posts, propose text changes. The model runs on the person's side; the platform holds no model API keys.

Three rules shape everything on this page:

  1. The connection acts with the person's rights, never more. Access is resolved exactly as the dashboard resolves it, and every tool checks the same permissions.
  2. It only touches platform data. An MCP connection reads and proposes content that lives in the platform: brand, posts, pages, media, requests. It has no access to any site's source code, repository, server or deployment. Changing a site's code happens with your own tools in the site repository, not through this connection.
  3. AI assists, never leads. Writes are proposals or offline drafts. Nothing is published, scheduled or approved through MCP, and the brand kit is read-only.

Two ways to connect

Personal tokenOAuth
ForClaude Code, Claude Desktop, scripts, any client that can send a headerclaude.ai custom connectors and any client that implements MCP authorization
You doCreate a key under Koppelingen, paste it into the clientPaste the MCP address into the client; approve in the browser
Tokenkwiagent_…, shown oncekwiagent_…, handed to the client by the token endpoint

Both end in the same kind of token, listed in the same place and revoked the same way.

Personal tokens: Koppelingen

Tokens are personal and live on the page Koppelingen (connections):

  • inside a client: Instellingen → Koppelingen beheren (/w/<client>/instellingen/koppelingen);
  • in the agency environment: Instellingen → Koppelingen (/bureau/<agency>/instellingen/koppelingen).

Everyone with an account has this page, agency team and clients alike.

Creating one

  1. Click Nieuwe koppeling (new connection). The sheet Assistent koppelen opens.
  2. Give a Naam (name), for example Claude op mijn laptop.
  3. Optionally tick Enkel voor bepaalde klanten (only for certain clients) and pick them. Nothing picked means: every client you can open.
  4. Click Koppeling maken (create connection).
  5. Copy the Sleutel (key) now; it is shown once. The sheet also shows the ready-made command for Claude Code and the configuration for Claude Desktop with the key filled in.

The token has the form kwiagent_ followed by 43 base64url characters (32 random bytes). The platform stores only its SHA-256 hash and a short prefix for display; tokens are looked up by hash and compared in constant time. They have no expiry. A person can have at most 20 active agent tokens; the 21st is refused with Je hebt al 20 koppelingen. Trek er eerst een in.

Revoking and removing

Each connection shows when it was last used (Laatst gebruikt … / Nog niet gebruikt) and, if restricted, for which clients (Enkel …).

  • Intrekken (revoke) stops it on the very next request: there is no cache.
  • Revoked connections move under Ingetrokken (n). Verwijderen removes one from that list; revoked connections also disappear by themselves after 30 days.

The same page lists, under Terminal, the CLI logins made with kwisple login. Those are a different kind of token (kwicli_…, see the CLI reference): the MCP endpoint never accepts a CLI token and the CLI endpoints never accept an agent token.

Using the token

Claude Code:

claude mcp add --transport http mijn-bureau https://<dashboard>/api/mcp \
  --header "Authorization: Bearer kwiagent_…"

Claude Desktop (claude_desktop_config.json, through the mcp-remote bridge, then restart Claude Desktop):

{
  "mcpServers": {
    "mijn-bureau": {
      "command": "npx",
      "args": ["-y", "mcp-remote", "https://<dashboard>/api/mcp", "--header", "Authorization:${AUTH_HEADER}"],
      "env": { "AUTH_HEADER": "Bearer kwiagent_…" }
    }
  }
}

The connection name in the snippets the dashboard shows is derived from the agency's name. Any other client: Streamable HTTP, POST <dashboard>/api/mcp, header Authorization: Bearer kwiagent_….

The MCP endpoint

PropertyValue
AddressPOST <dashboard>/api/mcp
TransportMCP Streamable HTTP, stateless: one server per request, no session id, JSON responses
Other methodsGET and DELETE answer 405 (allow: POST)
AuthenticationAuthorization: Bearer kwiagent_… only; cookies are never read, so CSRF does not apply. No CORS headers.
Body limit256 kB, checked on Content-Length and while reading → 413
Rate limit120 requests per minute per token → 429 with Retry-After
Missing or bad token401 with WWW-Authenticate: Bearer realm="mcp", error="invalid_token", resource_metadata="<dashboard>/.well-known/oauth-protected-resource"

Errors outside a tool call are JSON-RPC errors ({"jsonrpc":"2.0","error":{…},"id":null}).

The server introduces itself with the agency's (white-label) name as its title, never the platform's, and sends the platform rules for agents as its instructions. The same rules are available as the resource guidelines://platform and through the get_guidelines tool.

OAuth 2.1 for connectors

claude.ai and other clients that implement MCP authorization sign the person in with OAuth instead of a pasted key. The dashboard runs a deliberately small authorization server of its own. The access token it issues is a personal agent token, so the endpoint, the limits and Koppelingen work the same.

To connect claude.ai: in claude.ai go to Instellingen → Connectors → Aangepaste connector toevoegen (custom connector), paste the address shown on Koppelingen next to claude.ai (it ends in /api/mcp), and approve in the dashboard.

Discovery

EndpointWhat
GET /.well-known/oauth-protected-resource (also /.well-known/oauth-protected-resource/api/mcp)RFC 9728: resource = <dashboard>/api/mcp, authorization_servers = the dashboard, bearer_methods_supported: ["header"], scopes_supported: ["mcp"].
GET /.well-known/oauth-authorization-serverRFC 8414 metadata, below.

Both answer with access-control-allow-origin: * and may be cached for five minutes.

{
  "issuer": "https://<dashboard>",
  "authorization_endpoint": "https://<dashboard>/oauth/authorize",
  "token_endpoint": "https://<dashboard>/api/oauth/token",
  "registration_endpoint": "https://<dashboard>/api/oauth/register",
  "response_types_supported": ["code"],
  "grant_types_supported": ["authorization_code"],
  "code_challenge_methods_supported": ["S256"],
  "token_endpoint_auth_methods_supported": ["none"],
  "scopes_supported": ["mcp"],
  "authorization_response_iss_parameter_supported": true
}

Dynamic client registration

POST /api/oauth/register (RFC 7591) with client_name and redirect_uris:

  • Public clients only: token_endpoint_auth_method must be absent or none; there are no client secrets.
  • 1 to 5 redirect URIs, each https, or http on localhost, 127.0.0.1 or [::1] (local tools listen on a port), at most 500 characters, no fragment, no credentials.
  • The name is cleaned of control, bidirectional and zero-width characters and cut to 60 characters.
  • Registering the same name with the same addresses returns the existing client.
  • Answer 201 with client_id, client_name, redirect_uris, token_endpoint_auth_method: "none", grant_types: ["authorization_code"], response_types: ["code"].
  • At most 300 registrations per hour per address. Clients that never complete a sign-in are removed after 30 days.

Registration grants nothing by itself: every authorization still needs a signed-in person's approval.

Authorization: /oauth/authorize

Authorization code with PKCE S256 only. Query parameters:

ParameterRule
client_idA registered client.
redirect_uriExactly one of the client's registered URIs.
response_typecode.
code_challenge43 base64url characters (an S256 challenge).
code_challenge_methodS256.
stateOptional, at most 500 characters, returned unchanged.

No parameter may appear twice. An unknown client or unregistered redirect never sends the browser anywhere: the page shows Deze link werkt niet (this link does not work). Any other protocol error goes back to the client's redirect URI with error (invalid_request, unsupported_response_type), iss (RFC 9207) and state.

The page requires login (the query survives the login page) and shows the agency's identity.

  • The headline names the app and the host the code goes to: Claude op claude.ai wil met je account werken, or … op deze computer wil met je account werken for a localhost callback.
  • Bekende app (known app) appears only when the redirect URI belongs to a connector the platform recognises (claude.ai's and claude.com's MCP auth callbacks). Any other app chose its own name, and the page says so with a warning that names the host to expect.
  • Below: App, Stuurt je terug naar (sends you back to: the host) and Aangemeld als (signed in as).
  • Voor welke klanten (for which clients), when the person can open more than one: Alles waar ik toegang toe heb or Alleen . The choice restricts the token like Enkel voor bepaalde klanten does.
  • Toegang geven (give access) or Weigeren (refuse). Refusing returns error=access_denied to the client.

The request is read again from the query on approval, never trusted from the browser, and the chosen client must be one the person can open. With 20 active agent tokens already, approval is refused with Je hebt het maximum aantal koppelingen bereikt. Trek er eerst een in onder Instellingen → Koppelingen.

Token endpoint: POST /api/oauth/token

Form-encoded or JSON body: grant_type=authorization_code, code, redirect_uri, client_id, code_verifier (43–128 unreserved characters).

{ "access_token": "kwiagent_…", "token_type": "Bearer", "scope": "mcp" }
  • Codes live 10 minutes, are stored hashed and are single-use: the code is claimed atomically before it is checked, so a replay finds nothing and a wrong verifier burns it.
  • Errors: unsupported_grant_type, invalid_request, invalid_grant (400), invalid_client (401, the code belongs to another client). When the person reached the limit of 20 tokens meanwhile: invalid_request with an error_description.
  • Rate limit: 120 requests per 10 minutes per client_id (429, slow_down).
  • No refresh tokens, no expiry. The token appears under Koppelingen named after the app and the host it went to, for example Claude (claude.ai). A revoked connector simply signs in again.

What every tool may do

Every tool opens the client with agentWorkspace(): the person's access, resolved exactly as the dashboard does, plus the token's client restriction. Bekijk als klant never applies. An unknown client and a forbidden one give the same error, so client slugs cannot be probed. Then the tool checks the permission the dashboard would check (can()), and fails with Not allowed: … otherwise.

tools/list only lists tools the person can use in at least one client the token may open; the others are registered disabled. Each tool still checks the permission of the client it is called for. Results are compact JSON text, cut at 60,000 characters.

Reading

ToolNeedsReturns
get_guidelines—The platform rules for agents.
list_workspaces—The clients the token may open: slug, name, side (agency team or client), role and permissions.
get_brandbrand.viewVoice (tagline, mission, tone words, je/u, do's and don'ts, examples, languages), colours, fonts, imagery.
waiting_for_you—The same Wacht op jou list the dashboard shows.
list_posts, get_postsocial.viewThe social calendar; one post with caption, brief, visual text and per platform its format, caption length against the limit and time.
list_requestsrequest.viewRequests between client and agency: title, description, link, status, whose turn.
list_siteswebsite.viewSites with mode, address, languages and sections (external sites: no content).
list_entries, get_entrywebsite.viewEntries of a section; one entry's field definitions and published values.
list_mediamedia.viewThe media library, with missingAlt to find images without a description.
list_campaignssocial.viewCampaigns, or one campaign with its pages and posts.

Proposing

Stored as a labelled proposal, Voorstel via assistent, that a person accepts or ignores in the dashboard. Accepting needs the same right as proposing.

ToolNeedsDoes
propose_postpost.createA new post as an undated idea for one or more platforms, marked as suggested by the assistant. A suggested date only goes into the brief.
withdraw_postpost.createMoves your own untouched idea to the trash; once a person edited it, it stays.
propose_captionpost.edit1–5 caption variants for an unpublished post.
propose_entry_changepage.editContentNew field values (and title) for one language of a page, validated against the content model. Never a draft or revision; a person applies it to their draft and publishes.
propose_alt_textmedia.uploadAlt text for up to 50 images; existing alt text is not changed.

Preparing

ToolNeedsDoes
create_campaignsocial.campaignsA campaign: an internal planning frame.
create_pagepage.create (+ post.edit with a campaign)A new page as an offline draft, fields validated, optionally linked to a campaign. Refuses sections that hold one fixed page.

Asking (dashboard chat only)

request_publish_page (page.editContent, plus page.create for a later date) and request_post_step (post.edit to send for approval, post.schedule to schedule) exist only for the short-lived tokens of the dashboard's own assistant chat. They store a request and show the person a confirmation card; only the person's click acts, through the normal actions with their own permission checks. A personal token or an OAuth connection never sees these tools.

What no tool does

Publish content, schedule or approve posts, change a schedule, edit the brand kit, change settings or access, read form submissions, or reach anything outside the platform: no site repository, no source code, no hosting, no deployments. If a tool answers Not allowed, the person lacks the permission; the agent is told to ask the agency rather than look for a workaround.

Security summary

  • Same access resolution and the same can() rules as the dashboard; a client restriction on the token is enforced on every call.
  • Only the token's hash is stored; revocation is immediate.
  • Bearer tokens only on /api/mcp; no cookies, no CORS.
  • Limits: 256 kB per request, 120 requests per minute per token.
  • OAuth: public clients with PKCE S256, exact redirect URI match, single-use 10-minute codes, a consent screen that names the destination host.

Last updated 2026-10-30