From 215ab839b127df3fa6ee7372312a8852e4a1aa5d Mon Sep 17 00:00:00 2001 From: Justin Hutchings Date: Mon, 24 Aug 2026 08:24:24 -0700 Subject: [PATCH 1/2] chore: ignore .worktrees directory --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 40bc1f2..2e00c03 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ node_modules/ .env dist/ *.log +.worktrees/ From 871d0e6eb83c5726d8b1c8260febe416161c0fa2 Mon Sep 17 00:00:00 2001 From: Justin Hutchings <12853539+jhutchings1@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:13:26 -0700 Subject: [PATCH 2/2] feat: set User-Agent: cloudflare-mcp on all outbound Cloudflare API requests --- src/constants.ts | 3 +++ src/utils/fetch-retry.ts | 28 +++++++++++++++++++++++++++- tests/fetch-retry.test.ts | 4 ++-- tests/non-codemode.test.ts | 12 ++++++------ 4 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 2b1e57d..cdc79cc 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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 diff --git a/src/utils/fetch-retry.ts b/src/utils/fetch-retry.ts index 47f993d..a636a55 100644 --- a/src/utils/fetch-retry.ts +++ b/src/utils/fetch-retry.ts @@ -1,3 +1,5 @@ +import { USER_AGENT } from '../constants' + export interface RetryOptions { maxRetries?: number baseDelayMs?: number @@ -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 | 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 diff --git a/tests/fetch-retry.test.ts b/tests/fetch-retry.test.ts index b1a6d21..440a99b 100644 --- a/tests/fetch-retry.test.ts +++ b/tests/fetch-retry.test.ts @@ -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) @@ -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"}' }) }) diff --git a/tests/non-codemode.test.ts b/tests/non-codemode.test.ts index 96802ec..3f35674 100644 --- a/tests/non-codemode.test.ts +++ b/tests/non-codemode.test.ts @@ -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')