Skip to content

Commit 5ba230a

Browse files
committed
more workers observability prompt improvements
1 parent d598bf8 commit 5ba230a

File tree

3 files changed

+132
-57
lines changed

3 files changed

+132
-57
lines changed

apps/workers-observability/src/tools/observability.ts

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -13,35 +13,46 @@ export function registerObservabilityTools(agent: ObservabilityMCP) {
1313
// Register the worker logs analysis tool by worker name
1414
agent.server.tool(
1515
'query_worker_observability',
16-
`
17-
Query the Workers Observability API to analyze recent logs from your Cloudflare Workers.
16+
`Query the Workers Observability API to analyze logs and metrics from your Cloudflare Workers.
1817
19-
This API can do 3 things. Here are the capabilities with an example
18+
## When to Use This tool
2019
21-
List events - Show me errors for the worker api-proxy.
22-
Do calculations - what is the p99 of the wall time for the invocations of the worker api-proxy.
23-
Find invocations - Find a request with an error where the user was thomas.
20+
- Investigate errors or performance issues in your Cloudflare Workers
21+
- Monitor Worker usage patterns and resource consumption
22+
- Debug specific request failures or unexpected behaviors
23+
- Verify recent deployments are working as expected
24+
- Generate performance reports for specific Workers or endpoints
25+
- Track down user-reported issues with request ID or user information
26+
- Analyze trends in response times, error rates, or request volumes
2427
25-
These capabilities can be selected using the view field.
28+
## Core Capabilities
29+
This tool provides three primary views of your Worker data:
30+
1. **List Events** - Browse individual request logs and errors
31+
2. **Calculate Metrics** - Compute statistics across requests (avg, p99, etc.)
32+
3. **Find Specific Invocations** - Locate individual requests matching criteria
2633
27-
When selecting a calculation unless its a count you need to find a value to pass in:
28-
I.e. avg would require that you find a number field and pass that in as the key for the calculation.
29-
You can only select calculations defined in the schema, other options are not available.
34+
## Filtering Best Practices
35+
- Before applying filters, use the observability_keys and observability_values tools to confirm available filter fields and the correct filter value to add unless you have the data in a response from a previous query.
36+
- Common filter fields: $metadata.service, $metadata.trigger, $metadata.message, $metadata.level, $metadata.requestId,
3037
38+
## Output handling
39+
Once you have ran this query you must IMMEDIATELY present the user with this information.
3140
32-
When filtering unless you are extremely confident about the filter you are adding run the observability_keys and observability_values query to confirm the filter will be effective.
41+
- **Events**: Display as a table with key fields. For detailed inspection, show full JSON of individual events.
42+
- **Calculations**: Use appropriate charts based on the data (bar charts for comparisons, line charts for time series)
43+
- **Invocations**: Show full request/response details with syntax highlighting for important fields
3344
34-
For parsing the results here are some suggestions:
35-
* Show Invocations in a table.
36-
* Use a chart to visualise the calculations.
37-
* Show a table for events but if a user asks you to see more show the JSON for a single event. The user might hint for a value in the event. Show them the event that matches their hint.
45+
## Troubleshooting
46+
- If no results are returned, suggest broadening the time range or relaxing filters
47+
- For errors about invalid fields, recommend using observability_keys to see available options
48+
- Handle rate limiting by suggesting query optimization
3849
`,
3950

4051
{
4152
query: zQueryRunRequest,
4253
},
4354
async ({ query }) => {
44-
const accountId = agent.getActiveAccountId()
55+
const accountId = await agent.getActiveAccountId()
4556
if (!accountId) {
4657
return {
4758
content: [
@@ -81,7 +92,7 @@ For parsing the results here are some suggestions:
8192
Find keys in the workers observability Data. This tool should be used to ensure that the filters or calculations that you are adding to your query are valid.
8293
Filters can be added to this query but because it is faster to return lots of keys set a high limit and only add the filter $metadata.service to filter by worker name.
8394
`, { keysQuery: zKeysRequest }, async ({ keysQuery }) => {
84-
const accountId = agent.getActiveAccountId()
95+
const accountId = await agent.getActiveAccountId()
8596
if (!accountId) {
8697
return {
8798
content: [
@@ -122,7 +133,7 @@ Find values in the workers observability Data. This tool should be used to ensur
122133
`,
123134
{ valuesQuery: zValuesRequest },
124135
async ({ valuesQuery }) => {
125-
const accountId = agent.getActiveAccountId()
136+
const accountId = await agent.getActiveAccountId()
126137
if (!accountId) {
127138
return {
128139
content: [

packages/mcp-common/src/api/workers-observability.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,25 @@
1+
import { time } from 'console'
2+
13
import { fetchCloudflareApi } from '../cloudflare-api'
2-
import { zKeysResponse, zReturnedQueryRunResult, zValuesResponse } from '../types/workers-logs-schemas'
4+
import {
5+
zKeysResponse,
6+
zReturnedQueryRunResult,
7+
zValuesResponse,
8+
} from '../types/workers-logs-schemas'
39
import { V4Schema } from '../v4-api'
410

511
import type { z } from 'zod'
612
import type { zKeysRequest, zQueryRunRequest, zValuesRequest } from '../types/workers-logs-schemas'
713

814
type QueryRunRequest = z.infer<typeof zQueryRunRequest>
915

16+
function fixTimeframe(timeframe: QueryRunRequest['timeframe']) {
17+
return {
18+
from: new Date(timeframe.from).getTime(),
19+
to: new Date(timeframe.to).getTime(),
20+
}
21+
}
22+
1023
export async function queryWorkersObservability(
1124
apiToken: string,
1225
accountId: string,
@@ -22,7 +35,7 @@ export async function queryWorkersObservability(
2235
headers: {
2336
'Content-Type': 'application/json',
2437
},
25-
body: JSON.stringify(query),
38+
body: JSON.stringify({ ...query, timeframe: fixTimeframe(query.timeframe) }),
2639
},
2740
})
2841

@@ -45,7 +58,7 @@ export async function handleWorkerLogsKeys(
4558
headers: {
4659
'Content-Type': 'application/json',
4760
},
48-
body: JSON.stringify(keysQuery),
61+
body: JSON.stringify({ ...keysQuery, timeframe: fixTimeframe(keysQuery.timeframe) }),
4962
},
5063
})
5164

@@ -67,7 +80,7 @@ export async function handleWorkerLogsValues(
6780
headers: {
6881
'Content-Type': 'application/json',
6982
},
70-
body: JSON.stringify(valuesQuery),
83+
body: JSON.stringify({ ...valuesQuery, timeframe: fixTimeframe(valuesQuery.timeframe) }),
7184
},
7285
})
7386

packages/mcp-common/src/types/workers-logs-schemas.ts

Lines changed: 86 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -55,22 +55,48 @@ export const zFilterCombination = z.enum(['and', 'or', 'AND', 'OR'])
5555
export const zPrimitiveUnion = z.union([z.string(), z.number(), z.boolean()])
5656

5757
export const zQueryFilter = z.object({
58-
key: z.string().describe(`The key to filter on. The key must be a string.
59-
It is strongly recommended you use either the key from a previous response or the keys endpoint to get the available keys for your query. Do not guess keys.
60-
The following keys are special and should be used if available because they are more efficient and guaranteed to be available:
61-
* $metadata.service - the worker service name
62-
* $metadata.message - The log message. Almost every log has a message.
63-
* $metadata.error - The error message from the log if available
64-
65-
Do not guess keys. Use the keys endpoint to get the available keys for your query.
66-
67-
If you are already calling the keys endpoint you can just set the limit to be very high (1000+) and not set a filter here to return all keys.
58+
key: z.string().describe(`Filter field name. IMPORTANT:
59+
60+
• DO NOT guess keys - always use verified keys from either:
61+
- Previous query results
62+
- The '/keys' endpoint response
63+
64+
• PREFERRED KEYS (faster & always available):
65+
- $metadata.service: Worker service name
66+
- $metadata.origin: Trigger type (e.g., fetch, scheduled, etc.)
67+
- $metadata.trigger: Trigger type (e.g., GET /users, POST /orders, etc.)
68+
- $metadata.message: Log message text (present in nearly all logs)
69+
- $metadata.error: Error message (when applicable)
70+
71+
• ADVANCED USAGE:
72+
When using the '/keys' endpoint, set limit=1000+ to retrieve comprehensive key options
73+
without needing additional filtering
6874
`),
6975
operation: zQueryOperation,
70-
value: zPrimitiveUnion.optional().describe(`The value to filter on. Do not guess.
71-
Use the events of a previous query or the values endpoint to get the available values for your query.`),
76+
value: zPrimitiveUnion.optional().describe(`Filter comparison value. IMPORTANT:
77+
78+
• MUST match actual values in your logs
79+
• VERIFY using either:
80+
- Actual values from previous query results
81+
- The '/values' endpoint with your selected key
82+
83+
• TYPE MATCHING:
84+
- Ensure value type (string/number/boolean) matches the field type
85+
- String comparisons are case-sensitive unless using specific operations
86+
87+
• PATTERN USAGE:
88+
- For 'contains', use simple wildcard patterns
89+
- For 'regex', MUST use ClickHouse regex syntax:
90+
- Uses RE2 syntax (not PCRE/JavaScript)
91+
- No lookaheads/lookbehinds
92+
- Examples: '^5\\d{2}$' for HTTP 5xx codes, '\\bERROR\\b' for word boundary
93+
- Escape backslashes with double backslash`),
7294
type: z.enum(['string', 'number', 'boolean']),
73-
})
95+
}).describe(`
96+
## Filtering Best Practices
97+
- Before applying filters, use the observability_keys and observability_values queries to confirm available filter fields and values.
98+
- If the query is asking to find something you should check that it exists. I.e. to requests with errors filter for $metadata.error exists.
99+
`)
74100

75101
export const zQueryCalculation = z.object({
76102
key: z.string().optional(),
@@ -126,6 +152,27 @@ export const zStatistics = z.object({
126152
bytes_read: z.number(),
127153
})
128154

155+
export const zTimeframe = z
156+
.object({
157+
to: z.string(),
158+
from: z.string(),
159+
})
160+
.describe(
161+
`Timeframe for your query (ISO-8601 format).
162+
163+
• Current server time: ${new Date()}
164+
• Default: Last hour from current time
165+
• Maximum range: Last 7 days
166+
• Format: "YYYY-MM-DDTHH:MM:SSZ" (e.g., "2025-04-29T14:30:00Z")
167+
168+
Examples:
169+
- Last 30 minutes: from="2025-04-29T14:00:00Z", to="2025-04-29T14:30:00Z"
170+
- Yesterday: from="2025-04-28T00:00:00Z", to="2025-04-29T00:00:00Z"
171+
172+
Note: Narrower timeframes provide faster responses and more specific results.
173+
Omit this parameter entirely to use the default (last hour).`
174+
)
175+
129176
const zCloudflareMiniEventDetailsRequest = z.object({
130177
url: z.string().optional(),
131178
method: z.string().optional(),
@@ -260,7 +307,7 @@ export const zQueryRunRequest = z.object({
260307
// TODO: Fix these types
261308
queryId: z.string(),
262309
parameters: z.object({
263-
datasets: z.array(z.string()).optional(),
310+
datasets: z.array(z.string()).optional().describe('Leave this empty to use the default datasets'),
264311
filters: z.array(zQueryFilter).optional(),
265312
filterCombination: zFilterCombination.optional(),
266313
calculations: z.array(zQueryCalculation).optional(),
@@ -271,20 +318,32 @@ export const zQueryRunRequest = z.object({
271318
order: z.enum(['asc', 'desc']).optional(),
272319
})
273320
.optional(),
274-
limit: z.number().int().nonnegative().max(100).optional().describe('Use this limit when a group by is present. 10 is a sensible default'),
321+
limit: z.number().int().nonnegative().max(100).optional().describe('Use this limit when view is calculation and a group by is present. 10 is a sensible default'),
275322
needle: zSearchNeedle.optional(),
276323
}),
277-
timeframe: z.object({
278-
to: z.number(),
279-
from: z.number(),
280-
}),
324+
timeframe: zTimeframe,
281325
granularity: z.number().optional(),
282326
limit: z.number().max(100).optional().default(5).describe('Use this limit to limit the number of events returned when the view is events. 5 is a sensible default'),
283-
view: zViews.optional().default('calculations'),
284-
dry: z.boolean().optional().default(false),
285-
offset: z.string().optional(),
327+
view: zViews.optional().default('calculations').describe(`## Examples by View Type
328+
### Events View
329+
- "Show me all errors for the worker api-proxy in the last 30 minutes"
330+
- "List successful requests for the image-resizer worker with status code 200"
331+
- "Show events from worker auth-service where the path contains /login"
332+
333+
### Calculation View
334+
- "What is the p99 of wall time for worker api-proxy?"
335+
- "What's the count of requests by status code for worker cdn-router?"
336+
337+
### Invocation View
338+
- "Find a request to worker api-proxy that resulted in a 500 error"
339+
- "Find the slowest request to worker image-processor in the last hour"
340+
341+
TRACES AND PATTERNS ARE NOT CURRENTLY SUPPORTED
342+
`),
343+
dry: z.boolean().optional().default(true),
344+
offset: z.string().optional().describe('The offset to use for pagination. Use the $metadata.id field to get the next offset.'),
286345
offsetBy: z.number().optional(),
287-
offsetDirection: z.string().optional(),
346+
offsetDirection: z.string().optional().describe('The direction to use for pagination. Use "next" or "prev".'),
288347
})
289348

290349
/**
@@ -304,13 +363,8 @@ export const zReturnedQueryRunResult = z.object({
304363
* Keys Request
305364
*/
306365
export const zKeysRequest = z.object({
307-
timeframe: z
308-
.object({
309-
to: z.number().describe('End of the timeframe in epoch milliseconds'),
310-
from: z.number().describe('Start of the timeframe in epoch milliseconds'),
311-
})
312-
.optional(),
313-
datasets: z.array(z.string()).default([]),
366+
timeframe: zTimeframe,
367+
datasets: z.array(z.string()).default([]).describe('Leave this empty to use the default datasets'),
314368
filters: z.array(zQueryFilter).default([]),
315369
limit: z.number().optional(),
316370
needle: zSearchNeedle.optional(),
@@ -334,13 +388,10 @@ export const zKeysResponse = z.array(
334388
* Values Request
335389
*/
336390
export const zValuesRequest = z.object({
337-
timeframe: z.object({
338-
to: z.number(),
339-
from: z.number(),
340-
}),
391+
timeframe: zTimeframe,
341392
key: z.string(),
342393
type: z.enum(['string', 'boolean', 'number']),
343-
datasets: z.array(z.string()),
394+
datasets: z.array(z.string()).default([]).describe('Leave this empty to use the default datasets'),
344395
filters: z.array(zQueryFilter).default([]),
345396
limit: z.number().default(50),
346397
needle: zSearchNeedle.optional(),

0 commit comments

Comments
 (0)