# MCP server and OAuth for outside AI clients

Dec 2025 – Sep 2026 · Staff · Shipped

I built the product’s Model Context Protocol server, the standard plug-in interface that lets AI agents call outside tools. It exposes the product’s data and actions as 72 typed tools: the product’s own assistant uses it, and so can customers’ AI apps such as Claude, ChatGPT, Cursor and VS Code.

**Scale:** 72 tools · 2 transports · 2 AWS regions · 4 client families

- Permissions on every call: each tool runs with the signed-in user’s real role and application access; write tools are labelled destructive and risky tools are off.
- Standard OAuth sign-in for outside clients: a small service publishes the protected-resource metadata document (RFC 9728) and a token checker in each region validates tokens.
- Public endpoint protection: a web application firewall with a per-user rate limit and a block list, plus a per-company on/off switch.
- Usability for models: tool annotations (read-only, destructive, open-world), templated workflow guides and on-demand tool discovery, so the model sees only relevant tools.
- A plain REST endpoint exposes the same tools to systems that do not speak MCP.

**Stack:** Python, MCP, OAuth 2 / RFC 9728, JWT, AWS WAF, CDK

**My role:** Creator and primary author of the MCP server and the OAuth discovery service, and author of the MCP authorizer changes.

## Context

Serving two callers sets the constraints. Outside clients need a public endpoint and standard OAuth sign-in in more than one region, and every tool call has to carry the user’s real permissions, not a shared service account.

The tools are called by models, so they also have to be easy for a model to choose and to call in the right order.

## Timeline

- **Dec 2025:** Prototype inside the backend, then its own service.
- **Feb 2026:** Typed schemas and the integration-metadata tools.
- **Mar 2026:** Workflow guides consolidated.
- **Jul 2026:** OAuth discovery, the dedicated authorizer, per-region deploys, provisioned concurrency.
- **Aug 2026:** Tool annotations, the firewall, role and application scoping.
- **Sep 2026:** The same tools as a plain REST endpoint.

## Architecture

Outside clients discover sign-in, pass the firewall and a per-region token check; the product’s assistant uses an internal API. Both reach the same typed tools.

- **Callers:** Outside AI clients (Claude, ChatGPT, Cursor, VS Code); Other systems (Plain REST, no MCP needed); In-product assistant (The signed-in user’s session)
- **Edge · per region:** OAuth metadata (RFC 9728 document, no function to run); Firewall (Per-user rate limit, IP block list); Token checker (Region’s issuer and audiences); Internal API (Session-authenticated)
- **MCP server · AWS Lambda:** Transports (Streamable HTTP and SSE); Tool registry (72 typed tools, scoped to the caller)
- **Product:** Backend services (Invoked per tool call)

Flows:

1. Outside AI clients → OAuth metadata: Discover the authorization server and the token audience
2. Outside AI clients → Firewall: Tool calls with a bearer token
3. Other systems → Firewall: The same tools as a plain REST call
4. Firewall → Token checker: Rate-limited per user; known bad IPs blocked
5. Token checker → Transports: A valid token, with the user’s role and application access
6. In-product assistant → Internal API: Tool calls from the product’s own agent
7. Internal API → Transports: Same tools, same permission checks
8. Transports → Tool registry: Typed input, validated against the tool’s schema
9. Tool registry → Backend services: The product’s backend functions do the work

## Inside the system

### Tools a model can use well

- A schema-generating registry: Pydantic inputs, typed outputs and versioning.
- Annotations on every tool (read-only, destructive, open-world); policy create and update tools are marked destructive, and risky tools are off.
- Templated workflow guides, served as tools, teach the order of a query: read which filters exist, then filter by value.
- Covers alerts, assets, applications, identities, tags, policies and SLAs, automations, fixes, tasks and integration metadata.

### Permissions on every call

- Each call runs with the role and application access the authorizer passes on, read from the user’s real permissions, not hardcoded.
- A per-company switch turns the public endpoint on or off.

### Sign-in clients find on their own

- The protected-resource metadata document (RFC 9728) at the standard well-known path, from an API Gateway mock integration with per-region values set at deploy time.
- A dedicated OAuth-only authorizer in each region.
- New customer organizations are created ready for AI-client login from day one.

### Running a public endpoint

- A dedicated firewall with a per-user rate limit and an AWS-managed list of known bad IPs, answering 429 when a client goes over.
- Provisioned concurrency, so a client’s first connect does not wait on a cold start; ARM64 functions.
- Region-matrix deploy workflows.

## Hard problems

### Sign-in for any compliant client, in every region

**Problem.** Outside clients find their sign-in through OAuth discovery. The product runs in more than one region, each with its own issuer, and permissions are scoped by role and by application.

**What I did.**

- A tiny metadata service publishes the protected-resource document per region.
- A dedicated OAuth-only authorizer validates each token against its region’s issuer and audiences, and forwards role and application scope to the tool registry.
- A first authorizer attempt was replaced by this cleaner split.
- New customer organizations are set up MCP-ready when they are created.

**Result.** Sign-in merged in Jul 2026, role and application scoping in Aug: outside AI clients get single sign-on, and each user’s permissions apply on every tool.

### Teaching a model a two-step query

**Problem.** Filtering product data takes two steps: read which filters a data type supports, then filter by value. Every client’s model has to know that order.

**What I did.**

- Workflow guides served as tools, templated per ticketing vendor and for generic filtering.
- The filter guides consolidated into one set the model can discover.

**Result.** The order lives in the server, not in every client’s prompt.

## Decisions

- **Discovery:** chose an API Gateway mock integration with deploy-time values, over a function that serves the JSON. Nothing to run, scale or patch; each region’s values come from parameters at deploy time.
- **Callers:** chose two APIs (session and OAuth) over one tool registry, over a separate server per caller. One set of tools and one set of permission checks for the product’s agent and for outside clients.
- **Transport:** chose stateless Streamable HTTP on Lambda, with SSE kept, over a long-lived server process. Stateless requests fit Lambda; the SSE transport keeps older clients working.

## Outcomes

- 72 typed tools.
- 2 transports: Streamable HTTP and SSE.
- 2 AWS regions.
- Supported clients: Claude, ChatGPT, Cursor and VS Code, plus the product’s own assistant.

## Limits

- No request-volume or latency figures are quoted.
