From b84cd415aca2c4c44218f9c92124ef882728df2b Mon Sep 17 00:00:00 2001 From: Maximo Guk <62088388+Maximo-Guk@users.noreply.github.com> Date: Mon, 14 Apr 2025 11:23:38 -0500 Subject: [PATCH] Move API calls in to mcp-common --- .../src/tools/account.ts | 36 +- apps/workers-observability/src/tools/logs.ts | 352 +---------------- .../src/tools/workers.ts | 77 +--- package.json | 6 +- packages/mcp-common/package.json | 2 + packages/mcp-common/src/api/account.ts | 32 ++ packages/mcp-common/src/api/analytics.ts | 198 ++++++++++ packages/mcp-common/src/api/bindings.ts | 346 +++++++++++++++++ packages/mcp-common/src/api/cron.ts | 142 +++++++ packages/mcp-common/src/api/d1.ts | 181 +++++++++ .../mcp-common/src/api/durable-objects.ts | 352 +++++++++++++++++ packages/mcp-common/src/api/kv.ts | 207 ++++++++++ packages/mcp-common/src/api/logs.ts | 319 +++++++++++++++ packages/mcp-common/src/api/queues.ts | 365 ++++++++++++++++++ packages/mcp-common/src/api/r2.ts | 291 ++++++++++++++ packages/mcp-common/src/api/routing.ts | 166 ++++++++ packages/mcp-common/src/api/secrets.ts | 124 ++++++ packages/mcp-common/src/api/templates.ts | 137 +++++++ packages/mcp-common/src/api/versions.ts | 134 +++++++ packages/mcp-common/src/api/workers-ai.ts | 247 ++++++++++++ .../src/api/workers-for-platforms.ts | 354 +++++++++++++++++ packages/mcp-common/src/api/workers.ts | 73 ++++ packages/mcp-common/src/api/workflows.ts | 243 ++++++++++++ packages/mcp-common/src/api/wrangler.ts | 80 ++++ packages/mcp-common/src/api/zones.ts | 117 ++++++ packages/mcp-common/src/v4-api.ts | 24 ++ .../mcp-common/tests}/logs.spec.ts | 2 +- .../mcp-common/tests}/workers.spec.ts | 2 +- packages/mcp-common/types.d.ts | 5 + packages/mcp-common/vitest.config.ts | 9 + pnpm-lock.yaml | 168 +------- 31 files changed, 4162 insertions(+), 629 deletions(-) create mode 100644 packages/mcp-common/src/api/account.ts create mode 100644 packages/mcp-common/src/api/analytics.ts create mode 100644 packages/mcp-common/src/api/bindings.ts create mode 100644 packages/mcp-common/src/api/cron.ts create mode 100644 packages/mcp-common/src/api/d1.ts create mode 100644 packages/mcp-common/src/api/durable-objects.ts create mode 100644 packages/mcp-common/src/api/kv.ts create mode 100644 packages/mcp-common/src/api/logs.ts create mode 100644 packages/mcp-common/src/api/queues.ts create mode 100644 packages/mcp-common/src/api/r2.ts create mode 100644 packages/mcp-common/src/api/routing.ts create mode 100644 packages/mcp-common/src/api/secrets.ts create mode 100644 packages/mcp-common/src/api/templates.ts create mode 100644 packages/mcp-common/src/api/versions.ts create mode 100644 packages/mcp-common/src/api/workers-ai.ts create mode 100644 packages/mcp-common/src/api/workers-for-platforms.ts create mode 100644 packages/mcp-common/src/api/workers.ts create mode 100644 packages/mcp-common/src/api/workflows.ts create mode 100644 packages/mcp-common/src/api/wrangler.ts create mode 100644 packages/mcp-common/src/api/zones.ts create mode 100644 packages/mcp-common/src/v4-api.ts rename {apps/workers-observability/src/tools => packages/mcp-common/tests}/logs.spec.ts (99%) rename {apps/workers-observability/src/tools => packages/mcp-common/tests}/workers.spec.ts (99%) create mode 100644 packages/mcp-common/types.d.ts diff --git a/apps/workers-observability/src/tools/account.ts b/apps/workers-observability/src/tools/account.ts index fcbc7f91..37047b4c 100644 --- a/apps/workers-observability/src/tools/account.ts +++ b/apps/workers-observability/src/tools/account.ts @@ -1,40 +1,6 @@ import { z } from "zod"; import type { MyMCP } from "../index"; - -const AccountSchema = z.object({ - id: z.string(), - name: z.string(), - created_on: z.string(), -}); -type AccountsListResponseSchema = z.infer; -const AccountsListResponseSchema = z.object({ - result: z.array(AccountSchema), - success: z.boolean(), - errors: z.array(z.any()), - messages: z.array(z.any()), -}); - -export async function handleAccountsList({ - apiToken, -}: { - apiToken: string; -}): Promise { - // Currently limited to 50 accounts - const response = await fetch("https://api.cloudflare.com/client/v4/accounts?per_page=50", { - method: "GET", - headers: { - Authorization: `Bearer ${apiToken}`, - Accept: "application/javascript", - }, - }); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Cloudflare API request failed: ${error}`); - } - - return AccountsListResponseSchema.parse(await response.json()).result; -} +import { handleAccountsList } from "@repo/mcp-common/src/api/account" export function registerAccountTools(agent: MyMCP) { // Tool to list all accounts diff --git a/apps/workers-observability/src/tools/logs.ts b/apps/workers-observability/src/tools/logs.ts index d84895e6..84e549eb 100644 --- a/apps/workers-observability/src/tools/logs.ts +++ b/apps/workers-observability/src/tools/logs.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import type { MyMCP } from "../index"; -import { fetchCloudflareApi } from "@repo/mcp-common/src/cloudflare-api"; +import { handleWorkerLogsKeys, handleWorkerLogs } from "@repo/mcp-common/src/api/logs" // Worker logs parameter schema const workerNameParam = z.string().describe("The name of the worker to analyze logs for"); @@ -19,356 +19,6 @@ const minutesAgoParam = z .describe("Minutes in the past to look for logs (1-1440, default 30)"); const rayIdParam = z.string().optional().describe("Filter logs by specific Cloudflare Ray ID"); -const RelevantLogInfoSchema = z.object({ - timestamp: z.string(), - path: z.string().nullable(), - method: z.string().nullable(), - status: z.number().nullable(), - outcome: z.string(), - eventType: z.string(), - duration: z.number().nullable(), - error: z.string().nullable(), - message: z.string().nullable(), - requestId: z.string(), - rayId: z.string().nullable(), - exceptionStack: z.string().nullable(), -}); -type RelevantLogInfo = z.infer; - -const TelemetryKeySchema = z.object({ - key: z.string(), - type: z.enum(["string", "number", "boolean"]), - lastSeen: z.number().optional(), -}); -type TelemetryKey = z.infer; - -const LogsKeysResponseSchema = z.object({ - success: z.boolean(), - result: z.array(TelemetryKeySchema).optional().default([]), - errors: z - .array( - z.object({ - message: z.string().optional(), - }), - ) - .optional() - .default([]), - messages: z - .array( - z.object({ - message: z.string().optional(), - }), - ) - .optional() - .default([]), -}); - -const WorkerRequestSchema = z.object({ - url: z.string().optional(), - method: z.string().optional(), - path: z.string().optional(), - search: z.record(z.string()).optional(), -}); - -const WorkerResponseSchema = z.object({ - status: z.number().optional(), -}); - -const WorkerEventDetailsSchema = z.object({ - request: WorkerRequestSchema.optional(), - response: WorkerResponseSchema.optional(), - rpcMethod: z.string().optional(), - rayId: z.string().optional(), - executionModel: z.string().optional(), -}); - -const WorkerInfoSchema = z.object({ - scriptName: z.string(), - outcome: z.string(), - eventType: z.string(), - requestId: z.string(), - event: WorkerEventDetailsSchema.optional(), - wallTimeMs: z.number().optional(), - cpuTimeMs: z.number().optional(), - executionModel: z.string().optional(), -}); - -const WorkerSourceSchema = z.object({ - exception: z - .object({ - stack: z.string().optional(), - name: z.string().optional(), - message: z.string().optional(), - timestamp: z.number().optional(), - }) - .optional(), -}); - -type WorkerEventType = z.infer; -const WorkerEventSchema = z.object({ - $workers: WorkerInfoSchema.optional(), - timestamp: z.number(), - source: WorkerSourceSchema, - dataset: z.string(), - $metadata: z.object({ - id: z.string(), - message: z.string().optional(), - trigger: z.string().optional(), - error: z.string().optional(), - }), -}); - -const LogsEventsSchema = z.object({ - events: z.array(WorkerEventSchema).optional().default([]), -}); - -const LogsResponseSchema = z.object({ - success: z.boolean(), - result: z - .object({ - events: LogsEventsSchema.optional().default({ events: [] }), - }) - .optional() - .default({ events: { events: [] } }), - errors: z - .array( - z.object({ - message: z.string().optional(), - }), - ) - .optional() - .default([]), - messages: z - .array( - z.object({ - message: z.string().optional(), - }), - ) - .optional() - .default([]), -}); - -/** - * Extracts only the most relevant information from a worker log event - * @param event The raw worker log event - * @returns Relevant information extracted from the log - */ -function extractRelevantLogInfo(event: WorkerEventType): RelevantLogInfo { - const workers = event.$workers; - const metadata = event.$metadata; - const source = event.source; - - let path = null; - let method = null; - let status = null; - - if (workers?.event?.request) { - path = workers.event.request.path ?? null; - method = workers.event.request.method ?? null; - } - - if (workers?.event?.response) { - status = workers.event.response.status ?? null; - } - - let error = null; - if (metadata.error) { - error = metadata.error; - } - - let message = metadata?.message ?? null; - if (!message) { - if (workers?.event?.rpcMethod) { - message = `RPC: ${workers.event.rpcMethod}`; - } else if (path && method) { - message = `${method} ${path}`; - } - } - - // Calculate duration - const duration = (workers?.wallTimeMs || 0) + (workers?.cpuTimeMs || 0); - - // Extract rayId if available - const rayId = workers?.event?.rayId ?? null; - - // Extract exception stack if available - const exceptionStack = source?.exception?.stack ?? null; - - return { - timestamp: new Date(event.timestamp).toISOString(), - path, - method, - status, - outcome: workers?.outcome || "unknown", - eventType: workers?.eventType || "unknown", - duration: duration || null, - error, - message, - requestId: workers?.requestId || metadata?.id || "unknown", - rayId, - exceptionStack, - }; -} - -/** - * Fetches recent logs for a specified Cloudflare Worker - * @param scriptName Name of the worker script to get logs for - * @param accountId Cloudflare account ID - * @param apiToken Cloudflare API token - * @returns The logs analysis result with filtered relevant information - */ -export async function handleWorkerLogs({ - limit, - minutesAgo, - accountId, - apiToken, - shouldFilterErrors, - scriptName, - rayId, -}: { - limit: number; - minutesAgo: number; - accountId: string; - apiToken: string; - shouldFilterErrors: boolean; - scriptName?: string; - rayId?: string; -}): Promise<{ relevantLogs: RelevantLogInfo[]; from: number; to: number }> { - if (scriptName === undefined && rayId === undefined) { - throw new Error("Either scriptName or rayId must be provided"); - } - // Calculate timeframe based on minutesAgo parameter - const now = Date.now(); - const fromTimestamp = now - minutesAgo * 60 * 1000; - - type QueryFilter = { id: string; key: string; type: string; operation: string; value?: string }; - const filters: QueryFilter[] = []; - - // Build query to fetch logs - if (scriptName) { - filters.push({ - id: "worker-name-filter", - key: "$metadata.service", - type: "string", - value: scriptName, - operation: "eq", - }); - } - - if (shouldFilterErrors === true) { - filters.push({ - id: "error-filter", - key: "$metadata.error", - type: "string", - operation: "exists", - }); - } - - // Add Ray ID filter if provided - if (rayId) { - filters.push({ - id: "ray-id-filter", - key: "$workers.event.rayId", - type: "string", - value: rayId, - operation: "eq", - }); - } - - const queryPayload = { - queryId: "workers-logs", - timeframe: { - from: fromTimestamp, - to: now, - }, - parameters: { - datasets: ["cloudflare-workers"], - filters, - calculations: [], - groupBys: [], - havings: [], - }, - view: "events", - limit, - }; - - const data = await fetchCloudflareApi({ - endpoint: "/workers/observability/telemetry/query", - accountId, - apiToken, - responseSchema: LogsResponseSchema, - options: { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(queryPayload), - }, - }); - - const events = data.result?.events?.events || []; - - // Extract relevant information from each event - const relevantLogs = events.map(extractRelevantLogInfo); - - return { relevantLogs, from: fromTimestamp, to: now }; -} - -/** - * Fetches available telemetry keys for a specified Cloudflare Worker - * @param scriptName Name of the worker script to get keys for - * @param accountId Cloudflare account ID - * @param apiToken Cloudflare API token - * @returns List of telemetry keys available for the worker - */ -export async function handleWorkerLogsKeys( - scriptName: string, - minutesAgo: number, - accountId: string, - apiToken: string, -): Promise { - // Calculate timeframe (last 24 hours to ensure we get all keys) - const now = Date.now(); - const fromTimestamp = now - minutesAgo * 60 * 1000; - - // Build query for telemetry keys - const queryPayload = { - queryId: "workers-keys", - timeframe: { - from: fromTimestamp, - to: now, - }, - parameters: { - datasets: ["cloudflare-workers"], - filters: [ - { - id: "service-filter", - key: "$metadata.service", - type: "string", - value: `${scriptName}`, - operation: "eq", - }, - ], - }, - }; - - const data = await fetchCloudflareApi({ - endpoint: "/workers/observability/telemetry/keys", - accountId, - apiToken, - responseSchema: LogsKeysResponseSchema, - options: { - method: "POST", - headers: { - "Content-Type": "application/json", - "portal-version": "2", - }, - body: JSON.stringify(queryPayload), - }, - }); - - return data.result || []; -} /** * Registers the logs analysis tool with the MCP server diff --git a/apps/workers-observability/src/tools/workers.ts b/apps/workers-observability/src/tools/workers.ts index 31d61764..507d2093 100644 --- a/apps/workers-observability/src/tools/workers.ts +++ b/apps/workers-observability/src/tools/workers.ts @@ -1,81 +1,6 @@ import { z } from "zod"; import type { MyMCP } from "../index"; -import { fetchCloudflareApi } from "@repo/mcp-common/src/cloudflare-api"; - -const WorkerSchema = z.object({ - // id is usually the worker name - id: z.string(), - created_on: z.string().optional(), - modified_on: z.string().optional(), -}); - -const CloudflareWorkerListResponseSchema = z.object({ - result: z.array(WorkerSchema), - success: z.boolean(), - errors: z.array(z.any()), - messages: z.array(z.any()), -}); - -/** - * Fetches list of workers from Cloudflare API - * @param accountId Cloudflare account ID - * @param apiToken Cloudflare API token - * @returns List of workers - */ -export async function handleWorkersList({ - accountId, - apiToken, -}: { - accountId: string; - apiToken: string; -}) { - const response = await fetchCloudflareApi({ - endpoint: "/workers/scripts", - accountId, - apiToken, - responseSchema: CloudflareWorkerListResponseSchema, - options: { - method: "GET", - }, - }); - - return response.result; -} - -/** - * Downloads a specific worker script from Cloudflare API - * @param scriptName Name of the worker script to download - * @param accountId Cloudflare account ID - * @param apiToken Cloudflare API token - * @returns The worker script content - */ -export async function handleWorkerScriptDownload({ - scriptName, - accountId, - apiToken, -}: { - scriptName: string; - accountId: string; - apiToken: string; -}): Promise { - const response = await fetch( - `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${scriptName}`, - { - method: "GET", - headers: { - Authorization: `Bearer ${apiToken}`, - Accept: "application/javascript", - }, - }, - ); - - if (!response.ok) { - const error = await response.text(); - throw new Error(`Failed to download worker script: ${error}`); - } - - return await response.text(); -} +import { handleWorkersList, handleWorkerScriptDownload } from "@repo/mcp-common/src/api/workers" /** * Registers the workers tools with the MCP server diff --git a/package.json b/package.json index 4cee34b2..7b12c2d9 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "module": "dist/index.js", "types": "dist/index.d.ts", "files": [ - "dist", - "README.md" + "dist", + "README.md" ], "access": "public", "sideEffects": false, @@ -28,7 +28,6 @@ "@repo/eslint-config": "workspace:*", "@repo/tools": "workspace:*", "@repo/typescript-config": "workspace:*", - "@sentry/cli": "2.43.0", "@vitest/ui": "3.0.9", "prettier": "3.5.3", "syncpack": "13.0.3", @@ -38,6 +37,7 @@ "packageManager": "pnpm@10.8.0", "pnpm": { "onlyBuiltDependencies": [ + "esbuild", "sharp", "workerd" ], diff --git a/packages/mcp-common/package.json b/packages/mcp-common/package.json index 90986973..1b5aed9a 100644 --- a/packages/mcp-common/package.json +++ b/packages/mcp-common/package.json @@ -13,6 +13,8 @@ "dependencies": { "@cloudflare/workers-oauth-provider": "0.0.2", "@hono/zod-validator": "0.4.3", + "@modelcontextprotocol/sdk": "1.8.0", + "agents": "0.0.49", "hono": "4.7.6", "zod": "3.24.2" }, diff --git a/packages/mcp-common/src/api/account.ts b/packages/mcp-common/src/api/account.ts new file mode 100644 index 00000000..74d13a71 --- /dev/null +++ b/packages/mcp-common/src/api/account.ts @@ -0,0 +1,32 @@ +import { z } from "zod"; +import { V4Schema } from "../v4-api"; + +type AccountSchema = z.infer +const AccountSchema = z.object({ + id: z.string(), + name: z.string(), + created_on: z.string(), +}); +const AccountsListResponseSchema = V4Schema(z.array(AccountSchema)); + +export async function handleAccountsList({ + apiToken, +}: { + apiToken: string; +}): Promise { + // Currently limited to 50 accounts + const response = await fetch("https://api.cloudflare.com/client/v4/accounts?per_page=50", { + method: "GET", + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/javascript", + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Cloudflare API request failed: ${error}`); + } + + return AccountsListResponseSchema.parse(await response.json()).result ?? []; +} diff --git a/packages/mcp-common/src/api/analytics.ts b/packages/mcp-common/src/api/analytics.ts new file mode 100644 index 00000000..0a9b973d --- /dev/null +++ b/packages/mcp-common/src/api/analytics.ts @@ -0,0 +1,198 @@ +import { z } from "zod"; + +// GraphQL error schema +const GraphQLErrorSchema = z.object({ + message: z.string(), + locations: z + .array( + z.object({ + line: z.number(), + column: z.number(), + }) + ) + .optional(), + path: z.array(z.string()).optional(), + extensions: z.record(z.any()).optional(), +}); + +// GraphQL response schema +const GraphQLResponseSchema = z.object({ + data: z.any().optional(), + errors: z.array(GraphQLErrorSchema).optional(), +}); + +/** + * Get zone analytics data from Cloudflare GraphQL API + * @param zoneId The zone ID to get analytics for + * @param apiToken Cloudflare API token + * @param since Optional start time for analytics (ISO string) + * @returns Analytics data for the specified zone + */ +export async function handleGetZoneAnalytics({ + zoneId, + apiToken, + since, +}: { + zoneId: string; + apiToken: string; + since?: string; +}): Promise { + const date = since ? new Date(since).toISOString().split("T")[0] : new Date().toISOString().split("T")[0]; + + const graphqlQuery = { + query: `query { + viewer { + zones(filter: {zoneTag: "${zoneId}"}) { + httpRequests1dGroups( + limit: 1, + filter: {date: "${date}"}, + orderBy: [date_DESC] + ) { + dimensions { + date + } + sum { + requests + bytes + threats + pageViews + } + uniq { + uniques + } + } + } + } + }`, + }; + + const analyticsResponse = await fetch("https://api.cloudflare.com/client/v4/graphql", { + method: "POST", + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(graphqlQuery), + }); + + if (!analyticsResponse.ok) { + throw new Error(`Analytics API error: ${await analyticsResponse.text()}`); + } + + const analyticsData = GraphQLResponseSchema.parse(await analyticsResponse.json()); + + // Check for GraphQL errors + if (analyticsData.errors) { + throw new Error(`GraphQL error: ${JSON.stringify(analyticsData.errors)}`); + } + + return analyticsData.data; +} + +/** + * Search Workers analytics data for a specific time period + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @param scriptName Optional name of the Worker script to search for + * @param startTime Optional start time for analytics search (ISO string) + * @param endTime Optional end time for analytics search (ISO string) + * @param limit Optional maximum number of results to return (default: 100) + * @param status Optional filter by status (e.g., "success", "error") + * @returns Workers analytics data for the specified criteria + */ +export async function handleWorkersAnalyticsSearch({ + accountId, + apiToken, + scriptName, + startTime, + endTime, + limit, + status, +}: { + accountId: string; + apiToken: string; + scriptName?: string; + startTime?: string; + endTime?: string; + limit?: number; + status?: string; +}): Promise { + // Set default time range if not provided (last 24 hours) + const now = new Date(); + const defaultEndTime = now.toISOString(); + const defaultStartTime = new Date(now.getTime() - 24 * 60 * 60 * 1000).toISOString(); + + const datetimeStart = startTime || defaultStartTime; + const datetimeEnd = endTime || defaultEndTime; + const resultLimit = limit || 100; + + // Build filter object for the GraphQL query + const filter: Record = { + datetime_geq: datetimeStart, + datetime_leq: datetimeEnd, + }; + + // Add optional filters if provided + if (scriptName) { + filter.scriptName = scriptName; + } + + if (status) { + filter.status = status; + } + + // Construct the GraphQL query + const graphqlQuery = { + query: ` + query GetWorkersAnalytics($accountTag: String!, $limit: Int!, $filter: WorkersInvocationsAdaptiveFilter_InputObject!) { + viewer { + accounts(filter: {accountTag: $accountTag}) { + workersInvocationsAdaptive(limit: $limit, filter: $filter) { + sum { + subrequests + requests + errors + } + quantiles { + cpuTimeP50 + cpuTimeP99 + } + dimensions { + datetime + scriptName + status + } + } + } + } + } + `, + variables: { + accountTag: accountId, + limit: resultLimit, + filter: filter, + }, + }; + + const analyticsResponse = await fetch("https://api.cloudflare.com/client/v4/graphql", { + method: "POST", + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(graphqlQuery), + }); + + if (!analyticsResponse.ok) { + throw new Error(`Workers Analytics API error: ${await analyticsResponse.text()}`); + } + + const analyticsData = GraphQLResponseSchema.parse(await analyticsResponse.json()); + + // Check for GraphQL errors + if (analyticsData.errors) { + throw new Error(`GraphQL error: ${JSON.stringify(analyticsData.errors)}`); + } + + return analyticsData.data; +} \ No newline at end of file diff --git a/packages/mcp-common/src/api/bindings.ts b/packages/mcp-common/src/api/bindings.ts new file mode 100644 index 00000000..911b14d5 --- /dev/null +++ b/packages/mcp-common/src/api/bindings.ts @@ -0,0 +1,346 @@ +import { z } from "zod"; +import { fetchCloudflareApi } from "../cloudflare-api"; +import { V4Schema } from "../v4-api"; + +// Service binding schema +type ServiceBindingSchema = z.infer +const ServiceBindingSchema = z.object({ + name: z.string(), + service: z.string(), + environment: z.string().optional(), + created_on: z.string().optional(), + modified_on: z.string().optional(), +}); + +// Environment variable schema +type EnvVarSchema = z.infer +const EnvVarSchema = z.object({ + name: z.string(), + value: z.string().optional(), + created_on: z.string().optional(), + modified_on: z.string().optional(), +}); + +// Worker binding schema (for list bindings) +type WorkerBindingSchema = z.infer +const WorkerBindingSchema = z.object({ + name: z.string(), + type: z.string(), + // Additional fields based on binding type + kv_namespace_id: z.string().optional(), + bucket_name: z.string().optional(), + namespace_id: z.string().optional(), + service: z.string().optional(), + environment: z.string().optional(), +}); + +// Response schemas using V4Schema +const ServiceBindingsResponseSchema = V4Schema(z.array(ServiceBindingSchema)); +const EnvVarsResponseSchema = V4Schema(z.array(EnvVarSchema)); +const WorkerBindingsResponseSchema = V4Schema(z.array(WorkerBindingSchema)); + +/** + * List all service bindings for a Worker + * @param scriptName The name of the Worker script to list bindings for + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of service bindings + */ +export async function handleServiceBindingList({ + scriptName, + accountId, + apiToken, +}: { + scriptName: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/scripts/${scriptName}/bindings/service`, + accountId, + apiToken, + responseSchema: ServiceBindingsResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} + +/** + * Create a service binding between Workers + * @param scriptName The name of the Worker script to add the binding to + * @param bindingName Name for the service binding + * @param service Name of the target Worker service + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @param environment Optional environment of the target Worker + * @returns Created service binding information + */ +export async function handleServiceBindingCreate({ + scriptName, + bindingName, + service, + accountId, + apiToken, + environment, +}: { + scriptName: string; + bindingName: string; + service: string; + accountId: string; + apiToken: string; + environment?: string; +}): Promise> { + const requestBody: Record = { + name: bindingName, + service, + }; + + if (environment) { + requestBody.environment = environment; + } + + const response = await fetchCloudflareApi({ + endpoint: `/workers/scripts/${scriptName}/bindings/service`, + accountId, + apiToken, + responseSchema: V4Schema(ServiceBindingSchema), + options: { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + }, + }); + + return response.result!; +} + +/** + * Update a service binding + * @param scriptName The name of the Worker script containing the binding + * @param bindingName Name of the service binding to update + * @param service New name of the target Worker service + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @param environment Optional new environment of the target Worker + * @returns Updated service binding information + */ +export async function handleServiceBindingUpdate({ + scriptName, + bindingName, + service, + accountId, + apiToken, + environment, +}: { + scriptName: string; + bindingName: string; + service: string; + accountId: string; + apiToken: string; + environment?: string; +}): Promise> { + const requestBody: Record = { + service, + }; + + if (environment) { + requestBody.environment = environment; + } + + const response = await fetchCloudflareApi({ + endpoint: `/workers/scripts/${scriptName}/bindings/service/${bindingName}`, + accountId, + apiToken, + responseSchema: V4Schema(ServiceBindingSchema), + options: { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + }, + }); + + return response.result!; +} + +/** + * Delete a service binding + * @param scriptName The name of the Worker script containing the binding + * @param bindingName Name of the service binding to delete + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Success message + */ +export async function handleServiceBindingDelete({ + scriptName, + bindingName, + accountId, + apiToken, +}: { + scriptName: string; + bindingName: string; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${scriptName}/bindings/service/${bindingName}`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to delete service binding: ${error}`); + } + + return "Successfully deleted service binding"; +} + +/** + * List environment variables for a Worker + * @param scriptName The name of the Worker script + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of environment variables + */ +export async function handleEnvVarList({ + scriptName, + accountId, + apiToken, +}: { + scriptName: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/scripts/${scriptName}/vars`, + accountId, + apiToken, + responseSchema: EnvVarsResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} + +/** + * Delete an environment variable + * @param scriptName The name of the Worker script + * @param key Name of the environment variable to delete + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Success message + */ +export async function handleEnvVarDelete({ + scriptName, + key, + accountId, + apiToken, +}: { + scriptName: string; + key: string; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${scriptName}/vars/${key}`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to delete environment variable: ${error}`); + } + + return "Successfully deleted environment variable"; +} + +/** + * List all bindings for a Worker service and environment + * @param serviceName Name of the Worker service + * @param envName Name of the environment + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of bindings + */ +export async function handleBindingsList({ + serviceName, + envName, + accountId, + apiToken, +}: { + serviceName: string; + envName: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/services/${serviceName}/environments/${envName}/bindings`, + accountId, + apiToken, + responseSchema: WorkerBindingsResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} + +/** + * Update bindings for a Worker service and environment + * @param serviceName Name of the Worker service + * @param envName Name of the environment + * @param bindings Array of bindings to set + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Success message + */ +export async function handleBindingsUpdate({ + serviceName, + envName, + bindings, + accountId, + apiToken, +}: { + serviceName: string; + envName: string; + bindings: any[]; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/services/${serviceName}/environments/${envName}/bindings`; + + const response = await fetch(url, { + method: "PUT", + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + bindings, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to update bindings: ${error}`); + } + + return "Successfully updated bindings"; +} \ No newline at end of file diff --git a/packages/mcp-common/src/api/cron.ts b/packages/mcp-common/src/api/cron.ts new file mode 100644 index 00000000..453bdd71 --- /dev/null +++ b/packages/mcp-common/src/api/cron.ts @@ -0,0 +1,142 @@ +import { z } from "zod"; +import { fetchCloudflareApi } from "../cloudflare-api"; +import { V4Schema } from "../v4-api"; + +// Cron trigger schema +type CronTriggerSchema = z.infer +const CronTriggerSchema = z.object({ + cron: z.string(), + created_on: z.string().optional(), + modified_on: z.string().optional(), +}); + +// Response schemas using V4Schema +const CronTriggersResponseSchema = V4Schema(z.array(CronTriggerSchema)); + +/** + * Create a CRON trigger for a Worker + * @param scriptName The name of the Worker script + * @param cronExpression CRON expression (e.g., "*\/5 * * * *" for every 5 minutes) + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Created cron trigger information + */ +export async function handleCronCreate({ + scriptName, + cronExpression, + accountId, + apiToken, +}: { + scriptName: string; + cronExpression: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/scripts/${scriptName}/schedules`, + accountId, + apiToken, + responseSchema: CronTriggersResponseSchema, + options: { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + cron: [cronExpression], + }), + }, + }); + + return response.result ?? []; +} + +/** + * Delete CRON triggers for a Worker + * @param scriptName The name of the Worker script + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Success message + */ +export async function handleCronDelete({ + scriptName, + accountId, + apiToken, +}: { + scriptName: string; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${scriptName}/schedules`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to delete CRON trigger: ${error}`); + } + + return "Successfully deleted CRON trigger"; +} + +/** + * List CRON triggers for a Worker + * @param scriptName The name of the Worker script + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of cron triggers + */ +export async function handleCronList({ + scriptName, + accountId, + apiToken, +}: { + scriptName: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/scripts/${scriptName}/schedules`, + accountId, + apiToken, + responseSchema: CronTriggersResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} + +/** + * Update CRON triggers for a Worker + * @param scriptName The name of the Worker script + * @param cronExpression New CRON expression + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Updated cron trigger information + */ +export async function handleCronUpdate({ + scriptName, + cronExpression, + accountId, + apiToken, +}: { + scriptName: string; + cronExpression: string; + accountId: string; + apiToken: string; +}): Promise { + // This is effectively the same as create, as the PUT endpoint replaces existing triggers + return handleCronCreate({ + scriptName, + cronExpression, + accountId, + apiToken, + }); +} \ No newline at end of file diff --git a/packages/mcp-common/src/api/d1.ts b/packages/mcp-common/src/api/d1.ts new file mode 100644 index 00000000..6b1fea40 --- /dev/null +++ b/packages/mcp-common/src/api/d1.ts @@ -0,0 +1,181 @@ +import { z } from "zod"; +import { fetchCloudflareApi } from "../cloudflare-api"; +import { V4Schema } from "../v4-api"; + +// D1 database schema +type D1DatabaseSchema = z.infer +const D1DatabaseSchema = z.object({ + uuid: z.string(), + name: z.string(), + version: z.string(), + created_at: z.string(), + updated_at: z.string(), +}); + +// D1 query meta schema +const D1QueryMetaSchema = z.object({ + changed_db: z.boolean(), + changes: z.number().optional(), + duration: z.number(), + last_row_id: z.number().optional(), + rows_read: z.number().optional(), + rows_written: z.number().optional(), +}); + +// Response schemas using V4Schema +const D1DatabasesResponseSchema = V4Schema(z.array(D1DatabaseSchema)); + +// For query responses, we need a custom schema since result can be any array +const D1QueryResponseSchema = z.object({ + result: z.array(z.any()), + success: z.boolean(), + errors: z.array(z.any()).optional(), + messages: z.array(z.any()).optional(), + meta: D1QueryMetaSchema.optional(), +}); + +/** + * Lists all D1 databases in a Cloudflare account + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of D1 databases + */ +export async function handleD1ListDatabases({ + accountId, + apiToken, +}: { + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: "/d1/database", + accountId, + apiToken, + responseSchema: D1DatabasesResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} + +/** + * Creates a new D1 database + * @param name Name of the database to create + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Created database information + */ +export async function handleD1CreateDatabase({ + name, + accountId, + apiToken, +}: { + name: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: "/d1/database", + accountId, + apiToken, + responseSchema: D1DatabasesResponseSchema, + options: { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ name }), + }, + }); + + return response.result ?? []; +} + +/** + * Deletes a D1 database + * @param databaseId ID of the database to delete + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Success message + */ +export async function handleD1DeleteDatabase({ + databaseId, + accountId, + apiToken, +}: { + databaseId: string; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/d1/database/${databaseId}`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to delete D1 database: ${error}`); + } + + return "Successfully deleted database"; +} + +/** + * Executes a SQL query against a D1 database + * @param databaseId ID of the database to query + * @param query SQL query to execute + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @param params Optional array of parameters for prepared statements + * @returns Query results and metadata + */ +export async function handleD1Query({ + databaseId, + query, + accountId, + apiToken, + params, +}: { + databaseId: string; + query: string; + accountId: string; + apiToken: string; + params?: string[]; +}): Promise<{ + result: any[]; + meta?: z.infer; +}> { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/d1/database/${databaseId}/query`; + + const body = { + sql: query, + params: params || [], + }; + + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(body), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to execute D1 query: ${error}`); + } + + const data = D1QueryResponseSchema.parse(await response.json()); + + return { + result: data.result, + meta: data.meta, + }; +} \ No newline at end of file diff --git a/packages/mcp-common/src/api/durable-objects.ts b/packages/mcp-common/src/api/durable-objects.ts new file mode 100644 index 00000000..5b294ca3 --- /dev/null +++ b/packages/mcp-common/src/api/durable-objects.ts @@ -0,0 +1,352 @@ +import { z } from "zod"; +import { fetchCloudflareApi } from "../cloudflare-api"; +import { V4Schema } from "../v4-api"; + +// Durable Objects namespace schema +type DONamespaceSchema = z.infer +const DONamespaceSchema = z.object({ + id: z.string(), + name: z.string(), + script_name: z.string(), + class_name: z.string(), + created_on: z.string().optional(), + modified_on: z.string().optional(), +}); + +// Durable Objects object schema +type DOObjectSchema = z.infer +const DOObjectSchema = z.object({ + id: z.string(), + created_on: z.string().optional(), + deleted_on: z.string().optional(), + metadata: z.record(z.string()).optional(), +}); + +// Durable Objects alarm schema +const DOAlarmSchema = z.object({ + scheduled_time: z.string().nullable(), +}); + +// Response schemas using V4Schema +const DONamespacesResponseSchema = V4Schema(z.array(DONamespaceSchema)); +const DONamespaceResponseSchema = V4Schema(DONamespaceSchema); +const DOObjectsResponseSchema = V4Schema(z.array(DOObjectSchema)); +const DOObjectResponseSchema = V4Schema(DOObjectSchema); +const DOAlarmResponseSchema = V4Schema(DOAlarmSchema); + +/** + * List Durable Objects namespaces + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of Durable Objects namespaces + */ +export async function handleListNamespaces({ + accountId, + apiToken, +}: { + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: "/workers/durable_objects/namespaces", + accountId, + apiToken, + responseSchema: DONamespacesResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} + +/** + * Create a new Durable Objects namespace + * @param name Name for the new Durable Objects namespace + * @param script The Worker script that implements this Durable Object + * @param className The class name that implements this Durable Object + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Created namespace information + */ +export async function handleCreateNamespace({ + name, + script, + className, + accountId, + apiToken, +}: { + name: string; + script: string; + className: string; + accountId: string; + apiToken: string; +}): Promise> { + const response = await fetchCloudflareApi({ + endpoint: "/workers/durable_objects/namespaces", + accountId, + apiToken, + responseSchema: DONamespaceResponseSchema, + options: { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name, + script_name: script, + class_name: className, + }), + }, + }); + + return response.result!; +} + +/** + * Delete a Durable Objects namespace + * @param namespaceId ID of the Durable Objects namespace to delete + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Success message + */ +export async function handleDeleteNamespace({ + namespaceId, + accountId, + apiToken, +}: { + namespaceId: string; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/durable_objects/namespaces/${namespaceId}`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to delete Durable Objects namespace: ${error}`); + } + + return "Successfully deleted Durable Objects namespace"; +} + +/** + * Get a specific Durable Object instance + * @param namespaceId ID of the Durable Objects namespace + * @param objectId ID of the Durable Object instance + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Durable Object instance information + */ +export async function handleGetObject({ + namespaceId, + objectId, + accountId, + apiToken, +}: { + namespaceId: string; + objectId: string; + accountId: string; + apiToken: string; +}): Promise> { + const response = await fetchCloudflareApi({ + endpoint: `/workers/durable_objects/namespaces/${namespaceId}/objects/${objectId}`, + accountId, + apiToken, + responseSchema: DOObjectResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result!; +} + +/** + * List Durable Object instances in a namespace + * @param namespaceId ID of the Durable Objects namespace + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @param limit Maximum number of objects to return + * @returns List of Durable Object instances + */ +export async function handleListObjects({ + namespaceId, + accountId, + apiToken, + limit, +}: { + namespaceId: string; + accountId: string; + apiToken: string; + limit?: number; +}): Promise { + let endpoint = `/workers/durable_objects/namespaces/${namespaceId}/objects`; + + if (limit) { + endpoint += `?limit=${limit}`; + } + + const response = await fetchCloudflareApi({ + endpoint, + accountId, + apiToken, + responseSchema: DOObjectsResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} + +/** + * Delete a specific Durable Object instance + * @param namespaceId ID of the Durable Objects namespace + * @param objectId ID of the Durable Object instance to delete + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Success message + */ +export async function handleDeleteObject({ + namespaceId, + objectId, + accountId, + apiToken, +}: { + namespaceId: string; + objectId: string; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/durable_objects/namespaces/${namespaceId}/objects/${objectId}`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to delete Durable Object: ${error}`); + } + + return "Successfully deleted Durable Object"; +} + +/** + * List alarms for a Durable Object + * @param namespaceId ID of the Durable Objects namespace + * @param objectId ID of the Durable Object instance + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Alarm information + */ +export async function handleAlarmList({ + namespaceId, + objectId, + accountId, + apiToken, +}: { + namespaceId: string; + objectId: string; + accountId: string; + apiToken: string; +}): Promise> { + const response = await fetchCloudflareApi({ + endpoint: `/workers/durable_objects/namespaces/${namespaceId}/objects/${objectId}/alarms`, + accountId, + apiToken, + responseSchema: DOAlarmResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result!; +} + +/** + * Set an alarm for a Durable Object + * @param namespaceId ID of the Durable Objects namespace + * @param objectId ID of the Durable Object instance + * @param scheduledTime ISO timestamp for when the alarm should trigger + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Operation result + */ +export async function handleAlarmSet({ + namespaceId, + objectId, + scheduledTime, + accountId, + apiToken, +}: { + namespaceId: string; + objectId: string; + scheduledTime: string; + accountId: string; + apiToken: string; +}): Promise> { + const response = await fetchCloudflareApi({ + endpoint: `/workers/durable_objects/namespaces/${namespaceId}/objects/${objectId}/alarms`, + accountId, + apiToken, + responseSchema: DOAlarmResponseSchema, + options: { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + scheduled_time: scheduledTime, + }), + }, + }); + + return response.result!; +} + +/** + * Delete an alarm for a Durable Object + * @param namespaceId ID of the Durable Objects namespace + * @param objectId ID of the Durable Object instance + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Success message + */ +export async function handleAlarmDelete({ + namespaceId, + objectId, + accountId, + apiToken, +}: { + namespaceId: string; + objectId: string; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/durable_objects/namespaces/${namespaceId}/objects/${objectId}/alarms`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to delete Durable Object alarm: ${error}`); + } + + return "Successfully deleted Durable Object alarm"; +} \ No newline at end of file diff --git a/packages/mcp-common/src/api/kv.ts b/packages/mcp-common/src/api/kv.ts new file mode 100644 index 00000000..42af53fc --- /dev/null +++ b/packages/mcp-common/src/api/kv.ts @@ -0,0 +1,207 @@ +import { z } from "zod"; +import { fetchCloudflareApi } from "../cloudflare-api"; +import { V4Schema } from "../v4-api"; + +// KV namespace schema +type KVNamespaceSchema = z.infer +const KVNamespaceSchema = z.object({ + id: z.string(), + title: z.string(), + supports_url_encoding: z.boolean().optional(), +}); + +// KV keys schema +type KVKeySchema = z.infer +const KVKeySchema = z.object({ + name: z.string(), + expiration: z.number().optional(), +}); + +// Response schemas using V4Schema +const KVNamespacesResponseSchema = V4Schema(z.array(KVNamespaceSchema)); +const KVKeysResponseSchema = V4Schema(z.array(KVKeySchema)); + +/** + * Lists all KV namespaces in a Cloudflare account + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of KV namespaces + */ +export async function handleListKVNamespaces({ + accountId, + apiToken, +}: { + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: "/storage/kv/namespaces", + accountId, + apiToken, + responseSchema: KVNamespacesResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} + +/** + * Gets a value from a KV namespace + * @param namespaceId KV namespace ID + * @param key Key to retrieve + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns The value as a string + */ +export async function handleKVGet({ + namespaceId, + key, + accountId, + apiToken, +}: { + namespaceId: string; + key: string; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${key}`; + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to get KV value: ${error}`); + } + + return await response.text(); +} + +/** + * Puts a value in a KV namespace + * @param namespaceId KV namespace ID + * @param key Key to store + * @param value Value to store + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @param expirationTtl Optional expiration time in seconds + * @returns Success message + */ +export async function handleKVPut({ + namespaceId, + key, + value, + accountId, + apiToken, + expirationTtl, +}: { + namespaceId: string; + key: string; + value: string; + accountId: string; + apiToken: string; + expirationTtl?: number; +}): Promise { + let url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${key}`; + + // Add expiration_ttl to query parameters if provided + if (expirationTtl) { + url += `?expiration_ttl=${expirationTtl}`; + } + + const response = await fetch(url, { + method: "PUT", + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "text/plain", + }, + body: value, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to put KV value: ${error}`); + } + + return "Successfully stored value"; +} + +/** + * Deletes a key from a KV namespace + * @param namespaceId KV namespace ID + * @param key Key to delete + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Success message + */ +export async function handleKVDelete({ + namespaceId, + key, + accountId, + apiToken, +}: { + namespaceId: string; + key: string; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/storage/kv/namespaces/${namespaceId}/values/${key}`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to delete KV key: ${error}`); + } + + return "Successfully deleted key"; +} + +/** + * Lists keys in a KV namespace + * @param namespaceId KV namespace ID + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @param prefix Optional prefix to filter keys + * @param limit Optional maximum number of keys to return + * @returns List of keys + */ +export async function handleKVListKeys({ + namespaceId, + accountId, + apiToken, + prefix, + limit, +}: { + namespaceId: string; + accountId: string; + apiToken: string; + prefix?: string; + limit?: number; +}): Promise { + const params = new URLSearchParams(); + if (prefix) params.append("prefix", prefix); + if (limit) params.append("limit", limit.toString()); + + const response = await fetchCloudflareApi({ + endpoint: `/storage/kv/namespaces/${namespaceId}/keys?${params}`, + accountId, + apiToken, + responseSchema: KVKeysResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} \ No newline at end of file diff --git a/packages/mcp-common/src/api/logs.ts b/packages/mcp-common/src/api/logs.ts new file mode 100644 index 00000000..13867dfa --- /dev/null +++ b/packages/mcp-common/src/api/logs.ts @@ -0,0 +1,319 @@ +import { z } from "zod"; +import { fetchCloudflareApi } from "../cloudflare-api"; +import { V4Schema } from "../v4-api"; + +const RelevantLogInfoSchema = z.object({ + timestamp: z.string(), + path: z.string().nullable(), + method: z.string().nullable(), + status: z.number().nullable(), + outcome: z.string(), + eventType: z.string(), + duration: z.number().nullable(), + error: z.string().nullable(), + message: z.string().nullable(), + requestId: z.string(), + rayId: z.string().nullable(), + exceptionStack: z.string().nullable(), +}); +type RelevantLogInfo = z.infer; + +const TelemetryKeySchema = z.object({ + key: z.string(), + type: z.enum(["string", "number", "boolean"]), + lastSeen: z.number().optional(), +}); +type TelemetryKey = z.infer; + +const LogsKeysResponseSchema = V4Schema( + z.array(TelemetryKeySchema).optional().default([]) +); + +const WorkerRequestSchema = z.object({ + url: z.string().optional(), + method: z.string().optional(), + path: z.string().optional(), + search: z.record(z.string()).optional(), +}); + +const WorkerResponseSchema = z.object({ + status: z.number().optional(), +}); + +const WorkerEventDetailsSchema = z.object({ + request: WorkerRequestSchema.optional(), + response: WorkerResponseSchema.optional(), + rpcMethod: z.string().optional(), + rayId: z.string().optional(), + executionModel: z.string().optional(), +}); + +const WorkerInfoSchema = z.object({ + scriptName: z.string(), + outcome: z.string(), + eventType: z.string(), + requestId: z.string(), + event: WorkerEventDetailsSchema.optional(), + wallTimeMs: z.number().optional(), + cpuTimeMs: z.number().optional(), + executionModel: z.string().optional(), +}); + +const WorkerSourceSchema = z.object({ + exception: z + .object({ + stack: z.string().optional(), + name: z.string().optional(), + message: z.string().optional(), + timestamp: z.number().optional(), + }) + .optional(), +}); + +type WorkerEventType = z.infer; +const WorkerEventSchema = z.object({ + $workers: WorkerInfoSchema.optional(), + timestamp: z.number(), + source: WorkerSourceSchema, + dataset: z.string(), + $metadata: z.object({ + id: z.string(), + message: z.string().optional(), + trigger: z.string().optional(), + error: z.string().optional(), + }), +}); + +const LogsEventsSchema = z.object({ + events: z.array(WorkerEventSchema).optional().default([]), +}); + +const LogsResponseSchema = V4Schema( + z.object({ + events: LogsEventsSchema.optional().default({ events: [] }), + }) + .optional() + .default({ events: { events: [] } }) +); + +/** + * Extracts only the most relevant information from a worker log event + * @param event The raw worker log event + * @returns Relevant information extracted from the log + */ +function extractRelevantLogInfo(event: WorkerEventType): RelevantLogInfo { + const workers = event.$workers; + const metadata = event.$metadata; + const source = event.source; + + let path = null; + let method = null; + let status = null; + + if (workers?.event?.request) { + path = workers.event.request.path ?? null; + method = workers.event.request.method ?? null; + } + + if (workers?.event?.response) { + status = workers.event.response.status ?? null; + } + + let error = null; + if (metadata.error) { + error = metadata.error; + } + + let message = metadata?.message ?? null; + if (!message) { + if (workers?.event?.rpcMethod) { + message = `RPC: ${workers.event.rpcMethod}`; + } else if (path && method) { + message = `${method} ${path}`; + } + } + + // Calculate duration + const duration = (workers?.wallTimeMs || 0) + (workers?.cpuTimeMs || 0); + + // Extract rayId if available + const rayId = workers?.event?.rayId ?? null; + + // Extract exception stack if available + const exceptionStack = source?.exception?.stack ?? null; + + return { + timestamp: new Date(event.timestamp).toISOString(), + path, + method, + status, + outcome: workers?.outcome || "unknown", + eventType: workers?.eventType || "unknown", + duration: duration || null, + error, + message, + requestId: workers?.requestId || metadata?.id || "unknown", + rayId, + exceptionStack, + }; +} + +/** + * Fetches recent logs for a specified Cloudflare Worker + * @param scriptName Name of the worker script to get logs for + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns The logs analysis result with filtered relevant information + */ +export async function handleWorkerLogs({ + limit, + minutesAgo, + accountId, + apiToken, + shouldFilterErrors, + scriptName, + rayId, +}: { + limit: number; + minutesAgo: number; + accountId: string; + apiToken: string; + shouldFilterErrors: boolean; + scriptName?: string; + rayId?: string; +}): Promise<{ relevantLogs: RelevantLogInfo[]; from: number; to: number }> { + if (scriptName === undefined && rayId === undefined) { + throw new Error("Either scriptName or rayId must be provided"); + } + // Calculate timeframe based on minutesAgo parameter + const now = Date.now(); + const fromTimestamp = now - minutesAgo * 60 * 1000; + + type QueryFilter = { id: string; key: string; type: string; operation: string; value?: string }; + const filters: QueryFilter[] = []; + + // Build query to fetch logs + if (scriptName) { + filters.push({ + id: "worker-name-filter", + key: "$metadata.service", + type: "string", + value: scriptName, + operation: "eq", + }); + } + + if (shouldFilterErrors === true) { + filters.push({ + id: "error-filter", + key: "$metadata.error", + type: "string", + operation: "exists", + }); + } + + // Add Ray ID filter if provided + if (rayId) { + filters.push({ + id: "ray-id-filter", + key: "$workers.event.rayId", + type: "string", + value: rayId, + operation: "eq", + }); + } + + const queryPayload = { + queryId: "workers-logs", + timeframe: { + from: fromTimestamp, + to: now, + }, + parameters: { + datasets: ["cloudflare-workers"], + filters, + calculations: [], + groupBys: [], + havings: [], + }, + view: "events", + limit, + }; + + const data = await fetchCloudflareApi({ + endpoint: "/workers/observability/telemetry/query", + accountId, + apiToken, + responseSchema: LogsResponseSchema, + options: { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(queryPayload), + }, + }); + + const events = data.result?.events?.events || []; + + // Extract relevant information from each event + const relevantLogs = events.map(extractRelevantLogInfo); + + return { relevantLogs, from: fromTimestamp, to: now }; +} + +/** + * Fetches available telemetry keys for a specified Cloudflare Worker + * @param scriptName Name of the worker script to get keys for + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of telemetry keys available for the worker + */ +export async function handleWorkerLogsKeys( + scriptName: string, + minutesAgo: number, + accountId: string, + apiToken: string, +): Promise { + // Calculate timeframe (last 24 hours to ensure we get all keys) + const now = Date.now(); + const fromTimestamp = now - minutesAgo * 60 * 1000; + + // Build query for telemetry keys + const queryPayload = { + queryId: "workers-keys", + timeframe: { + from: fromTimestamp, + to: now, + }, + parameters: { + datasets: ["cloudflare-workers"], + filters: [ + { + id: "service-filter", + key: "$metadata.service", + type: "string", + value: `${scriptName}`, + operation: "eq", + }, + ], + }, + }; + + const data = await fetchCloudflareApi({ + endpoint: "/workers/observability/telemetry/keys", + accountId, + apiToken, + responseSchema: LogsKeysResponseSchema, + options: { + method: "POST", + headers: { + "Content-Type": "application/json", + "portal-version": "2", + }, + body: JSON.stringify(queryPayload), + }, + }); + + return data.result || []; +} diff --git a/packages/mcp-common/src/api/queues.ts b/packages/mcp-common/src/api/queues.ts new file mode 100644 index 00000000..83ebd66b --- /dev/null +++ b/packages/mcp-common/src/api/queues.ts @@ -0,0 +1,365 @@ +import { z } from "zod"; +import { fetchCloudflareApi } from "../cloudflare-api"; +import { V4Schema } from "../v4-api"; + +// Queue schema +type QueueSchema = z.infer +const QueueSchema = z.object({ + queue_id: z.string(), + name: z.string(), + created_on: z.string().optional(), + modified_on: z.string().optional(), + producers: z.array( + z.object({ + name: z.string(), + script: z.string(), + }) + ).optional(), + consumers: z.array( + z.object({ + name: z.string(), + script: z.string(), + settings: z.object({ + batch_size: z.number().optional(), + max_retries: z.number().optional(), + max_wait_time_ms: z.number().optional(), + }).optional(), + }) + ).optional(), +}); + +// Queue message schema +type QueueMessageSchema = z.infer +const QueueMessageSchema = z.object({ + message_id: z.string(), + body: z.string(), + receipt_handle: z.string(), + created_at: z.string().optional(), + lease_expires_at: z.string().optional(), +}); + +// Response schemas using V4Schema +const QueuesResponseSchema = V4Schema(z.array(QueueSchema)); +const QueueResponseSchema = V4Schema(QueueSchema); +const QueueOperationResponseSchema = V4Schema(z.any()); + +/** + * Lists all queues in a Cloudflare account + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of queues + */ +export async function handleListQueues({ + accountId, + apiToken, +}: { + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: "/queues", + accountId, + apiToken, + responseSchema: QueuesResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} + +/** + * Creates a new queue + * @param name Name of the queue to create + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Created queue information + */ +export async function handleCreateQueue({ + name, + accountId, + apiToken, +}: { + name: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: "/queues", + accountId, + apiToken, + responseSchema: QueueResponseSchema, + options: { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ name }), + }, + }); + + return response.result!; +} + +/** + * Gets details about a specific queue + * @param queueId ID of the queue to get details for + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Queue details + */ +export async function handleGetQueue({ + queueId, + accountId, + apiToken, +}: { + queueId: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/queues/${queueId}`, + accountId, + apiToken, + responseSchema: QueueResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result!; +} + +/** + * Deletes a queue + * @param queueId ID of the queue to delete + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Success message + */ +export async function handleDeleteQueue({ + queueId, + accountId, + apiToken, +}: { + queueId: string; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/queues/${queueId}`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to delete queue: ${error}`); + } + + return "Successfully deleted queue"; +} + +/** + * Sends a message to a queue + * @param queueId ID of the queue to send a message to + * @param message Message to send + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Operation result + */ +export async function handleSendMessage({ + queueId, + message, + accountId, + apiToken, +}: { + queueId: string; + message: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/queues/${queueId}/messages`, + accountId, + apiToken, + responseSchema: QueueOperationResponseSchema, + options: { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + body: message, + }), + }, + }); + + return response.result; +} + +/** + * Sends multiple messages to a queue + * @param queueId ID of the queue to send messages to + * @param messages Array of messages to send + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Operation result + */ +export async function handleSendBatch({ + queueId, + messages, + accountId, + apiToken, +}: { + queueId: string; + messages: string[]; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/queues/${queueId}/messages/batch`, + accountId, + apiToken, + responseSchema: QueueOperationResponseSchema, + options: { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + messages: messages.map((message) => ({ body: message })), + }), + }, + }); + + return response.result; +} + +/** + * Gets a message from a queue + * @param queueId ID of the queue to get a message from + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @param visibilityTimeout Optional visibility timeout in seconds + * @returns Message or null if queue is empty + */ +export async function handleGetMessage({ + queueId, + accountId, + apiToken, + visibilityTimeout, +}: { + queueId: string; + accountId: string; + apiToken: string; + visibilityTimeout?: number; +}): Promise { + let endpoint = `/queues/${queueId}/messages`; + + if (visibilityTimeout) { + endpoint += `?visibility_timeout=${visibilityTimeout}`; + } + + const response = await fetchCloudflareApi({ + endpoint, + accountId, + apiToken, + responseSchema: QueueOperationResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result; +} + +/** + * Deletes a message from a queue + * @param queueId ID of the queue the message belongs to + * @param messageId ID of the message to delete + * @param receiptHandle Receipt handle for the message + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Operation result + */ +export async function handleDeleteMessage({ + queueId, + messageId, + receiptHandle, + accountId, + apiToken, +}: { + queueId: string; + messageId: string; + receiptHandle: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/queues/${queueId}/messages/${messageId}`, + accountId, + apiToken, + responseSchema: QueueOperationResponseSchema, + options: { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + receipt_handle: receiptHandle, + }), + }, + }); + + return response.result; +} + +/** + * Updates the visibility timeout for a message + * @param queueId ID of the queue the message belongs to + * @param messageId ID of the message to update + * @param receiptHandle Receipt handle for the message + * @param visibilityTimeout New visibility timeout in seconds + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Operation result + */ +export async function handleUpdateVisibility({ + queueId, + messageId, + receiptHandle, + visibilityTimeout, + accountId, + apiToken, +}: { + queueId: string; + messageId: string; + receiptHandle: string; + visibilityTimeout: number; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/queues/${queueId}/messages/${messageId}/visibility`, + accountId, + apiToken, + responseSchema: QueueOperationResponseSchema, + options: { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + receipt_handle: receiptHandle, + visibility_timeout: visibilityTimeout, + }), + }, + }); + + return response.result; +} \ No newline at end of file diff --git a/packages/mcp-common/src/api/r2.ts b/packages/mcp-common/src/api/r2.ts new file mode 100644 index 00000000..ec2985f6 --- /dev/null +++ b/packages/mcp-common/src/api/r2.ts @@ -0,0 +1,291 @@ +import { z } from "zod"; +import { fetchCloudflareApi } from "../cloudflare-api"; +import { V4Schema } from "../v4-api"; + +// R2 bucket schema +type R2BucketSchema = z.infer +const R2BucketSchema = z.object({ + name: z.string(), + creation_date: z.string(), +}); + +// R2 object schema +const R2ObjectSchema = z.object({ + key: z.string(), + size: z.number(), + uploaded: z.string(), + etag: z.string(), + httpEtag: z.string(), + version: z.string(), +}); + +// R2 objects response schema +type R2ObjectsResponseSchema = z.infer +const R2ObjectsResponseSchema = z.object({ + objects: z.array(R2ObjectSchema), + delimitedPrefixes: z.array(z.string()), + truncated: z.boolean(), +}); + +// Response schemas using V4Schema +const R2BucketsResponseSchema = V4Schema(z.array(R2BucketSchema)); + +/** + * Lists all R2 buckets in a Cloudflare account + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of R2 buckets + */ +export async function handleR2ListBuckets({ + accountId, + apiToken, +}: { + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: "/r2/buckets", + accountId, + apiToken, + responseSchema: R2BucketsResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} + +/** + * Creates a new R2 bucket + * @param name Name of the bucket to create + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Success message + */ +export async function handleR2CreateBucket({ + name, + accountId, + apiToken, +}: { + name: string; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/r2/buckets`; + + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ name }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to create R2 bucket: ${error}`); + } + + return "Successfully created bucket"; +} + +/** + * Deletes an R2 bucket + * @param name Name of the bucket to delete + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Success message + */ +export async function handleR2DeleteBucket({ + name, + accountId, + apiToken, +}: { + name: string; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/r2/buckets/${name}`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to delete R2 bucket: ${error}`); + } + + return "Successfully deleted bucket"; +} + +/** + * Lists objects in an R2 bucket + * @param bucket Name of the bucket + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @param prefix Optional prefix to filter objects + * @param delimiter Optional delimiter for hierarchical listing + * @param limit Optional maximum number of objects to return + * @returns List of objects and prefixes + */ +export async function handleR2ListObjects({ + bucket, + accountId, + apiToken, + prefix, + delimiter, + limit, +}: { + bucket: string; + accountId: string; + apiToken: string; + prefix?: string; + delimiter?: string; + limit?: number; +}): Promise { + const params = new URLSearchParams(); + if (prefix) params.append("prefix", prefix); + if (delimiter) params.append("delimiter", delimiter); + if (limit) params.append("limit", limit.toString()); + + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/r2/buckets/${bucket}/objects?${params}`; + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to list R2 objects: ${error}`); + } + + return R2ObjectsResponseSchema.parse(await response.json()); +} + +/** + * Gets an object from an R2 bucket + * @param bucket Name of the bucket + * @param key Key of the object to get + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Object content as a string + */ +export async function handleR2GetObject({ + bucket, + key, + accountId, + apiToken, +}: { + bucket: string; + key: string; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/r2/buckets/${bucket}/objects/${key}`; + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to get R2 object: ${error}`); + } + + return await response.text(); +} + +/** + * Puts an object into an R2 bucket + * @param bucket Name of the bucket + * @param key Key of the object to put + * @param content Content to store in the object + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @param contentType Optional MIME type of the content + * @returns Success message + */ +export async function handleR2PutObject({ + bucket, + key, + content, + accountId, + apiToken, + contentType, +}: { + bucket: string; + key: string; + content: string; + accountId: string; + apiToken: string; + contentType?: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/r2/buckets/${bucket}/objects/${key}`; + + const headers: Record = { + Authorization: `Bearer ${apiToken}`, + }; + + if (contentType) { + headers["Content-Type"] = contentType; + } + + const response = await fetch(url, { + method: "PUT", + headers, + body: content, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to put R2 object: ${error}`); + } + + return "Successfully uploaded object"; +} + +/** + * Deletes an object from an R2 bucket + * @param bucket Name of the bucket + * @param key Key of the object to delete + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Success message + */ +export async function handleR2DeleteObject({ + bucket, + key, + accountId, + apiToken, +}: { + bucket: string; + key: string; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/r2/buckets/${bucket}/objects/${key}`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to delete R2 object: ${error}`); + } + + return "Successfully deleted object"; +} \ No newline at end of file diff --git a/packages/mcp-common/src/api/routing.ts b/packages/mcp-common/src/api/routing.ts new file mode 100644 index 00000000..1a56f8f7 --- /dev/null +++ b/packages/mcp-common/src/api/routing.ts @@ -0,0 +1,166 @@ +import { z } from "zod"; +import { V4Schema } from "../v4-api"; + +// Route schema +type RouteSchema = z.infer +const RouteSchema = z.object({ + id: z.string(), + pattern: z.string(), + script: z.string(), + created_on: z.string().optional(), + modified_on: z.string().optional(), +}); + +// Response schemas using V4Schema +const RoutesResponseSchema = V4Schema(z.array(RouteSchema)); +const RouteResponseSchema = V4Schema(RouteSchema); + +/** + * List all routes for a zone + * @param zoneId ID of the zone to list routes for + * @param apiToken Cloudflare API token + * @returns List of routes + */ +export async function handleRouteList({ + zoneId, + apiToken, +}: { + zoneId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/zones/${zoneId}/workers/routes`; + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to list routes: ${error}`); + } + + const data = RoutesResponseSchema.parse(await response.json()); + return data.result ?? []; +} + +/** + * Create a route that maps to a Worker + * @param zoneId ID of the zone to create a route in + * @param pattern The URL pattern for the route (e.g., "example.com/*") + * @param scriptName Name of the Worker script to route to + * @param apiToken Cloudflare API token + * @returns Created route information + */ +export async function handleRouteCreate({ + zoneId, + pattern, + scriptName, + apiToken, +}: { + zoneId: string; + pattern: string; + scriptName: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/zones/${zoneId}/workers/routes`; + + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + pattern, + script: scriptName, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to create route: ${error}`); + } + + const data = RouteResponseSchema.parse(await response.json()); + return data.result!; +} + +/** + * Update a route + * @param zoneId ID of the zone containing the route + * @param routeId ID of the route to update + * @param pattern The new URL pattern for the route + * @param scriptName Name of the Worker script to route to + * @param apiToken Cloudflare API token + * @returns Updated route information + */ +export async function handleRouteUpdate({ + zoneId, + routeId, + pattern, + scriptName, + apiToken, +}: { + zoneId: string; + routeId: string; + pattern: string; + scriptName: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/zones/${zoneId}/workers/routes/${routeId}`; + + const response = await fetch(url, { + method: "PUT", + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + pattern, + script: scriptName, + }), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to update route: ${error}`); + } + + const data = RouteResponseSchema.parse(await response.json()); + return data.result!; +} + +/** + * Delete a route + * @param zoneId ID of the zone containing the route + * @param routeId ID of the route to delete + * @param apiToken Cloudflare API token + * @returns Success message + */ +export async function handleRouteDelete({ + zoneId, + routeId, + apiToken, +}: { + zoneId: string; + routeId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/zones/${zoneId}/workers/routes/${routeId}`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to delete route: ${error}`); + } + + return "Successfully deleted route"; +} \ No newline at end of file diff --git a/packages/mcp-common/src/api/secrets.ts b/packages/mcp-common/src/api/secrets.ts new file mode 100644 index 00000000..83b68ea5 --- /dev/null +++ b/packages/mcp-common/src/api/secrets.ts @@ -0,0 +1,124 @@ +import { z } from "zod"; +import { fetchCloudflareApi } from "../cloudflare-api"; +import { V4Schema } from "../v4-api"; + +// Secret schema +type SecretSchema = z.infer +const SecretSchema = z.object({ + name: z.string(), + type: z.string(), + created_on: z.string().optional(), + modified_on: z.string().optional(), +}); + +// Response schemas using V4Schema +const SecretsResponseSchema = V4Schema(z.array(SecretSchema)); +const SecretOperationResponseSchema = V4Schema(z.any()); + +/** + * List all secrets for a Worker + * @param scriptName The name of the Worker script + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of secrets + */ +export async function handleSecretList({ + scriptName, + accountId, + apiToken, +}: { + scriptName: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/scripts/${scriptName}/secrets`, + accountId, + apiToken, + responseSchema: SecretsResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} + +/** + * Add a secret to a Worker + * @param scriptName The name of the Worker script + * @param secretName Name of the secret + * @param secretValue Value of the secret + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Operation result + */ +export async function handleSecretPut({ + scriptName, + secretName, + secretValue, + accountId, + apiToken, +}: { + scriptName: string; + secretName: string; + secretValue: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/scripts/${scriptName}/secrets`, + accountId, + apiToken, + responseSchema: SecretOperationResponseSchema, + options: { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name: secretName, + text: secretValue, + type: "secret_text", + }), + }, + }); + + return response.result; +} + +/** + * Delete a secret from a Worker + * @param scriptName The name of the Worker script + * @param secretName Name of the secret to delete + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Success message + */ +export async function handleSecretDelete({ + scriptName, + secretName, + accountId, + apiToken, +}: { + scriptName: string; + secretName: string; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${scriptName}/secrets/${secretName}`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to delete secret: ${error}`); + } + + return "Successfully deleted secret"; +} \ No newline at end of file diff --git a/packages/mcp-common/src/api/templates.ts b/packages/mcp-common/src/api/templates.ts new file mode 100644 index 00000000..3d57803f --- /dev/null +++ b/packages/mcp-common/src/api/templates.ts @@ -0,0 +1,137 @@ +import { z } from "zod"; +import { fetchCloudflareApi } from "../cloudflare-api"; +import { V4Schema } from "../v4-api"; + +// Template schema +type TemplateSchema = z.infer +const TemplateSchema = z.object({ + id: z.string(), + name: z.string(), + description: z.string(), + type: z.string(), + tags: z.array(z.string()), + created_on: z.string().optional(), + modified_on: z.string().optional(), +}); + +// Template details schema (extends basic template info with content) +type TemplateDetailsSchema = z.infer +const TemplateDetailsSchema = TemplateSchema.extend({ + content: z.record(z.string()), +}); + +// Worker creation result schema +type WorkerCreationResultSchema = z.infer +const WorkerCreationResultSchema = z.object({ + name: z.string(), + created_from: z.string(), + created_on: z.string().optional(), +}); + +// Response schemas using V4Schema +const TemplatesResponseSchema = V4Schema(z.array(TemplateSchema)); +const TemplateDetailsResponseSchema = V4Schema(TemplateDetailsSchema); +const WorkerCreationResponseSchema = V4Schema(WorkerCreationResultSchema); + +/** + * List available Worker templates + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of templates + */ +export async function handleListTemplates({ + accountId, + apiToken, +}: { + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: "/workers/templates", + accountId, + apiToken, + responseSchema: TemplatesResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} + +/** + * Get details for a specific template + * @param templateId ID of the template to get details for + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Template details including content + */ +export async function handleGetTemplate({ + templateId, + accountId, + apiToken, +}: { + templateId: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/templates/${templateId}`, + accountId, + apiToken, + responseSchema: TemplateDetailsResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result!; +} + +/** + * Create a Worker from a template + * @param templateId ID of the template to use + * @param name Name for the new Worker + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @param config Optional configuration values for the template + * @returns Worker creation result + */ +export async function handleCreateWorkerFromTemplate({ + templateId, + name, + accountId, + apiToken, + config, +}: { + templateId: string; + name: string; + accountId: string; + apiToken: string; + config?: Record; +}): Promise { + const requestBody: Record = { + template_id: templateId, + name, + }; + + if (config) { + requestBody.config = config; + } + + const response = await fetchCloudflareApi({ + endpoint: "/workers/services/from-template", + accountId, + apiToken, + responseSchema: WorkerCreationResponseSchema, + options: { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + }, + }); + + return response.result!; +} \ No newline at end of file diff --git a/packages/mcp-common/src/api/versions.ts b/packages/mcp-common/src/api/versions.ts new file mode 100644 index 00000000..9b2b9ef9 --- /dev/null +++ b/packages/mcp-common/src/api/versions.ts @@ -0,0 +1,134 @@ +import { z } from "zod"; +import { fetchCloudflareApi } from "../cloudflare-api"; +import { V4Schema } from "../v4-api"; + +// Version schema +type VersionSchema = z.infer +const VersionSchema = z.object({ + id: z.string(), + tag: z.string().optional(), + created_on: z.string(), + modified_on: z.string().optional(), + message: z.string().optional(), +}); + +// Version details schema (extends basic version info with content) +type VersionDetailsSchema = z.infer +const VersionDetailsSchema = VersionSchema.extend({ + script: z.string().optional(), + handlers: z.array(z.string()).optional(), + bindings: z.record(z.any()).optional(), +}); + +// Rollback result schema +type RollbackResultSchema = z.infer +const RollbackResultSchema = z.object({ + id: z.string(), + success: z.boolean(), + old_version: z.string().optional(), + new_version: z.string(), +}); + +// Response schemas using V4Schema +const VersionsResponseSchema = V4Schema(z.array(VersionSchema)); +const VersionDetailsResponseSchema = V4Schema(VersionDetailsSchema); +const RollbackResponseSchema = V4Schema(RollbackResultSchema); + +/** + * List versions of a Worker + * @param scriptName The name of the Worker script + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of versions + */ +export async function handleVersionList({ + scriptName, + accountId, + apiToken, +}: { + scriptName: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/scripts/${scriptName}/versions`, + accountId, + apiToken, + responseSchema: VersionsResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} + +/** + * Get a specific version of a Worker + * @param scriptName The name of the Worker script + * @param versionId ID of the version to get + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Version details + */ +export async function handleVersionGet({ + scriptName, + versionId, + accountId, + apiToken, +}: { + scriptName: string; + versionId: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/scripts/${scriptName}/versions/${versionId}`, + accountId, + apiToken, + responseSchema: VersionDetailsResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result!; +} + +/** + * Rollback to a previous version + * @param scriptName The name of the Worker script + * @param versionId ID of the version to rollback to + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Rollback result + */ +export async function handleVersionRollback({ + scriptName, + versionId, + accountId, + apiToken, +}: { + scriptName: string; + versionId: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/scripts/${scriptName}/rollback`, + accountId, + apiToken, + responseSchema: RollbackResponseSchema, + options: { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + version_id: versionId, + }), + }, + }); + + return response.result!; +} \ No newline at end of file diff --git a/packages/mcp-common/src/api/workers-ai.ts b/packages/mcp-common/src/api/workers-ai.ts new file mode 100644 index 00000000..dee969a4 --- /dev/null +++ b/packages/mcp-common/src/api/workers-ai.ts @@ -0,0 +1,247 @@ +import { z } from "zod"; +import { fetchCloudflareApi } from "../cloudflare-api"; +import { V4Schema } from "../v4-api"; + +// AI Model schema +type AIModelSchema = z.infer +const AIModelSchema = z.object({ + id: z.string(), + name: z.string(), + version: z.string().optional(), + description: z.string().optional(), + task_type: z.string().optional(), + input_schema: z.any().optional(), + output_schema: z.any().optional(), + status: z.string().optional(), +}); + +// Response schemas using V4Schema +const AIModelsResponseSchema = V4Schema(z.array(AIModelSchema)); +const AIModelResponseSchema = V4Schema(AIModelSchema); + +// For inference results, the schema will depend on the model type, +// so we'll use a generic schema for now +const AIInferenceResponseSchema = V4Schema(z.any()); + +/** + * List available AI models + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of available AI models + */ +export async function handleListModels({ + accountId, + apiToken, +}: { + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: "/ai/models", + accountId, + apiToken, + responseSchema: AIModelsResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} + +/** + * Get details about a specific AI model + * @param model The model to get details for + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Model details + */ +export async function handleGetModel({ + model, + accountId, + apiToken, +}: { + model: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/ai/models/${model}`, + accountId, + apiToken, + responseSchema: AIModelResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result!; +} + +/** + * Run inference on a model with Workers AI + * @param model The model to run inference with + * @param input Input data for the model + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @param options Optional settings for the inference request + * @returns Inference results + */ +export async function handleAiInference({ + model, + input, + accountId, + apiToken, + options, +}: { + model: string; + input: any; + accountId: string; + apiToken: string; + options?: any; +}): Promise { + const requestBody: Record = { input }; + if (options) { + requestBody.options = options; + } + + // Check if the response is expected to be binary (like image generation) + const expectBinary = model.includes("stable-diffusion") || model.includes("imagen"); + + if (expectBinary) { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/ai/run/${model}`; + + const response = await fetch(url, { + method: "POST", + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to run inference: ${error}`); + } + + // Check content type to determine if we got JSON or binary + const contentType = response.headers.get("content-type") || ""; + + if (contentType.includes("application/json")) { + const data = await response.json(); + // @ts-ignore + return data.result; + } else { + // Handle binary image data + const buffer = await response.arrayBuffer(); + const base64 = Buffer.from(buffer).toString("base64"); + return { image: `data:${contentType};base64,${base64}` }; + } + } else { + const response = await fetchCloudflareApi({ + endpoint: `/ai/run/${model}`, + accountId, + apiToken, + responseSchema: AIInferenceResponseSchema, + options: { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + }, + }); + + return response.result; + } +} + +/** + * Generate embeddings from text using Workers AI + * @param model The embedding model to use + * @param text The text to generate embeddings for + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Embeddings + */ +export async function handleEmbeddings({ + model, + text, + accountId, + apiToken, +}: { + model: string; + text: string; + accountId: string; + apiToken: string; +}): Promise { + return handleAiInference({ + model, + input: { text }, + accountId, + apiToken, + }); +} + +/** + * Generate text using an AI model + * @param model The model to use for text generation + * @param prompt The prompt to generate text from + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @param options Optional settings for the text generation + * @returns Generated text + */ +export async function handleTextGeneration({ + model, + prompt, + accountId, + apiToken, + options, +}: { + model: string; + prompt: string; + accountId: string; + apiToken: string; + options?: any; +}): Promise { + return handleAiInference({ + model, + input: { prompt }, + accountId, + apiToken, + options, + }); +} + +/** + * Generate images using an AI model + * @param model The model to use for image generation + * @param prompt The prompt to generate an image from + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @param options Optional settings for the image generation + * @returns Generated image data + */ +export async function handleImageGeneration({ + model, + prompt, + accountId, + apiToken, + options, +}: { + model: string; + prompt: string; + accountId: string; + apiToken: string; + options?: any; +}): Promise { + return handleAiInference({ + model, + input: { prompt }, + accountId, + apiToken, + options, + }); +} \ No newline at end of file diff --git a/packages/mcp-common/src/api/workers-for-platforms.ts b/packages/mcp-common/src/api/workers-for-platforms.ts new file mode 100644 index 00000000..ea44b227 --- /dev/null +++ b/packages/mcp-common/src/api/workers-for-platforms.ts @@ -0,0 +1,354 @@ +import { z } from "zod"; +import { fetchCloudflareApi } from "../cloudflare-api"; +import { V4Schema } from "../v4-api"; + +// Dispatch namespace schema +type DispatchNamespaceSchema = z.infer +const DispatchNamespaceSchema = z.object({ + id: z.string(), + name: z.string(), + created_on: z.string().optional(), + modified_on: z.string().optional(), +}); + +// Custom domain schema +type CustomDomainSchema = z.infer +const CustomDomainSchema = z.object({ + id: z.string(), + hostname: z.string(), + zone_id: z.string().optional(), + zone_name: z.string().optional(), + status: z.string().optional(), + created_on: z.string().optional(), + modified_on: z.string().optional(), +}); + +// Script schema +type ScriptSchema = z.infer +const ScriptSchema = z.object({ + id: z.string(), + name: z.string(), + created_on: z.string().optional(), + modified_on: z.string().optional(), +}); + +// Response schemas using V4Schema +const DispatchNamespacesResponseSchema = V4Schema(z.array(DispatchNamespaceSchema)); +const DispatchNamespaceResponseSchema = V4Schema(DispatchNamespaceSchema); +const CustomDomainsResponseSchema = V4Schema(z.array(CustomDomainSchema)); +const CustomDomainResponseSchema = V4Schema(CustomDomainSchema); +const ScriptsResponseSchema = V4Schema(z.array(ScriptSchema)); +const ScriptResponseSchema = V4Schema(ScriptSchema); + +/** + * List all dispatch namespaces + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of dispatch namespaces + */ +export async function handleListDispatchNamespaces({ + accountId, + apiToken, +}: { + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: "/workers/dispatch/namespaces", + accountId, + apiToken, + responseSchema: DispatchNamespacesResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} + +/** + * Create a namespace for dispatching custom domains + * @param name Name for the new dispatch namespace + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Created namespace information + */ +export async function handleCreateDispatchNamespace({ + name, + accountId, + apiToken, +}: { + name: string; + accountId: string; + apiToken: string; +}): Promise> { + const response = await fetchCloudflareApi({ + endpoint: "/workers/dispatch/namespaces", + accountId, + apiToken, + responseSchema: DispatchNamespaceResponseSchema, + options: { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name, + }), + }, + }); + + return response.result!; +} + +/** + * Delete a dispatch namespace + * @param namespaceId ID of the dispatch namespace to delete + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Success message + */ +export async function handleDeleteDispatchNamespace({ + namespaceId, + accountId, + apiToken, +}: { + namespaceId: string; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/dispatch/namespaces/${namespaceId}`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to delete dispatch namespace: ${error}`); + } + + return "Successfully deleted dispatch namespace"; +} + +/** + * List all custom domains in a dispatch namespace + * @param namespaceId ID of the dispatch namespace + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of custom domains + */ +export async function handleListCustomDomains({ + namespaceId, + accountId, + apiToken, +}: { + namespaceId: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/dispatch/namespaces/${namespaceId}/domains`, + accountId, + apiToken, + responseSchema: CustomDomainsResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} + +/** + * Add a custom domain to a dispatch namespace + * @param namespaceId ID of the dispatch namespace + * @param hostname The custom domain hostname to add + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @param zoneId Optional Cloudflare zone ID for the domain + * @returns Domain addition result + */ +export async function handleAddCustomDomain({ + namespaceId, + hostname, + accountId, + apiToken, + zoneId, +}: { + namespaceId: string; + hostname: string; + accountId: string; + apiToken: string; + zoneId?: string; +}): Promise> { + const requestBody: Record = { + hostname, + }; + + if (zoneId) { + requestBody.zone_id = zoneId; + } + + const response = await fetchCloudflareApi({ + endpoint: `/workers/dispatch/namespaces/${namespaceId}/domains`, + accountId, + apiToken, + responseSchema: CustomDomainResponseSchema, + options: { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + }, + }); + + return response.result!; +} + +/** + * Remove a custom domain from a dispatch namespace + * @param namespaceId ID of the dispatch namespace + * @param hostname The custom domain hostname to remove + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Success message + */ +export async function handleRemoveCustomDomain({ + namespaceId, + hostname, + accountId, + apiToken, +}: { + namespaceId: string; + hostname: string; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/dispatch/namespaces/${namespaceId}/domains/${hostname}`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to remove custom domain: ${error}`); + } + + return "Successfully removed custom domain"; +} + +/** + * List scripts in a dispatch namespace + * @param namespace Name of the dispatch namespace + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of scripts + */ +export async function handleListScripts({ + namespace, + accountId, + apiToken, +}: { + namespace: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/dispatch/namespaces/${namespace}/scripts`, + accountId, + apiToken, + responseSchema: ScriptsResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} + +/** + * Update a script in a dispatch namespace + * @param namespace Name of the dispatch namespace + * @param scriptName Name of the script to update + * @param script The script content + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Update operation result + */ +export async function handleUpdateScript({ + namespace, + scriptName, + script, + accountId, + apiToken, +}: { + namespace: string; + scriptName: string; + script: string; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/dispatch/namespaces/${namespace}/scripts/${scriptName}`; + + const response = await fetch(url, { + method: "PUT", + headers: { + Authorization: `Bearer ${apiToken}`, + "Content-Type": "application/javascript", + }, + body: script, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to update script: ${error}`); + } + + const data = await response.json(); + return ScriptResponseSchema.parse(data).result!; +} + +/** + * Delete a script from a dispatch namespace + * @param namespace Name of the dispatch namespace + * @param scriptName Name of the script to delete + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Success message + */ +export async function handleDeleteScript({ + namespace, + scriptName, + accountId, + apiToken, +}: { + namespace: string; + scriptName: string; + accountId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/dispatch/namespaces/${namespace}/scripts/${scriptName}`; + + const response = await fetch(url, { + method: "DELETE", + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to delete script: ${error}`); + } + + return "Successfully deleted script"; +} \ No newline at end of file diff --git a/packages/mcp-common/src/api/workers.ts b/packages/mcp-common/src/api/workers.ts new file mode 100644 index 00000000..a158b2a4 --- /dev/null +++ b/packages/mcp-common/src/api/workers.ts @@ -0,0 +1,73 @@ +import { z } from "zod"; +import { fetchCloudflareApi } from "../cloudflare-api"; +import { V4Schema } from "../v4-api"; + +const WorkerSchema = z.object({ + // id is usually the worker name + id: z.string(), + created_on: z.string().optional(), + modified_on: z.string().optional(), +}); + +const CloudflareWorkerListResponseSchema = V4Schema(z.array(WorkerSchema)); + +/** + * Fetches list of workers from Cloudflare API + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of workers + */ +export async function handleWorkersList({ + accountId, + apiToken, +}: { + accountId: string; + apiToken: string; +}) { + const response = await fetchCloudflareApi({ + endpoint: "/workers/scripts", + accountId, + apiToken, + responseSchema: CloudflareWorkerListResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} + +/** + * Downloads a specific worker script from Cloudflare API + * @param scriptName Name of the worker script to download + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns The worker script content + */ +export async function handleWorkerScriptDownload({ + scriptName, + accountId, + apiToken, +}: { + scriptName: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetch( + `https://api.cloudflare.com/client/v4/accounts/${accountId}/workers/scripts/${scriptName}`, + { + method: "GET", + headers: { + Authorization: `Bearer ${apiToken}`, + Accept: "application/javascript", + }, + }, + ); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to download worker script: ${error}`); + } + + return await response.text(); +} diff --git a/packages/mcp-common/src/api/workflows.ts b/packages/mcp-common/src/api/workflows.ts new file mode 100644 index 00000000..5320a728 --- /dev/null +++ b/packages/mcp-common/src/api/workflows.ts @@ -0,0 +1,243 @@ +import { z } from "zod"; +import { fetchCloudflareApi } from "../cloudflare-api"; +import { V4Schema } from "../v4-api"; + +// Workflow schema definitions +const WorkflowStepSchema = z.object({ + name: z.string(), + type: z.string(), + script: z.string().optional(), + timeout: z.number().optional(), + // Add other properties as needed based on step types +}); + +type WorkflowSchema = z.infer +const WorkflowSchema = z.object({ + id: z.string(), + name: z.string(), + steps: z.array(WorkflowStepSchema).optional(), + created_on: z.string().optional(), +}); + +type WorkflowExecutionSchema = z.infer +const WorkflowExecutionSchema = z.object({ + id: z.string(), + workflow_id: z.string(), + status: z.string(), + created_on: z.string().optional(), +}); + +type WorkflowDeleteResultSchema = z.infer +const WorkflowDeleteResultSchema = z.object({ + id: z.string(), + deleted: z.boolean(), + message: z.string().optional(), +}); + +// Response schemas using V4Schema +const WorkflowResponseSchema = V4Schema(WorkflowSchema); +const WorkflowsListResponseSchema = V4Schema(z.array(WorkflowSchema)); +const WorkflowExecutionResponseSchema = V4Schema(WorkflowExecutionSchema); +const WorkflowDeleteResponseSchema = V4Schema(WorkflowDeleteResultSchema); + +/** + * Gets details about a specific workflow + * @param workflowId ID of the workflow to get details for + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Workflow details + */ +export async function handleGetWorkflow({ + workflowId, + accountId, + apiToken, +}: { + workflowId: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/workflows/${workflowId}`, + accountId, + apiToken, + responseSchema: WorkflowResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result; +} + +/** + * Creates a new workflow + * @param name Name for the new workflow + * @param content Workflow definition content + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Created workflow details + */ +export async function handleCreateWorkflow({ + name, + content, + accountId, + apiToken, +}: { + name: string; + content: any; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/workflows`, + accountId, + apiToken, + responseSchema: WorkflowResponseSchema, + options: { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + name, + content, + }), + }, + }); + + return response.result; +} + +/** + * Deletes a workflow + * @param workflowId ID of the workflow to delete + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Delete operation result + */ +export async function handleDeleteWorkflow({ + workflowId, + accountId, + apiToken, +}: { + workflowId: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/workflows/${workflowId}`, + accountId, + apiToken, + responseSchema: WorkflowDeleteResponseSchema, + options: { + method: "DELETE", + }, + }); + + return response.result; +} + +/** + * Lists all workflows in an account + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of workflows + */ +export async function handleListWorkflows({ + accountId, + apiToken, +}: { + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/workflows`, + accountId, + apiToken, + responseSchema: WorkflowsListResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} + +/** + * Updates a workflow + * @param workflowId ID of the workflow to update + * @param content Updated workflow definition content + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Updated workflow details + */ +export async function handleUpdateWorkflow({ + workflowId, + content, + accountId, + apiToken, +}: { + workflowId: string; + content: any; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/workflows/${workflowId}`, + accountId, + apiToken, + responseSchema: WorkflowResponseSchema, + options: { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + content, + }), + }, + }); + + return response.result; +} + +/** + * Executes a workflow + * @param workflowId ID of the workflow to execute + * @param input Optional input data for the workflow execution + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns Execution details + */ +export async function handleExecuteWorkflow({ + workflowId, + input, + accountId, + apiToken, +}: { + workflowId: string; + input?: any; + accountId: string; + apiToken: string; +}): Promise { + const requestBody: Record = {}; + if (input !== undefined) { + requestBody.input = input; + } + + const response = await fetchCloudflareApi({ + endpoint: `/workers/workflows/${workflowId}/executions`, + accountId, + apiToken, + responseSchema: WorkflowExecutionResponseSchema, + options: { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + }, + }); + + return response.result; +} \ No newline at end of file diff --git a/packages/mcp-common/src/api/wrangler.ts b/packages/mcp-common/src/api/wrangler.ts new file mode 100644 index 00000000..bf0206bb --- /dev/null +++ b/packages/mcp-common/src/api/wrangler.ts @@ -0,0 +1,80 @@ +import { z } from "zod"; +import { fetchCloudflareApi } from "../cloudflare-api"; +import { V4Schema } from "../v4-api"; + +// Schema definitions +type WranglerConfigSchema = z.infer +const WranglerConfigSchema = z.object({ + content: z.string(), + // Adding additional fields as they may be present in the API response + last_updated: z.string().optional(), + etag: z.string().optional(), +}); + +// Response schemas using V4Schema +const WranglerConfigResponseSchema = V4Schema(WranglerConfigSchema); + +/** + * Gets the wrangler.toml configuration for a Worker script + * @param scriptName The name of the Worker script + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns The wrangler.toml configuration + */ +export async function handleWranglerConfigGet({ + scriptName, + accountId, + apiToken, +}: { + scriptName: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/scripts/${scriptName}/config`, + accountId, + apiToken, + responseSchema: WranglerConfigResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result; +} + +/** + * Updates the wrangler.toml configuration for a Worker script + * @param scriptName The name of the Worker script + * @param configContent The new wrangler.toml configuration content + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns The updated wrangler.toml configuration + */ +export async function handleWranglerConfigUpdate({ + scriptName, + configContent, + accountId, + apiToken, +}: { + scriptName: string; + configContent: string; + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: `/workers/scripts/${scriptName}/config`, + accountId, + apiToken, + responseSchema: WranglerConfigResponseSchema, + options: { + method: "PUT", + headers: { + "Content-Type": "application/toml", + }, + body: configContent, + }, + }); + + return response.result; +} \ No newline at end of file diff --git a/packages/mcp-common/src/api/zones.ts b/packages/mcp-common/src/api/zones.ts new file mode 100644 index 00000000..f32ef3c5 --- /dev/null +++ b/packages/mcp-common/src/api/zones.ts @@ -0,0 +1,117 @@ +import { z } from "zod"; +import { fetchCloudflareApi } from "../cloudflare-api"; +import { V4Schema } from "../v4-api"; + +// Zone schema +type ZoneSchema = z.infer +const ZoneSchema = z.object({ + id: z.string(), + name: z.string(), + status: z.string(), + paused: z.boolean(), + type: z.string(), + development_mode: z.number(), + created_on: z.string().optional(), + modified_on: z.string().optional(), +}); + +// Custom domain schema +type CustomDomainSchema = z.infer +const CustomDomainSchema = z.object({ + id: z.string(), + zone_id: z.string().optional(), + zone_name: z.string().optional(), + hostname: z.string(), + service: z.string(), + environment: z.string().optional(), + created_on: z.string().optional(), +}); + +// Response schemas using V4Schema +const ZonesResponseSchema = V4Schema(z.array(ZoneSchema)); +const ZoneResponseSchema = V4Schema(ZoneSchema); +const CustomDomainsResponseSchema = V4Schema(z.array(CustomDomainSchema)); + +/** + * List all zones in a Cloudflare account + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of zones + */ +export async function handleZonesList({ + apiToken, +}: { + apiToken: string; +}): Promise { + const url = "https://api.cloudflare.com/client/v4/zones"; + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to list zones: ${error}`); + } + + const data = ZonesResponseSchema.parse(await response.json()); + return data.result ?? []; +} + +/** + * Get details about a specific zone + * @param zoneId ID of the zone to get details for + * @param apiToken Cloudflare API token + * @returns Zone details + */ +export async function handleZoneGet({ + zoneId, + apiToken, +}: { + zoneId: string; + apiToken: string; +}): Promise { + const url = `https://api.cloudflare.com/client/v4/zones/${zoneId}`; + + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${apiToken}`, + }, + }); + + if (!response.ok) { + const error = await response.text(); + throw new Error(`Failed to get zone details: ${error}`); + } + + const data = ZoneResponseSchema.parse(await response.json()); + return data.result!; +} + +/** + * List custom domains attached to Workers + * @param accountId Cloudflare account ID + * @param apiToken Cloudflare API token + * @returns List of custom domains + */ +export async function handleDomainsList({ + accountId, + apiToken, +}: { + accountId: string; + apiToken: string; +}): Promise { + const response = await fetchCloudflareApi({ + endpoint: "/workers/domains", + accountId, + apiToken, + responseSchema: CustomDomainsResponseSchema, + options: { + method: "GET", + }, + }); + + return response.result ?? []; +} \ No newline at end of file diff --git a/packages/mcp-common/src/v4-api.ts b/packages/mcp-common/src/v4-api.ts new file mode 100644 index 00000000..4880ed7f --- /dev/null +++ b/packages/mcp-common/src/v4-api.ts @@ -0,0 +1,24 @@ +import { z } from "zod" + +type V4ErrorSchema = typeof V4ErrorSchema +const V4ErrorSchema = z.array( + z.object({ + code: z.number().optional(), + message: z.string(), + }) +) + +export const V4Schema = ( + resultType: TResultType +): z.ZodObject<{ + result: z.ZodNullable + success: z.ZodBoolean + errors: V4ErrorSchema + messages: z.ZodArray +}> => + z.object({ + result: resultType.nullable(), + success: z.boolean(), + errors: V4ErrorSchema, + messages: z.array(z.any()), + }) \ No newline at end of file diff --git a/apps/workers-observability/src/tools/logs.spec.ts b/packages/mcp-common/tests/logs.spec.ts similarity index 99% rename from apps/workers-observability/src/tools/logs.spec.ts rename to packages/mcp-common/tests/logs.spec.ts index c60bca64..c119793b 100644 --- a/apps/workers-observability/src/tools/logs.spec.ts +++ b/packages/mcp-common/tests/logs.spec.ts @@ -1,6 +1,6 @@ import { env, fetchMock } from "cloudflare:test"; import { afterEach, beforeAll, describe, expect, it } from "vitest"; -import { handleWorkerLogs, handleWorkerLogsKeys } from "./logs"; +import { handleWorkerLogs, handleWorkerLogsKeys } from "../src/api/logs"; beforeAll(() => { // Enable outbound request mocking... diff --git a/apps/workers-observability/src/tools/workers.spec.ts b/packages/mcp-common/tests/workers.spec.ts similarity index 99% rename from apps/workers-observability/src/tools/workers.spec.ts rename to packages/mcp-common/tests/workers.spec.ts index 852eb8ed..121c2fa2 100644 --- a/apps/workers-observability/src/tools/workers.spec.ts +++ b/packages/mcp-common/tests/workers.spec.ts @@ -1,6 +1,6 @@ import { env, fetchMock } from "cloudflare:test"; import { afterEach, beforeAll, describe, expect, it } from "vitest"; -import { handleWorkerScriptDownload, handleWorkersList } from "./workers"; +import { handleWorkerScriptDownload, handleWorkersList } from "../src/api/workers"; beforeAll(() => { // Enable outbound request mocking... diff --git a/packages/mcp-common/types.d.ts b/packages/mcp-common/types.d.ts new file mode 100644 index 00000000..9e1cd186 --- /dev/null +++ b/packages/mcp-common/types.d.ts @@ -0,0 +1,5 @@ +import type { TestEnv } from "./vitest.config"; + +declare module "cloudflare:test" { + interface ProvidedEnv extends TestEnv {} +} diff --git a/packages/mcp-common/vitest.config.ts b/packages/mcp-common/vitest.config.ts index 90b459dc..cc3bd71c 100644 --- a/packages/mcp-common/vitest.config.ts +++ b/packages/mcp-common/vitest.config.ts @@ -1,5 +1,10 @@ import { defineWorkersProject } from '@cloudflare/vitest-pool-workers/config' +export interface TestEnv { + CLOUDFLARE_MOCK_ACCOUNT_ID: string; + CLOUDFLARE_MOCK_API_TOKEN: string; +} + export default defineWorkersProject({ test: { poolOptions: { @@ -8,6 +13,10 @@ export default defineWorkersProject({ miniflare: { compatibilityDate: '2025-03-10', compatibilityFlags: ['nodejs_compat'], + bindings: { + CLOUDFLARE_MOCK_ACCOUNT_ID: "mock-account-id", + CLOUDFLARE_MOCK_API_TOKEN: "mock-api-token", + } satisfies Partial, }, }, }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3d9f18b4..8fb107aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -30,9 +30,6 @@ importers: '@repo/typescript-config': specifier: workspace:* version: link:packages/typescript-config - '@sentry/cli': - specifier: 2.43.0 - version: 2.43.0 '@vitest/ui': specifier: 3.0.9 version: 3.0.9(vitest@3.0.9) @@ -142,6 +139,12 @@ importers: '@hono/zod-validator': specifier: 0.4.3 version: 0.4.3(hono@4.7.6)(zod@3.24.2) + '@modelcontextprotocol/sdk': + specifier: 1.8.0 + version: 1.8.0 + agents: + specifier: 0.0.49 + version: 0.0.49(@cloudflare/workers-types@4.20250410.0) hono: specifier: 4.7.6 version: 4.7.6 @@ -811,58 +814,6 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@sentry/cli-darwin@2.43.0': - resolution: {integrity: sha512-0MYvRHJowXOMNY5W6XF4p9GQNH3LuQ+IHAQwVbZOsfwnEv8e20rf9BiPPzmJ9sIjZSWYR4yIqm6dBp6ABJFbGQ==} - engines: {node: '>=10'} - os: [darwin] - - '@sentry/cli-linux-arm64@2.43.0': - resolution: {integrity: sha512-7URSaNjbEJQZyYJ33XK3pVKl6PU2oO9ETF6R/4Cz2FmU3fecACLKVldv7+OuNl9aspLZ62mnPMDvT732/Fp2Ug==} - engines: {node: '>=10'} - cpu: [arm64] - os: [linux, freebsd] - - '@sentry/cli-linux-arm@2.43.0': - resolution: {integrity: sha512-c2Fwb6HrFL1nbaGV4uRhHC1wEJPR+wfpKN5y06PgSNNbd10YrECAB3tqBHXC8CEmhuDyFR+ORGZ7VbswfCWEEQ==} - engines: {node: '>=10'} - cpu: [arm] - os: [linux, freebsd] - - '@sentry/cli-linux-i686@2.43.0': - resolution: {integrity: sha512-bFo/tpMZeMJ275HPGmAENREchnBxhALOOpZAphSyalUu3pGZ+EETEtlSLrKyVNJo26Dye5W7GlrYUV9+rkyCtg==} - engines: {node: '>=10'} - cpu: [x86, ia32] - os: [linux, freebsd] - - '@sentry/cli-linux-x64@2.43.0': - resolution: {integrity: sha512-EbAmKXUNU/Ii4pNGVRCepU6ks1M43wStMKx3pibrUTllrrCwqYKyPxRRdoFYySHkduwCxnoKZcLEg9vWZ3qS6A==} - engines: {node: '>=10'} - cpu: [x64] - os: [linux, freebsd] - - '@sentry/cli-win32-arm64@2.43.0': - resolution: {integrity: sha512-KmJRCdQQGLSErJvrcGcN+yWo68m+5OdluhyJHsVYMOQknwu8YMOWLm12EIa+4t4GclDvwg5xcxLccCuiWMJUZw==} - engines: {node: '>=10'} - cpu: [arm64] - os: [win32] - - '@sentry/cli-win32-i686@2.43.0': - resolution: {integrity: sha512-ZWxZdOyZX7NJ/CTskzg+dJ2xTpobFLXVNMOMq0HiwdhqXP2zYYJzKnIt3mHNJYA40zYFODGSgxIamodjpB8BuA==} - engines: {node: '>=10'} - cpu: [x86, ia32] - os: [win32] - - '@sentry/cli-win32-x64@2.43.0': - resolution: {integrity: sha512-S/IRQYAziEnjpyROhnqzTqShDq3m8jcevXx+q5f49uQnFbfYcTgS1sdrEPqqao/K2boOWbffxYtTkvBiB/piQQ==} - engines: {node: '>=10'} - cpu: [x64] - os: [win32] - - '@sentry/cli@2.43.0': - resolution: {integrity: sha512-gBE3bkx+PBJxopTrzIJLX4xHe5S0w87q5frIveWKDZ5ulVIU6YWnVumay0y07RIEweUEj3IYva1qH6HG2abfiA==} - engines: {node: '>= 10'} - hasBin: true - '@sindresorhus/merge-streams@2.3.0': resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} engines: {node: '>=18'} @@ -1010,10 +961,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - agent-base@6.0.2: - resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} - engines: {node: '>= 6.0.0'} - agents@0.0.49: resolution: {integrity: sha512-AzyV8iyW9uT9xunfbegIXyOqKjU1Kxza2VSaM7cLKEZdZqzXHn8QuQdXz7jtGfdGo9K9K/opOsFoUriArK5/Ow==} @@ -1782,10 +1729,6 @@ packages: resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==} engines: {node: '>= 0.8'} - https-proxy-agent@5.0.1: - resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} - engines: {node: '>= 6'} - human-id@4.1.1: resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==} hasBin: true @@ -2188,15 +2131,6 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - node-fetch@2.7.0: - resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - npm-package-arg@12.0.2: resolution: {integrity: sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==} engines: {node: ^18.17.0 || >=20.5.0} @@ -2398,10 +2332,6 @@ packages: resolution: {integrity: sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==} engines: {node: ^18.17.0 || >=20.5.0} - progress@2.0.3: - resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} - engines: {node: '>=0.4.0'} - prompts@2.4.2: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} @@ -2410,9 +2340,6 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} - proxy-from-env@1.1.0: - resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2735,9 +2662,6 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} - tr46@0.0.3: - resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} - ts-api-utils@1.3.0: resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} engines: {node: '>=16'} @@ -2923,12 +2847,6 @@ packages: jsdom: optional: true - webidl-conversions@3.0.1: - resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} - - whatwg-url@5.0.0: - resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} - which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} @@ -3598,50 +3516,6 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@sentry/cli-darwin@2.43.0': - optional: true - - '@sentry/cli-linux-arm64@2.43.0': - optional: true - - '@sentry/cli-linux-arm@2.43.0': - optional: true - - '@sentry/cli-linux-i686@2.43.0': - optional: true - - '@sentry/cli-linux-x64@2.43.0': - optional: true - - '@sentry/cli-win32-arm64@2.43.0': - optional: true - - '@sentry/cli-win32-i686@2.43.0': - optional: true - - '@sentry/cli-win32-x64@2.43.0': - optional: true - - '@sentry/cli@2.43.0': - dependencies: - https-proxy-agent: 5.0.1 - node-fetch: 2.7.0 - progress: 2.0.3 - proxy-from-env: 1.1.0 - which: 2.0.2 - optionalDependencies: - '@sentry/cli-darwin': 2.43.0 - '@sentry/cli-linux-arm': 2.43.0 - '@sentry/cli-linux-arm64': 2.43.0 - '@sentry/cli-linux-i686': 2.43.0 - '@sentry/cli-linux-x64': 2.43.0 - '@sentry/cli-win32-arm64': 2.43.0 - '@sentry/cli-win32-i686': 2.43.0 - '@sentry/cli-win32-x64': 2.43.0 - transitivePeerDependencies: - - encoding - - supports-color - '@sindresorhus/merge-streams@2.3.0': {} '@standard-schema/spec@1.0.0': {} @@ -3821,12 +3695,6 @@ snapshots: acorn@8.14.0: {} - agent-base@6.0.2: - dependencies: - debug: 4.4.0 - transitivePeerDependencies: - - supports-color - agents@0.0.49(@cloudflare/workers-types@4.20250410.0): dependencies: '@modelcontextprotocol/sdk': 1.8.0 @@ -4756,13 +4624,6 @@ snapshots: statuses: 2.0.1 toidentifier: 1.0.1 - https-proxy-agent@5.0.1: - dependencies: - agent-base: 6.0.2 - debug: 4.4.0 - transitivePeerDependencies: - - supports-color - human-id@4.1.1: {} iconv-lite@0.4.24: @@ -5093,10 +4954,6 @@ snapshots: negotiator@1.0.0: {} - node-fetch@2.7.0: - dependencies: - whatwg-url: 5.0.0 - npm-package-arg@12.0.2: dependencies: hosted-git-info: 8.0.2 @@ -5275,8 +5132,6 @@ snapshots: proc-log@5.0.0: {} - progress@2.0.3: {} - prompts@2.4.2: dependencies: kleur: 3.0.3 @@ -5287,8 +5142,6 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 - proxy-from-env@1.1.0: {} - punycode@2.3.1: {} pure-rand@6.1.0: {} @@ -5682,8 +5535,6 @@ snapshots: totalist@3.0.1: {} - tr46@0.0.3: {} - ts-api-utils@1.3.0(typescript@5.5.4): dependencies: typescript: 5.5.4 @@ -5874,13 +5725,6 @@ snapshots: - supports-color - terser - webidl-conversions@3.0.1: {} - - whatwg-url@5.0.0: - dependencies: - tr46: 0.0.3 - webidl-conversions: 3.0.1 - which-boxed-primitive@1.0.2: dependencies: is-bigint: 1.0.4