Skip to content

Commit 8b6ed47

Browse files
wasaganickytonline
authored andcommitted
update readme with explanations (#45)
[README preview](https://github.com/pomerium/mcp-app-demo/tree/wasaga/readme)
1 parent fa16c19 commit 8b6ed47

3 files changed

Lines changed: 310 additions & 105 deletions

File tree

README.md

Lines changed: 230 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
1-
Welcome to the Pomerium chat app, a minimal chat application for showcasing remote MCP servers secured with [Pomerium](https://pomerium.com).
1+
Welcome to the Pomerium Chat, a minimal chat application for showcasing remote Model Context Protocol servers secured with [Pomerium](https://pomerium.com).
22

3-
## Getting Started
3+
# Quick start
4+
5+
## Pre-requisites
6+
7+
1. Linux or MacOS host
8+
2. Docker and Docker Compose
9+
3. Your machine should have port 443 exposed to the internet so that it could acquire TLS certificates from LetsEncrypt and OpenAI could call your MCP server endpoints.
10+
4. OpenAI API Key
11+
12+
## Quickstart
413

514
### Environment Variables
615

@@ -10,7 +19,225 @@ Create a `.env` file in the root directory and add the following environment var
1019
OPENAI_API_KEY=your_api_key_here
1120
```
1221

13-
### Development
22+
### Pomerium Config
23+
24+
Update [`pomerium-config.yaml`](./pomerium-config.yaml) and replace **YOUR-DOMAIN** with the subdomain you control. Create A DNS records for relevant hosts (or **`*.YOUR-DOMAIN`**).
25+
26+
By default, the access policy limits access to users with emails in **YOUR-DOMAIN**. See [policy language reference](https://www.pomerium.com/docs/internals/ppl) if you need to adjust it.
27+
28+
### Docker Compose
29+
30+
See [`docker-compose.yaml`](./docker-compose.yaml) file in this repo.
31+
32+
```bash
33+
docker compose up -d
34+
```
35+
36+
### Testing
37+
38+
Now you should be able to navigate to `https://mcp-app-demo.YOUR-DOMAIN/`.
39+
A sign-in page would open. After you signed in, you should be redirected to the application itself.
40+
41+
There should be a demo database server (Northwind DB) acessible and in Connected status. Click on it to use it in the conversation.
42+
43+
Now you may ask some questions like "What were our sales by year", and see how OpenAI large language model inference would interact with the MCP database server running on your computer to obtain the answers.
44+
45+
# How does it work
46+
47+
## Token Vocabulary
48+
49+
- **External Token (TE):**
50+
An externally-facing token issued by Pomerium that represents the user's session. This token is used by external clients (such as Claude.ai, OpenAI, or your own apps) to authenticate requests to Pomerium-protected MCP servers.
51+
Example: The token you provide to an LLM API or agentic framework to allow it to call your MCP server.
52+
53+
- **Internal Token (TI):**
54+
An internal authentication token that Pomerium obtains from an upstream OAuth2 provider (such as Notion, Google Drive, GitHub, etc.) on behalf of the user. This token is never exposed to external clients. Pomerium uses this token to authenticate requests to the upstream service when proxying requests to your MCP server.
55+
56+
Pomerium acts as a secure gateway between Model Context Protocol (MCP) clients and servers. It provides authentication and authorization for local HTTP MCP servers, using OAuth 2.1 flows. This setup is especially useful when your MCP server needs to access upstream APIs that require OAuth tokens (such as Notion, Google Drive, GitHub, etc.).
57+
58+
It also enables you to build internal applications that use agentic frameworks or LLM APIs capable of invoking MCP servers, as demonstrated in this repository.
59+
60+
To understand this setup, let's look at how an MCP client communicates with MCP servers that are protected by Pomerium.
61+
62+
## 1. Exposing an Internal MCP Server to a Remote Client
63+
64+
Suppose you want to allow an external MCP client (like Claude.ai) to access your internal MCP server, but you want to keep it secure. Pomerium sits in front of your server and manages authentication and authorization for all incoming requests.
65+
66+
This means you can safely share access to internal resources (like a database) with external clients, without exposing them directly to the internet.
67+
68+
You configure your Pomerium Route as usual with an additional `mcp` property that signifies that this route represents a Model Context Protocol server route.
69+
70+
```yaml
71+
routes:
72+
- from: https://my-mcp-server.your-domain.com
73+
to: http://my-mcp-server.int:8080/mcp
74+
name: My MCP Server
75+
mcp: {}
76+
```
77+
78+
```mermaid
79+
sequenceDiagram
80+
actor U as User
81+
participant C as MCP Client
82+
participant P as Pomerium
83+
participant S as MCP Server
84+
U ->> C: Adds a server URL
85+
C ->> P: Registers client, initiates auth
86+
P ->> C: Sign-in URL
87+
C ->> U: Redirect to sign-in URL
88+
U ->> P: Sign-in
89+
P ->> C: Redirect to client
90+
C ->> P: Obtain Token
91+
C ->> P: GET https://mcp-server Authorization: Bearer Token
92+
P ->> S: Proxy request to MCP Server
93+
```
94+
95+
## 2. MCP Server Needs Upstream OAuth
96+
97+
If your MCP server needs to access an upstream service that requires OAuth (for example, GitHub or Google Drive), Pomerium can handle the OAuth flow for you. Here’s how the process works:
98+
99+
1. The user adds the MCP server URL in the client (e.g., Claude.ai).
100+
2. The client registers with Pomerium and starts authentication.
101+
3. Pomerium gives the client a sign-in URL, which is shown to the user.
102+
4. The user signs in to Pomerium, then is redirected to the upstream OAuth provider.
103+
5. The user authenticates with the upstream provider. The provider returns an **Internal Token (TI)** to Pomerium.
104+
6. Pomerium finishes the sign-in and redirects the user back to the client.
105+
7. The client receives an **External Token (TE)** from Pomerium.
106+
8. The client uses **TE** to make requests to the MCP server.
107+
9. Pomerium refreshes the upstream token (**TI**) as needed and proxies requests to the MCP server, passing **TI** in the `Authorization` header.
108+
109+
**Key benefits:**
110+
111+
- External clients (like Claude.ai) never see your upstream OAuth tokens.
112+
- Your MCP server always receives a valid upstream token.
113+
- The MCP server can remain stateless and does not need to manage OAuth flows or tokens.
114+
115+
**Route configuration:**
116+
117+
```yaml
118+
routes:
119+
- from: https://github.your-domain
120+
to: http://github-mcp.int:8080/mcp
121+
name: GitHub
122+
mcp:
123+
upstream_oauth2:
124+
client_id: xxxxxxxxxxxx
125+
client_secret: yyyyyyyyy
126+
scopes: ['read:user', 'user:email']
127+
endpoint:
128+
auth_url: 'https://github.com/login/oauth/authorize'
129+
token_url: 'https://github.com/login/oauth/access_token'
130+
```
131+
132+
```mermaid
133+
sequenceDiagram
134+
actor U as User
135+
participant C as MCP Client
136+
participant O as Upstream OAuth
137+
participant P as Pomerium
138+
participant S as MCP Server
139+
U ->> C: Adds a server URL
140+
C ->> P: Registers client, initiates auth
141+
P ->> C: Sign-in URL
142+
C ->> U: Redirect to sign-in URL
143+
U ->> P: Sign-in
144+
P ->> U: Redirect to upstream OAuth
145+
U ->> O: Authenticate with upstream OAuth
146+
O ->> P: Return Internal Token (TI)
147+
P ->> C: Redirect to client
148+
C ->> P: Obtain External Token (TE)
149+
C ->> P: GET https://mcp-server Authorization: Bearer (TE)
150+
P ->> O: Refresh (TI) if necessary
151+
P ->> S: Proxy request to MCP Server, Bearer (TI)
152+
```
153+
154+
### 3. Calling internal MCP server from your app
155+
156+
Some inference APIs, such as the [OpenAI API](https://platform.openai.com/docs/guides/tools-remote-mcp) and [Claude API](https://docs.anthropic.com/en/docs/agents-and-tools/mcp-connector), now support direct invocation of MCP servers. This trend is expected to grow, and many agentic frameworks are adding support for MCP server calls. You can also implement MCP tool calls manually in your app using LLM function calling capabilities. All these approaches require providing an `Authorization: Bearer` **External Token (TE)** for the MCP server so that requests can be securely routed through Pomerium.
157+
158+
If you are building your own internal application and need to obtain such a token, Pomerium offers a _client MCP mode_ for routes. By setting the `mcp.pass_upstream_access_token` option, Pomerium will supply your upstream application with an `Authorization: Bearer` **External Token (TE)** representing the current user session. You can then pass this token to external LLMs or agentic frameworks, allowing them to access MCP servers behind Pomerium according to your authorization policy.
159+
160+
The following flow illustrates this process, assuming the user is already authenticated with Pomerium:
161+
162+
```mermaid
163+
sequenceDiagram
164+
actor U as User
165+
participant C as Your App Backend
166+
participant P as Pomerium
167+
participant S as MCP Server
168+
participant I as LLM API
169+
U ->> P: GET https://mcp-app-demo.your-domain.com
170+
P ->> C: GET http://mcp-app-demo:3000 Authorization: Bearer (TE)
171+
C ->> I: call tool https://mcp-server.your-domain Authorization: Bearer (TE)
172+
I ->> P: GET https://mcp-server.your-domain Authorization: Bearer (TE)
173+
C ->> P: GET https://mcp-server
174+
```
175+
176+
Example route configuration:
177+
178+
```yaml
179+
routes:
180+
- from: https://mcp-app-demo.your-domain.com
181+
to: http://mcp-app-demo:3000
182+
mcp:
183+
pass_upstream_access_token: true
184+
policy: {} # define your policy here
185+
- from: https://mcp-server.your-domain.com
186+
to: http://mcp-server.int:8080/mcp
187+
name: My MCP Server
188+
mcp: {}
189+
policy: {} # define your policy here
190+
```
191+
192+
### 4. Listing available MCP servers from your app
193+
194+
You can provide users with a dynamic list of MCP servers protected by the same Pomerium instance as your application. To do this, issue an HTTP request to your app backend using the same `Authorization: Bearer` token your backend received. The response will include the list and connection status of each MCP server upstream available to this Pomerium cluster.
195+
196+
The **connected** property indicates whether the current user has all required internal tokens for upstream OAuth (if needed):
197+
198+
- **true** – The user has all required internal tokens from upstream OAuth providers, or none are required for this server.
199+
- **false** – The user needs to authenticate with the upstream OAuth provider before accessing this MCP server.
200+
201+
A later section will explain how to ensure your user has all required internal tokens.
202+
203+
```
204+
GET https://mcp-demo-app.yourdomain.com/ HTTP/1.1
205+
Accept: application/json
206+
Authorization: Bearer (TE)
207+
208+
Content-Type: application/json
209+
{
210+
"servers": [
211+
{
212+
"name": "DB",
213+
"url": "https://db-mcp.your-domain.com",
214+
"connected": true
215+
},
216+
{
217+
"name": "GitHub",
218+
"url": "https://github-mcp.your-domain.com",
219+
"connected": false
220+
}
221+
]
222+
}
223+
```
224+
225+
## 5. Ensuring your current user has authenticated with an upstream OAuth2 provider
226+
227+
If your target MCP server shows `connected: false`, the user needs to authenticate with the required upstream OAuth2 provider.
228+
To do this, redirect the user's browser to the special `/.pomerium/mcp/connect` path on the MCP server route (for example: `https://db-mcp.your-domain.com/.pomerium/mcp/connect`).
229+
Include a `redirect_url` query parameter that points back to your application's page—this is where the user should return after authentication, and where you can reload the MCP server list and their connection status.
230+
231+
**Note:** For security, the `redirect_url` must be a host that matches one of your MCP Client routes.
232+
233+
After the user completes authentication, the MCP server's `connected` status should become `true`.
234+
235+
## 6. Obtaining User Details
236+
237+
To access the authenticated user's identity and claims, both your MCP client application and MCP server should read the [`X-Pomerium-Assertion`](https://www.pomerium.com/docs/get-started/fundamentals/core/jwt-verification#manually-verify-the-jwt) HTTP header.
238+
This header contains a signed JWT with user information, which you can decode and verify to obtain details such as the user's email, name, and other claims.
239+
240+
# Development
14241
15242
To run this application in development mode:
16243
@@ -30,15 +257,6 @@ npm run build
30257
npm run start
31258
```
32259

33-
### Docker Deployment
34-
35-
You can also run the application using Docker:
36-
37-
```bash
38-
docker build -t mcp-app-demo .
39-
docker run -p 3000:3000 -e OPENAI_API_KEY=your_api_key_here mcp-app-demo
40-
```
41-
42260
## Features
43261

44262
- AI-powered chat interface using OpenAI
@@ -87,96 +305,3 @@ pnpx shadcn@latest add button
87305
## Routing
88306

89307
This project uses [TanStack Router](https://tanstack.com/router). The initial setup is a file based router. Which means that the routes are managed as files in `src/routes`.
90-
91-
### Adding A Route
92-
93-
To add a new route to your application just add another a new file in the `./src/routes` directory.
94-
95-
TanStack will automatically generate the content of the route file for you.
96-
97-
Now that you have two routes you can use a `Link` component to navigate between them.
98-
99-
### Adding Links
100-
101-
To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`.
102-
103-
```tsx
104-
import { Link } from '@tanstack/react-router'
105-
```
106-
107-
Then anywhere in your JSX you can use it like so:
108-
109-
```tsx
110-
<Link to="/about">About</Link>
111-
```
112-
113-
This will create a link that will navigate to the `/about` route.
114-
115-
More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent).
116-
117-
### Using A Layout
118-
119-
In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you use the `<Outlet />` component.
120-
121-
Here is an example layout that includes a header:
122-
123-
```tsx
124-
import { Outlet, createRootRoute } from '@tanstack/react-router'
125-
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
126-
127-
import { Link } from '@tanstack/react-router'
128-
129-
export const Route = createRootRoute({
130-
component: () => (
131-
<>
132-
<header>
133-
<nav>
134-
<Link to="/">Home</Link>
135-
<Link to="/about">About</Link>
136-
</nav>
137-
</header>
138-
<Outlet />
139-
<TanStackRouterDevtools />
140-
</>
141-
),
142-
})
143-
```
144-
145-
The `<TanStackRouterDevtools />` component is not required so you can remove it if you don't want it in your layout.
146-
147-
More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts).
148-
149-
## Data Fetching
150-
151-
There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered.
152-
153-
For example:
154-
155-
```tsx
156-
const peopleRoute = createRoute({
157-
getParentRoute: () => rootRoute,
158-
path: '/people',
159-
loader: async () => {
160-
const response = await fetch('https://swapi.dev/api/people')
161-
return response.json() as Promise<{
162-
results: {
163-
name: string
164-
}[]
165-
}>
166-
},
167-
component: () => {
168-
const data = peopleRoute.useLoaderData()
169-
return (
170-
<ul>
171-
{data.results.map((person) => (
172-
<li key={person.name}>{person.name}</li>
173-
))}
174-
</ul>
175-
)
176-
},
177-
})
178-
```
179-
180-
# Learn More
181-
182-
You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com).

docker-compose.yaml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
services:
2+
# PostgreSQL database is required for Pomerium to interact with external MCP Clients such as claude.ai
3+
postgres:
4+
image: postgres:17
5+
environment:
6+
POSTGRES_USER: postgres
7+
POSTGRES_PASSWORD: postgres
8+
POSTGRES_DB: pomerium
9+
POSTGRES_HOST_AUTH_METHOD: trust
10+
ports:
11+
- 5432:5432
12+
volumes:
13+
- postgres-data:/var/lib/postgresql/data
14+
#
15+
# Pomerium is a secure access management solution that provides identity-aware access to applications and services.
16+
# It acts as a reverse proxy, managing authentication and authorization for web applications.
17+
pomerium:
18+
image: pomerium/pomerium:main
19+
ports:
20+
- '443:443'
21+
- '80:80'
22+
volumes:
23+
- ./pomerium-config.yaml:/pomerium/config.yaml
24+
- pomerium-autocert:/data/autocert
25+
#
26+
# MCP App Demo is a sample application that demonstrates the capabilities of Pomerium's Model Context Protocol integration (this repo).
27+
#
28+
mcp-app-demo:
29+
restart: unless-stopped
30+
image: pomerium/mcp-app-demo:main
31+
environment:
32+
__VITE_ADDITIONAL_SERVER_ALLOWED_HOSTS: mcp-app-demo
33+
env_file: .env
34+
expose:
35+
- 3000
36+
ports:
37+
- 3000:3000
38+
volumes:
39+
postgres-data:
40+
pomerium-autocert:

0 commit comments

Comments
 (0)