Skip to main content

AI Agents

Connect an AI agent to Typograph so it can find templates, generate press-ready PDFs and images, convert files, and manage projects and teams. It works for one signed-in user, with that user's rights: it can do what the user can do, and no more.

The Typograph MCP Server implements the Model Context Protocol — the open standard adopted by Anthropic, OpenAI, and Google for connecting AI to external tools and data.

Production URL: https://mcp.typograph.nl

What AI Agents Can Do​

With more than 60 tools available, your AI agents can:

  • Manage content - Create and organize teams, projects, and templates, or start a template from the gallery
  • Generate documents - Create PDFs and images from templates
  • Convert files - Transform between file formats
  • Collaborate - Share resources, manage permissions, invite users, ask for access and answer requests for it
  • Run the organization - List organizations and their members, change an organization's settings, invite people in, change a member's role or remove a member, and hand an organization to another member
  • Search - Find projects, templates, and resources
  • Manage webhooks - Set up, change and check the webhook subscriptions of your organization
  • Look after apps - Read an organization's apps and their scopes, and give an app its consent-screen logo
  • Look up the documentation - Search and read these docs, so an agent finds how Typograph works instead of guessing

Documentation for agents​

An agent can read this documentation in a form made for it, with or without a connection:

  • /llms.txt lists every page with its one-line summary (llms.txt).
  • /llms-full.txt holds the full text of every page.
  • /docs-index.json holds the same text cut into one entry per section: {revision, generated_at, site_url, sections: [{title, url, section, audience, headings, text, agent_tools}]}.
  • Through the MCP server, docs_search and docs_get search and read the same sections (see Documentation below).

All three files are made from the pages when the site is built, so they never differ from what you read here, and they hold only these public pages. A page names the tools that do what it explains, as agent_tools: the Invites page, for example, names organization_invitation_create and invitation_create.

What an agent cannot do yet​

  • Edit a template's content. No tool changes the elements of a template yet; template_update renames it. An agent generates output from a template as the designer saved it, or in batches from CSV data through the Publisher.
  • Create or change an app, or see its secret. Creating an app, changing its name, scopes or redirect URIs, deleting it and regenerating its secret stay in the portal: a new app would get your plan's scopes rather than the agent's, and a secret shown to an agent ends up with its model provider.
  • Be held to a template's roles on the server. The template's roles and limits are applied by the Viewer in the browser. Enforcing them on every API and MCP call is planned. Until then, connect agents with only the access they need.

Quick Start​

Typograph runs the MCP server as a hosted HTTP service at https://mcp.typograph.nl — there is nothing to install locally.

Let your agent set itself up​

Paste this into Claude, ChatGPT, Cursor or any other agent:

Fetch and follow the instructions at https://docs.typograph.nl/agent-setup/prompt.md to connect me to Typograph.

The agent reads the setup instructions, adds the connection for its own app (or walks you through the clicks when it cannot), and checks that the sign-in worked. The portal dashboard offers the same prompt under Connect your AI assistant.

Claude Desktop (and other MCP clients)​

Point your MCP client's mcpServers config at the hosted URL:

{
"mcpServers": {
"typograph": {
"url": "https://mcp.typograph.nl/mcp"
}
}
}

Your client signs you in on the first tool call: the server answers 401 Unauthorized and points the client to the Typograph sign-in (see Authentication). The client keeps the token and sends it with every request.

Direct HTTP (JSON-RPC)​

POST https://mcp.typograph.nl/mcp
Content-Type: application/json
Authorization: Bearer <access token>

{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "team_list",
"arguments": {}
}
}

Authentication​

The MCP server holds no credentials and keeps no sessions. Your MCP client signs in at the Typograph API with OAuth 2.0 (authorization code with PKCE) and sends the access token as Authorization: Bearer <token> on every request. The API checks the token on every tool call.

How Authentication Works​

  1. Discovery - initialize and tools/list work without a token, so the client can see what the server offers.
  2. First tool call - Without a token, the server answers 401 Unauthorized with a WWW-Authenticate header that names the protected resource metadata. The client reads it, finds the Typograph authorization server and opens the sign-in in a browser.
  3. Every call after - The client sends the access token, and refreshes it with the refresh token (offline_access) when it expires.
  4. A refused token - When the token is invalid, expired or revoked, the server answers 401 again, with error="invalid_token", so the client signs in again.

Signing out​

Sign out in your MCP client: disconnect or remove Typograph there. There is no tool for it. Revoking a token is the act of the client it was issued to (RFC 7009), and the MCP server is not that client: it only passes your token on. An agent also cannot sign you out by accident, or because a document told it to.

The 401 response​

HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error="invalid_token", resource_metadata="https://mcp.typograph.nl/.well-known/oauth-protected-resource"
Content-Type: application/json

{
"jsonrpc": "2.0",
"error": {
"code": -32001,
"message": "The access token was refused. Sign in again.",
"data": {
"type": "auth_required",
"resource_metadata": "https://mcp.typograph.nl/.well-known/oauth-protected-resource"
}
},
"id": null
}

Without a token, the header is Bearer resource_metadata="..." and the message is Authentication required.

Required Scopes​

ScopeDescription
identity:readUser profile access, and listing organizations and their members
identity:deleteDelete the user's own account, or an organization the user owns, with the code the user receives by email
fileTeam, project, and template management
documentDocument validation
publisherPDF and image generation
converterFile format conversion
webhookYour organization's webhook subscriptions and their deliveries
subscription:readSubscription and usage info
offline_accessRefresh token support

An MCP client gets these scopes at most, also when it asks for more in the metadata document it identifies itself with (the JSON document at its client_id URL, which clients such as Claude Desktop publish), and your organization's plan can narrow them further. identity:write is not among them: it also changes member roles and ownership. Tools that need it, such as setting an app's logo, work only with an access token of your own app that holds identity:write.

Available Tools​

Account Management​

ToolDescription
identity_whoamiGet current user profile (ID, email, role, scopes)
identity_profile_updateChange the user's own first and last name, phone number and settings (notification emails, the theme: light, dark or system, language)
identity_account_deletion_codeStep 1 of deleting the user's own account: email the user a 6-digit code
identity_account_deleteStep 2: delete the account with the code the user read from that email, and the optional reason the user gave for leaving; answers deletion_scheduled_at

Changing the profile works like the profile page in the portal. The agent passes only what to change, and the rest keeps its value; an empty phone number clears it, but the first and last name cannot be cleared. The phone number is in international format, starting with + and the country code, such as +31 20 123 4567, and the language is en or nl. The name others see follows the first and last name, as in the portal, and the email address does not change here. The tool's description asks the agent to confirm the changes with you first. It needs the identity or identity:write scope, so a client that signs in on its own is refused and the agent sends you to the portal; an agent connected through an app of your organization with that scope can change the profile. See Update current user for the endpoint.

Deleting an account always needs the user. The code goes only to the user's mailbox, so the agent asks the user for it and never guesses one. Without it, nothing is deleted. The optional reason is one of no_longer_needed, missing_features, too_expensive, too_difficult, switching_tool or other, passed only when the user said why they are leaving, never guessed. The account stops working at once, and the user can keep it by signing in within 30 days. If the user owns an organization that has other members, both tools answer with what the user has to do first. The endpoints behind them are in the Identity Service reference.

Organizations​

ToolDescription
organization_listList the organizations you belong to, with your role in each
organization_createCreate another organization; you become its owner, on the free plan
organization_updateChange an organization's name, slug, description, website_url, contact email and billing details
organization_assistant_getRead what the assistant in the Typograph portal may do in the organization (ai_access: read_write, read_only or off) and whether guests may use its AI credits (guests_use_credits)
organization_assistant_updateSet ai_access, guests_use_credits or both (off is part of Enterprise); asks for confirmation and needs identity:write
organization_guest_apps_getRead whether the apps of guests may read the organization's teams, and which apps were let read
organization_guest_apps_updateLet the apps of guests read the organization's teams, or stop them; asks for confirmation and needs identity:write
organization_member_listList an organization's members, with the member_id and user_id of each
organization_member_update_roleMake a member an admin, or an admin a member
organization_member_removeRemove a member from an organization, or leave it yourself
organization_transfer_ownershipMake another member the owner; you become an admin
organization_deletion_codeStep 1 of deleting an organization you own: email you a 6-digit code
organization_deleteStep 2: delete the organization with the code you read from that email
organization_invitation_createInvite an email address into an organization, as a member or an admin; refused with member_limit_reached when members and open invitations are at the plan's limit, which subscription_usage_get tells beforehand
organization_invitation_listList an organization's invitations that nobody answered yet
organization_invitation_resendMail an invitation again, with a new link for another 7 days
organization_invitation_revokeWithdraw an invitation; its link stops working
organization_invitation_list_mineList the invitations into organizations sent to your own address
organization_invitation_acceptAccept one of your invitations and join the organization
organization_invitation_declineDecline one of your invitations

An agent works in the organization you picked when you connected it. organization_list shows all your organizations, but the other tools reach only that one: for another organization they answer organization_not_bound, and the agent tells you to connect again and pick the other organization. Your own invitations (organization_invitation_list_mine, accept and decline) are yours, not an organization's, so they work from any connection. See Which organization a token reaches.

Only the owner can transfer an organization, whoever calls the tool, and the owner cannot take it back afterwards: only the new owner can transfer it again. The tool asks for confirmation before it acts. The transfer needs the identity or identity:write scope, which is not among the scopes above that the MCP server's sign-in asks for. So a client that signs in on its own, such as Claude Desktop, can list organizations and members but not transfer one. An agent that connects through an app of your organization with one of those scopes can. See Transferring Ownership for the same flow in the portal.

Changing a member's role and removing a member follow the same role rules as in the portal: only an owner or admin changes or removes someone else, and an admin never touches the owner. As over the API, any member can also leave by removing their own membership. Nobody is made owner with organization_member_update_role, and the owner is never demoted or removed: the owner transfers ownership first. Both tools take the member's member_id from organization_member_list, not the user_id, and both ask for confirmation before they act. They need the identity or identity:write scope as well, so a client that signs in on its own can list the members, but you change and remove them in the portal; an agent connected through an app of your organization with that scope can do it. See Managing Members for the portal, and Organization members for the endpoints.

Creating an organization needs the same identity or identity:write scope, so organization_create works only for an agent that connects through such an app. A client that signs in on its own is refused, and the tool's description tells the agent to send you to the portal instead: see Creating an Organization.

Changing an organization's settings follows the portal's rules: only an owner or admin changes them, whoever calls the tool. The agent passes only the settings to change, and the rest keep their values; an empty value clears a setting such as the website_url or a billing detail, but never the name, the slug or the contact email, which the portal requires too. The billing country is its two-letter code, such as NL. A slug another organization already uses is refused, and the agent asks you for another. The tool's description asks the agent to confirm the changes with you first. It needs the identity or identity:write scope as well, so a client that signs in on its own is refused and the agent sends you to the organization's Settings in the portal; an agent connected through an app of your organization with that scope can change them. See Update an organization for the endpoint.

Only an owner or admin invites, lists, sends again and revokes invitations. Inviting, sending again and revoking ask for confirmation first, so you see the address before a mail goes out. Everything but the two lists needs the identity or identity:write scope, so a client that signs in on its own can list invitations, but you invite, accept and decline in the portal; an agent connected through an app of your organization with that scope can do all of it. Inviting, sending again and revoking also work with the narrower identity:invitations, which reaches no other route; the assistant in the portal invites with it. See Invites for the portal, and Organization invitations for the endpoints.

Deleting an organization works like deleting the account: the code goes only to the owner's mailbox, so the agent asks you for it, and without it nothing is deleted. Both tools need identity:delete, which the MCP server's sign-in asks for. Only the owner can delete an organization, and never their only one: then the tools answer that you need to create or join another organization first. Neither you nor the agent can undo it (support can restore it within 30 days, see Deleting an Organization), and the tool asks for confirmation before it acts. See Deleting an Organization for the same flow in the portal, and the Identity Service for the endpoints.

Apps​

ToolDescription
app_listList the apps (OAuth clients) of an organization you are a member of
app_getGet one app by its client ID; never its secret
app_logo_uploadUpload or replace an app's consent-screen logo: the image as base64 with its MIME type, a square PNG, JPEG or WebP of 128 to 1024 pixels, at most 512 KB. Takes the owner or admin role and identity:write
app_logo_deleteRemove an app's logo. Takes the owner or admin role and identity:write
scope_listList every scope with what it allows: what an app's scopes mean

Teams​

ToolDescription
team_listList teams with pagination
team_getGet team details by ID
team_createCreate a new team; organization_id names the organization it is created in, and is left out for a connection that reaches one organization. Refused with team_limit_reached beyond the plan's teams (Free allows one), which subscription_usage_get tells beforehand
team_updateUpdate team name
team_deleteSoft delete a team
team_restoreRestore a deleted team; it counts against the plan's teams again

Projects​

ToolDescription
project_listList projects in a team
project_list_sharedList projects shared with you
project_getGet project details
project_createCreate a new project
project_updateUpdate project name
project_deleteSoft delete a project
project_restoreRestore a deleted project

Templates​

ToolDescription
template_listList templates in a project
template_list_sharedList templates shared with you
template_getGet template details
template_fieldsWhat a row of data can fill in a template: its merge fields and the JSON Schema of a row (Merge fields)
template_createCreate a new template
template_updateUpdate template name
template_touchUpdate modification timestamp
template_deleteSoft delete a template
template_restoreRestore a deleted template
template_publishMake a PDF or image of a stored template, billed to the organization that owns it (Publishing a stored template); asks for confirmation
template_job_getRead the status and the download links of a job template_publish started

Pre-made Templates​

The portal shows a gallery of ready-made designs under a project's New template and on Recents. An agent can browse it and start from one, as you do in Starting From the Gallery.

ToolDescription
premade_template_listList the gallery, featured templates first. Filter with category (a category's code) and is_featured. Each template says whether it is usable, in is_usable
premade_template_list_categoriesList the gallery's categories, with the code to filter on; has_templates leaves out the empty ones
premade_template_getGet one pre-made template by its code
premade_template_useCopy a pre-made template into a project (project_id, code). Answers the new template, which is the project's own from then on

premade_template_use needs edit rights on the project and the file or file:write scope; browsing needs file or file:read. A template whose is_usable is false cannot be used yet, and the tool says why. The endpoints behind these tools are in the File Service reference.

Media, Fonts & Clips​

ToolDescription
media_listList media assets in a project, with their tags and, for an image, its width and height in pixels; tag_ids with tag_match (all or any), untagged, search and sort narrow and order it
media_tagAdd tags to media and remove them, by name, on up to 100 media at once; a new name makes the tag
tag_listList the tags of a project's media, each with media_count
tag_renameRename a tag; a name another tag of the project has merges the two
tag_deleteDelete a tag from every media file that has it
media_get_urlsGet presigned download URLs for media; include_original adds the file as uploaded, include_download a link that saves it under the media's name
media_deleteMove a media file to the trash
media_restoreRestore a media file from the trash
font_listList fonts in a project
font_updateChange a font's display name, its label
font_get_urlsGet presigned download URLs for fonts
font_deleteMove a font to the trash
font_restoreRestore a font from the trash
clip_listList the clips in a project; search matches the name. What is in the trash: trash_list
clip_deleteMove a clip to the trash
clip_restoreRestore a clip from the trash

Each takes the project_id and the item's ID (media_id, font_id or clip_id; the tag tools a tag_id, media_tag its media_ids). Tagging, renaming and deleting a tag need edit rights on the project, as in the portal. Moving a file or clip to the trash needs edit rights on its project, as in the portal; it stays restorable for 30 days, then it is deleted for good. See Trash for what restoring brings back.

File Uploads​

ToolDescription
upload_listList upload jobs
upload_createCreate upload job and get presigned URL
upload_processStart processing an uploaded file
upload_getGet one upload, to follow it until its status is success or failed

Document Generation (Publisher)​

ToolDescription
publisher_job_listList generation jobs with status
publisher_job_getGet job details by ID
publisher_job_createGenerate a PDF or images from a template: the template and manifest URLs, the upload URL, the output format, and an optional CSV to merge, with its columns, rows and delivery
publisher_job_filesGet output files from completed job
merge_validateCheck a merge before printing it: the template, manifest and CSV URLs, the merge's options and check_overflow_rows, the rows to lay out to find text that does not fit; answers what Checking a merge before printing describes, is_valid and plan.is_proof included

Example: Generate a PDF

{
"name": "publisher_job_create",
"arguments": {
"template_url": "https://your-server.com/templates/invoice.json",
"manifest_url": "https://your-server.com/templates/manifest.json",
"upload_url": "https://your-bucket.example.com/invoice.pdf?X-Amz-Signature=...",
"output_format": "pdf"
}
}
ArgumentRequiredDescription
template_urlYesThe Typograph template JSON
manifest_urlYesThe template's manifest JSON, which names its fonts and images
upload_urlYesWhere the publisher uploads the result, with an HTTP PUT of the file: a presigned storage URL, or an endpoint of your own (see Upload Request Format)
output_formatNopdf (default), or an image per page: jpg, jpeg, png or webp
csv_urlNoA CSV file to merge: one document per row, uploaded as one file (Batch Processing with CSV)
mappingNoWith csv_url: which column fills which field, {"column name": "field key"} (Choosing columns and rows)
rowsNoWith csv_url: the rows to print, "3" or "1-10"
merge_modeNoWith csv_url: single (one PDF) or per_row (a ZIP with a file per row) (One file for the merge)
filenameNoWith merge_mode per_row: each row's file name, with field keys in {{…}}
on_overflowNoWith csv_url: fail (the default) or warn, to print a row whose text does not fit, cut off

Every URL must be https on a public host. The tool sends the publisher's default configuration (300 dpi, RGB). For CMYK, bleed, crop marks or a template bundle, call the Publisher API directly.

File Conversion (Converter)​

ToolDescription
converter_job_listList conversion jobs
converter_job_getGet conversion job details
converter_job_createConvert a file: the source URL and its format, the format to convert to, and the upload URL
converter_job_filesGet output files from completed job

Example: Convert an image

{
"name": "converter_job_create",
"arguments": {
"source_url": "https://your-server.com/photos/cover.tiff",
"source_format": "tiff",
"target_format": "png",
"upload_url": "https://your-bucket.example.com/cover.png?X-Amz-Signature=..."
}
}

All four arguments are required. The tool makes these conversions:

From source_formatTo target_format
indesign_package, pod_packagetypograph_package
pdf, or ai saved with PDF compatibilitytypograph_package, see Convert a PDF
A font: eot, otf, ttf, woff, woff2, ttcttf, otf, woff, woff2, or font_variants
An image: bmp, ai, eps, gif, psd, jpg, jpeg, svg, png, webp, tiff, tifjpg, png, webp, svg (an ai file not to jpg or webp), or image_variants
A zip of fonts: font_packagefont_variants
A zip of images: image_packageimage_variants

font_variants and image_variants are a zip of every file a store serves for the font or the image. The converter uploads the result to upload_url with an HTTP PUT, as the publisher does. A combination it cannot make is refused with a bad_request that says why. For image sizes, fit modes and the other options, see the Converter API.

Document Validation​

ToolDescription
document_validateValidate a document template against schema; answers has_violations, violation_count and the violations, as the Document Service names them. Optional template_headers for a template behind authentication: they pass through your MCP client and its model, so prefer a short-lived presigned URL for anything sensitive
ToolDescription
searchSearch the templates, projects, teams, media and fonts the user can open, by name, query 2 to 255 characters, or by a filter alone with query left out (types with min_updated_at lists what changed since then, and with max_updated_at too what changed in a period that has ended, such as last month); narrow it with types, team_ids and project_ids (in either), min_updated_at and max_updated_at (before, not at, that moment), add the trash with include_deleted; answers a count per type (template_count, media_count, …) beside the matches

Example: Search templates

{
"name": "search",
"arguments": {
"query": "invoice",
"types": ["templates"]
}
}

Documentation​

ToolDescription
docs_searchSearch this documentation by keyword: query (a few words, at most 200 characters), optionally audience (portal, shop or api) and limit (1-20, default 5). Answers results, the best sections first, each with title, url, section, a snippet and agent_tools, the tools that do what the section explains
docs_getRead a page as Markdown by the url that docs_search answered, or one section when the url ends in its #anchor. Answers title, url, markdown and agent_tools

Both only read public pages: they need a signed-in connection, like every tool, but no scope.

Example: How do I invite someone?

{
"name": "docs_search",
"arguments": {
"query": "invite someone to a project",
"audience": "portal"
}
}

Notifications​

ToolDescription
notification_listList user notifications
notification_mark_readMark notification as read
notification_mark_unreadMark notification as unread
notification_mark_all_readMark all notifications as read

Invitations & Sharing​

ToolDescription
invitation_list_resourceList the pending invitations to a resource
invitation_list_myList the invitations sent to you
invitation_createInvite up to 20 email addresses at once (emails), with one permission and an optional message; every address must be valid before any is sent, and the result lists the invitations sent and the addresses refused, with the code and message of each. edit or admin takes one of the plan's editor seats
invitation_acceptAccept an invitation (invitation_id); one to edit or admin waits while the editor seats are taken
invitation_declineDecline an invitation (invitation_id)
invitation_deleteDelete (revoke) a pending invitation (invitation_id)

A resource is a team, project or template; a permission is view, comment, edit or admin.

An admin of a team stays admin of every project and template in it, and an admin of a project of every template in it. invitation_create, invitation_accept and role_update refuse to give such an admin less there: the API refuses it with 409 and admin_inherited, and the tool answers the error bad_request with the API's message, such as "ada@example.com is admin of the team this project is in, and stays admin here. Make them admin here, or leave their access as it is." Editors and below can be given less on a project or template than on its team.

edit and admin take one of the plan's editor seats, view and comment never do; Free has one seat, the owner's. When the seats are all taken the tool answers the error editor_seats_taken, and when the plan allows no more teams team_limit_reached, each with the API's message, such as "Free allows one team. Request another plan, or delete a team first.", and the agent tells you what you can do. When the plan cannot be checked it answers service_unavailable: try again in a moment.

Access Control​

ToolDescription
permission_getYour own access to a team, project or template: your role, where it is given (granted_on), the role from above when this place differs (inherited_permission), when it ends (expires_at) and the admins to ask for more
role_listList roles for a resource
role_updateUpdate user/team role; raising someone to edit or admin takes an editor seat
role_deleteRemove user/team from resource

Asking for Access​

ToolDescription
access_request_createAsk the admins of a team, project or template you can open for a higher permission there (comment, edit or admin), with an optional message of up to 500 characters
access_request_listThe open requests on a team, project or template that you may answer; without resource_type and resource_id, your own open requests
access_request_approveGive the requester the role they asked for, or the permission you pass instead; returns their role
access_request_declineDecline a request; the requester is notified
access_request_withdrawWithdraw your own request

Each takes the resource_type and resource_id of the place (optional on access_request_list, which without them lists your own), and the answering tools and access_request_withdraw the access_request_id from access_request_list. The rules are those of the portal and the API: you can ask only where you can open the place already, once per place (a second request answers bad_request with the API's message for access_request_open: withdraw the first), and for no more than 20 places at a time. A request stays open for 30 days; the admins are notified when it is sent, and the requester when it is answered or lapses.

To answer a request you need edit on the place, for a role up to edit given to someone who holds no role on the place itself; giving admin, or raising a role the requester already holds there, takes an admin. access_request_list lists only the requests you may answer. edit and admin take one of the plan's editor seats: when they are all taken, access_request_approve answers editor_seats_taken with the API's message. A role no higher than the requester holds by now is refused the same way.

Contacts​

ToolDescription
contact_listList team contacts

Favorites & Recent​

ToolDescription
favorite_listList favorite projects/templates
favorite_toggleToggle favorite status
recent_templatesList the templates the user opened most recently; a template changed without being opened since is not in it (for that, search with min_updated_at)
recent_searchesList recent search queries

Trash​

ToolDescription
trash_listList what is in the trash: teams, projects, templates, clips, media and fonts, each kind in its own list, at most 50 of each per page, and each item with can_restore: whether the user may restore it

Restore an item whose can_restore is true with the restore tool of its kind: team_restore, project_restore, template_restore, clip_restore, media_restore or font_restore. A team takes a team admin, a project or template its admin, and a clip, media file or font anyone who can edit its project. The templates and files of a deleted project are not listed on their own; restoring the project brings them back.

Plans​

The plans an organization can be on and what each allows, so an agent can say what another plan would give, and how much of its plan the organization uses, so it can say a limit is reached before it asks you for the details of something the plan does not allow. Read-only. A plan is changed by asking Typograph, not through an agent.

ToolDescription
subscription_plan_listList the plans (Free, Pro, Enterprise) in order, each with code, name, description, price_monthly and price_yearly (null: on request), currency, max_teams and editor_seats (null: no limit; the tool lifts them out of the API's features), and quotas
subscription_plan_getGet one plan by its code, in the same shape; a custom or unknown plan answers not_found
subscription_usage_getHow much the organization organization_id uses of its plan: its tier, plan_usage with teams, editor_seats and members (open invitations included), each used (null when they could not be counted just now) and limit (null: no limit) as GET /v1/subscription/usage answers them, and is_reached, which the tool adds: true once used is at the limit (a team in the trash does not count), and this month's usage against monthly_limits, such as ai_credits and storage in bytes. Only the organization the connection is for: another answers organization_not_bound

All three need subscription:read, which the MCP server's sign-in asks for.

Webhooks​

The webhook subscriptions of your organization and their deliveries: the same calls as the Webhook Management API, made with the signed-in user's rights.

ToolDescription
webhook_listList webhook subscriptions; client_id lists those of one OAuth client
webhook_getGet a webhook subscription by ID
webhook_createCreate a subscription for an https endpoint, with its event types and up to 5 custom headers
webhook_updateChange the name, endpoint URL, headers or event types, or switch deliveries on or off with is_active
webhook_deleteDelete a subscription for good, with its delivery history
webhook_rotate_secretGive a subscription a new signing secret; revoke_previous stops the old one at once
webhook_list_deliveriesList the deliveries of a subscription, with status, attempt_count and the HTTP status your endpoint answered
webhook_retry_deliverySend a delivery that has not arrived (pending, failed or retrying) to its endpoint again, now, with the same event; a delivered one is refused, and one delivery is retried at most once a minute (Retry a Failed Delivery)
webhook_list_event_typesList the event types a subscription can subscribe to

webhook_create and webhook_rotate_secret return the subscription's signing secret, as secret and standard_webhooks_secret (see Secret Format). It is shown once, in that result only, and it passes through your MCP client and its model: have the agent store it with the integration it sets up straight away. No other tool returns it. If it is lost, rotate it; if it may have leaked, rotate it with revoke_previous, since a plain rotate keeps the previous secret signing for 24 hours.

Endpoint header values come back masked as ***, and webhook_update replaces all headers at once: a header given the value *** keeps the value it has, and an empty object {} removes them all. Header values you give pass through your MCP client and its model too.

The webhook tools need a token with the webhook scope, or its parts webhook:read, webhook:write and webhook:delete; without them a call answers forbidden. The MCP server's sign-in asks for webhook (see Required Scopes). webhook_create takes the client_id of an OAuth client of your organization, which receives the job events of the jobs it starts.

webhook_create, webhook_update, webhook_delete and webhook_rotate_secret ask for confirmation before they act. Every tool also carries annotations in tools/list, so your MCP client can ask you before webhook_retry_delivery runs too.

Tools that ask for confirmation​

33 tools act only once the user confirms: those that delete something or move it to the trash, take away or hand over access, email someone or send data to another address, or use billed capacity.

WhyTools
Delete or move to the trashapp_logo_delete, clip_delete, font_delete, media_delete, tag_delete, tag_rename (it merges into a tag of the same name), project_delete, team_delete, template_delete, role_delete, invitation_delete, webhook_delete, organization_delete, identity_account_delete
Take away or hand over accessorganization_assistant_update, organization_guest_apps_update, organization_member_remove, organization_member_update_role, organization_transfer_ownership, organization_invitation_revoke, role_update, access_request_approve, access_request_decline, webhook_rotate_secret
Email someone, or send data to another addressinvitation_create, organization_invitation_create, organization_invitation_resend, access_request_create, webhook_create, webhook_update
Use billed capacitypublisher_job_create, converter_job_create, template_publish

In tools/list these tools take an optional confirmation_token argument, and their description ends with how the confirmation works. The answer to initialize carries instructions that say the same, for the model behind your client.

The flow​

  1. Call the tool without confirmation_token. It changes nothing: it only reads, with your token, the names of what it would change. It answers, as the text of its content item:

    {
    "status": "confirmation_required",
    "summary": "Delete the project \"Brochures\" (…7a8b9c0d)",
    "steps": ["Move the project \"Brochures\" (…7a8b9c0d) to the trash; project_restore brings it back"],
    "confirmation_token": "Jc0t…",
    "expires_at": "<ISO 8601 timestamp>"
    }

    summary says in one line what the call would do, and steps what happens, in order, both with the call's own arguments filled in. An argument that is the ID of a team, project, template, organization, member, invitation, access request, role, app, webhook subscription, clip, font or tag is shown by its name, in quotes, followed by the last eight characters of its ID, so two of the same name still look different: "Marketing" (…3f9c2a1b). Where that name cannot be read with your token (it is not found, or not yours to see), the whole ID stays. A media file is shown by its ID. A URL in them is shown without its credentials (user name and password, query string, fragment).

    They are in English, or in Dutch when your request asks for it with the Accept-Language header (Accept-Language: nl): the first language of the header, by weight, that the server writes. Values that are words follow: "Nodig joe@example.com uit voor het project \"Brochures\" (…7a8b9c0d) als Bewerker". Names, addresses and URLs stay as they are.

  2. Show the summary and the steps to the user, and wait for their answer.

  3. Only when the user agrees, call the tool again with the same arguments plus confirmation_token. That call runs the tool and answers as it always does. If the user declines, do nothing: the token expires by itself.

The token is bound to:

  • the tool — a token for project_delete does not confirm team_delete;
  • its arguments — every argument, with its default where the call leaves it out, in any order; change one and the token no longer fits;
  • the caller — the bearer token the first call was made with.

It is valid for 5 minutes and once: presenting it uses it up, whether it fits or not. A token that does not fit, is used or has expired changes nothing, and answers:

{
"success": false,
"error": {
"code": "confirmation_invalid",
"message": "The confirmation_token is unknown, used, expired, or was issued for other arguments. Nothing was done. Call the tool again without confirmation_token to ask the user again."
}
}

Call the tool again without the token to ask the user again. When confirmation cannot be checked just now, the tool answers service_unavailable and does nothing either.

The confirmation comes before the API is asked to act: the first call only reads names. So it answers confirmation_required even when the user may not do what it asks; the confirmed call then answers the API's refusal, such as forbidden. A call without a bearer token gets the 401 response as any tool call does.

The confirmation is as strong as your client

The server issues the token, but your client decides when to send it back. Keep that decision with the user: send the token only after the user agreed, never because the model decided to on its own, and never because a document, a web page or a tool result asks for it. A client that hands the answer to its model relies on that model to wait for the user.

Tool annotations​

Every tool carries MCP annotations in tools/list, so your client can decide how to treat it before it runs:

AnnotationSays
titleA short, human-readable name, such as "Move a project to the trash"
readOnlyHinttrue when the tool only reads
destructiveHinttrue when the tool can remove or overwrite something
idempotentHinttrue when calling it again with the same arguments has no further effect
{
"name": "project_delete",
"annotations": {
"title": "Move a project to the trash",
"readOnlyHint": false,
"destructiveHint": true,
"idempotentHint": false
}
}

The annotations are hints for your client; the confirmation is what the server enforces.

HTTP Endpoints​

EndpointDescription
POST /mcpJSON-RPC 2.0 endpoint for tool calls
OPTIONS /mcpCORS preflight, for clients in a browser
GET /.well-known/oauth-authorization-serverOAuth server metadata (RFC 8414)
GET /.well-known/oauth-protected-resourceProtected resource metadata (RFC 9728)
GET /healthHealth check endpoint

A request body to POST /mcp may hold a JSON-RPC batch, a list of requests, of at most 20. A longer batch is answered 400 with the JSON-RPC error -32600, Invalid Request: a batch holds at most 20 requests, and none of its requests runs.

OAuth Discovery​

The MCP server implements OAuth 2.0 metadata discovery:

curl https://mcp.typograph.nl/.well-known/oauth-authorization-server

Response:

{
"issuer": "https://api.typograph.nl",
"authorization_endpoint": "https://api.typograph.nl/oauth/authorize",
"token_endpoint": "https://api.typograph.nl/oauth/token",
"scopes_supported": ["identity:read", "identity:delete", "file", "document", "publisher", "converter", "webhook", "subscription:read", "offline_access"],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"code_challenge_methods_supported": ["S256"]
}

Response Format​

A tool answers a tools/call with one text content item. Its text is the JSON below.

Success Response​

{
"success": true,
"data": {
"items": [
{
"id": "019b28fb-a11e-7641-a28f-e978f892ec06",
"name": "Marketing",
"created_at": "<ISO 8601 timestamp>"
}
],
"total": 1,
"page": 1,
"page_size": 20,
"total_pages": 1
}
}

A list tool answers items with the page fields; any other tool answers the resource itself under data, with the field names the API sends (is_active, is_expired, has_more, can_use_quota, …).

A tool that only acts (delete, restore, touch, accept, decline, revoke, remove, mark, mail a code) answers success: true with the ID it acted on, and no flag of its own that repeats it: {"success": true, "data": {"template_id": "…"}}. notification_mark_all_read answers data: null.

A tool that asks for confirmation answers its first call with "status": "confirmation_required" instead, and no success.

Error Response​

When the API refuses a call, the tool answers an error with a fixed code and message. It never passes on the details of the API:

{
"success": false,
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
CodeWhen
forbiddenThe token does not allow the action, for example a missing scope
not_foundThe resource does not exist, or the user cannot see it
team_limit_reached, editor_seats_taken, member_limit_reachedThe organization's plan allows no more teams, editor seats or members; the message says what the user can do: request another plan, or delete or free something first
bad_requestThe API refused the request, as invalid or because of what is in the way (any other 409); the message says why when the API did
validation_errorAn argument was refused before the call, for example an empty name or a URL that is not https
rate_limitedToo many requests; the message says when to try again when the API did
organization_off, organization_read_only, organization_required, route_closed_to_assistantMet only by the assistant in the Typograph portal: the organization keeps the assistant out or lets it only read (AI Assistant Settings), a tool without a resource needs organization_id, or the action is for the person to do in the portal
confirmation_invalidThe confirmation_token is unknown, used, expired, or for another tool, other arguments or another caller; nothing was done (confirmation)
service_unavailableThe API could not answer, or confirmation could not be checked
errorAnything else

A refused token is no tool error: it is the 401 response.

Example Workflows​

A step with a tool that asks for confirmation takes two calls: the first answers confirmation_required, the second, with the user's consent, runs it.

Generate a Document​

1. team_list → Get available teams
2. project_list → List projects in team
3. template_list → Find the template
4. publisher_job_create → Ask the user to confirm (confirmation_required), then create
the job with the template, manifest, upload URLs and the confirmation_token
5. publisher_job_get → Check job status (poll until completed)
6. publisher_job_files → Download generated files

Upload and Use Media​

1. upload_create → Get presigned upload URL
2. [Upload file to presigned URL]
3. upload_process → Process the uploaded file
4. upload_get → Check its status (poll until success or failed)
5. media_list → Verify media is available
6. template_update → Reference media in template

Set Up a Webhook​

1. webhook_list_event_types → Choose the events to receive
2. webhook_create → Ask the user to confirm (confirmation_required), then create
the subscription with the confirmation_token; store the secret it returns
3. webhook_list_deliveries → Check that deliveries reach the endpoint
4. webhook_retry_delivery → Send a failed delivery again once the endpoint is fixed

Share a Project​

1. project_get → Get project details
2. invitation_create → Ask the user to confirm (confirmation_required), then
invite the collaborators (emails) with the confirmation_token
3. role_list → Verify roles assigned

Security​

No stored tokens​

The MCP server is an OAuth 2.0 resource server: it passes each request's token to the Typograph API and stores none. There are no sessions to take over.

Output Sanitization​

Sensitive data (tokens, secrets) is automatically redacted from tool responses. The one exception is a webhook's signing secret, which webhook_create and webhook_rotate_secret return on purpose (see Webhooks): it only signs the deliveries to your own endpoint, cannot call the API, and webhook_rotate_secret with revoke_previous withdraws it at once (a plain rotate keeps the previous secret valid for 24 hours).

Configuration​

Environment Variables​

VariableDescriptionDefault
TYPOGRAPH_API_BASE_URLAPI base URL the tools callhttps://api.typograph.nl
MCP_SERVER_PUBLIC_URLThe server's own URL, in the protected resource metadata and the 401https://mcp.typograph.nl
OAUTH_ISSUER_URLThe authorization server the metadata points tohttps://api.typograph.nl
SERVICE_DOCUMENTATION_URLDocumentation the metadata links tohttps://docs.typograph.nl
MCP_SERVER_HOSTServer host0.0.0.0
MCP_SERVER_PORTServer port3000
DOCS_INDEX_URLThe docs-index.json that docs_search and docs_get readhttps://docs.typograph.nl/docs-index.json

Rate Limits​

MCP server requests ultimately hit the Typograph API Gateway, so they're subject to the same tiered rate limits — categorized per endpoint and driven by your organization's subscription. See Rate Limiting.

Best Practices​

For AI Agent Developers​

  1. Handle a 401 - Sign the user in again when the server answers 401 Unauthorized; MCP clients that support OAuth do this themselves
  2. Poll job status - Use exponential backoff when checking job completion
  3. Use pagination - Large result sets are paginated; iterate through pages. A list tool's page_size is at most 50, as the API answers; organization_member_list, organization_invitation_list, organization_invitation_list_mine and premade_template_list take up to 100. A larger page_size is refused with validation_error
  4. Cache team/project IDs - Reduce API calls by caching resource identifiers
  5. Check scopes - Verify your client has required scopes before operations
  6. Ask the user on confirmation_required - Show the summary and the steps, and repeat the call with the confirmation_token only once the user agreed (confirmation)

Error Handling​

async function callTool(name, args) {
// An MCP client that supports OAuth signs in again by itself on a 401.
const response = await mcpClient.callTool(name, args);
const result = JSON.parse(response.content[0].text);

if (result.status === 'confirmation_required') {
// Nothing happened yet: ask the user, and call again only when they agree.
const isAgreed = await askUser(result.summary, result.steps);
return isAgreed
? callTool(name, { ...args, confirmation_token: result.confirmation_token })
: undefined;
}

if (!result.success) {
console.error(`Error: ${result.error.code} - ${result.error.message}`);
return;
}

return result.data;
}

Standards Compliance​

  • Transport: Streamable HTTP (introduced in protocol revision 2025-03-26) — a single POST /mcp endpoint answering with plain JSON, no SSE stream. Clients that only support the older HTTP+SSE transport (GET /sse) are not supported.
  • MCP Protocol: initialize reports protocolVersion: 2024-11-05, although the transport is the newer one above. Claude Code connects with this pairing; other clients follow the same negotiation but are not all verified.
  • OAuth 2.0: RFC 6749, RFC 7636 (PKCE)
  • OAuth Metadata: RFC 8414, RFC 9728
  • JSON-RPC: 2.0 specification; a batch holds at most 20 requests