Skip to content
Open
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 src/docs-search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ export function registerDocsTool(server: McpServer, env: DocsSearchEnv) {
)
},
annotations: {
title: 'Search Cloudflare Docs',
readOnlyHint: true
}
},
Expand Down
180 changes: 97 additions & 83 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ async function registerNonCodemodeTools(
(operation.description ? `\n\n${operation.description}` : '')

const inputSchema = buildInputSchema(operation, path)
const title = operation.summary || `${method.toUpperCase()} ${path}`

// account_id is auto-resolved at call time for account-token and
// single-account user-token sessions. The MCP SDK validates arguments
Expand Down Expand Up @@ -365,106 +366,110 @@ async function registerNonCodemodeTools(
.describe('Cloudflare account ID. Required for multi-account tokens.')
}

server.registerTool(toolName, { description, inputSchema }, async (params) => {
try {
// Build the URL with path parameters substituted
let resolvedPath = path
const pathParams = [...path.matchAll(/\{([^}]+)\}/g)].map((m) => m[1])
for (const paramName of pathParams) {
let value = params[paramName] as string | undefined

// Auto-resolve account_id
if (paramName === 'account_id' && !value) {
if (accountId) {
value = accountId
} else if (props?.type === 'user_token' && props.accounts.length === 1) {
value = props.accounts[0].id
server.registerTool(
toolName,
{ description, inputSchema, annotations: { title } },
async (params) => {
try {
// Build the URL with path parameters substituted
let resolvedPath = path
const pathParams = [...path.matchAll(/\{([^}]+)\}/g)].map((m) => m[1])
for (const paramName of pathParams) {
let value = params[paramName] as string | undefined

// Auto-resolve account_id
if (paramName === 'account_id' && !value) {
if (accountId) {
value = accountId
} else if (props?.type === 'user_token' && props.accounts.length === 1) {
value = props.accounts[0].id
}
}
}

if (!value) {
return {
content: [
{
type: 'text' as const,
text: `Error: missing required path parameter: ${paramName}`
}
],
isError: true
if (!value) {
return {
content: [
{
type: 'text' as const,
text: `Error: missing required path parameter: ${paramName}`
}
],
isError: true
}
}
resolvedPath = resolvedPath.replace(`{${paramName}}`, encodeURIComponent(value))
}
resolvedPath = resolvedPath.replace(`{${paramName}}`, encodeURIComponent(value))
}

// Build query string
const url = new URL(apiBase + resolvedPath)
if (operation.parameters) {
for (const param of operation.parameters) {
if (param.in === 'query' && params[param.name] !== undefined) {
url.searchParams.set(param.name, String(params[param.name]))
// Build query string
const url = new URL(apiBase + resolvedPath)
if (operation.parameters) {
for (const param of operation.parameters) {
if (param.in === 'query' && params[param.name] !== undefined) {
url.searchParams.set(param.name, String(params[param.name]))
}
}
}
}

// Build request
const headers: Record<string, string> = {
Authorization: `Bearer ${apiToken}`
}
// Build request
const headers: Record<string, string> = {
Authorization: `Bearer ${apiToken}`
}

// Add header parameters
if (operation.parameters) {
for (const param of operation.parameters) {
if (param.in === 'header') {
const headerKey = `header_${param.name.toLowerCase().replace(/-/g, '_')}`
if (params[headerKey] !== undefined) {
headers[param.name] = String(params[headerKey])
// Add header parameters
if (operation.parameters) {
for (const param of operation.parameters) {
if (param.in === 'header') {
const headerKey = `header_${param.name.toLowerCase().replace(/-/g, '_')}`
if (params[headerKey] !== undefined) {
headers[param.name] = String(params[headerKey])
}
}
}
}
}

let requestBody: string | undefined
if (params['body']) {
headers['Content-Type'] = (params['content_type'] as string) || 'application/json'
requestBody = params['body'] as string
}

const response = await fetchWithRetry(
url.toString(),
{
method: method.toUpperCase(),
headers,
body: requestBody
},
{ caller: 'non_codemode_tool_call' }
)

const contentType = response.headers.get('content-type') || ''
let result: string

if (contentType.includes('application/json')) {
const data = await response.json()
result = JSON.stringify(data, null, 2)
} else {
result = await response.text()
}
let requestBody: string | undefined
if (params['body']) {
headers['Content-Type'] = (params['content_type'] as string) || 'application/json'
requestBody = params['body'] as string
}

return {
content: [{ type: 'text' as const, text: truncateResponse(result) }],
isError: !response.ok
}
} catch (error) {
return {
content: [
const response = await fetchWithRetry(
url.toString(),
{
type: 'text' as const,
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
method: method.toUpperCase(),
headers,
body: requestBody
},
{ caller: 'non_codemode_tool_call' }
)

const contentType = response.headers.get('content-type') || ''
let result: string

if (contentType.includes('application/json')) {
const data = await response.json()
result = JSON.stringify(data, null, 2)
} else {
result = await response.text()
}

return {
content: [{ type: 'text' as const, text: truncateResponse(result) }],
isError: !response.ok
}
} catch (error) {
return {
content: [
{
type: 'text' as const,
text: `Error: ${error instanceof Error ? error.message : String(error)}`
}
],
isError: true
}
}
}
})
)
}
}
}
Expand Down Expand Up @@ -542,6 +547,9 @@ async () => {
}`,
inputSchema: {
code: z.string().describe('JavaScript async arrow function to search the OpenAPI spec')
},
annotations: {
title: 'Search Cloudflare API'
}
},
async ({ code }) => {
Expand Down Expand Up @@ -583,6 +591,9 @@ async () => {
description: executeDescription,
inputSchema: {
code: z.string().describe('JavaScript async arrow function to execute')
},
annotations: {
title: 'Execute Cloudflare API Code'
}
},
async ({ code }) => {
Expand Down Expand Up @@ -613,6 +624,9 @@ async () => {
? `Your Cloudflare account ID. Required — this token has access to multiple accounts: ${props.accounts.map((a) => `${a.id} (${a.name})`).join(', ')}`
: 'Your Cloudflare account ID. Optional if you have only one account (will be auto-selected)'
)
},
annotations: {
title: 'Execute Cloudflare API Code'
}
},
async ({ code, account_id }) => {
Expand Down
6 changes: 6 additions & 0 deletions tests/non-codemode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,6 +515,9 @@ describe('createServer with codemode=false', () => {
expect(toolNames).toContain('get_accounts_workers_scripts')
expect(toolNames).toContain('post_accounts_workers_scripts')
expect(toolNames).toContain('get_zones_dns_records')
expect(tools['get_accounts_workers_scripts'].annotations?.title).toBe('List Workers')
expect(tools['post_accounts_workers_scripts'].annotations?.title).toBe('Create Worker')
expect(tools['get_zones_dns_records'].annotations?.title).toBe('List DNS Records')

// Should NOT have codemode tools
expect(toolNames).not.toContain('search')
Expand All @@ -530,6 +533,7 @@ describe('createServer with codemode=false', () => {
const server = await createServer(env, ctx, 'test-token', 'test-account', undefined, true)

const docsTool = (server as any)._registeredTools['docs']
expect(docsTool.annotations?.title).toBe('Search Cloudflare Docs')
expect(docsTool.description).toContain(
'This tool should be used to answer any question about Cloudflare products or features'
)
Expand Down Expand Up @@ -558,6 +562,8 @@ describe('createServer with codemode=false', () => {
expect(toolNames).toContain('docs')
expect(toolNames).toContain('search')
expect(toolNames).toContain('execute')
expect(tools['search'].annotations?.title).toBe('Search Cloudflare API')
expect(tools['execute'].annotations?.title).toBe('Execute Cloudflare API Code')
expect(toolNames).not.toContain('get_accounts_workers_scripts')
})

Expand Down