Project at a glance
- Client: A SaaS platform vendor
- Who uses it: The vendor’s own customers. Hundreds of customer organisations use the solution, each with one or more of its own apps
- Engagement: Development and DevOps: architecture, implementation and ongoing support
- Launched: September 2026
- Core technologies: Amazon Bedrock AgentCore Gateway, Amazon Bedrock (Claude model), AWS Lambda, Amazon Cognito, Amazon VPC Lattice, AWS CloudFormation, AWS IAM, AWS Secrets Manager, AWS KMS, Amazon ECS on AWS Fargate, GitHub OIDC, Model Context Protocol (MCP)
Project background
Our client, a SaaS platform vendor, offers an AI assistant inside its product. It runs on a Claude model through Amazon Bedrock. An assistant is only as useful as the tools it can reach. The vendor can give it every tool it knows how to build: search, reports, exports. Then customers ask for the one thing the product can’t know: their systems. Their CRM, their scoring model, their internal workflow.
The Model Context Protocol (MCP) solves half of this. It gives every AI client, whether a chat assistant, an IDE or an autonomous agent, one standard way to discover tools and call them. The other half is the platform’s problem. Once customers can put their own code behind a shared MCP endpoint, the platform has to answer questions most MCP tutorials skip:
- Which tools does this user see?
- Who is the tool acting for, and can it trust that?
- What stops one customer’s tools from reaching another customer’s users?
The vendor asked BlueGrid.io to answer those questions in production. We designed, built and now support a single MCP endpoint on AWS. It serves the platform’s own tools alongside tools written by the vendor’s customers, scoped to each caller. The vendor’s existing API had to stay as it was.
BlueGrid.io’s role
BlueGrid.io worked as the vendor’s development and DevOps team for the whole lifecycle:
- Architecture: the trust model, the gateway and interceptor design, per-app isolation and the provisioning model.
- Development: the OAuth broker, the internal MCP server for the platform API, the Lambda interceptors and the provisioning control plane.
- DevOps: infrastructure as code, the per-app deployment pipeline with GitHub OIDC, observability and alerting.
- Ongoing support: running and improving the platform after the September 2026 launch.
What the customer gets
For the vendor’s customers, the experience is deliberately simple:
- Write a function and declare its tools: a name, a description and an input schema.
- Push it to their repository. The platform builds and deploys it.
- The tools appear in the assistant they already use, behind the login they already have, for exactly the users who are allowed to see them.
There is no MCP server to host, no OAuth to implement, no endpoint to publish and no separate security review for each tool. Their tools sit next to the platform’s own, so the assistant can combine them in one conversation. For example, it can pull data with a platform tool, then score it with the customer’s model.
MCP in two calls
For this design, two MCP methods matter:
tools/list: the client asks what tools exist. The answer is a catalogue of names, descriptions and JSON input schemas. The model reads it to decide what it can do.tools/call: the client invokes one tool by name, with arguments that match its schema.
Everything else (initialisation, notifications, capability negotiation) is protocol traffic. In a multi-tenant product, both calls have to be personal. Two users of the same assistant, from different customers, must get different catalogues. And every call has to be checked again, because a client can always send a tool name it was never shown.
MCP also defines how clients sign in: OAuth 2.1, discovered from the server. Any compliant client can connect to a protected MCP server without custom integration code, as long as the server supports that flow.
The trust model: every tool belongs to an app
Customers don’t publish loose tools. They publish an app: a small piece of code plus the list of tools it offers. The app is what answers the hard questions:
- Owner: the customer that built it.
- Audience: which customer accounts may see and use it.
- Boundary: its own compute, permissions, secrets and data, shared with no one.
So “can this user call this tool?” becomes “is this user’s account entitled to this app?”. That is one question with one answer, checked in one place. The entitlements come from the vendor’s existing platform, so no parallel permission system was introduced.
Solution architecture

One front door
Every MCP client is configured with a single URL. We built an OAuth broker that runs the authorization flow MCP clients expect, including server discovery and dynamic client registration. It is backed by an Amazon Cognito user pool, so users sign in with their existing product login. No customer runs their own auth.
Amazon Bedrock AgentCore Gateway as the MCP server
The gateway validates the JWT on every request. It merges tools from several targets into one catalogue:
- The platform target is the product’s existing API. A custom internal MCP server exposes it as MCP tools, and the gateway reaches it privately over Amazon VPC Lattice. Nothing about the existing API had to change.
- Each customer app is its own target, backed by an AWS Lambda function that runs the customer’s code.
Tool names carry their owner
The gateway prefixes every tool with its target name, <target>___<tool>, and every target is named after its app. So app-a___create_report always says where it came from. The prefix also turned out to be a security boundary (see the lessons below).
Two Lambda interceptors make the catalogue personal
AgentCore Gateway can run a Lambda function on every MCP request and response. We use one of each:
- The response interceptor filters
tools/listfor each caller. It asks the platform’s entitlement service which apps the caller’s account may use, then removes every other app’s tools. A user never learns that another customer’s tools exist. The model also gets a shorter, more relevant catalogue, so it isn’t tempted by tools it could never use. - The request interceptor guards
tools/call. For a customer tool, it checks the entitlement again, so a hidden tool can’t be called by guessing its name. It then adds the caller’s verified identity (user and account) to the call. That way the customer’s code knows who it is acting for without implementing any auth itself.
Entitlement answers are cached for five minutes per caller. Every MCP message passes through the interceptors, not only tool calls, so the cache means the lookup is paid once per session instead of once per message.

Isolation by construction
Each customer app is deployed as its own AWS CloudFormation stack. Isolation comes from how each stack is built, so it doesn’t depend on runtime code being careful:
- Its own permissions. Every app runs with its own IAM role, and every such role is capped by a permissions boundary. Even a mistake in a template can’t produce a role with more power than the boundary allows.
- Its own secrets. API keys and credentials live in AWS Secrets Manager, encrypted with a customer-managed AWS KMS key and readable only by that app.
- Its own data. The app reaches platform data with an app-scoped token, and anything it stores lives in its own index.
- Only the gateway can call it. Each function’s resource policy allows invocation only from this gateway. Nothing else in the account can reach a customer’s tool directly.
- No long-lived credentials. The customer’s repository deploys through GitHub OIDC into a role that can only update that one app.
- Private by default. Functions run inside the VPC, and the app’s other entry points, such as its scheduled jobs, are private.
An app can offer up to 25 tools. When a customer changes an app’s tool list, the platform updates the gateway for them. Customers never touch gateway configuration.
Provisioning as a product
A small control plane on Amazon ECS on AWS Fargate turns “an app was created in the UI” into running infrastructure:
- It renders the stack and applies it. The control plane renders the CloudFormation template directly and applies it. Desired state is declarative: re-sending an app’s definition brings it back to that state, and leaving a field out (jobs, tools) keeps what is already deployed.
- It reconciles the app’s gateway target with its declared tools, up to 25 per app. AgentCore target updates are asynchronous. So every write first waits for the target to leave
CREATING/UPDATING, retries “update in progress” rejections, and waits again for the result. - It reports the real outcome, not the accepted request. Each stack operation is watched until it settles. The team then sees either “infrastructure ready (CREATE_COMPLETE, 1m40s)” or the first failing resource and its reason.
- It inventories the fleet every five minutes. It compares stacks, gateway targets and schedules, and flags drift such as a failed stack, a missing target or an orphaned schedule.
Lessons learned: security and isolation
Filtering tools/list is UX. Checking tools/call is security. Hiding a tool from the catalogue keeps the model focused and stops users stumbling into tools they can’t use. But a client can send any tool name it likes. The entitlement check has to run again on every call, independently of what was listed.
Fail closed, but loudly. When the entitlement lookup fails, customer tools are hidden and denied while platform tools keep working. That is the safe direction, but “users silently lost their tools” is still an outage. Every fail-closed decision emits a metric, and that metric has an alert. A failed lookup is cached for the same five minutes as a successful one. During a downstream outage, each caller pays for one slow lookup per window, instead of creating a retry storm against a service that is already struggling.
Treat the tool namespace as a security boundary. Early on, a gateway configuration change made the platform’s own tools arrive without their prefix. The interceptor did exactly what it was told: unprefixed meant unknown, so it filtered all of them out, silently. Now two things count as anomalies that raise an alert, not routine filtering: any tool that arrives without a prefix, and any platform tool that gets dropped.
Never trust identity the client can write. The customer’s tool needs to know who is calling, and the easy way to tell it is a field in the tool arguments. But the client controls the arguments. So that field lives in a reserved namespace, and the interceptor always clears it before writing the verified identity. A value supplied by the client is treated as an attempted spoof. Clearing is a separate, explicitly tested step, so a later refactor can’t quietly drop it.
Hardening can break things silently. We added a confused-deputy condition (aws:SourceAccount) to a role that a managed AWS service assumes in order to write logs. The configuration still validated. But that service’s delivery path assumes the role without the key, so no log lines were written and nothing reported an error. We found it by comparing against a working setup elsewhere. After tightening a trust policy for a managed service, verify the data path end to end, not just the configuration call.
Results
The solution went live in September 2026. Within its first month, hundreds of the vendor’s customer organisations were using it, some with a single app and others with several. For the vendor and its customers, this means:
- Time to value: a customer goes from “I have a function” to “my assistant can use it” with a push, not a project.
- Works with the clients they already use: anything that speaks MCP connects to one URL with their existing login.
- Governance inherited, not rebuilt: login, entitlements, isolation and audit come from the platform, so there is no new security review for every tool.
- One assistant, not N integrations: platform tools and the customer’s own tools work together in the same conversation, visible only to the right people.
- No change to the existing product API: the platform’s tools are exposed through MCP privately, without reworking the API behind them.
- Operations the vendor can trust: provisioning reports real outcomes, drift is caught every five minutes, and every fail-closed decision is measured and alerted on.
A pattern beyond one product
The approach applies beyond this platform: give every tool an owner and an audience, put one policy layer in front of every MCP message, and let infrastructure as code enforce isolation instead of hoping runtime code does.
Technology stack
- MCP and AI: Model Context Protocol, Amazon Bedrock (Claude model), Amazon Bedrock AgentCore Gateway
- Identity: OAuth 2.1 broker (custom), Amazon Cognito, JWT validation
- Compute: AWS Lambda (customer apps and interceptors), Amazon ECS on AWS Fargate (control plane), custom internal MCP server
- Networking: Amazon VPC, Amazon VPC Lattice
- Security: AWS IAM roles with permissions boundaries, Lambda resource policies, AWS Secrets Manager, AWS KMS customer-managed keys
- Infrastructure and delivery: AWS CloudFormation (one stack per app), GitHub with OIDC federation
- Observability: fail-closed and anomaly metrics with alerting, fleet drift inventory