diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 39a1e68a..ea03789f 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -1,9 +1,6 @@ // This configuration only applies to the package manager root. /** @type {import("eslint").Linter.Config} */ module.exports = { - ignorePatterns: [ - 'apps/**', - 'packages/**', - ], - extends: ['@repo/eslint-config/default.cjs'] + ignorePatterns: ['apps/**', 'packages/**'], + extends: ['@repo/eslint-config/default.cjs'], } diff --git a/.vscode/extensions.json b/.vscode/extensions.json index b640d7c8..60853896 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -3,10 +3,7 @@ // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp // List of extensions which should be recommended for users of this workspace. - "recommendations": [ - "esbenp.prettier-vscode", - "dbaeumer.vscode-eslint", - ], + "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"], // List of extensions recommended by VS Code that should not be recommended for users of this workspace. "unwantedRecommendations": [] } diff --git a/.vscode/settings.json b/.vscode/settings.json index 7d407b89..3c072771 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -13,11 +13,11 @@ "**/packages/tools/bin/*": "shellscript", "**/*.css": "tailwindcss", "turbo.json": "jsonc", - "**/packages/typescript-config/*.json": "jsonc", + "**/packages/typescript-config/*.json": "jsonc" }, "eslint.workingDirectories": [ { "mode": "auto" } - ], + ] } diff --git a/apps/workers-observability/src/index.ts b/apps/workers-observability/src/index.ts index 27f349d8..114182a5 100644 --- a/apps/workers-observability/src/index.ts +++ b/apps/workers-observability/src/index.ts @@ -1,56 +1,58 @@ -import OAuthProvider from "@cloudflare/workers-oauth-provider"; -import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; -import { McpAgent } from "agents/mcp"; -import type { Env } from "../worker-configuration"; -import { registerAccountTools } from "./tools/account"; -import { registerLogsTools } from "./tools/logs"; -import { registerWorkersTools } from "./tools/workers"; +import OAuthProvider from '@cloudflare/workers-oauth-provider' +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { McpAgent } from 'agents/mcp' + import { - type AccountSchema, CloudflareAuthHandler, - type UserSchema, handleTokenExchangeCallback, -} from "@repo/mcp-common/src/cloudflare-oauth-handler"; +} from '@repo/mcp-common/src/cloudflare-oauth-handler' + +import { registerAccountTools } from './tools/account' +import { registerLogsTools } from './tools/logs' +import { registerWorkersTools } from './tools/workers' + +import type { AccountSchema, UserSchema } from '@repo/mcp-common/src/cloudflare-oauth-handler' +import type { Env } from '../worker-configuration' // Context from the auth process, encrypted & stored in the auth token // and provided to the DurableMCP as this.props export type Props = { - accessToken: string; - user: UserSchema["result"]; - accounts: AccountSchema["result"]; -}; + accessToken: string + user: UserSchema['result'] + accounts: AccountSchema['result'] +} -export type State = { activeAccountId: string | null }; +export type State = { activeAccountId: string | null } export class MyMCP extends McpAgent { server = new McpServer({ - name: "Remote MCP Server with Workers Observability", - version: "1.0.0", - }); + name: 'Remote MCP Server with Workers Observability', + version: '1.0.0', + }) // TOOO: Why does this type need to be declared again on MyMCP? // @ts-ignore - env!: Env; + env!: Env initialState: State = { activeAccountId: null, - }; + } async init() { - registerAccountTools(this); + registerAccountTools(this) // Register Cloudflare Workers tools - registerWorkersTools(this); + registerWorkersTools(this) // Register Cloudflare Workers logs tools - registerLogsTools(this); + registerLogsTools(this) } getActiveAccountId() { // TODO: Figure out why this fail sometimes, and why we need to wrap this in a try catch try { - return this.state.activeAccountId ?? null; + return this.state.activeAccountId ?? null } catch (e) { - return null; + return null } } @@ -60,23 +62,23 @@ export class MyMCP extends McpAgent { this.setState({ ...this.state, activeAccountId: accountId, - }); + }) } catch (e) { - return null; + return null } } } export default new OAuthProvider({ - apiRoute: "/workers/observability/sse", + apiRoute: '/workers/observability/sse', // @ts-ignore - apiHandler: MyMCP.mount("/workers/observability/sse"), + apiHandler: MyMCP.mount('/workers/observability/sse'), // @ts-ignore defaultHandler: CloudflareAuthHandler, - authorizeEndpoint: "/oauth/authorize", - tokenEndpoint: "/token", + authorizeEndpoint: '/oauth/authorize', + tokenEndpoint: '/token', tokenExchangeCallback: handleTokenExchangeCallback, // Cloudflare access token TTL accessTokenTTL: 3600, - clientRegistrationEndpoint: "/register", -}); + clientRegistrationEndpoint: '/register', +}) diff --git a/apps/workers-observability/src/tools/account.ts b/apps/workers-observability/src/tools/account.ts index 8559a73e..f8a9cdd3 100644 --- a/apps/workers-observability/src/tools/account.ts +++ b/apps/workers-observability/src/tools/account.ts @@ -1,85 +1,89 @@ -import { z } from "zod"; -import type { MyMCP } from "../index"; -import { handleAccountsList } from "@repo/mcp-common/src/api/account" -import { getCloudflareClient } from "@repo/mcp-common/src/cloudflare-api"; +import { z } from 'zod' + +import { handleAccountsList } from '@repo/mcp-common/src/api/account' +import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api' + +import type { MyMCP } from '../index' export function registerAccountTools(agent: MyMCP) { // Tool to list all accounts agent.server.tool( - "accounts_list", - "List all accounts in your Cloudflare account", + 'accounts_list', + 'List all accounts in your Cloudflare account', {}, async () => { try { - const results = await handleAccountsList({client: getCloudflareClient(agent.props.accessToken)}); + const results = await handleAccountsList({ + client: getCloudflareClient(agent.props.accessToken), + }) // Sort accounts by created_on date (newest first) const accounts = results // order by created_on desc ( newest first ) .sort((a, b) => { - if (!a.created_on) return 1; - if (!b.created_on) return -1; - return new Date(b.created_on).getTime() - new Date(a.created_on).getTime(); - }); + if (!a.created_on) return 1 + if (!b.created_on) return -1 + return new Date(b.created_on).getTime() - new Date(a.created_on).getTime() + }) return { content: [ { - type: "text", + type: 'text', text: JSON.stringify({ accounts, count: accounts.length, }), }, ], - }; + } } catch (error) { return { content: [ { - type: "text", + type: 'text', text: `Error listing accounts: ${error instanceof Error && error.message}`, }, ], - }; + } } - }, - ); + } + ) const activeAccountIdParam = z .string() .describe( - "The accountId present in the users Cloudflare account, that should be the active accountId.", - ); + 'The accountId present in the users Cloudflare account, that should be the active accountId.' + ) agent.server.tool( - "set_active_account", - "Set active account to be used for tool calls that require accountId", + 'set_active_account', + 'Set active account to be used for tool calls that require accountId', { activeAccountIdParam, }, async (params) => { try { - const { activeAccountIdParam: activeAccountId } = params; - agent.setActiveAccountId(activeAccountId); + const { activeAccountIdParam: activeAccountId } = params + agent.setActiveAccountId(activeAccountId) return { content: [ { - type: "text", + type: 'text', text: JSON.stringify({ activeAccountId, }), }, ], - }; + } } catch (error) { return { content: [ { - type: "text", + type: 'text', text: `Error setting activeAccountID: ${error instanceof Error && error.message}`, }, ], - }; + } } - }, - ); + } + ) } diff --git a/apps/workers-observability/src/tools/logs.ts b/apps/workers-observability/src/tools/logs.ts index 84e549eb..5e921d9f 100644 --- a/apps/workers-observability/src/tools/logs.ts +++ b/apps/workers-observability/src/tools/logs.ts @@ -1,24 +1,25 @@ -import { z } from "zod"; -import type { MyMCP } from "../index"; -import { handleWorkerLogsKeys, handleWorkerLogs } from "@repo/mcp-common/src/api/logs" +import { z } from 'zod' + +import { handleWorkerLogs, handleWorkerLogsKeys } from '@repo/mcp-common/src/api/logs' + +import type { MyMCP } from '../index' // Worker logs parameter schema -const workerNameParam = z.string().describe("The name of the worker to analyze logs for"); -const filterErrorsParam = z.boolean().default(false).describe("If true, only shows error logs"); +const workerNameParam = z.string().describe('The name of the worker to analyze logs for') +const filterErrorsParam = z.boolean().default(false).describe('If true, only shows error logs') const limitParam = z .number() .min(1) .max(100) .default(100) - .describe("Maximum number of logs to retrieve (1-100, default 100)"); + .describe('Maximum number of logs to retrieve (1-100, default 100)') const minutesAgoParam = z .number() .min(1) .max(1440) .default(30) - .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"); - + .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') /** * Registers the logs analysis tool with the MCP server @@ -29,8 +30,8 @@ const rayIdParam = z.string().optional().describe("Filter logs by specific Cloud export function registerLogsTools(agent: MyMCP) { // Register the worker logs analysis tool by worker name agent.server.tool( - "worker_logs_by_worker_name", - "Analyze recent logs for a Cloudflare Worker by worker name", + 'worker_logs_by_worker_name', + 'Analyze recent logs for a Cloudflare Worker by worker name', { scriptName: workerNameParam, shouldFilterErrors: filterErrorsParam, @@ -39,20 +40,19 @@ export function registerLogsTools(agent: MyMCP) { rayId: rayIdParam, }, async (params) => { - const accountId = agent.getActiveAccountId(); + const accountId = agent.getActiveAccountId() if (!accountId) { return { content: [ { - type: "text", - text: "No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)", + type: 'text', + text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], - }; + } } try { - const { scriptName, shouldFilterErrors, limitParam, minutesAgoParam, rayId } = - params; + const { scriptName, shouldFilterErrors, limitParam, minutesAgoParam, rayId } = params const { relevantLogs, from, to } = await handleWorkerLogs({ scriptName, limit: limitParam, @@ -61,11 +61,11 @@ export function registerLogsTools(agent: MyMCP) { apiToken: agent.props.accessToken, shouldFilterErrors, rayId, - }); + }) return { content: [ { - type: "text", + type: 'text', text: JSON.stringify({ logs: relevantLogs, stats: { @@ -78,46 +78,46 @@ export function registerLogsTools(agent: MyMCP) { }), }, ], - }; + } } catch (error) { return { content: [ { - type: "text", + type: 'text', text: JSON.stringify({ error: `Error analyzing worker logs: ${error instanceof Error && error.message}`, }), }, ], - }; + } } - }, - ); + } + ) // Register tool to search logs by Ray ID across all workers agent.server.tool( - "worker_logs_by_rayid", - "Analyze recent logs across all workers for a specific request by Cloudflare Ray ID", + 'worker_logs_by_rayid', + 'Analyze recent logs across all workers for a specific request by Cloudflare Ray ID', { - rayId: z.string().describe("Filter logs by specific Cloudflare Ray ID"), + rayId: z.string().describe('Filter logs by specific Cloudflare Ray ID'), shouldFilterErrors: filterErrorsParam, limitParam, minutesAgoParam, }, async (params) => { - const accountId = agent.getActiveAccountId(); + const accountId = agent.getActiveAccountId() if (!accountId) { return { content: [ { - type: "text", - text: "No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)", + type: 'text', + text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], - }; + } } try { - const { rayId, shouldFilterErrors, limitParam, minutesAgoParam } = params; + const { rayId, shouldFilterErrors, limitParam, minutesAgoParam } = params const { relevantLogs, from, to } = await handleWorkerLogs({ limit: limitParam, minutesAgo: minutesAgoParam, @@ -125,11 +125,11 @@ export function registerLogsTools(agent: MyMCP) { apiToken: agent.props.accessToken, shouldFilterErrors, rayId, - }); + }) return { content: [ { - type: "text", + type: 'text', text: JSON.stringify({ logs: relevantLogs, stats: { @@ -142,86 +142,84 @@ export function registerLogsTools(agent: MyMCP) { }), }, ], - }; + } } catch (error) { return { content: [ { - type: "text", + type: 'text', text: JSON.stringify({ error: `Error analyzing logs by Ray ID: ${error instanceof Error && error.message}`, }), }, ], - }; + } } - }, - ); + } + ) // Register the worker telemetry keys tool agent.server.tool( - "worker_logs_keys", - "Get available telemetry keys for a Cloudflare Worker", + 'worker_logs_keys', + 'Get available telemetry keys for a Cloudflare Worker', { scriptName: workerNameParam, minutesAgoParam }, async (params) => { - const accountId = agent.getActiveAccountId(); + const accountId = agent.getActiveAccountId() if (!accountId) { return { content: [ { - type: "text", - text: "No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)", + type: 'text', + text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], - }; + } } try { - const { scriptName, minutesAgoParam } = params; + const { scriptName, minutesAgoParam } = params const keys = await handleWorkerLogsKeys( scriptName, minutesAgoParam, accountId, - agent.props.accessToken, - ); + agent.props.accessToken + ) return { content: [ { - type: "text", + type: 'text', text: JSON.stringify({ keys: keys.map((key) => ({ key: key.key, type: key.type, - lastSeen: key.lastSeen - ? new Date(key.lastSeen).toISOString() - : null, + lastSeen: key.lastSeen ? new Date(key.lastSeen).toISOString() : null, })), stats: { total: keys.length, byType: keys.reduce( (acc, key) => { - acc[key.type] = (acc[key.type] || 0) + 1; - return acc; + acc[key.type] = (acc[key.type] || 0) + 1 + return acc }, - {} as Record, + {} as Record ), }, }), }, ], - }; + } } catch (error) { return { content: [ { - type: "text", + type: 'text', text: JSON.stringify({ error: `Error retrieving worker telemetry keys: ${error instanceof Error && error.message}`, }), }, ], - }; + } } - }, - ); + } + ) } diff --git a/apps/workers-observability/src/tools/workers.ts b/apps/workers-observability/src/tools/workers.ts index d3f4ce59..d75d9674 100644 --- a/apps/workers-observability/src/tools/workers.ts +++ b/apps/workers-observability/src/tools/workers.ts @@ -1,7 +1,9 @@ -import { z } from "zod"; -import type { MyMCP } from "../index"; -import { handleWorkersList, handleWorkerScriptDownload } from "@repo/mcp-common/src/api/workers" -import { getCloudflareClient } from "@repo/mcp-common/src/cloudflare-api"; +import { z } from 'zod' + +import { handleWorkerScriptDownload, handleWorkersList } from '@repo/mcp-common/src/api/workers' +import { getCloudflareClient } from '@repo/mcp-common/src/cloudflare-api' + +import type { MyMCP } from '../index' /** * Registers the workers tools with the MCP server @@ -10,111 +12,106 @@ import { getCloudflareClient } from "@repo/mcp-common/src/cloudflare-api"; * @param apiToken Cloudflare API token */ // Define the scriptName parameter schema -const workerNameParam = z.string().describe("The name of the worker script to retrieve"); +const workerNameParam = z.string().describe('The name of the worker script to retrieve') export function registerWorkersTools(agent: MyMCP) { // Tool to list all workers - agent.server.tool( - "workers_list", - "List all Workers in your Cloudflare account", - {}, - async () => { - const accountId = agent.getActiveAccountId(); - if (!accountId) { - return { - content: [ - { - type: "text", - text: "No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)", - }, - ], - }; + agent.server.tool('workers_list', 'List all Workers in your Cloudflare account', {}, async () => { + const accountId = agent.getActiveAccountId() + if (!accountId) { + return { + content: [ + { + type: 'text', + text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', + }, + ], } - try { - const results = await handleWorkersList({ - client: getCloudflareClient(agent.props.accessToken), - accountId, - }); - // Extract worker details and sort by created_on date (newest first) - const workers = results - .map((worker) => ({ - name: worker.id, - modified_on: worker.modified_on || null, - created_on: worker.created_on || null, - })) - // order by created_on desc ( newest first ) - .sort((a, b) => { - if (!a.created_on) return 1; - if (!b.created_on) return -1; - return new Date(b.created_on).getTime() - new Date(a.created_on).getTime(); - }); + } + try { + const results = await handleWorkersList({ + client: getCloudflareClient(agent.props.accessToken), + accountId, + }) + // Extract worker details and sort by created_on date (newest first) + const workers = results + .map((worker) => ({ + name: worker.id, + modified_on: worker.modified_on || null, + created_on: worker.created_on || null, + })) + // order by created_on desc ( newest first ) + .sort((a, b) => { + if (!a.created_on) return 1 + if (!b.created_on) return -1 + return new Date(b.created_on).getTime() - new Date(a.created_on).getTime() + }) - return { - content: [ - { - type: "text", - text: JSON.stringify({ - workers, - count: workers.length, - }), - }, - ], - }; - } catch (error) { - return { - content: [ - { - type: "text", - text: `Error listing workers: ${error instanceof Error && error.message}`, - }, - ], - }; + return { + content: [ + { + type: 'text', + text: JSON.stringify({ + workers, + count: workers.length, + }), + }, + ], + } + } catch (error) { + return { + content: [ + { + type: 'text', + text: `Error listing workers: ${error instanceof Error && error.message}`, + }, + ], } - }, - ); + } + }) // Tool to get a specific worker's script content agent.server.tool( - "worker_get_worker", - "Get the source code of a Cloudflare Worker", + 'worker_get_worker', + 'Get the source code of a Cloudflare Worker', { scriptName: workerNameParam }, async (params) => { - const accountId = agent.getActiveAccountId(); + const accountId = agent.getActiveAccountId() if (!accountId) { return { content: [ { - type: "text", - text: "No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)", + type: 'text', + text: 'No currently active accountId. Try listing your accounts (accounts_list) and then setting an active account (set_active_account)', }, ], - }; + } } try { - const { scriptName } = params; + const { scriptName } = params const scriptContent = await handleWorkerScriptDownload({ client: getCloudflareClient(agent.props.accessToken), scriptName, accountId, - }); + }) return { content: [ { - type: "text", + type: 'text', text: scriptContent, }, ], - }; + } } catch (error) { return { content: [ { - type: "text", + type: 'text', text: `Error retrieving worker script: ${error instanceof Error && error.message}`, }, ], - }; + } } - }, - ); + } + ) } diff --git a/apps/workers-observability/types.d.ts b/apps/workers-observability/types.d.ts index 9e1cd186..e090f9a7 100644 --- a/apps/workers-observability/types.d.ts +++ b/apps/workers-observability/types.d.ts @@ -1,5 +1,5 @@ -import type { TestEnv } from "./vitest.config"; +import type { TestEnv } from './vitest.config' -declare module "cloudflare:test" { +declare module 'cloudflare:test' { interface ProvidedEnv extends TestEnv {} } diff --git a/apps/workers-observability/vitest.config.ts b/apps/workers-observability/vitest.config.ts index d24823cb..acf7b330 100644 --- a/apps/workers-observability/vitest.config.ts +++ b/apps/workers-observability/vitest.config.ts @@ -1,10 +1,10 @@ -import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; +import { defineWorkersConfig } from '@cloudflare/vitest-pool-workers/config' -import type { Env } from "./worker-configuration"; +import type { Env } from './worker-configuration' export interface TestEnv extends Env { - CLOUDFLARE_MOCK_ACCOUNT_ID: string; - CLOUDFLARE_MOCK_API_TOKEN: string; + CLOUDFLARE_MOCK_ACCOUNT_ID: string + CLOUDFLARE_MOCK_API_TOKEN: string } export default defineWorkersConfig({ @@ -14,11 +14,11 @@ export default defineWorkersConfig({ wrangler: { configPath: `${__dirname}/wrangler.jsonc` }, miniflare: { bindings: { - CLOUDFLARE_MOCK_ACCOUNT_ID: "mock-account-id", - CLOUDFLARE_MOCK_API_TOKEN: "mock-api-token", + CLOUDFLARE_MOCK_ACCOUNT_ID: 'mock-account-id', + CLOUDFLARE_MOCK_API_TOKEN: 'mock-api-token', } satisfies Partial, }, }, }, }, -}); +}) diff --git a/apps/workers-observability/wrangler.jsonc b/apps/workers-observability/wrangler.jsonc index a3041a6d..a3292f40 100644 --- a/apps/workers-observability/wrangler.jsonc +++ b/apps/workers-observability/wrangler.jsonc @@ -54,7 +54,7 @@ "binding": "OAUTH_KV", "id": "18e839155d00407095d793dcf7e78f25" } - ], + ] }, "production": { "name": "mcp-cloudflare-production", @@ -73,7 +73,7 @@ "binding": "OAUTH_KV", "id": "f9782295993747df90c29c45ca89edb1" } - ], + ] } - }, + } } diff --git a/packages/mcp-common/src/api/account.ts b/packages/mcp-common/src/api/account.ts index 629bb203..a55ad26e 100644 --- a/packages/mcp-common/src/api/account.ts +++ b/packages/mcp-common/src/api/account.ts @@ -1,12 +1,8 @@ -import type { Cloudflare } from 'cloudflare'; -import type { Account } from "cloudflare/resources/accounts/accounts.mjs"; +import type { Cloudflare } from 'cloudflare' +import type { Account } from 'cloudflare/resources/accounts/accounts.mjs' -export async function handleAccountsList({ - client, -}: { - client: Cloudflare -}): Promise { +export async function handleAccountsList({ client }: { client: Cloudflare }): Promise { // Currently limited to 50 accounts - const response = await client.accounts.list({query: {per_page: 50}}) + const response = await client.accounts.list({ query: { per_page: 50 } }) return response.result } diff --git a/packages/mcp-common/src/api/logs.ts b/packages/mcp-common/src/api/logs.ts index 13867dfa..76da1e48 100644 --- a/packages/mcp-common/src/api/logs.ts +++ b/packages/mcp-common/src/api/logs.ts @@ -1,6 +1,7 @@ -import { z } from "zod"; -import { fetchCloudflareApi } from "../cloudflare-api"; -import { V4Schema } from "../v4-api"; +import { z } from 'zod' + +import { fetchCloudflareApi } from '../cloudflare-api' +import { V4Schema } from '../v4-api' const RelevantLogInfoSchema = z.object({ timestamp: z.string(), @@ -15,30 +16,28 @@ const RelevantLogInfoSchema = z.object({ requestId: z.string(), rayId: z.string().nullable(), exceptionStack: z.string().nullable(), -}); -type RelevantLogInfo = z.infer; +}) +type RelevantLogInfo = z.infer const TelemetryKeySchema = z.object({ key: z.string(), - type: z.enum(["string", "number", "boolean"]), + type: z.enum(['string', 'number', 'boolean']), lastSeen: z.number().optional(), -}); -type TelemetryKey = z.infer; +}) +type TelemetryKey = z.infer -const LogsKeysResponseSchema = V4Schema( - z.array(TelemetryKeySchema).optional().default([]) -); +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(), @@ -46,7 +45,7 @@ const WorkerEventDetailsSchema = z.object({ rpcMethod: z.string().optional(), rayId: z.string().optional(), executionModel: z.string().optional(), -}); +}) const WorkerInfoSchema = z.object({ scriptName: z.string(), @@ -57,7 +56,7 @@ const WorkerInfoSchema = z.object({ wallTimeMs: z.number().optional(), cpuTimeMs: z.number().optional(), executionModel: z.string().optional(), -}); +}) const WorkerSourceSchema = z.object({ exception: z @@ -68,9 +67,9 @@ const WorkerSourceSchema = z.object({ timestamp: z.number().optional(), }) .optional(), -}); +}) -type WorkerEventType = z.infer; +type WorkerEventType = z.infer const WorkerEventSchema = z.object({ $workers: WorkerInfoSchema.optional(), timestamp: z.number(), @@ -82,19 +81,20 @@ const WorkerEventSchema = z.object({ 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: [] } }) -); + z + .object({ + events: LogsEventsSchema.optional().default({ events: [] }), + }) + .optional() + .default({ events: { events: [] } }) +) /** * Extracts only the most relevant information from a worker log event @@ -102,60 +102,60 @@ const LogsResponseSchema = V4Schema( * @returns Relevant information extracted from the log */ function extractRelevantLogInfo(event: WorkerEventType): RelevantLogInfo { - const workers = event.$workers; - const metadata = event.$metadata; - const source = event.source; + const workers = event.$workers + const metadata = event.$metadata + const source = event.source - let path = null; - let method = null; - let status = null; + 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; + path = workers.event.request.path ?? null + method = workers.event.request.method ?? null } if (workers?.event?.response) { - status = workers.event.response.status ?? null; + status = workers.event.response.status ?? null } - let error = null; + let error = null if (metadata.error) { - error = metadata.error; + error = metadata.error } - let message = metadata?.message ?? null; + let message = metadata?.message ?? null if (!message) { if (workers?.event?.rpcMethod) { - message = `RPC: ${workers.event.rpcMethod}`; + message = `RPC: ${workers.event.rpcMethod}` } else if (path && method) { - message = `${method} ${path}`; + message = `${method} ${path}` } } // Calculate duration - const duration = (workers?.wallTimeMs || 0) + (workers?.cpuTimeMs || 0); + const duration = (workers?.wallTimeMs || 0) + (workers?.cpuTimeMs || 0) // Extract rayId if available - const rayId = workers?.event?.rayId ?? null; + const rayId = workers?.event?.rayId ?? null // Extract exception stack if available - const exceptionStack = source?.exception?.stack ?? null; + const exceptionStack = source?.exception?.stack ?? null return { timestamp: new Date(event.timestamp).toISOString(), path, method, status, - outcome: workers?.outcome || "unknown", - eventType: workers?.eventType || "unknown", + outcome: workers?.outcome || 'unknown', + eventType: workers?.eventType || 'unknown', duration: duration || null, error, message, - requestId: workers?.requestId || metadata?.id || "unknown", + requestId: workers?.requestId || metadata?.id || 'unknown', rayId, exceptionStack, - }; + } } /** @@ -174,92 +174,92 @@ export async function handleWorkerLogs({ scriptName, rayId, }: { - limit: number; - minutesAgo: number; - accountId: string; - apiToken: string; - shouldFilterErrors: boolean; - scriptName?: string; - rayId?: string; + 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"); + 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; + 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[] = []; + 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", + id: 'worker-name-filter', + key: '$metadata.service', + type: 'string', value: scriptName, - operation: "eq", - }); + operation: 'eq', + }) } if (shouldFilterErrors === true) { filters.push({ - id: "error-filter", - key: "$metadata.error", - type: "string", - operation: "exists", - }); + 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", + id: 'ray-id-filter', + key: '$workers.event.rayId', + type: 'string', value: rayId, - operation: "eq", - }); + operation: 'eq', + }) } const queryPayload = { - queryId: "workers-logs", + queryId: 'workers-logs', timeframe: { from: fromTimestamp, to: now, }, parameters: { - datasets: ["cloudflare-workers"], + datasets: ['cloudflare-workers'], filters, calculations: [], groupBys: [], havings: [], }, - view: "events", + view: 'events', limit, - }; + } const data = await fetchCloudflareApi({ - endpoint: "/workers/observability/telemetry/query", + endpoint: '/workers/observability/telemetry/query', accountId, apiToken, responseSchema: LogsResponseSchema, options: { - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, body: JSON.stringify(queryPayload), }, - }); + }) - const events = data.result?.events?.events || []; + const events = data.result?.events?.events || [] // Extract relevant information from each event - const relevantLogs = events.map(extractRelevantLogInfo); + const relevantLogs = events.map(extractRelevantLogInfo) - return { relevantLogs, from: fromTimestamp, to: now }; + return { relevantLogs, from: fromTimestamp, to: now } } /** @@ -273,47 +273,47 @@ export async function handleWorkerLogsKeys( scriptName: string, minutesAgo: number, accountId: string, - apiToken: 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; + const now = Date.now() + const fromTimestamp = now - minutesAgo * 60 * 1000 // Build query for telemetry keys const queryPayload = { - queryId: "workers-keys", + queryId: 'workers-keys', timeframe: { from: fromTimestamp, to: now, }, parameters: { - datasets: ["cloudflare-workers"], + datasets: ['cloudflare-workers'], filters: [ { - id: "service-filter", - key: "$metadata.service", - type: "string", + id: 'service-filter', + key: '$metadata.service', + type: 'string', value: `${scriptName}`, - operation: "eq", + operation: 'eq', }, ], }, - }; + } const data = await fetchCloudflareApi({ - endpoint: "/workers/observability/telemetry/keys", + endpoint: '/workers/observability/telemetry/keys', accountId, apiToken, responseSchema: LogsKeysResponseSchema, options: { - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/json", - "portal-version": "2", + 'Content-Type': 'application/json', + 'portal-version': '2', }, body: JSON.stringify(queryPayload), }, - }); + }) - return data.result || []; + return data.result || [] } diff --git a/packages/mcp-common/src/api/workers.ts b/packages/mcp-common/src/api/workers.ts index 911b5da6..4e095bf2 100644 --- a/packages/mcp-common/src/api/workers.ts +++ b/packages/mcp-common/src/api/workers.ts @@ -1,4 +1,4 @@ -import type { Cloudflare } from 'cloudflare'; +import type { Cloudflare } from 'cloudflare' /** * Fetches list of workers from Cloudflare API @@ -11,9 +11,9 @@ export async function handleWorkersList({ accountId, }: { client: Cloudflare - accountId: string, + accountId: string }): Promise { - return (await client.workers.scripts.list({account_id: accountId})).result + return (await client.workers.scripts.list({ account_id: accountId })).result } /** @@ -29,8 +29,8 @@ export async function handleWorkerScriptDownload({ accountId, }: { client: Cloudflare - scriptName: string, - accountId: string, + scriptName: string + accountId: string }): Promise { - return await client.workers.scripts.get(scriptName, {account_id: accountId}) + return await client.workers.scripts.get(scriptName, { account_id: accountId }) } diff --git a/packages/mcp-common/src/cloudflare-api.ts b/packages/mcp-common/src/cloudflare-api.ts index 8d0548c2..2cbde68e 100644 --- a/packages/mcp-common/src/cloudflare-api.ts +++ b/packages/mcp-common/src/cloudflare-api.ts @@ -1,5 +1,6 @@ -import type { z } from "zod"; -import { Cloudflare } from 'cloudflare'; +import { Cloudflare } from 'cloudflare' + +import type { z } from 'zod' export function getCloudflareClient(apiToken: string) { return new Cloudflare({ apiToken }) @@ -20,13 +21,13 @@ export async function fetchCloudflareApi({ responseSchema, options = {}, }: { - endpoint: string; - accountId: string; - apiToken: string; - responseSchema?: z.ZodType; - options?: RequestInit; + endpoint: string + accountId: string + apiToken: string + responseSchema?: z.ZodType + options?: RequestInit }): Promise { - const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}${endpoint}`; + const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}${endpoint}` const response = await fetch(url, { ...options, @@ -34,19 +35,19 @@ export async function fetchCloudflareApi({ Authorization: `Bearer ${apiToken}`, ...(options.headers || {}), }, - }); + }) if (!response.ok) { - const error = await response.text(); - throw new Error(`Cloudflare API request failed: ${error}`); + const error = await response.text() + throw new Error(`Cloudflare API request failed: ${error}`) } - const data = await response.json(); + const data = await response.json() // If a schema is provided, validate the response if (responseSchema) { - return responseSchema.parse(data); + return responseSchema.parse(data) } - return data as T; + return data as T } diff --git a/packages/mcp-common/src/cloudflare-auth.ts b/packages/mcp-common/src/cloudflare-auth.ts index f7656206..b2966fd5 100644 --- a/packages/mcp-common/src/cloudflare-auth.ts +++ b/packages/mcp-common/src/cloudflare-auth.ts @@ -1,48 +1,50 @@ -import type { AuthRequest } from "@cloudflare/workers-oauth-provider"; -import { z } from "zod"; -import { McpError } from "./mcp-error"; +import { z } from 'zod' + +import { McpError } from './mcp-error' + +import type { AuthRequest } from '@cloudflare/workers-oauth-provider' // Constants -const PKCE_CHARSET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"; -const RECOMMENDED_CODE_VERIFIER_LENGTH = 96; +const PKCE_CHARSET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~' +const RECOMMENDED_CODE_VERIFIER_LENGTH = 96 export const DefaultScopes = { - "account:read": "See your account info such as account details, analytics, and memberships.", - "user:read": "See your user info such as name, email address, and account memberships.", - "workers:write": - "See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.", - "workers_observability:read": "See observability logs for your account", - offline_access: "Grants refresh tokens for long-lived access.", -} as const; + 'account:read': 'See your account info such as account details, analytics, and memberships.', + 'user:read': 'See your user info such as name, email address, and account memberships.', + 'workers:write': + 'See and change Cloudflare Workers data such as zones, KV storage, namespaces, scripts, and routes.', + 'workers_observability:read': 'See observability logs for your account', + offline_access: 'Grants refresh tokens for long-lived access.', +} as const function base64urlEncode(value: string): string { - let base64 = btoa(value); - base64 = base64.replace(/\+/g, "-"); - base64 = base64.replace(/\//g, "_"); - base64 = base64.replace(/=/g, ""); - return base64; + let base64 = btoa(value) + base64 = base64.replace(/\+/g, '-') + base64 = base64.replace(/\//g, '_') + base64 = base64.replace(/=/g, '') + return base64 } interface PKCECodes { - codeChallenge: string; - codeVerifier: string; + codeChallenge: string + codeVerifier: string } async function generatePKCECodes(): Promise { - const output = new Uint32Array(RECOMMENDED_CODE_VERIFIER_LENGTH); - crypto.getRandomValues(output); + const output = new Uint32Array(RECOMMENDED_CODE_VERIFIER_LENGTH) + crypto.getRandomValues(output) const codeVerifier = base64urlEncode( Array.from(output) .map((num: number) => PKCE_CHARSET[num % PKCE_CHARSET.length]) - .join(""), - ); - const buffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier)); - const hash = new Uint8Array(buffer); - let binary = ""; - const hashLength = hash.byteLength; + .join('') + ) + const buffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier)) + const hash = new Uint8Array(buffer) + let binary = '' + const hashLength = hash.byteLength for (let i = 0; i < hashLength; i++) { - binary += String.fromCharCode(hash[i]); + binary += String.fromCharCode(hash[i]) } - const codeChallenge = base64urlEncode(binary); //btoa(binary); - return { codeChallenge, codeVerifier }; + const codeChallenge = base64urlEncode(binary) //btoa(binary); + return { codeChallenge, codeVerifier } } function generateAuthUrl({ @@ -51,23 +53,23 @@ function generateAuthUrl({ state, code_challenge, }: { - client_id: string; - redirect_uri: string; - code_challenge: string; - state: string; + client_id: string + redirect_uri: string + code_challenge: string + state: string }) { const params = new URLSearchParams({ - response_type: "code", + response_type: 'code', client_id, redirect_uri, state, code_challenge, - code_challenge_method: "S256", - scope: Object.keys(DefaultScopes).join(" "), - }); + code_challenge_method: 'S256', + scope: Object.keys(DefaultScopes).join(' '), + }) - const upstream = new URL(`https://dash.cloudflare.com/oauth2/auth?${params.toString()}`); - return upstream.href; + const upstream = new URL(`https://dash.cloudflare.com/oauth2/auth?${params.toString()}`) + return upstream.href } /** @@ -85,11 +87,11 @@ export async function getAuthorizationURL({ redirect_uri, state, }: { - client_id: string; - redirect_uri: string; - state: AuthRequest; + client_id: string + redirect_uri: string + state: AuthRequest }): Promise<{ authUrl: string; codeVerifier: string }> { - const { codeChallenge, codeVerifier } = await generatePKCECodes(); + const { codeChallenge, codeVerifier } = await generatePKCECodes() return { authUrl: generateAuthUrl({ @@ -99,17 +101,17 @@ export async function getAuthorizationURL({ code_challenge: codeChallenge, }), codeVerifier: codeVerifier, - }; + } } -type AuthorizationToken = z.infer; +type AuthorizationToken = z.infer const AuthorizationToken = z.object({ access_token: z.string(), expires_in: z.number(), refresh_token: z.string(), scope: z.string(), token_type: z.string(), -}); +}) /** * Fetches an authorization token from Cloudflare. * @@ -128,38 +130,38 @@ export async function getAuthToken({ code_verifier, code, }: { - client_id: string; - client_secret: string; - redirect_uri: string; - code_verifier: string; - code: string; + client_id: string + client_secret: string + redirect_uri: string + code_verifier: string + code: string }): Promise { if (!code) { - throw new McpError("Missing code", 400); + throw new McpError('Missing code', 400) } const params = new URLSearchParams({ - grant_type: "authorization_code", + grant_type: 'authorization_code', client_id, redirect_uri, code, code_verifier, - }).toString(); - const resp = await fetch("https://dash.cloudflare.com/oauth2/token", { - method: "POST", + }).toString() + const resp = await fetch('https://dash.cloudflare.com/oauth2/token', { + method: 'POST', headers: { Authorization: `Basic ${btoa(`${client_id}:${client_secret}`)}`, - "Content-Type": "application/x-www-form-urlencoded", + 'Content-Type': 'application/x-www-form-urlencoded', }, body: params, - }); + }) if (!resp.ok) { - console.log(await resp.text()); - throw new McpError("Failed to get OAuth token", 500); + console.log(await resp.text()) + throw new McpError('Failed to get OAuth token', 500) } - return AuthorizationToken.parse(await resp.json()); + return AuthorizationToken.parse(await resp.json()) } export async function refreshAuthToken({ @@ -167,28 +169,28 @@ export async function refreshAuthToken({ client_secret, refresh_token, }: { - client_id: string; - client_secret: string; - refresh_token: string; + client_id: string + client_secret: string + refresh_token: string }): Promise { const params = new URLSearchParams({ - grant_type: "refresh_token", + grant_type: 'refresh_token', client_id, refresh_token, - }); + }) - const resp = await fetch("https://dash.cloudflare.com/oauth2/token", { - method: "POST", + const resp = await fetch('https://dash.cloudflare.com/oauth2/token', { + method: 'POST', body: params.toString(), headers: { Authorization: `Basic ${btoa(`${client_id}:${client_secret}`)}`, - "Content-Type": "application/x-www-form-urlencoded", + 'Content-Type': 'application/x-www-form-urlencoded', }, - }); + }) if (!resp.ok) { - console.log(await resp.text()); - throw new McpError("Failed to get OAuth token", 500); + console.log(await resp.text()) + throw new McpError('Failed to get OAuth token', 500) } - return AuthorizationToken.parse(await resp.json()); + return AuthorizationToken.parse(await resp.json()) } diff --git a/packages/mcp-common/src/cloudflare-oauth-handler.ts b/packages/mcp-common/src/cloudflare-oauth-handler.ts index e7f6f3e4..b6a44bd7 100644 --- a/packages/mcp-common/src/cloudflare-oauth-handler.ts +++ b/packages/mcp-common/src/cloudflare-oauth-handler.ts @@ -1,102 +1,105 @@ -import { env } from "cloudflare:workers"; +import { zValidator } from '@hono/zod-validator' +import { env } from 'cloudflare:workers' +import { Hono } from 'hono' +import { z } from 'zod' + +import { + DefaultScopes, + getAuthorizationURL, + getAuthToken, + refreshAuthToken, +} from './cloudflare-auth' +import { McpError } from './mcp-error' + import type { AuthRequest, OAuthHelpers, TokenExchangeCallbackOptions, TokenExchangeCallbackResult, -} from "@cloudflare/workers-oauth-provider"; -import { zValidator } from "@hono/zod-validator"; -import { type Context, Hono } from "hono"; -import { z } from "zod"; -import type { Env } from "../../../apps/workers-observability/worker-configuration"; -import type { Props } from "../../../apps/workers-observability/src/index"; -import { - DefaultScopes, - getAuthToken, - getAuthorizationURL, - refreshAuthToken, -} from "./cloudflare-auth"; -import { McpError } from "./mcp-error"; +} from '@cloudflare/workers-oauth-provider' +import type { Context } from 'hono' +import type { Props } from '../../../apps/workers-observability/src/index' +import type { Env } from '../../../apps/workers-observability/worker-configuration' -type AuthContext = { Bindings: Env & { OAUTH_PROVIDER: OAuthHelpers } }; -const app = new Hono(); +type AuthContext = { Bindings: Env & { OAUTH_PROVIDER: OAuthHelpers } } +const app = new Hono() const AuthQuery = z.object({ - code: z.string().describe("OAuth code from CF dash"), - state: z.string().describe("Value of the OAuth state"), - scope: z.string().describe("OAuth scopes granted"), -}); + code: z.string().describe('OAuth code from CF dash'), + state: z.string().describe('Value of the OAuth state'), + scope: z.string().describe('OAuth scopes granted'), +}) -export type UserSchema = z.infer; +export type UserSchema = z.infer const UserResponseSchema = z.object({ result: z.object({ id: z.string(), email: z.string(), }), -}); +}) -export type AccountSchema = z.infer; +export type AccountSchema = z.infer const AccountResponseSchema = z.object({ result: z.array( z.object({ name: z.string(), id: z.string(), - }), + }) ), -}); +}) async function getTokenAndUser( c: Context, code: string, - code_verifier: string, + code_verifier: string ): Promise<{ - accessToken: string; - refreshToken: string; - user: UserSchema["result"]; - accounts: AccountSchema["result"]; + accessToken: string + refreshToken: string + user: UserSchema['result'] + accounts: AccountSchema['result'] }> { // Exchange the code for an access token const { access_token: accessToken, refresh_token: refreshToken } = await getAuthToken({ client_id: c.env.CLOUDFLARE_CLIENT_ID, client_secret: c.env.CLOUDFLARE_CLIENT_SECRET, - redirect_uri: new URL("/oauth/callback", c.req.url).href, + redirect_uri: new URL('/oauth/callback', c.req.url).href, code, code_verifier, - }); + }) const [userResponse, accountsResponse] = await Promise.all([ - fetch("https://api.cloudflare.com/client/v4/user", { + fetch('https://api.cloudflare.com/client/v4/user', { headers: { Authorization: `Bearer ${accessToken}`, }, }), - fetch("https://api.cloudflare.com/client/v4/accounts", { + fetch('https://api.cloudflare.com/client/v4/accounts', { headers: { Authorization: `Bearer ${accessToken}`, }, }), - ]); + ]) if (!userResponse.ok) { - console.log(await userResponse.text()); - throw new McpError("Failed to fetch user", 500); + console.log(await userResponse.text()) + throw new McpError('Failed to fetch user', 500) } if (!accountsResponse.ok) { - console.log(await accountsResponse.text()); - throw new McpError("Failed to fetch accounts", 500); + console.log(await accountsResponse.text()) + throw new McpError('Failed to fetch accounts', 500) } // Fetch the user & accounts info from Cloudflare - const { result: user } = UserResponseSchema.parse(await userResponse.json()); - const { result: accounts } = AccountResponseSchema.parse(await accountsResponse.json()); + const { result: user } = UserResponseSchema.parse(await userResponse.json()) + const { result: accounts } = AccountResponseSchema.parse(await accountsResponse.json()) - return { accessToken, refreshToken, user, accounts }; + return { accessToken, refreshToken, user, accounts } } export async function handleTokenExchangeCallback( - options: TokenExchangeCallbackOptions, + options: TokenExchangeCallbackOptions ): Promise { // options.props contains the current props - if (options.grantType === "refresh_token") { + if (options.grantType === 'refresh_token') { // handle token refreshes const { access_token: accessToken, @@ -106,7 +109,7 @@ export async function handleTokenExchangeCallback( client_id: (env as Env).CLOUDFLARE_CLIENT_ID, client_secret: (env as Env).CLOUDFLARE_CLIENT_SECRET, refresh_token: options.props.refreshToken, - }); + }) return { newProps: { @@ -115,7 +118,7 @@ export async function handleTokenExchangeCallback( refreshToken, }, accessTokenTTL: expires_in, - }; + } } } @@ -128,29 +131,29 @@ export async function handleTokenExchangeCallback( * Then it redirects the user to GitHub's authorization page with the appropriate * parameters so the user can authenticate and grant permissions. */ -app.get("/oauth/authorize", async (c) => { +app.get('/oauth/authorize', async (c) => { try { - const oauthReqInfo = await c.env.OAUTH_PROVIDER.parseAuthRequest(c.req.raw); - oauthReqInfo.scope = Object.keys(DefaultScopes); + const oauthReqInfo = await c.env.OAUTH_PROVIDER.parseAuthRequest(c.req.raw) + oauthReqInfo.scope = Object.keys(DefaultScopes) if (!oauthReqInfo.clientId) { - return c.text("Invalid request", 400); + return c.text('Invalid request', 400) } const res = await getAuthorizationURL({ client_id: c.env.CLOUDFLARE_CLIENT_ID, - redirect_uri: new URL("/oauth/callback", c.req.url).href, + redirect_uri: new URL('/oauth/callback', c.req.url).href, state: oauthReqInfo, - }); + }) - return Response.redirect(res.authUrl, 302); + return Response.redirect(res.authUrl, 302) } catch (e) { if (e instanceof McpError) { - return c.text(e.message, { status: e.code }); + return c.text(e.message, { status: e.code }) } - console.error(e); - return c.text("Internal Error", 500); + console.error(e) + return c.text('Internal Error', 500) } -}); +}) /** * OAuth Callback Endpoint @@ -160,22 +163,22 @@ app.get("/oauth/authorize", async (c) => { * user metadata & the auth token as part of the 'props' on the token passed * down to the client. It ends by redirecting the client back to _its_ callback URL */ -app.get("/oauth/callback", zValidator("query", AuthQuery), async (c) => { +app.get('/oauth/callback', zValidator('query', AuthQuery), async (c) => { try { - const { state, code } = c.req.valid("query"); - const oauthReqInfo = JSON.parse(atob(state)) as AuthRequest & { codeVerifier: string }; + const { state, code } = c.req.valid('query') + const oauthReqInfo = JSON.parse(atob(state)) as AuthRequest & { codeVerifier: string } // Get the oathReqInfo out of KV if (!oauthReqInfo.clientId) { - throw new McpError("Invalid State", 400); + throw new McpError('Invalid State', 400) } const [{ accessToken, refreshToken, user, accounts }] = await Promise.all([ getTokenAndUser(c, code, oauthReqInfo.codeVerifier), c.env.OAUTH_PROVIDER.createClient({ clientId: oauthReqInfo.clientId, - tokenEndpointAuthMethod: "none", + tokenEndpointAuthMethod: 'none', }), - ]); + ]) // TODO: Implement auth restriction in staging // if ( @@ -203,16 +206,16 @@ app.get("/oauth/callback", zValidator("query", AuthQuery), async (c) => { accessToken, refreshToken, } as Props, - }); + }) - return Response.redirect(redirectTo, 302); + return Response.redirect(redirectTo, 302) } catch (e) { - console.error(e); + console.error(e) if (e instanceof McpError) { - return c.text(e.message, { status: e.code }); + return c.text(e.message, { status: e.code }) } - return c.text("Internal Error", 500); + return c.text('Internal Error', 500) } -}); +}) -export const CloudflareAuthHandler = app; +export const CloudflareAuthHandler = app diff --git a/packages/mcp-common/src/mcp-error.ts b/packages/mcp-common/src/mcp-error.ts index 9401953a..ab177f3b 100644 --- a/packages/mcp-common/src/mcp-error.ts +++ b/packages/mcp-common/src/mcp-error.ts @@ -1,10 +1,10 @@ -import type { ContentfulStatusCode } from "hono/utils/http-status"; +import type { ContentfulStatusCode } from 'hono/utils/http-status' export class McpError extends Error { - code: ContentfulStatusCode; + code: ContentfulStatusCode constructor(message: string, code: ContentfulStatusCode) { - super(message); - this.code = code; - this.name = "MCPError"; + super(message) + this.code = code + this.name = 'MCPError' } } diff --git a/packages/mcp-common/src/v4-api.ts b/packages/mcp-common/src/v4-api.ts index 4880ed7f..135c56b1 100644 --- a/packages/mcp-common/src/v4-api.ts +++ b/packages/mcp-common/src/v4-api.ts @@ -1,4 +1,4 @@ -import { z } from "zod" +import { z } from 'zod' type V4ErrorSchema = typeof V4ErrorSchema const V4ErrorSchema = z.array( @@ -21,4 +21,4 @@ export const V4Schema = ( success: z.boolean(), errors: V4ErrorSchema, messages: z.array(z.any()), - }) \ No newline at end of file + }) diff --git a/packages/mcp-common/tests/logs.spec.ts b/packages/mcp-common/tests/logs.spec.ts index c119793b..ff9dfffa 100644 --- a/packages/mcp-common/tests/logs.spec.ts +++ b/packages/mcp-common/tests/logs.spec.ts @@ -1,112 +1,113 @@ -import { env, fetchMock } from "cloudflare:test"; -import { afterEach, beforeAll, describe, expect, it } from "vitest"; -import { handleWorkerLogs, handleWorkerLogsKeys } from "../src/api/logs"; +import { env, fetchMock } from 'cloudflare:test' +import { afterEach, beforeAll, describe, expect, it } from 'vitest' + +import { handleWorkerLogs, handleWorkerLogsKeys } from '../src/api/logs' beforeAll(() => { // Enable outbound request mocking... - fetchMock.activate(); + fetchMock.activate() // ...and throw errors if an outbound request isn't mocked - fetchMock.disableNetConnect(); -}); + fetchMock.disableNetConnect() +}) // Ensure we matched every mock we defined -afterEach(() => fetchMock.assertNoPendingInterceptors()); +afterEach(() => fetchMock.assertNoPendingInterceptors()) -describe("Logs API", () => { - describe("handleWorkerLogs", () => { - it("should fetch and analyze worker logs correctly", async () => { - const scriptName = "test-worker"; +describe('Logs API', () => { + describe('handleWorkerLogs', () => { + it('should fetch and analyze worker logs correctly', async () => { + const scriptName = 'test-worker' // Create mock log events const mockEvents = [ { timestamp: Date.now() - 100000, $workers: { - scriptName: "test-worker", - outcome: "ok", - eventType: "fetch", - requestId: "123456abcdef", + scriptName: 'test-worker', + outcome: 'ok', + eventType: 'fetch', + requestId: '123456abcdef', wallTimeMs: 45.2, cpuTimeMs: 12.8, event: { request: { - method: "GET", - path: "/api/v1/resource", - url: "https://example.com/api/v1/resource", + method: 'GET', + path: '/api/v1/resource', + url: 'https://example.com/api/v1/resource', }, response: { status: 200, }, - rayId: "ray123abc456def", + rayId: 'ray123abc456def', }, }, source: { - message: "Successful request to resource", + message: 'Successful request to resource', }, - dataset: "cloudflare-workers", + dataset: 'cloudflare-workers', $metadata: { - id: "1", - message: "GET /api/v1/resource", + id: '1', + message: 'GET /api/v1/resource', }, }, { timestamp: Date.now() - 200000, $workers: { - scriptName: "test-worker", - outcome: "ok", - eventType: "fetch", - requestId: "456789bcdef01", + scriptName: 'test-worker', + outcome: 'ok', + eventType: 'fetch', + requestId: '456789bcdef01', wallTimeMs: 88.7, cpuTimeMs: 33.2, event: { request: { - method: "POST", - path: "/api/v1/resource/create", - url: "https://example.com/api/v1/resource/create", + method: 'POST', + path: '/api/v1/resource/create', + url: 'https://example.com/api/v1/resource/create', }, response: { status: 201, }, - rayId: "ray456def789ghi", + rayId: 'ray456def789ghi', }, }, source: { - message: "Created new resource", + message: 'Created new resource', }, - dataset: "cloudflare-workers", + dataset: 'cloudflare-workers', $metadata: { - id: "2", - message: "POST /api/v1/resource/create", + id: '2', + message: 'POST /api/v1/resource/create', }, }, { timestamp: Date.now() - 300000, $workers: { - scriptName: "test-worker", - outcome: "error", - eventType: "fetch", - requestId: "789012defghi34", + scriptName: 'test-worker', + outcome: 'error', + eventType: 'fetch', + requestId: '789012defghi34', wallTimeMs: 112.3, cpuTimeMs: 45.8, event: { request: { - method: "PUT", - path: "/api/v1/resource/update", - url: "https://example.com/api/v1/resource/update", + method: 'PUT', + path: '/api/v1/resource/update', + url: 'https://example.com/api/v1/resource/update', }, - rayId: "ray789ghi012jkl", + rayId: 'ray789ghi012jkl', }, }, source: { - message: "Resource not found", + message: 'Resource not found', }, - dataset: "cloudflare-workers", + dataset: 'cloudflare-workers', $metadata: { - id: "3", - message: "Error updating resource", + id: '3', + message: 'Error updating resource', }, }, - ]; + ] const mockResponse = { success: true, @@ -116,19 +117,19 @@ describe("Logs API", () => { }, }, errors: [], - messages: [{ message: "Successful request" }], - }; + messages: [{ message: 'Successful request' }], + } fetchMock - .get("https://api.cloudflare.com") + .get('https://api.cloudflare.com') .intercept({ - method: "POST", + method: 'POST', path: `/client/v4/accounts/${env.CLOUDFLARE_MOCK_ACCOUNT_ID}/workers/observability/telemetry/query`, }) - .reply(200, mockResponse); + .reply(200, mockResponse) - const limit = 100; - const minutesAgo = 30; + const limit = 100 + const minutesAgo = 30 const result = await handleWorkerLogs({ scriptName, limit, @@ -136,40 +137,40 @@ describe("Logs API", () => { accountId: env.CLOUDFLARE_MOCK_ACCOUNT_ID, apiToken: env.CLOUDFLARE_MOCK_API_TOKEN, shouldFilterErrors: false, - }); + }) // Verify that the timestamp range is set correctly - expect(result).toHaveProperty("from"); - expect(result).toHaveProperty("to"); - expect(result.from).toBeLessThan(result.to); + expect(result).toHaveProperty('from') + expect(result).toHaveProperty('to') + expect(result.from).toBeLessThan(result.to) // Verify that we have relevant logs that match our mock data - expect(result.relevantLogs).toHaveLength(3); - - const getLog = result.relevantLogs[0]; - expect(getLog.method).toBe(mockEvents[0].$workers.event.request.method); - expect(getLog.path).toBe(mockEvents[0].$workers.event.request.path); - expect(getLog.status).toBe(mockEvents[0].$workers.event.response?.status); - expect(getLog.outcome).toBe(mockEvents[0].$workers.outcome); - expect(getLog.rayId).toBe(mockEvents[0].$workers.event.rayId); - expect(getLog.duration).toBeGreaterThan(0); - - const postLog = result.relevantLogs[1]; - expect(postLog.method).toBe(mockEvents[1].$workers.event.request.method); - expect(postLog.path).toBe(mockEvents[1].$workers.event.request.path); - expect(postLog.status).toBe(mockEvents[1].$workers.event.response?.status); - expect(postLog.outcome).toBe(mockEvents[1].$workers.outcome); - expect(postLog.rayId).toBe(mockEvents[1].$workers.event.rayId); - - const errorLog = result.relevantLogs[2]; - expect(errorLog.method).toBe(mockEvents[2].$workers.event.request.method); - expect(errorLog.path).toBe(mockEvents[2].$workers.event.request.path); - expect(errorLog.outcome).toBe(mockEvents[2].$workers.outcome); - expect(errorLog.rayId).toBe(mockEvents[2].$workers.event.rayId); - }); - - it("should handle empty logs", async () => { - const scriptName = "empty-worker"; + expect(result.relevantLogs).toHaveLength(3) + + const getLog = result.relevantLogs[0] + expect(getLog.method).toBe(mockEvents[0].$workers.event.request.method) + expect(getLog.path).toBe(mockEvents[0].$workers.event.request.path) + expect(getLog.status).toBe(mockEvents[0].$workers.event.response?.status) + expect(getLog.outcome).toBe(mockEvents[0].$workers.outcome) + expect(getLog.rayId).toBe(mockEvents[0].$workers.event.rayId) + expect(getLog.duration).toBeGreaterThan(0) + + const postLog = result.relevantLogs[1] + expect(postLog.method).toBe(mockEvents[1].$workers.event.request.method) + expect(postLog.path).toBe(mockEvents[1].$workers.event.request.path) + expect(postLog.status).toBe(mockEvents[1].$workers.event.response?.status) + expect(postLog.outcome).toBe(mockEvents[1].$workers.outcome) + expect(postLog.rayId).toBe(mockEvents[1].$workers.event.rayId) + + const errorLog = result.relevantLogs[2] + expect(errorLog.method).toBe(mockEvents[2].$workers.event.request.method) + expect(errorLog.path).toBe(mockEvents[2].$workers.event.request.path) + expect(errorLog.outcome).toBe(mockEvents[2].$workers.outcome) + expect(errorLog.rayId).toBe(mockEvents[2].$workers.event.rayId) + }) + + it('should handle empty logs', async () => { + const scriptName = 'empty-worker' const mockResponse = { success: true, @@ -179,19 +180,19 @@ describe("Logs API", () => { }, }, errors: [], - messages: [{ message: "Successful request" }], - }; + messages: [{ message: 'Successful request' }], + } fetchMock - .get("https://api.cloudflare.com") + .get('https://api.cloudflare.com') .intercept({ - method: "POST", + method: 'POST', path: `/client/v4/accounts/${env.CLOUDFLARE_MOCK_ACCOUNT_ID}/workers/observability/telemetry/query`, }) - .reply(200, mockResponse); + .reply(200, mockResponse) - const limit = 100; - const minutesAgo = 30; + const limit = 100 + const minutesAgo = 30 const result = await handleWorkerLogs({ scriptName, limit, @@ -199,24 +200,24 @@ describe("Logs API", () => { accountId: env.CLOUDFLARE_MOCK_ACCOUNT_ID, apiToken: env.CLOUDFLARE_MOCK_API_TOKEN, shouldFilterErrors: false, - }); + }) - expect(result.relevantLogs.length).toBe(0); - }); + expect(result.relevantLogs.length).toBe(0) + }) - it("should handle API errors", async () => { - const scriptName = "error-worker"; + it('should handle API errors', async () => { + const scriptName = 'error-worker' fetchMock - .get("https://api.cloudflare.com") + .get('https://api.cloudflare.com') .intercept({ - method: "POST", + method: 'POST', path: `/client/v4/accounts/${env.CLOUDFLARE_MOCK_ACCOUNT_ID}/workers/observability/telemetry/query`, }) - .reply(500, "Server error"); + .reply(500, 'Server error') - const limit = 100; - const minutesAgo = 30; + const limit = 100 + const minutesAgo = 30 await expect( handleWorkerLogs({ scriptName, @@ -225,111 +226,111 @@ describe("Logs API", () => { accountId: env.CLOUDFLARE_MOCK_ACCOUNT_ID, apiToken: env.CLOUDFLARE_MOCK_API_TOKEN, shouldFilterErrors: false, - }), - ).rejects.toThrow("Cloudflare API request failed"); - }); + }) + ).rejects.toThrow('Cloudflare API request failed') + }) - it("should filter logs by error status when requested", async () => { - const scriptName = "test-worker"; + it('should filter logs by error status when requested', async () => { + const scriptName = 'test-worker' const mockEvents = [ { timestamp: Date.now() - 100000, $workers: { - scriptName: "test-worker", - outcome: "ok", - eventType: "fetch", - requestId: "123456abcdef", + scriptName: 'test-worker', + outcome: 'ok', + eventType: 'fetch', + requestId: '123456abcdef', wallTimeMs: 45.2, cpuTimeMs: 12.8, event: { request: { - method: "GET", - path: "/api/v1/resource", - url: "https://example.com/api/v1/resource", + method: 'GET', + path: '/api/v1/resource', + url: 'https://example.com/api/v1/resource', }, response: { status: 200, }, - rayId: "ray123abc456def", + rayId: 'ray123abc456def', }, }, source: {}, - dataset: "cloudflare-workers", - $metadata: { id: "1" }, + dataset: 'cloudflare-workers', + $metadata: { id: '1' }, }, { timestamp: Date.now() - 200000, $workers: { - scriptName: "test-worker", - outcome: "error", - eventType: "fetch", - requestId: "456789bcdef01", + scriptName: 'test-worker', + outcome: 'error', + eventType: 'fetch', + requestId: '456789bcdef01', wallTimeMs: 88.7, cpuTimeMs: 33.2, event: { request: { - method: "POST", - path: "/api/v1/resource/create", - url: "https://example.com/api/v1/resource/create", + method: 'POST', + path: '/api/v1/resource/create', + url: 'https://example.com/api/v1/resource/create', }, - rayId: "ray456def789ghi", + rayId: 'ray456def789ghi', }, }, source: { - message: "Invalid request data", + message: 'Invalid request data', }, - dataset: "cloudflare-workers", - $metadata: { id: "2", error: "Invalid request data" }, + dataset: 'cloudflare-workers', + $metadata: { id: '2', error: 'Invalid request data' }, }, { timestamp: Date.now() - 300000, $workers: { - scriptName: "test-worker", - outcome: "error", - eventType: "fetch", - requestId: "789012defghi34", + scriptName: 'test-worker', + outcome: 'error', + eventType: 'fetch', + requestId: '789012defghi34', wallTimeMs: 112.3, cpuTimeMs: 45.8, event: { request: { - method: "PUT", - path: "/api/v1/resource/update", - url: "https://example.com/api/v1/resource/update", + method: 'PUT', + path: '/api/v1/resource/update', + url: 'https://example.com/api/v1/resource/update', }, - rayId: "ray789ghi012jkl", + rayId: 'ray789ghi012jkl', }, }, source: { - message: "Resource not found", + message: 'Resource not found', }, - dataset: "cloudflare-workers", - $metadata: { id: "3", error: "Resource not found" }, + dataset: 'cloudflare-workers', + $metadata: { id: '3', error: 'Resource not found' }, }, - ]; + ] const mockResponse = { success: true, result: { events: { - events: mockEvents.filter((event) => event.$workers.outcome === "error"), + events: mockEvents.filter((event) => event.$workers.outcome === 'error'), }, }, errors: [], - messages: [{ message: "Successful request" }], - }; + messages: [{ message: 'Successful request' }], + } fetchMock - .get("https://api.cloudflare.com") + .get('https://api.cloudflare.com') .intercept({ - method: "POST", + method: 'POST', path: `/client/v4/accounts/${env.CLOUDFLARE_MOCK_ACCOUNT_ID}/workers/observability/telemetry/query`, }) - .reply(200, mockResponse); + .reply(200, mockResponse) // error filtering enabled - const limit = 100; - const minutesAgo = 30; + const limit = 100 + const minutesAgo = 30 const result = await handleWorkerLogs({ scriptName, limit, @@ -337,103 +338,99 @@ describe("Logs API", () => { accountId: env.CLOUDFLARE_MOCK_ACCOUNT_ID, apiToken: env.CLOUDFLARE_MOCK_API_TOKEN, shouldFilterErrors: true, - }); + }) // Check results - we should only get error logs - expect(result.relevantLogs.filter((log) => log.outcome === "error").length).toBe(2); - expect(result.relevantLogs.length).toBe(2); - - const firstErrorLog = result.relevantLogs.find( - (log) => log.error === "Invalid request data", - ); - expect(firstErrorLog).toBeDefined(); - expect(firstErrorLog?.method).toBe("POST"); - expect(firstErrorLog?.rayId).toBe("ray456def789ghi"); - - const secondErrorLog = result.relevantLogs.find( - (log) => log.error === "Resource not found", - ); - expect(secondErrorLog).toBeDefined(); - expect(secondErrorLog?.method).toBe("PUT"); - expect(secondErrorLog?.rayId).toBe("ray789ghi012jkl"); - }); - }); - - describe("handleWorkerLogsKeys", () => { - it("should fetch worker telemetry keys correctly", async () => { - const scriptName = "test-worker"; + expect(result.relevantLogs.filter((log) => log.outcome === 'error').length).toBe(2) + expect(result.relevantLogs.length).toBe(2) + + const firstErrorLog = result.relevantLogs.find((log) => log.error === 'Invalid request data') + expect(firstErrorLog).toBeDefined() + expect(firstErrorLog?.method).toBe('POST') + expect(firstErrorLog?.rayId).toBe('ray456def789ghi') + + const secondErrorLog = result.relevantLogs.find((log) => log.error === 'Resource not found') + expect(secondErrorLog).toBeDefined() + expect(secondErrorLog?.method).toBe('PUT') + expect(secondErrorLog?.rayId).toBe('ray789ghi012jkl') + }) + }) + + describe('handleWorkerLogsKeys', () => { + it('should fetch worker telemetry keys correctly', async () => { + const scriptName = 'test-worker' // Mock telemetry keys response const mockKeysResponse = { success: true, result: [ { - key: "$workers.outcome", - type: "string", + key: '$workers.outcome', + type: 'string', lastSeen: Date.now() - 1000000, }, { - key: "$workers.wallTimeMs", - type: "number", + key: '$workers.wallTimeMs', + type: 'number', lastSeen: Date.now() - 2000000, }, { - key: "$workers.event.error", - type: "boolean", + key: '$workers.event.error', + type: 'boolean', lastSeen: Date.now() - 3000000, }, ], errors: [], - messages: [{ message: "Successful request" }], - }; + messages: [{ message: 'Successful request' }], + } fetchMock - .get("https://api.cloudflare.com") + .get('https://api.cloudflare.com') .intercept({ - method: "POST", + method: 'POST', path: `/client/v4/accounts/${env.CLOUDFLARE_MOCK_ACCOUNT_ID}/workers/observability/telemetry/keys`, }) - .reply(200, mockKeysResponse); + .reply(200, mockKeysResponse) - const minutesAgo = 1440; + const minutesAgo = 1440 const result = await handleWorkerLogsKeys( scriptName, minutesAgo, env.CLOUDFLARE_MOCK_ACCOUNT_ID, - env.CLOUDFLARE_MOCK_API_TOKEN, - ); - - expect(result).toEqual(mockKeysResponse.result); - expect(result.length).toBe(3); - expect(result[0].key).toBe("$workers.outcome"); - expect(result[0].type).toBe("string"); - expect(result[1].key).toBe("$workers.wallTimeMs"); - expect(result[1].type).toBe("number"); - expect(result[2].key).toBe("$workers.event.error"); - expect(result[2].type).toBe("boolean"); - }); - - it("should handle API errors when fetching keys", async () => { - const scriptName = "error-worker"; + env.CLOUDFLARE_MOCK_API_TOKEN + ) + + expect(result).toEqual(mockKeysResponse.result) + expect(result.length).toBe(3) + expect(result[0].key).toBe('$workers.outcome') + expect(result[0].type).toBe('string') + expect(result[1].key).toBe('$workers.wallTimeMs') + expect(result[1].type).toBe('number') + expect(result[2].key).toBe('$workers.event.error') + expect(result[2].type).toBe('boolean') + }) + + it('should handle API errors when fetching keys', async () => { + const scriptName = 'error-worker' // Setup mock for error response fetchMock - .get("https://api.cloudflare.com") + .get('https://api.cloudflare.com') .intercept({ - method: "POST", + method: 'POST', path: `/client/v4/accounts/${env.CLOUDFLARE_MOCK_ACCOUNT_ID}/workers/observability/telemetry/keys`, }) - .reply(500, "Server error"); + .reply(500, 'Server error') - const minutesAgo = 1440; + const minutesAgo = 1440 await expect( handleWorkerLogsKeys( scriptName, minutesAgo, env.CLOUDFLARE_MOCK_ACCOUNT_ID, - env.CLOUDFLARE_MOCK_API_TOKEN, - ), - ).rejects.toThrow("Cloudflare API request failed"); - }); - }); -}); + env.CLOUDFLARE_MOCK_API_TOKEN + ) + ).rejects.toThrow('Cloudflare API request failed') + }) + }) +}) diff --git a/packages/mcp-common/types.d.ts b/packages/mcp-common/types.d.ts index 9e1cd186..e090f9a7 100644 --- a/packages/mcp-common/types.d.ts +++ b/packages/mcp-common/types.d.ts @@ -1,5 +1,5 @@ -import type { TestEnv } from "./vitest.config"; +import type { TestEnv } from './vitest.config' -declare module "cloudflare:test" { +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 cc3bd71c..601865c4 100644 --- a/packages/mcp-common/vitest.config.ts +++ b/packages/mcp-common/vitest.config.ts @@ -1,8 +1,8 @@ import { defineWorkersProject } from '@cloudflare/vitest-pool-workers/config' export interface TestEnv { - CLOUDFLARE_MOCK_ACCOUNT_ID: string; - CLOUDFLARE_MOCK_API_TOKEN: string; + CLOUDFLARE_MOCK_ACCOUNT_ID: string + CLOUDFLARE_MOCK_API_TOKEN: string } export default defineWorkersProject({ @@ -14,8 +14,8 @@ export default defineWorkersProject({ compatibilityDate: '2025-03-10', compatibilityFlags: ['nodejs_compat'], bindings: { - CLOUDFLARE_MOCK_ACCOUNT_ID: "mock-account-id", - CLOUDFLARE_MOCK_API_TOKEN: "mock-api-token", + CLOUDFLARE_MOCK_ACCOUNT_ID: 'mock-account-id', + CLOUDFLARE_MOCK_API_TOKEN: 'mock-api-token', } satisfies Partial, }, }, diff --git a/packages/typescript-config/workers.json b/packages/typescript-config/workers.json index 2a178135..7555e214 100644 --- a/packages/typescript-config/workers.json +++ b/packages/typescript-config/workers.json @@ -13,7 +13,11 @@ "jsx": "react", "module": "es2022", "moduleResolution": "bundler", - "types": ["./worker-configuration.d.ts", "@cloudflare/vitest-pool-workers", "@cloudflare/workers-types/2023-07-01"], + "types": [ + "./worker-configuration.d.ts", + "@cloudflare/vitest-pool-workers", + "@cloudflare/workers-types/2023-07-01" + ], "resolveJsonModule": true, "allowJs": true, "checkJs": false,