Skip to content

Commit 75c4dbc

Browse files
authored
feat: set User-Agent: cloudflare-mcp on all outbound Cloudflare API requests (#203)
* chore: ignore .worktrees directory * feat: set User-Agent: cloudflare-mcp on all outbound Cloudflare API requests --------- Co-authored-by: Justin Hutchings <12853539+jhutchings1@users.noreply.github.com>
1 parent 3be5560 commit 75c4dbc

5 files changed

Lines changed: 39 additions & 9 deletions

File tree

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ node_modules/
33
.env
44
dist/
55
*.log
6+
.worktrees/

‎src/constants.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ export type ServerInfo = { name: string; version: string }
1212
*/
1313
export const SERVER_INFO: ServerInfo = { name: 'cloudflare-api', version: '0.1.0' }
1414

15+
/** User-Agent header sent on all outbound requests to Cloudflare APIs. */
16+
export const USER_AGENT = 'cloudflare-mcp'
17+
1518
/**
1619
* TypeScript declarations describing the `cloudflare` helper and `accountId`
1720
* binding available to the `execute` tool's sandboxed code. Inlined into the

‎src/utils/fetch-retry.ts‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { USER_AGENT } from '../constants'
2+
13
export interface RetryOptions {
24
maxRetries?: number
35
baseDelayMs?: number
@@ -46,9 +48,33 @@ export async function fetchWithRetry(
4648
let lastResponse: Response | undefined
4749
let lastError: unknown
4850

51+
// Inject User-Agent so Cloudflare can identify traffic from this server.
52+
// When input is a Request, clone it to preserve its headers alongside the new header.
53+
// When input is a string/URL, merge into init.headers as a plain object.
54+
let fetchInput: RequestInfo
55+
let fetchInit: RequestInit | undefined
56+
if (input instanceof Request) {
57+
const headers = new Headers(input.headers)
58+
headers.set('User-Agent', USER_AGENT)
59+
fetchInput = new Request(input, { headers })
60+
fetchInit = init
61+
} else {
62+
fetchInput = input
63+
// Spread existing headers as a plain object to preserve casing, then set User-Agent last
64+
// so it always takes precedence over any caller-supplied value.
65+
const existingHeaders =
66+
init?.headers instanceof Headers
67+
? Object.fromEntries(init.headers)
68+
: ((init?.headers as Record<string, string> | undefined) ?? {})
69+
fetchInit = {
70+
...init,
71+
headers: { ...existingHeaders, 'User-Agent': USER_AGENT }
72+
}
73+
}
74+
4975
for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
5076
try {
51-
const response = await fetch(input, init)
77+
const response = await fetch(fetchInput, fetchInit)
5278

5379
if (response.status !== 429) {
5480
return response

‎tests/fetch-retry.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ describe('fetchWithRetry', () => {
203203
expect(mock).toHaveBeenCalledTimes(2)
204204
})
205205

206-
it('passes through request init options', async () => {
206+
it('passes through request init options and injects User-Agent', async () => {
207207
const mockResponse = new Response('ok', { status: 200 })
208208
globalThis.fetch = vi.fn().mockResolvedValue(mockResponse)
209209

@@ -215,7 +215,7 @@ describe('fetchWithRetry', () => {
215215

216216
expect(globalThis.fetch).toHaveBeenCalledWith('https://api.example.com/test', {
217217
method: 'POST',
218-
headers: { Authorization: 'Bearer token' },
218+
headers: { Authorization: 'Bearer token', 'User-Agent': 'cloudflare-mcp' },
219219
body: '{"key":"value"}'
220220
})
221221
})

‎tests/non-codemode.test.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -464,13 +464,13 @@ describe('createServer with codemode=false', () => {
464464
account_id: 'acct-123'
465465
})
466466

467-
expect(globalThis.fetch).toHaveBeenCalledWith(
468-
'https://api.cloudflare.com/client/v4/accounts/acct-123/workers/scripts',
469-
expect.objectContaining({
470-
method: 'GET',
471-
headers: expect.objectContaining({ Authorization: 'Bearer test-token' })
472-
})
467+
const [calledUrl, calledOpts] = (globalThis.fetch as any).mock.calls[0]
468+
expect(calledUrl).toBe(
469+
'https://api.cloudflare.com/client/v4/accounts/acct-123/workers/scripts'
473470
)
471+
expect(calledOpts.method).toBe('GET')
472+
expect(calledOpts.headers['Authorization']).toBe('Bearer test-token')
473+
expect(calledOpts.headers['User-Agent']).toBe('cloudflare-mcp')
474474

475475
expect(result.isError).toBeFalsy()
476476
expect(result.content[0].text).toContain('my-worker')

0 commit comments

Comments
 (0)