diff --git a/apps/workers-observability/package.json b/apps/workers-observability/package.json index d3c21dfc..0cf5c7f6 100644 --- a/apps/workers-observability/package.json +++ b/apps/workers-observability/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@cloudflare/workers-oauth-provider": "0.0.5", + "@fast-csv/format": "5.0.2", "@hono/zod-validator": "0.4.3", "@modelcontextprotocol/sdk": "1.10.2", "@repo/mcp-common": "workspace:*", diff --git a/apps/workers-observability/src/tools/observability.ts b/apps/workers-observability/src/tools/observability.ts index 85a1a14d..a50f2fef 100644 --- a/apps/workers-observability/src/tools/observability.ts +++ b/apps/workers-observability/src/tools/observability.ts @@ -1,3 +1,5 @@ +import { writeToString } from '@fast-csv/format' + import { handleWorkerLogsKeys, handleWorkerLogsValues, @@ -60,6 +62,100 @@ This tool provides three primary views of your Worker data: } try { const response = await queryWorkersObservability(agent.props.accessToken, accountId, query) + + if (query.view === 'calculations') { + let data = '' + for (const calculation of response?.calculations || []) { + const alias = calculation.alias || calculation.calculation + const aggregates = calculation.aggregates.map((agg) => { + const keys = agg.groups?.reduce( + (acc, group) => { + acc[`${group.key}`] = `${group.value}` + return acc + }, + {} as Record + ) + return { + ...keys, + [alias]: agg.value, + } + }) + + const aggregatesString = await writeToString(aggregates, { + headers: true, + delimiter: '\t', + }) + + const series = calculation.series.map(({ time, data }) => { + return { + time, + ...data.reduce( + (acc, point) => { + const key = point.groups?.reduce((acc, group) => { + return `${acc} * ${group.value}` + }, '') + if (!key) { + return { + ...acc, + [alias]: point.value, + } + } + return { + ...acc, + key, + [alias]: point.value, + } + }, + {} as Record + ), + } + }) + const seriesString = await writeToString(series, { headers: true, delimiter: '\t' }) + data = data + '\n' + `## ${alias}` + data = data + '\n' + `### Aggregation` + data = data + '\n' + aggregatesString + data = data + '\n' + `### Series` + data = data + '\n' + seriesString + } + + return { + content: [ + { + type: 'text', + text: data, + }, + ], + } + } + + if (query.view === 'events') { + const events = response?.events?.events + return { + content: [ + { + type: 'text', + text: JSON.stringify(events), + }, + ], + } + } + + if (query.view === 'invocations') { + const invocations = Object.entries(response?.invocations || {}).map(([_, logs]) => { + const invocationLog = logs.find((log) => log.$metadata.type === 'cf-worker-event') + return invocationLog?.$metadata ?? logs[0]?.$metadata + }) + + const tsv = await writeToString(invocations, { headers: true, delimiter: '\t' }) + return { + content: [ + { + type: 'text', + text: tsv, + }, + ], + } + } return { content: [ { @@ -110,11 +206,16 @@ This tool provides three primary views of your Worker data: } try { const result = await handleWorkerLogsKeys(agent.props.accessToken, accountId, keysQuery) + + const tsv = await writeToString( + result.map((key) => ({ type: key.type, key: key.key })), + { headers: true, delimiter: '\t' } + ) return { content: [ { type: 'text', - text: JSON.stringify(result), + text: tsv, }, ], } @@ -155,11 +256,15 @@ This tool provides three primary views of your Worker data: } try { const result = await handleWorkerLogsValues(agent.props.accessToken, accountId, valuesQuery) + const tsv = await writeToString( + result?.map((value) => ({ type: value.type, value: value.value })) || [], + { headers: true, delimiter: '\t' } + ) return { content: [ { type: 'text', - text: JSON.stringify(result), + text: tsv, }, ], } diff --git a/packages/mcp-common/src/api/workers-observability.ts b/packages/mcp-common/src/api/workers-observability.ts index 379d94f4..6b41e105 100644 --- a/packages/mcp-common/src/api/workers-observability.ts +++ b/packages/mcp-common/src/api/workers-observability.ts @@ -1,3 +1,5 @@ +import { env } from 'cloudflare:workers' + import { fetchCloudflareApi } from '../cloudflare-api' import { zKeysResponse, @@ -23,6 +25,8 @@ export async function queryWorkersObservability( accountId: string, query: QueryRunRequest ): Promise | null> { + // @ts-expect-error We don't have actual env in this package + const environment = env.ENVIRONMENT const data = await fetchCloudflareApi({ endpoint: '/workers/observability/telemetry/query', accountId, @@ -32,6 +36,7 @@ export async function queryWorkersObservability( method: 'POST', headers: { 'Content-Type': 'application/json', + 'workers-observability-origin': `workers-observability-mcp-${environment}`, }, body: JSON.stringify({ ...query, timeframe: fixTimeframe(query.timeframe) }), }, diff --git a/packages/mcp-common/src/cloudflare-api.ts b/packages/mcp-common/src/cloudflare-api.ts index 5bbc9e74..17e2d303 100644 --- a/packages/mcp-common/src/cloudflare-api.ts +++ b/packages/mcp-common/src/cloudflare-api.ts @@ -40,6 +40,16 @@ export async function fetchCloudflareApi({ }): Promise { const url = `https://api.cloudflare.com/client/v4/accounts/${accountId}${endpoint}` + // @ts-expect-error We don't have actual env in this package + if (env.DEV_DISABLE_OAUTH) { + options.headers = { + ...options.headers, + // @ts-expect-error We don't have actual env in this package + 'X-Auth-Email': env.DEV_CLOUDFLARE_EMAIL, + // @ts-expect-error We don't have actual env in this package + 'X-Auth-Key': env.DEV_CLOUDFLARE_API_TOKEN, + } + } const response = await fetch(url, { ...options, headers: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1873e394..46cbc7f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -778,6 +778,9 @@ importers: '@cloudflare/workers-oauth-provider': specifier: 0.0.5 version: 0.0.5 + '@fast-csv/format': + specifier: 5.0.2 + version: 5.0.2 '@hono/zod-validator': specifier: 0.4.3 version: 0.4.3(hono@4.7.6)(zod@3.24.2) @@ -1384,6 +1387,9 @@ packages: resolution: {integrity: sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + '@fast-csv/format@5.0.2': + resolution: {integrity: sha512-fRYcWvI8vs0Zxa/8fXd/QlmQYWWkJqKZPAXM+vksnplb3owQFKTPPh9JqOtD0L3flQw/AZjjXdPkD7Kp/uHm8g==} + '@fastify/busboy@2.1.1': resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} engines: {node: '>=14'} @@ -3075,6 +3081,22 @@ packages: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + lodash.isfunction@3.0.9: + resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} + + lodash.isnil@4.0.0: + resolution: {integrity: sha512-up2Mzq3545mwVnMhTDMdfoG1OurpA/s5t88JmQX809eH3C8491iu2sfKhTfhQtKY78oPNhiaHJUpT/dUDAAtng==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -4611,6 +4633,14 @@ snapshots: '@eslint/js@8.57.0': {} + '@fast-csv/format@5.0.2': + dependencies: + lodash.escaperegexp: 4.1.2 + lodash.isboolean: 3.0.3 + lodash.isequal: 4.5.0 + lodash.isfunction: 3.0.9 + lodash.isnil: 4.0.0 + '@fastify/busboy@2.1.1': {} '@hapi/hoek@9.3.0': {} @@ -6418,6 +6448,16 @@ snapshots: dependencies: p-locate: 5.0.0 + lodash.escaperegexp@4.1.2: {} + + lodash.isboolean@3.0.3: {} + + lodash.isequal@4.5.0: {} + + lodash.isfunction@3.0.9: {} + + lodash.isnil@4.0.0: {} + lodash.merge@4.6.2: {} lodash.startcase@4.4.0: {}