Announcing CIMD support for MCP Client registration
Learn more

Should you use Contentful MCP or Contentful API for building AI Agents?

Nishant Choudhary
Tech Evangelist

TL;DR

  • Contentful's Remote MCP server went GA on July 21, 2026, free to all customers. Webhooks, Releases, Scheduled Actions, bulk actions, and editorial workflows are API-only.
  • MCP auth is OAuth 2.1, gated twice: by the user's Contentful permissions, then by a per-environment allow-list in the Contentful MCP app. The direct API's documented OAuth flow is still the implicit grant, which RFC 9700 advises against and OAuth 2.1 drops.
  • The MCP path has no headless mode. Personal access tokens and the local server do, but both run every call as one token owner, flattening per-editor attribution in sys.updatedBy and in version history.
  • Both paths hit the same Content Management API ceiling of 7 requests per second, and neither stores, rotates, or revokes the per-user credential a multi-tenant agent needs.
  • Scalekit's Contentful MCP connector vaults the per-editor credential, scopes the 70 tools to what one agent needs, and logs every tool call with full attribution.

Your agent needs to work inside Contentful: find the entries missing an SEO description, draft one for each, leave them unpublished for review. Contentful ships a hosted MCP server and a Content Management API that has been production-grade for a decade. Both can do that job. They diverge on what happens when the agent runs with nobody watching, when a second editor starts using it, and when someone asks who published the wrong pricing page.

What Contentful MCP and the Contentful APIs actually are

Contentful ships three distinct surfaces an agent can target: a hosted MCP server, a local MCP server you run yourself, and the underlying REST and GraphQL APIs. They are not tiers of the same thing. They differ in who the caller is.

Contentful Remote MCP

Contentful hosts the Remote MCP server at mcp.contentful.com/mcp, with an EU endpoint at mcp.eu.contentful.com/mcp for data residency. Region is fixed by the endpoint you connect to; there is no in-session switch.

Authentication is OAuth 2.1, following the MCP authorization spec for HTTP transports. The client opens a browser consent flow, the user picks which space and environment pairs are in scope for that session, and the server issues an MCP-specific token for subsequent tool calls. Scalekit's catalog lists the auth model as OAuth 2.1 with Dynamic Client Registration (DCR).

One prerequisite is easy to miss: the server cannot be used in a space or environment until the Contentful MCP app is installed and configured there. Details are in the Contentful MCP server documentation.

The local Contentful MCP server

The local server runs as a Node.js process via npx @contentful/mcp-server. It authenticates with a Content Management API personal access token (PAT) supplied through CONTENTFUL_MANAGEMENT_ACCESS_TOKEN, with no OAuth flow at all.

It exposes the same core toolset but skips the MCP app entirely. Every tool call runs as the PAT's owner with whatever permissions that PAT carries. The one guardrail is PROTECTED_ENVIRONMENTS, a comma-separated list of environment IDs blocked from write and delete calls. That guard is opt-in, case-sensitive, and enforced only inside the MCP process; direct CMA calls and the Contentful web app are unaffected.

The Contentful API surface

The Content Management API sits at api.contentful.com and is the read-write surface underneath everything else. It is versioned through the application/vnd.contentful.management.v1+json content type and uses optimistic locking via the X-Contentful-Version header. See the Content Management API reference for the full resource list.

Three other APIs matter for agents. The Content Delivery API serves published content from a CDN with unlimited cache hits. The Content Preview API serves drafts. The GraphQL Content API handles nested queries in one round trip. Each takes its own space-scoped and environment-scoped access token.

Comparing them where it matters for agents

The MCP surface is genuinely broad for a vendor server. Scalekit's connector lists 70 tools, and Contentful groups them into eleven categories. The gap is not in content operations; it is in everything that surrounds them.

What your agent can actually do

Capability
Contentful Remote MCP
Contentful APIs
Search entries with query filters
Yes: search_entries
Yes: CMA and CDA
Semantic (vector) search
Yes: semantic_search, requires enabling per environment
Yes: CMA content semantics endpoints
Create, update, publish entries
Yes, subject to per-environment gating
Yes: CMA
Resolve linked references in one call
Yes: resolve_entry_references
Yes: CMA entry references
Entry snapshots for rollback
Yes: get_entry_snapshot
Yes: CMA snapshots
Content model and field changes
Yes
Yes: CMA content types
Asset upload
Yes, via a two-phase upload session
Yes: CMA uploads
Taxonomy concepts and concept schemes
Yes
Yes: CMA taxonomy
AI Actions: create and invoke
Yes
Yes: CMA AI Actions
Cached delivery of published content at volume
No
Yes: CDA, CDN-backed
Draft content preview
No
Yes: CPA
GraphQL queries
No
Yes: GraphQL Content API
Webhooks and change events
No
Yes: CMA webhooks
Scheduled Actions and Releases
No
Yes: CMA
Bulk publish, unpublish, validate
No
Yes: CMA bulk actions
Editorial workflows, comments, tasks
No
Yes: CMA
Environment aliases
No, documented as unsupported
Yes: CMA

Where the MCP ceiling sits

The pattern in that table is consistent. The MCP server covers what an editor does inside a single entry or content type. It does not cover what a content operations platform does around those entries.

Three gaps decide real builds. There are no webhooks, so an agent that reacts when an entry changes cannot be built on MCP alone. There are no Releases or Scheduled Actions, so coordinated multi-entry launches stay on the CMA. And environment aliases are explicitly unsupported: point a client at an alias and tool calls fail with a Failed to fetch app installation: Forbidden error, so you must reference the underlying environment ID.

The write semantics that differ

This is where MCP quietly does you a favour. The CMA does not merge changes; update an entry with a subset of properties and every property you left out is gone. You fetch, modify, and write back the whole body with the current version number.

The MCP update_entry tool merges the fields you supply with the existing ones and requires the entry's sys.version from a prior get_entry, rejecting the write if the entry moved underneath you. Contentful also shipped append_entry_field, which appends to an array field server-side and deduplicates, precisely because an agent working from a truncated read can silently drop items from a large reference array. That failure mode is handled in the tool layer rather than in your code.

The auth path each one puts you on

Both paths eventually call the CMA. What differs is whose identity the call carries and how that identity was obtained.

MCP auth: OAuth 2.1 with two permission layers

The Remote MCP server enforces two independent checks. The first is the user's own Contentful permissions. The second is the per-environment allow-list managed by the Contentful MCP app, where an admin picks which tool categories are exposed and whether each is read-only or read-write.

Disabled tools are rejected even if a client calls them directly. This is a good model, and it is the reason Contentful recommends starting read-only and enabling writes deliberately. It is also configuration you own in Contentful, not in your agent.

API auth: three options, each with a catch

The CMA accepts a bearer token from three sources, and picking one sets your operational posture.

A personal access token inherits the full access rights of the user's Contentful account across every organization and space that account can reach. The web app requires an expiry, capped at five years. Its scopes limit it to read or manage, not to a particular space.

An OAuth application issues tokens scoped to content_management_read or content_management_manage. Contentful's documented flow redirects to be.contentful.com/oauth/authorize with response_type=token and returns the token in the redirect URI's hash fragment.

An app access token comes from App Identity. You sign an RS256 JWT with an app private key and exchange it for a token valid ten minutes, scoped to the one space environment where the app is installed.

Why the implicit grant matters here

That OAuth application flow is the implicit grant. The token arrives in a URL fragment with no authorization code exchange and no documented refresh token.

RFC 9700, the Best Current Practice for OAuth 2.0 Security published in January 2025, advises against the implicit grant and notes that browser fragment handling has changed under it. OAuth 2.1 drops the mode entirely. The consequence for an agent builder is narrow but sharp: no refresh primitive, so re-consent is your only recovery path.

Note the inversion. Across most tools in this series, MCP is the newer surface with the weaker auth story. Contentful is the reverse.

The headless gap and the attribution gap

The Remote MCP server has no non-interactive mode. A nightly job that backfills metadata across ten spaces cannot complete a browser consent flow on its own.

The obvious workaround creates a worse problem. Drop to a PAT or to the local MCP server and every write is attributed to one token owner. In a CMS that surfaces sys.updatedBy in the entry sidebar and keeps snapshots for rollback, that is not a cosmetic loss. Your version history now says one person edited four hundred entries overnight, and nobody can answer which editor's agent run caused it.

App Identity is the honest headless answer. Ten-minute tokens, scoped to one space environment, attributed to the app rather than a human. It is the right shape for background automation, and it is CMA-only.

What you own in production

Choosing a path shifts where the work sits; it does not remove it. This is what stays on your side of the line in each case.

On the MCP path

Contentful owns hosting, scaling, and the tool schemas. You own the per-user OAuth credential, the MCP app configuration in every space and environment your agent touches, and the session scoping step, which must be repeated on every new connection.

You also own asset uploads, which are stranger than they look. create_upload_session returns an uploadHandle and an uploadUrl; you PUT the raw bytes to that URL, which is intentionally unauthenticated because the handle itself is the capability token. Sessions expire after one hour and are single-use. Treat that handle like a secret.

On the API path

You own everything above plus the full stack: endpoint selection, the fetch-modify-write cycle that version locking demands, pagination, retries, and adapter code for each of the four APIs you touch. More surface area, more control, and no dependency on a per-environment app installation.

Rate limits are the shared ceiling

The CMA enforces a default of 7 requests per second. That number is low, and it is the number that matters, because the Remote MCP server routes to api.contentful.com underneath.

An agent auditing a thousand entries issues a search, then a get_entry per candidate, then an update_entry, then a publish. That is thousands of sequential CMA calls against a 7 per second budget. The CDA's 55 uncached requests per second with unlimited CDN hits exists for exactly this reason, and it is unreachable from MCP. If your agent is read-heavy over published content, the CDA is the right surface and MCP is not. For a deeper look at why MCP costs more than direct API calls, the tradeoffs extend beyond rate limits.

Schema drift

The Contentful MCP server repo has shipped more than seventy releases. Tool schemas change when Contentful updates the server, and you do not pin a version.

The CMA is the opposite bargain: an explicit content type version, an optimistic locking header, and a deprecation process. For a deterministic pipeline where an unexpected schema change is an incident rather than an inconvenience, that predictability is worth the extra adapter code.

When to use MCP, when to use the API

Neither path wins outright, and most production Contentful agents will end up using both. Here is the split that holds up.

Use Contentful Remote MCP when

  • The agent is interactive and an editor is present: a content assistant that finds stale entries, drafts copy, and leaves changes unpublished for review
  • You want per-environment governance you can hand to a space admin, including read-only enablement before any write tool is turned on
  • The work is entry-level and asset-level rather than release-level: search, read, draft, update, publish one item at a time
  • You want semantic_search and resolve_entry_references without building embedding infrastructure or an N+1 reference walker yourself
  • You are pointing the agent at a sandbox environment and merging to master by hand, which is the safest default for anything that can reach a live site

Use the Contentful APIs directly when

  • The agent runs on a schedule with no user present, where App Identity's ten-minute app access tokens are the correct credential and MCP simply has no equivalent
  • The agent must react to content changes, which requires CMA webhooks
  • The work spans a Release, a Scheduled Action, a bulk publish, or an editorial workflow transition
  • The agent reads published content at volume, where the CDA's CDN caching is the difference between working and rate-limited
  • You need GraphQL to pull a deeply nested content tree in one round trip

Connecting a Contentful agent with Scalekit

Scalekit's Contentful MCP connector sits in front of the vendor MCP server and turns it into a per-user, credential-vaulted tool surface. Your agent never sees a Contentful token, and the OAuth flow is one SDK call.

Retrieve the tools this editor is authorized to call

Before any code, the distinction worth naming: the agent is not loading a connector catalog. It is loading the tools this editor's connected account is authorized to call, which is what separates a per-user agent from a shared-credential one.

The connection_name string below must match the connection name configured in your Scalekit dashboard exactly. This is the single most common integration error.

import os import anthropic import scalekit.client from google.protobuf.json_format import MessageToDict scalekit_client = scalekit.client.ScalekitClient( client_id=os.getenv("SCALEKIT_CLIENT_ID"), client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"), env_url=os.getenv("SCALEKIT_ENV_URL"), ) actions = scalekit_client.actions CONNECTION = "contentfulmcp" # must match the connection name in the Scalekit dashboard IDENTIFIER = "editor_412" # resolve from your authenticated session, never client input account = actions.get_or_create_connected_account( connection_name=CONNECTION, identifier=IDENTIFIER, ) if account.connected_account.status != "ACTIVE": link = actions.get_authorization_link( connection_name=CONNECTION, identifier=IDENTIFIER, ) print("Authorize Contentful:", link.link) input("Press Enter after authorizing...") scoped_response, _ = actions.tools.list_scoped_tools( identifier=IDENTIFIER, filter={"connection_names": [CONNECTION]}, page_size=100, # the connector exposes 70 tools; the default page misses some )

Run the Claude tool-use loop

execute_tool resolves the vaulted credential for this identifier at call time and makes the Contentful call as that editor. What the editor cannot do in Contentful, the agent cannot do either.

llm_tools = [ { "name": MessageToDict(t.tool).get("definition", {}).get("name"), "description": MessageToDict(t.tool).get("definition", {}).get("description", ""), "input_schema": MessageToDict(t.tool).get("definition", {}).get("input_schema", {}), } for t in scoped_response.tools ] client = anthropic.Anthropic() messages = [{ "role": "user", "content": ( "In space acme_prod, environment staging: find blogPost entries with an " "empty seoDescription, draft one for each from the body copy, and leave " "them unpublished." ), }] while True: response = client.messages.create( model="claude-sonnet-4-6", max_tokens=2048, tools=llm_tools, messages=messages, ) if response.stop_reason == "end_turn": print(response.content[0].text) break tool_results = [] for block in response.content: if block.type == "tool_use": result = actions.execute_tool( tool_name=block.name, identifier=IDENTIFIER, tool_input=block.input, ) tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": str(result.data), }) messages.append({"role": "assistant", "content": response.content}) messages.append({"role": "user", "content": tool_results})

The same pattern in TypeScript

The Node SDK mirrors the Python surface. Note that listScopedTools hangs off scalekit.tools while executeTool hangs off scalekit.actions.

import { ScalekitClient } from '@scalekit-sdk/node'; import { ConnectorStatus } from '@scalekit-sdk/node/lib/pkg/grpc/scalekit/v1/connected_accounts/connected_accounts_pb'; import Anthropic from '@anthropic-ai/sdk'; const scalekit = new ScalekitClient( process.env.SCALEKIT_ENV_URL!, process.env.SCALEKIT_CLIENT_ID!, process.env.SCALEKIT_CLIENT_SECRET!, ); const anthropic = new Anthropic(); const CONNECTION = 'contentfulmcp'; // must match the connection name in the Scalekit dashboard const IDENTIFIER = 'editor_412'; const { connectedAccount } = await scalekit.actions.getOrCreateConnectedAccount({ connectionName: CONNECTION, identifier: IDENTIFIER, }); if (connectedAccount?.status !== ConnectorStatus.ACTIVE) { const { link } = await scalekit.actions.getAuthorizationLink({ connectionName: CONNECTION, identifier: IDENTIFIER, }); console.log('Authorize Contentful:', link); } const { tools } = await scalekit.tools.listScopedTools(IDENTIFIER, { filter: { connectionNames: [CONNECTION] }, pageSize: 100, }); const llmTools = tools.map(t => ({ name: t.tool.definition.name, description: t.tool.definition.description, input_schema: t.tool.definition.input_schema, })); const messages: Anthropic.MessageParam[] = [ { role: 'user', content: 'List unpublished blogPost entries in space acme_prod, environment staging.' }, ]; while (true) { const response = await anthropic.messages.create({ model: 'claude-sonnet-4-6', max_tokens: 2048, tools: llmTools, messages, }); if (response.stop_reason === 'end_turn') { const text = response.content.find(b => b.type === 'text'); if (text?.type === 'text') console.log(text.text); break; } const toolResults: Anthropic.ToolResultBlockParam[] = []; for (const block of response.content) { if (block.type === 'tool_use') { const result = await scalekit.actions.executeTool({ toolName: block.name, identifier: IDENTIFIER, toolInput: block.input as Record<string, unknown>, }); toolResults.push({ type: 'tool_result', tool_use_id: block.id, content: JSON.stringify(result.data), }); } } messages.push({ role: 'assistant', content: response.content }); messages.push({ role: 'user', content: toolResults }); }

Runnable versions of both live in the Anthropic and LangChain code samples.

Virtual MCP servers for multi-tool, multi-tenant Contentful agents

Seventy tools is a lot to hand a model that needs five. At roughly 200 tokens per tool definition, the full Contentful surface burns around 14,000 tokens of context before the agent does any work, and a model choosing between seventy near-adjacent content operations picks worse than one choosing between five.

Tool bloat is an accuracy problem and a cost problem at the same time. The fix is not better prompting. It is surface reduction. Virtual MCP Servers do that at the tool level while keeping per-user credential isolation intact.

Define the server once per agent role

Create the server once, not once per user. Contentful is the highest-stakes connector in most content stacks, because a careless write does not corrupt a spreadsheet; it publishes the wrong pricing to a live page. An explicit allow-list that omits every delete_*, publish_*, and create_environment tool is cheap insurance.

from datetime import timedelta from scalekit.actions.models.mcp_config import McpConfigConnectionToolMapping vmcp = scalekit_client.actions.mcp.create_config( name="contentful-seo-backfill-agent", connection_tool_mappings=[ McpConfigConnectionToolMapping( connection_name="contentfulmcp", tools=[ "contentfulmcp_get_initial_context", "contentfulmcp_search_entries", "contentfulmcp_get_entry", "contentfulmcp_update_entry", ], ), ], ) config_id = vmcp.config.id mcp_server_url = vmcp.config.mcp_server_url

Mint a session token before each run

The endpoint is static; the identity is not. One server definition serves every editor, and each run gets a short-lived token bound to that editor's connected accounts.

accounts = scalekit_client.actions.mcp.list_mcp_connected_accounts( config_id=config_id, identifier="editor_412", include_auth_link=True, ) for account in accounts.connected_accounts: if account.connected_account_status != "ACTIVE": print(f"{account.connection_name} needs auth: {account.authentication_link}") token_response = scalekit_client.actions.mcp.create_session_token( mcp_config_id=config_id, identifier="editor_412", expiry=timedelta(minutes=30), ) token = token_response.token

Point a framework at the endpoint

Any MCP-capable framework consumes the URL with bearer auth. Adding Slack or Jira later means adding a mapping to the same server definition, not a second auth integration.

import asyncio from langchain_mcp_adapters.client import MultiServerMCPClient from langchain_openai import ChatOpenAI from langchain_core.messages import HumanMessage, ToolMessage async def run(mcp_url: str, session_token: str): async with MultiServerMCPClient({ "contentful": { "transport": "streamable_http", "url": mcp_url, "headers": {"Authorization": f"Bearer {session_token}"}, } }) as client: tools = client.get_tools() tool_map = {t.name: t for t in tools} llm = ChatOpenAI(model="gpt-4o").bind_tools(tools) messages = [HumanMessage("Find blogPost entries in staging with an empty seoDescription")] while True: response = await llm.ainvoke(messages) messages.append(response) if not response.tool_calls: print(response.content) break for tc in response.tool_calls: result = await tool_map[tc["name"]].ainvoke(tc["args"]) messages.append(ToolMessage(content=str(result), tool_call_id=tc["id"])) asyncio.run(run(mcp_server_url, token))

Setup details are in the Virtual MCP server guide, and the reasoning behind the model is covered in when to use a Virtual MCP server.

Observability: what the agent actually did to your content

Contentful records the outcome of a write. It does not record which agent run produced it, under whose delegation, with what tool arguments, or what came back. For a CMS that is a specific problem, because the artifact is public.

What downstream tool-call logs capture

Scalekit's agent tool observability records every downstream tool call with full attribution: who authorized, which agent ran it, which tool, what scope, and the response. They are queryable and exportable to Datadog, Splunk, or any SIEM, with retention that varies by plan.

The separation that matters operationally is failure attribution. A 401 from a revoked Contentful grant, a 429 from the 7 per second CMA limit, and a tool rejected by the MCP app's environment allow-list are three different incidents with three different owners. Logs that collapse them into "tool call failed" cost you a debugging session per occurrence.

Why attribution is the CMS-specific problem

Run the agent under a shared PAT and your audit answer is "the integration user did it." Run it under a per-editor connected account and the answer is "this editor's agent run updated these fourteen entries at this timestamp under this scope."

That is the difference between a security review you pass and one you postpone. The wider argument is in audit trails for agent auth and access control for multi-tenant AI agents.

The credential problem that exists on both paths

Whichever path you pick, you end up holding one Contentful credential per editor. Forty editors across eight customer organizations is forty credential lifecycles, not one integration.

What you still have to build

Storage, encrypted at rest and isolated per tenant. Detection when a grant is revoked, which on the implicit-grant path means a 401 with no refresh primitive to fall back on. Re-consent flows when that happens. Revocation when an editor leaves, remembering that an org admin deauthorizing a CMA token only removes it from that organization; the token stays active for every other org it is authorized for.

None of that is provided by the MCP server or by the CMA. The token type differs between the two paths; the infrastructure obligation is identical. Token refresh in particular is a proactive problem, not a reactive one, as covered in handling token refresh for AI agents.

Where Scalekit fits

Scalekit's Contentful connector handles the OAuth flow, per-editor token storage in an encrypted token vault, and lifecycle management for both paths. Credentials never touch the agent runtime or the model context.

If your agent needs a CMA capability the vendor MCP server does not expose, such as webhooks or Releases, you can register it through bring your own connector and keep the same identity model across both. Related content surfaces are already in the catalog: Webflow MCP, Sanity MCP, and WordPress MCP.

Which one to build against

If an editor is present and the work is entry-level, build on the Remote MCP server. You get OAuth 2.1, per-environment tool gating an admin can manage, semantic search, and reference resolution without writing a schema. Point it at a sandbox environment and merge to master yourself.

If the agent runs on a schedule, reacts to webhooks, coordinates a Release, or reads published content at volume, use the APIs directly. App Identity, the CDA, and CMA bulk actions are not optional conveniences on that side; they are the only things that work.

The deciding question

Ask whether a human is present at execution time. If yes, MCP is the faster and better-governed path. If no, MCP has no answer and the CMA does.

Either way you are storing one Contentful credential per editor, watching it for silent revocation, and answering for what your agent published. That is the part that needs production-grade infrastructure, and it is the same on both paths.

Building Contentful agents?

Browse the Scalekit Contentful MCP connector or the full connector catalog. Working patterns to start from: the auto release notes agent and the competitive intelligence briefing agent.

Need a Contentful tool the connector does not expose yet, or a framework adapter that is not listed? Ask in the Scalekit Slack community, or talk to our engineers for immediate help.

No items found.
Agent
Auth Quickstart
On this page
Share this article
Agent
Auth Quickstart

Acquire enterprise customers with
‍zero upfront cost.

Every feature unlocked. No hidden fees.