Skip to content
Merged
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 .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ node_modules/
.env
dist/
*.log
.worktrees/
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export type ServerInfo = { name: string; version: string }
*/
export const SERVER_INFO: ServerInfo = { name: 'cloudflare-api', version: '0.1.0' }

/** User-Agent header sent on all outbound requests to Cloudflare APIs. */
export const USER_AGENT = 'cloudflare-mcp'

/**
* TypeScript declarations describing the `cloudflare` helper and `accountId`
* binding available to the `execute` tool's sandboxed code. Inlined into the
Expand Down
28 changes: 27 additions & 1 deletion src/utils/fetch-retry.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { USER_AGENT } from '../constants'

export interface RetryOptions {
maxRetries?: number
baseDelayMs?: number
Expand Down Expand Up @@ -46,9 +48,33 @@ export async function fetchWithRetry(
let lastResponse: Response | undefined
let lastError: unknown

// Inject User-Agent so Cloudflare can identify traffic from this server.
// When input is a Request, clone it to preserve its headers alongside the new header.
// When input is a string/URL, merge into init.headers as a plain object.
let fetchInput: RequestInfo
let fetchInit: RequestInit | undefined
if (input instanceof Request) {
const headers = new Headers(input.headers)
headers.set('User-Agent', USER_AGENT)
fetchInput = new Request(input, { headers })
fetchInit = init
} else {
fetchInput = input
// Spread existing headers as a plain object to preserve casing, then set User-Agent last
// so it always takes precedence over any caller-supplied value.
const existingHeaders =
init?.headers instanceof Headers
? Object.fromEntries(init.headers)
: ((init?.headers as Record<string, string> | undefined) ?? {})
fetchInit = {
...init,
headers: { ...existingHeaders, 'User-Agent': USER_AGENT }
}
}

for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
try {
const response = await fetch(input, init)
const response = await fetch(fetchInput, fetchInit)

if (response.status !== 429) {
return response
Expand Down
4 changes: 2 additions & 2 deletions tests/fetch-retry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ describe('fetchWithRetry', () => {
expect(mock).toHaveBeenCalledTimes(2)
})

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

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

expect(globalThis.fetch).toHaveBeenCalledWith('https://api.example.com/test', {
method: 'POST',
headers: { Authorization: 'Bearer token' },
headers: { Authorization: 'Bearer token', 'User-Agent': 'cloudflare-mcp' },
body: '{"key":"value"}'
})
})
Expand Down
12 changes: 6 additions & 6 deletions tests/non-codemode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,13 +464,13 @@ describe('createServer with codemode=false', () => {
account_id: 'acct-123'
})

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

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