Skip to content

Commit a9da730

Browse files
committed
feat!: retire /sse and upgrade to workers-oauth-provider 1.2.1
/sse answers every request with a 410 naming /mcp. The OAuth protected resource is <origin>/mcp. Consent, Cloudflare sign-in state and remembered consent move to the provider's consent and upstream helpers.
1 parent ab883e5 commit a9da730

39 files changed

Lines changed: 774 additions & 1153 deletions
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
'@repo/mcp-common': minor
3+
'cloudflare-ai-gateway-mcp-server': minor
4+
'auditlogs': minor
5+
'cloudflare-autorag-mcp-server': minor
6+
'cloudflare-browser-mcp-server': minor
7+
'cloudflare-blog': minor
8+
'cloudflare-casb-mcp-server': minor
9+
'demo-day': minor
10+
'dex-analysis': minor
11+
'dns-analytics': minor
12+
'docs-ai-search': minor
13+
'graphql-mcp-server': minor
14+
'logpush': minor
15+
'cloudflare-radar-mcp-server': minor
16+
'containers-mcp': minor
17+
'stack-mcp': minor
18+
'workers-bindings': minor
19+
'workers-builds': minor
20+
'workers-observability': minor
21+
---
22+
23+
Retire the `/sse` URL and upgrade to `@cloudflare/workers-oauth-provider` 1.2.1.
24+
25+
- Every request to `/sse` now returns `410 Gone` with an `application/problem+json` body naming the server's `/mcp` URL, before any authentication. `/sse` used to serve Streamable HTTP as an alias of `/mcp`; clients configured with it must switch to `/mcp`. MCP SDK v1 and v2 clients show the message in their connection error. It is not a redirect, because OAuth clients that follow one reject the `/mcp` protected resource metadata with an error that never mentions `/mcp`.
26+
- The OAuth protected resource is now `<origin>/mcp`, and every token is bound to it. Grants bound to `/mcp` keep working. Grants bound to `/sse` fail their next refresh with `invalid_grant`, and the client signs in again at `/mcp`.
27+
- The consent page uses the provider's consent helpers. The authorization request stays server-side and the page posts only a single-use handle bound to the browser. Cancel now sends `access_denied` back to the MCP client. The page shows the redirect URI, the publishing domain of a Client ID Metadata Document client, and a warning when the tokens go to a local app.
28+
- Cloudflare sign-in uses `beginUpstream()` / `finishUpstream()` in place of the hand-rolled KV state and session cookie. Two tabs can authorize at once, and declining at Cloudflare sends `access_denied` back to the MCP client.
29+
- Remembered consent uses `isConsentRemembered()`. Approvals made before this change aren't carried over, so each browser sees the consent page once more.
30+
- Direct Cloudflare API tokens are validated for the `/mcp` resource.

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Model Context Protocol (MCP) is a [new, standardized protocol](https://modelcont
44

55
These MCP servers allow your [MCP Client](https://modelcontextprotocol.io/clients) to read configurations from your account, process information, make suggestions based on data, and even make those suggested changes for you. All of these actions can happen across Cloudflare's many services including application development, security and performance.
66

7-
Every server in this repository exposes the same stateless Streamable HTTP handler at `/mcp` and `/sse` through a fresh SDK v2 server factory. `/sse` remains as a URL compatibility alias; it does not use the deprecated HTTP+SSE transport. A legacy SSE `GET /sse` request receives a `410 Gone` Problem Details response with two migration options: configure the existing URL to use Streamable HTTP, or switch to the recommended `/mcp` URL for future compatibility. Modern 2026 requests and stateless 2025 requests share the same request-scoped implementation without an MCP protocol session. OAuth, credentials, account selection, application caches, and product Durable Objects remain application/security state where required.
7+
Every server in this repository exposes a stateless Streamable HTTP handler at `/mcp` through a fresh SDK v2 server factory. The retired `/sse` URL answers every request with a `410 Gone` Problem Details response that names the `/mcp` URL; MCP clients show its message in their connection error. Modern 2026 requests and stateless 2025 requests share the same request-scoped implementation without an MCP protocol session. OAuth, credentials, account selection, application caches, and product Durable Objects remain application/security state where required.
88

99
Cloudflare offers the following MCP servers. The domain-specific servers are included in this repository, while the recommended Code Mode server is maintained in [`cloudflare/mcp`](https://github.com/cloudflare/mcp):
1010

‎apps/ai-gateway/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ connections, with Cloudflare OAuth built-in.
66
It integrates tools powered by the [Cloudflare AI Gateway API](https://developers.cloudflare.com/ai-gateway/) to search
77
your AI Gateway logs, inspect prompts and responses, and get details about gateway usage.
88

9-
The `/mcp` and `/sse` URLs use the same stateless SDK v2 handler and create a fresh server with request-scoped auth/account context for every request. `/sse` is not the deprecated HTTP+SSE transport. OAuth grants and token validation remain durable security state; the server stores no MCP protocol session.
9+
The `/mcp` URL uses a stateless SDK v2 handler that creates a fresh server with request-scoped auth/account context for every request. The retired `/sse` URL returns `410 Gone` pointing at `/mcp`. OAuth grants and token validation remain durable security state; the server stores no MCP protocol session.
1010

1111
## 🔨 Available Tools
1212

‎apps/ai-gateway/src/auth-integration.spec.ts‎

Lines changed: 55 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -76,34 +76,45 @@ async function responseDocument(response: Response): Promise<Record<string, any>
7676
}
7777

7878
function helperOptions(): OAuthProviderOptions<Env> {
79+
// The worker's own provider configuration, so tokens minted here validate there.
7980
return {
8081
apiRoute: '/mcp',
8182
apiHandler: { fetch: () => new Response('unused') },
8283
defaultHandler: { fetch: () => new Response('unused') },
8384
authorizeEndpoint: '/oauth/authorize',
8485
tokenEndpoint: '/token',
85-
allowImplicitFlow: true,
86+
resourceMetadata: { resource: endpoint },
8687
}
8788
}
8889

89-
async function issueOAuthToken(resource: string) {
90+
async function s256(value: string): Promise<string> {
91+
const digest = new Uint8Array(
92+
await crypto.subtle.digest('SHA-256', new TextEncoder().encode(value))
93+
)
94+
return btoa(String.fromCharCode(...digest))
95+
.replace(/\+/g, '-')
96+
.replace(/\//g, '_')
97+
.replace(/=+$/, '')
98+
}
99+
100+
/** Complete an authorization with the provider helpers, then redeem the code at the worker's /token. */
101+
async function issueOAuthToken() {
90102
const helpers = getOAuthApi(helperOptions(), testEnv)
91103
const client = await helpers.createClient({
92104
redirectUris: ['https://client.example.com/callback'],
93105
tokenEndpointAuthMethod: 'none',
94-
// The provider validates registered capabilities, so the implicit flow
95-
// this helper uses to mint a token must be registered explicitly.
96-
grantTypes: ['implicit'],
97-
responseTypes: ['token'],
98106
})
99-
const result = await helpers.completeAuthorization({
107+
const verifier = 'auth-integration-verifier-'.repeat(3)
108+
const { redirectTo } = await helpers.completeAuthorization({
100109
request: {
101-
responseType: 'token',
110+
responseType: 'code',
102111
clientId: client.clientId,
103112
redirectUri: client.redirectUris[0],
104113
scope: ['account:read', 'aig:read'],
105114
state: 'test-state',
106-
resource,
115+
codeChallenge: await s256(verifier),
116+
codeChallengeMethod: 'S256',
117+
resource: endpoint,
107118
},
108119
userId: 'oauth-user',
109120
metadata: {},
@@ -114,8 +125,26 @@ async function issueOAuthToken(resource: string) {
114125
account: { id: 'oauth-account', name: 'OAuth account' },
115126
},
116127
})
117-
const token = new URLSearchParams(new URL(result.redirectTo).hash.slice(1)).get('access_token')
118-
if (!token) throw new Error('OAuth helper did not issue an access token')
128+
const code = new URL(redirectTo).searchParams.get('code')
129+
if (!code) throw new Error('OAuth helper did not issue an authorization code')
130+
131+
const response = await worker.fetch(
132+
new Request('https://ai-gateway.mcp.cloudflare.com/token', {
133+
method: 'POST',
134+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
135+
body: new URLSearchParams({
136+
grant_type: 'authorization_code',
137+
code,
138+
redirect_uri: client.redirectUris[0],
139+
client_id: client.clientId,
140+
code_verifier: verifier,
141+
}),
142+
}),
143+
testEnv,
144+
executionContext()
145+
)
146+
const { access_token: token } = (await response.json()) as { access_token?: string }
147+
if (!token) throw new Error(`Token endpoint did not issue an access token (${response.status})`)
119148
return token
120149
}
121150

@@ -127,7 +156,7 @@ afterEach(async () => {
127156

128157
describe('AI Gateway exported Worker authentication', () => {
129158
it('bridges a provider-validated OAuth token into a fresh SDK server and real tool call', async () => {
130-
const token = await issueOAuthToken(endpoint)
159+
const token = await issueOAuthToken()
131160
const response = await worker.fetch(toolRequest(token), testEnv, executionContext())
132161
const document = await responseDocument(response)
133162

@@ -137,29 +166,28 @@ describe('AI Gateway exported Worker authentication', () => {
137166
expect(getCloudflareClientMock).toHaveBeenCalledWith('oauth-upstream-token')
138167
})
139168

140-
it('serves the /sse URL through the same stateless handler with path-bound OAuth', async () => {
141-
const sseEndpoint = 'https://ai-gateway.mcp.cloudflare.com/sse'
142-
const token = await issueOAuthToken(sseEndpoint)
169+
it('answers /sse with a 410 naming /mcp, even with a valid OAuth token', async () => {
170+
const token = await issueOAuthToken()
143171
const response = await worker.fetch(
144-
toolRequest(token, sseEndpoint),
172+
toolRequest(token, 'https://ai-gateway.mcp.cloudflare.com/sse'),
145173
testEnv,
146174
executionContext()
147175
)
148-
const document = await responseDocument(response)
149176

150-
expect(response.status).toBe(200)
151-
expect(response.headers.get('mcp-session-id')).toBeNull()
152-
expect(document.result.content[0].text).toContain('oauth-account:oauth-upstream-token')
153-
expect(getCloudflareClientMock).toHaveBeenCalledWith('oauth-upstream-token')
177+
expect(response.status).toBe(410)
178+
expect(await response.json()).toMatchObject({ url: endpoint })
179+
expect(getCloudflareClientMock).not.toHaveBeenCalled()
154180
})
155181

156-
it('rejects an OAuth token bound to a different path on the same origin', async () => {
157-
const token = await issueOAuthToken('https://ai-gateway.mcp.cloudflare.com/other')
158-
const response = await worker.fetch(toolRequest(token), testEnv, executionContext())
182+
it('publishes /mcp as the protected resource', async () => {
183+
const response = await worker.fetch(
184+
new Request('https://ai-gateway.mcp.cloudflare.com/.well-known/oauth-protected-resource/mcp'),
185+
testEnv,
186+
executionContext()
187+
)
159188

160-
expect(response.status).toBe(401)
161-
expect(await response.json()).toMatchObject({ error: 'invalid_token' })
162-
expect(getCloudflareClientMock).not.toHaveBeenCalled()
189+
expect(response.status).toBe(200)
190+
expect(await response.json()).toMatchObject({ resource: endpoint })
163191
})
164192

165193
it('validates parallel API tokens through the exported Worker without leaking request props', async () => {

‎apps/auditlogs/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ connections, with Cloudflare OAuth built-in.
2929

3030
Audit logs summarize the history of changes made within your Cloudflare account. Audit logs include account level actions like zone configuration changes. The tool is powered by the [Audit Log API](https://developers.cloudflare.com/api/resources/accounts/subresources/logs/subresources/audit/methods/list/).
3131

32-
The `/mcp` and `/sse` URLs use the same stateless SDK v2 handler and create a fresh server with request-scoped auth/account context for every request. `/sse` is not the deprecated HTTP+SSE transport. OAuth grants and token validation remain durable security state; the server stores no MCP protocol session.
32+
The `/mcp` URL uses a stateless SDK v2 handler that creates a fresh server with request-scoped auth/account context for every request. The retired `/sse` URL returns `410 Gone` pointing at `/mcp`. OAuth grants and token validation remain durable security state; the server stores no MCP protocol session.
3333

3434
## 🔨 Available Tools
3535

‎apps/autorag/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ connections, with Cloudflare OAuth built-in.
3434

3535
It integrates tools powered by the [Cloudflare AutoRAG API](https://developers.cloudflare.com/autorag/) to allow you to access and query your account's AutoRAG instances.
3636

37-
The `/mcp` and `/sse` URLs use the same stateless SDK v2 handler and create a fresh server with request-scoped auth/account context for every request. `/sse` is not the deprecated HTTP+SSE transport. OAuth grants and token validation remain durable security state; the server stores no MCP protocol session.
37+
The `/mcp` URL uses a stateless SDK v2 handler that creates a fresh server with request-scoped auth/account context for every request. The retired `/sse` URL returns `410 Gone` pointing at `/mcp`. OAuth grants and token validation remain durable security state; the server stores no MCP protocol session.
3838

3939
## 🔨 Available Tools
4040

‎apps/browser-rendering/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ connections, with Cloudflare OAuth built-in.
66
It integrates tools powered by the [Cloudflare Browser Run API](https://developers.cloudflare.com/browser-run/) to fetch
77
web pages, convert them to markdown, and take screenshots.
88

9-
The `/mcp` and `/sse` URLs use the same stateless SDK v2 handler and create a fresh server with request-scoped auth/account context for every request. `/sse` is not the deprecated HTTP+SSE transport. OAuth grants and token validation remain durable security state; the server stores no MCP protocol session. The Browser Run session tools below manage application-level browser sessions, not MCP protocol sessions.
9+
The `/mcp` URL uses a stateless SDK v2 handler that creates a fresh server with request-scoped auth/account context for every request. The retired `/sse` URL returns `410 Gone` pointing at `/mcp`. OAuth grants and token validation remain durable security state; the server stores no MCP protocol session. The Browser Run session tools below manage application-level browser sessions, not MCP protocol sessions.
1010

1111
## 🔨 Available Tools
1212

‎apps/cloudflare-blog/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This is a [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server that provides tools for searching and reading the [Cloudflare Blog](https://blog.cloudflare.com).
44

5-
The `/mcp` and `/sse` URLs use the same stateless SDK v2 handler and create a fresh server for every request. `/sse` is not the deprecated HTTP+SSE transport. The handler supports modern MCP requests and stateless 2025 compatibility without an MCP protocol session.
5+
The `/mcp` URL uses a stateless SDK v2 handler that creates a fresh server for every request. The retired `/sse` URL returns `410 Gone` pointing at `/mcp`. The handler supports modern MCP requests and stateless 2025 compatibility without an MCP protocol session.
66

77
## 🔨 Available Tools
88

‎apps/cloudflare-one-casb/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This [Model Context Protocol (MCP)](https://modelcontextprotocol.io/introduction) server provides tools for inspecting Cloudflare One CASB integrations, assets, and asset categories. It supports Cloudflare OAuth and API-token authentication.
44

5-
The `/mcp` and `/sse` URLs use the same stateless SDK v2 handler and create a fresh server with request-scoped auth/account context for every request. `/sse` is not the deprecated HTTP+SSE transport. OAuth grants and token validation remain durable security state; the server stores no MCP protocol session.
5+
The `/mcp` URL uses a stateless SDK v2 handler that creates a fresh server with request-scoped auth/account context for every request. The retired `/sse` URL returns `410 Gone` pointing at `/mcp`. OAuth grants and token validation remain durable security state; the server stores no MCP protocol session.
66

77
## Available tools
88

‎apps/dex-analysis/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ connections, with Cloudflare OAuth built-in.
55

66
It integrates tools powered by the [Cloudflare DEX API](https://developers.cloudflare.com/api/resources/zero_trust/subresources/dex/) to provide visibility into device, network, and application performance across your Zero Trust organization
77

8-
The `/mcp` and `/sse` URLs use the same stateless SDK v2 handler and create a fresh server with request-scoped auth/account context for every request. `/sse` is not the deprecated HTTP+SSE transport. OAuth remains durable security state, and `WarpDiagReader` remains an application cache for downloaded diagnostics; no MCP protocol session is retained.
8+
The `/mcp` URL uses a stateless SDK v2 handler that creates a fresh server with request-scoped auth/account context for every request. The retired `/sse` URL returns `410 Gone` pointing at `/mcp`. OAuth remains durable security state, and `WarpDiagReader` remains an application cache for downloaded diagnostics; no MCP protocol session is retained.
99

1010
## 🔨 Available Tools
1111

0 commit comments

Comments
 (0)