Skip to main content
Glama

mcp-server-cloudflare

Official
by cloudflare
auditlogs.tools.ts10.5 kB
import { z } from 'zod' import { fetchCloudflareApi } from '@repo/mcp-common/src/cloudflare-api' import { getProps } from '@repo/mcp-common/src/get-props' import type { AuditlogMCP } from '../auditlogs.app' export const actionResults = z.enum(['success', 'failure', '']) export const actionTypes = z.enum(['create', 'delete', 'view', 'update', 'login']) export const actorContexts = z.enum(['api_key', 'api_token', 'dash', 'oauth', 'origin_ca_key']) export const actorTypes = z.enum(['cloudflare_admin', 'account', 'user', 'system']) export const resourceScopes = z.enum(['memberships', 'accounts', 'user', 'zones']) export const sortDirections = z.enum(['desc', 'asc']) export const auditLogsQuerySchema = z.object({ account_name: z.string().optional().describe('The account name to filter audit logs by.'), action_result: actionResults.optional().describe('Whether the action was a success or failure.'), action_type: actionTypes.optional().describe('The type of action that was performed.'), actor_context: actorContexts.optional().describe('The context in which the actor was operating.'), actor_email: z .string() .email() .optional() .describe('The email of the actor who triggered the event.'), actor_id: z.string().optional().describe('The unique identifier of the actor.'), actor_ip_address: z.string().optional().describe('The IP address of the actor.'), actor_token_id: z.string().optional().describe('The API token ID used by the actor.'), actor_token_name: z.string().optional().describe('The name of the API token used by the actor.'), actor_type: actorTypes.optional().describe('The type of actor (e.g., user, token).'), audit_log_id: z.string().optional().describe('The unique identifier of the audit log entry.'), raw_cf_ray_id: z .string() .optional() .describe('The Cloudflare Ray ID associated with the request.'), raw_method: z .string() .optional() .describe('The HTTP method used in the request (e.g., GET, POST).'), raw_status_code: z.number().optional().describe('The HTTP status code returned by the request.'), raw_uri: z.string().optional().describe('The URI accessed in the request.'), resource_id: z.string().optional().describe('The unique identifier of the resource affected.'), resource_product: z .string() .optional() .describe('The Cloudflare product related to the resource.'), resource_type: z.string().optional().describe('The type of resource affected.'), resource_scope: resourceScopes .optional() .describe('The scope of the resource (e.g., account, zone).'), zone_id: z.string().optional().describe('The ID of the zone associated with the log.'), zone_name: z.string().optional().describe('The name of the zone associated with the log.'), since: z .string() .describe( 'The start of the time slice to look at. Can be YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss.sssZ' ) .regex( /^(\d{4}-\d{2}-\d{2}|(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z))$/, 'Date must be in YYYY-MM-DD or ISO 8601 format with milliseconds (e.g., YYYY-MM-DDTHH:mm:ss.sssZ)' ), before: z .string() .describe('The end of the time slice to look at. Can be YYYY-MM-DD or YYYY-MM-DDTHH:mm:ss.sssZ') .regex( /^(\d{4}-\d{2}-\d{2}|(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z))$/, 'Date must be in YYYY-MM-DD or ISO 8601 format with milliseconds (e.g., YYYY-MM-DDTHH:mm:ss.sssZ)' ), direction: sortDirections.optional().describe('The sort direction of the logs (asc or desc).'), limit: z .number() .min(1) .max(1000) .optional() .describe('The number of results to return (max 1000).'), cursor: z.string().optional().describe('Pagination cursor for fetching the next set of results.'), }) // Core schema for one audit log entry const auditLogEntrySchema = z.object({ id: z.string().max(36).describe('Unique identifier for the audit log entry'), account: z .object({ id: z.string().describe('The ID of the account'), name: z.string().describe('The name of the account'), }) .describe('Account information associated with the audit log'), action: z .object({ description: z.string().optional().describe('Description of the action taken'), result: actionResults.describe('Result of the action'), time: z.string().datetime().describe('Timestamp of when the action occurred'), type: actionTypes.describe('Type of action performed'), }) .describe('Details of the action performed in the audit log'), actor: z .object({ context: actorContexts.optional().describe('Context associated with the actor'), email: z.string().email().optional().describe('Email of the actor'), id: z.string().optional().describe('ID of the actor'), ip_address: z.string().optional().describe('IP address of the actor'), type: actorTypes.optional().describe('Type of the actor'), token_id: z.string().optional().describe('Token ID if available'), token_name: z.string().optional().describe('Token name if available'), }) .optional() .describe('Information about the actor who performed the action'), resource: z .object({ id: z.string().optional().describe('Resource ID involved in the action'), product: z.string().optional().describe('Product related to the action'), request: z.record(z.unknown()).optional().describe('Request details of the action'), response: z.record(z.unknown()).optional().describe('Response details of the action'), scope: z .union([z.string(), z.object({})]) .optional() .describe('Scope of the resource, e.g., "accounts"'), type: z.string().optional().describe('Type of resource involved'), }) .optional() .describe('Details of the resource involved in the action'), raw: z .object({ cf_ray_id: z.string().optional().describe('Cloudflare Ray ID associated with the request'), method: z.string().optional().describe('HTTP method used for the request'), status_code: z.number().optional().describe('HTTP status code of the response'), uri: z.string().optional().describe('URI of the request'), user_agent: z.string().optional().describe('User-Agent header of the request'), }) .optional() .describe('Raw data related to the request made during the action'), zone: z .object({ id: z.string().optional().describe('ID of the zone involved in the action'), name: z.string().optional().describe('Name of the zone involved in the action'), }) .optional() .describe('Zone information related to the action'), }) // Wrapper schema for response export const resultInfoSchema = z.object({ count: z.number(), cursor: z.string().optional(), }) export const auditLogsResponseSchema = z.object({ success: z.literal(true), errors: z.array(z.object({ message: z.string() })).optional(), result: z.array(auditLogEntrySchema).optional(), result_info: resultInfoSchema, }) export const trimmedAuditLog = z.object({ description: z.string().optional().describe('Description of the action taken'), time: z.string().datetime().describe('Timestamp of when the action occurred'), product: z.string().optional().describe('Product related to the action'), type: z.string().optional().describe('Type of resource involved'), actor_email: z.string().email().optional().describe('Email of the actor'), actor_token_name: z.string().optional().describe('Token name if available'), }) export const trimmedAuditLogsResponseSchema = z.object({ logs: z.array(trimmedAuditLog), result_info: resultInfoSchema, }) export type AuditLogOptions = z.infer<typeof auditLogsQuerySchema> export async function handleGetAuditLogs( accountId: string, apiToken: string, options: AuditLogOptions ): Promise<z.infer<typeof trimmedAuditLogsResponseSchema>> { // Default to just getting the first 10 if (!options.limit) { options.limit = 10 } // Validate and parse query parameters using Zod const validatedParams = auditLogsQuerySchema.parse(options) // Build query string from validated parameters const queryParams = new URLSearchParams() for (const [key, value] of Object.entries(validatedParams)) { if (value !== undefined && value !== null) { queryParams.append(key, String(value)) // Ensure everything is converted to string } } // Call the Public API const data = await fetchCloudflareApi({ endpoint: `/logs/audit?${queryParams.toString()}`, accountId, apiToken, responseSchema: auditLogsResponseSchema, options: { method: 'GET', headers: { 'Content-Type': 'application/json', 'portal-version': '2', }, }, }) // Trim down the results to relevant information const results = (data.result || []).map((res) => { return { description: res.action.description || '', time: res.action.time, actor_email: res.actor?.email, actor_token_name: res.actor?.token_name, product: res.resource?.product, type: res.resource?.type, } as z.infer<typeof trimmedAuditLog> }) return { logs: results, result_info: data.result_info, } } /** * Registers the audit log tool with the MCP server * @param server The MCP server instance * @param accountId Cloudflare account ID * @param apiToken Cloudflare API token */ export function registerAuditLogTools(agent: AuditlogMCP) { // Register the audit log tool by account agent.server.tool( 'auditlogs_by_account_id', `Find all audit logs (a list of who made what change when) for a Cloudflare Account by ID. This can be used to query activity on your Cloudflare account at a particular time. Since and before are required to look at a slice of time and are dates with or without a time up to millisecond precision e.g YYYY-MM-DDTHH:mm:ss.sssZ. There can be more than one page of results and they can be paginated using the returned cursor`, auditLogsQuerySchema.shape, async (params) => { const accountId = await agent.getActiveAccountId() if (!accountId) { return { content: [ { type: 'text', text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], } } try { const props = getProps(agent) const result = await handleGetAuditLogs(accountId, props.accessToken, params) return { content: [ { type: 'text', text: JSON.stringify(result), }, ], } } catch (error) { return { content: [ { type: 'text', text: JSON.stringify({ error: `Error reading audit logs: ${error instanceof Error && error.message}`, }), }, ], } } } ) }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cloudflare/mcp-server-cloudflare'

If you have feedback or need assistance with the MCP directory API, please join our Discord server