create_campaign no longer ran inside the same public MCP layer that exposed it. MCP server authentication had become the harder problem. After I reworked the internal business-data MCP, Claude authenticated through Amazon Cognito and reached the tool through Amazon Bedrock AgentCore Gateway. A request interceptor checked the caller's identity, registry status, role, and scope; then a fixed write Lambda validated the input before n8n ever saw the request.
That is considerably more machinery than exposing an n8n workflow as an MCP tool. I made the change for one primary reason: authentication control.
The earlier MCP used bearer-token authentication, so it was not unauthenticated. But I now needed to know which human was connecting, whether that person's access was active, which tools they could discover, and who could change production data.
MCP server authentication became harder than the workflows
n8n is still one of the fastest ways I know to prototype an MCP layer. I can edit validation in a node, add a data source quickly, and inspect failed executions in one place.
I used the same approach when I built a five-tool MCP layer to validate an AI ticket agent. The point was to prove the workflow before a larger engineering investment. Stack purity would have slowed that validation.
This internal MCP started with the same speed-first logic. n8n made the tools useful and the executions visible. Nothing about moving the public MCP boundary to AWS changed the part n8n did well.
What changed was the responsibility I had assigned to it.
A shared bearer token could not express the access rules I needed
A protected MCP server is an OAuth resource server, not only a collection of tool definitions. The MCP authorization specification says a server must validate that an access token was issued for that server as the intended audience. It must accept only tokens valid for its own resources rather than treating a bearer token as a credential that can travel through the rest of the stack.
The bearer token protected an endpoint, but it could not express the new access rules. Each person needed an identity, a role, an active or disabled state, and a separate write scope. If the identity registry could not be checked, the request needed to fail closed.
I moved those decisions into AWS:
- Cognito handled OAuth and Google federation.
- AgentCore Gateway became the public MCP endpoint.
- A DynamoDB registry mapped approved identities to
user,admin, orowner. - A request interceptor checked token claims, application client, expiry, verified identity, registry status, role, and OAuth scopes.
- Gateway IAM could invoke only the approved Lambda functions.
AWS documents this pattern in its guidance for AgentCore Gateway inbound authorization and fine-grained access control. Request interceptors run before a target and can enforce tool-, operation-, or parameter-level access using JWT claims or an external registry.
The important change was not the AWS logo. Authentication and authorization moved into a boundary designed to make those decisions before workflow execution began.
That made MCP server authentication an enforceable boundary rather than a credential check buried inside the workflow layer.
Reads and writes now cross different trust boundaries
The read tools invoke fixed Lambda functions backed by curated views in a private RDS database. Those functions cannot run generic SQL, and MCP callers have no base-table privileges. A scheduled snapshot copies an explicit allowlist from the writable Supabase source into RDS every 15 minutes.
That creates an honest limitation: the read tools can lag behind a confirmed write. The write response returns the source-system result and warns that the read copy may not reflect it immediately. A cleaner security boundary did not make eventual consistency disappear.
Writes take a separate path:
Claude
-> Cognito OAuth
-> AgentCore Gateway
-> admin or owner + campaigns.write scope
-> fixed Lambda with a closed input schema
-> authenticated n8n webhook
-> Supabase source of truth

Only admin and owner roles with the campaigns.write scope can discover or invoke the write tools. The write Lambda rejects unknown fields, loads one dedicated n8n credential from AWS Secrets Manager, and can call only two fixed webhook paths. n8n then runs the existing parameterized create or update operation.
The inbound MCP token never reaches n8n. The workflow receives a separate machine credential instead, so a credential intended for the MCP resource does not become a general-purpose downstream token.
Keeping n8n reduced migration risk, but added an operational cost
I could have removed n8n and written both database operations directly in Lambda. For a new system with two narrow actions, that might be the cleaner design. It would remove a network hop, a webhook credential, and one runtime that can fail.
The existing workflows already contained working business rules, database credentials, parameterized queries, and useful execution history. Rewriting them would have changed the public security boundary and production mutation path at the same time.
I changed the higher-risk boundary first. Lambda narrows and validates the request; n8n executes the existing business operation. Duplicate campaign creation was tested as idempotent, and updates accept an expected_updated_at value so a stale request cannot silently overwrite a newer record.
I estimate the migration took approximately 10 hours. That covers moving the MCP and authorization boundary while retaining two working mutation workflows. It is not a measured estimate for rebuilding every tool from scratch.
The retained layer still has a price. It adds latency and another failure point. Its webhook secret needs rotation, the endpoints need monitoring, and the instance needs patching like any other production service. n8n's own security audit can detect unprotected webhooks, risky nodes, missing security settings, and an outdated instance.
That is a reasonable migration trade, not a reason to preserve the layer forever.
Lambda-only becomes attractive when the workflows become boring
For a new MCP with narrow, stable operations and proper tests, I would consider Gateway to Lambda to database. Fewer moving parts matter when Lambda can express the whole operation cleanly.
I would keep n8n behind the gateway when an action coordinates several APIs, business rules change frequently, execution history materially speeds up support, or an existing workflow has already earned production confidence. That is the same reason I still use n8n for conventional automation after self-hosting it on Render: control is valuable only while the operational cost remains worth paying.
For this MCP, n8n is no longer the front door. It is the execution layer behind a governed entrance. There are only two write paths into it, both authenticated, and neither accepts arbitrary instructions.
If those workflows eventually become boring database functions, I will move them into Lambda. Until then, rewriting working behavior would create architectural neatness without a proportional reduction in risk.
MCP server authentication is a boundary decision
Is n8n a bad place to run an MCP server?
No. It is a strong prototyping layer and can fit small, trusted deployments. The decision changes when the MCP needs per-user identity, scoped permissions, revocation, and governed production writes.
Why not put Cognito directly in front of n8n?
Authentication alone would not have created the fixed targets, narrow IAM permissions, private read layer, schema validation, or read-versus-write separation I needed. The gateway governed both tool discovery and invocation.
Would I start with the full AWS architecture?
Usually not. I would start with the smallest implementation that proves the tools and their data contracts. Once real users and production mutations enter the picture, I would define the trust boundaries before adding more tools.
If your MCP prototype is becoming a production service, do not begin by rewriting every workflow. Treat MCP server authentication as a boundary decision: find the component currently making the biggest security decision, move that decision to a boundary designed to enforce it, and then decide whether the existing execution layer is still earning its place.


