Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ pnpm tsx src/simpleStreamableHttp.ts
| Task interactive server | Task-based execution with interactive server→client requests. | [`src/simpleTaskInteractive.ts`](src/simpleTaskInteractive.ts) |
| Hono Streamable HTTP server | Streamable HTTP server built with Hono instead of Express. | [`src/honoWebStandardStreamableHttp.ts`](src/honoWebStandardStreamableHttp.ts) |
| SSE polling demo server | Legacy SSE server intended for polling demos. | [`src/ssePollingExample.ts`](src/ssePollingExample.ts) |
| OAuth without DCR (proxy pattern) | OAuth proxy for upstream providers without Dynamic Client Registration. | [`src/oauthWithoutDcr.ts`](src/oauthWithoutDcr.ts) |

## OAuth demo flags (Streamable HTTP server)

Expand Down
104 changes: 104 additions & 0 deletions examples/server/src/README-oauthWithoutDcr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# OAuth Without Dynamic Client Registration (DCR) Example

Many OAuth providers (GitHub, Google, Azure AD, etc.) do not support [RFC 7591 Dynamic Client Registration](https://datatracker.ietf.org/doc/html/rfc7591). This example demonstrates how to build an MCP server that authenticates users through such a provider using a **proxy
pattern**.

## The problem

The MCP specification's OAuth flow expects an Authorization Server that supports DCR. When connecting to a provider that only accepts pre-registered OAuth apps, MCP clients cannot complete the standard registration step.

## The solution: OAuth proxy

The MCP server runs a lightweight OAuth Authorization Server that sits between the MCP client and the upstream provider:

```
MCP Client <--> OAuth Proxy (this server) <--> Upstream Provider (GitHub)
- Accepts DCR from clients - No DCR support
- Proxies auth to upstream - Pre-registered OAuth app
- Issues its own tokens - Issues upstream tokens
```

The proxy:

1. **Accepts DCR** from MCP clients (issuing proxy-level client credentials)
2. **Redirects authorization** to the upstream provider using pre-registered credentials
3. **Exchanges upstream tokens** and issues its own tokens to MCP clients
4. **Maps proxy tokens to upstream tokens** so the MCP server can call upstream APIs on behalf of the user

## Setup

### 1. Register an OAuth app with the upstream provider

For GitHub:

1. Go to [GitHub Developer Settings](https://github.com/settings/developers)
2. Click "New OAuth App"
3. Set the **Authorization callback URL** to `http://localhost:3001/callback`
4. Note the **Client ID** and generate a **Client Secret**

### 2. Set environment variables

```bash
export OAUTH_CLIENT_ID="your-github-client-id"
export OAUTH_CLIENT_SECRET="your-github-client-secret"
```

### 3. Run the server

From the SDK root:

```bash
pnpm --filter @modelcontextprotocol/examples-server exec tsx src/oauthWithoutDCR.ts
```

Or from within this package:

Check warning on line 55 in examples/server/src/README-oauthWithoutDcr.md

View check run for this annotation

Claude / Claude Code Review

README references wrong filename case (oauthWithoutDCR.ts vs oauthWithoutDcr.ts)

The README run commands on lines 51 and 57 reference `oauthWithoutDCR.ts` (uppercase DCR), but the actual file is `oauthWithoutDcr.ts` (lowercase). On case-sensitive filesystems (Linux), these commands will fail with file-not-found. The same typo appears in the .ts file's own usage comment (line 37).
Comment on lines +49 to +55
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The README run commands on lines 51 and 57 reference oauthWithoutDCR.ts (uppercase DCR), but the actual file is oauthWithoutDcr.ts (lowercase). On case-sensitive filesystems (Linux), these commands will fail with file-not-found. The same typo appears in the .ts file's own usage comment (line 37).

Extended reasoning...

Bug: Filename case mismatch in run commands

The file added by this PR is named oauthWithoutDcr.ts (lowercase dcr), as confirmed by the diff header and the filesystem. However, the accompanying README (README-oauthWithoutDcr.md) references the file as oauthWithoutDCR.ts (uppercase DCR) in two run commands:

  • Line 51: pnpm --filter @modelcontextprotocol/examples-server exec tsx src/oauthWithoutDCR.ts
  • Line 57: pnpm tsx src/oauthWithoutDCR.ts

Additionally, the TypeScript file itself contains the wrong case in its own usage docstring comment at line 37: OAUTH_CLIENT_ID=xxx OAUTH_CLIENT_SECRET=yyy tsx src/oauthWithoutDCR.ts.

Why this matters

On case-sensitive filesystems (Linux, which is the standard deployment target and CI environment), oauthWithoutDCR.ts and oauthWithoutDcr.ts are different filenames. Running the documented commands will produce a Cannot find module / file-not-found error. This would only work silently on case-insensitive filesystems like macOS (default) or Windows.

Step-by-step proof

  1. User clones the repo on a Linux machine
  2. Follows the README instructions and runs: pnpm tsx src/oauthWithoutDCR.ts
  3. Node/tsx looks for src/oauthWithoutDCR.ts on the filesystem
  4. The actual file is src/oauthWithoutDcr.ts — these are different paths on Linux
  5. The command fails with a file-not-found error

Note: the parent examples/server/README.md correctly references src/oauthWithoutDcr.ts (lowercase), so only the dedicated README and the .ts file's own comment are affected.

Fix

Replace oauthWithoutDCR.ts with oauthWithoutDcr.ts on lines 51 and 57 of the README, and on line 37 of the .ts file.

```bash
pnpm tsx src/oauthWithoutDCR.ts
```

### 4. Connect a client

Use the example OAuth client:

```bash
pnpm --filter @modelcontextprotocol/examples-client exec tsx src/simpleOAuthClient.ts
```

## Configuration

| Variable | Default | Description |
| --------------------- | --------------------------------------------- | ------------------------------------ |
| `OAUTH_CLIENT_ID` | (required) | Client ID from upstream provider |
| `OAUTH_CLIENT_SECRET` | (required) | Client secret from upstream provider |
| `MCP_PORT` | `3000` | Port for the MCP server |
| `PROXY_PORT` | `3001` | Port for the OAuth proxy server |
| `OAUTH_AUTHORIZE_URL` | `https://github.com/login/oauth/authorize` | Upstream authorization endpoint |
| `OAUTH_TOKEN_URL` | `https://github.com/login/oauth/access_token` | Upstream token endpoint |
| `OAUTH_SCOPES` | `read:user user:email` | Space-separated scopes for upstream |

## Adapting to other providers

To use Google instead of GitHub, set:

```bash
export OAUTH_AUTHORIZE_URL="https://accounts.google.com/o/oauth2/v2/auth"
export OAUTH_TOKEN_URL="https://oauth2.googleapis.com/token"
export OAUTH_SCOPES="openid email profile"
```

The proxy pattern works with any standard OAuth 2.0 provider. The only requirement is that you pre-register an OAuth application and provide the credentials via environment variables.

## Security considerations

This example is for **demonstration purposes**. For production use:

- Use HTTPS for all endpoints
- Persist client registrations and tokens in a database (not in-memory)
- Implement proper PKCE validation end-to-end
- Implement token refresh and revocation
- Restrict CORS origins
- Add rate limiting to the registration and token endpoints
- Validate redirect URIs strictly (exact match, no open redirects)
- Set appropriate token expiration times
- Consider adding CSRF protection to the authorization flow
Loading
Loading