Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ MCP server for the Datadog API, enabling incident management and more.
- `groupStates` (optional array): States to filter (e.g., alert, warn, no data, ok).
- `name` (optional string): Filter by name.
- `tags` (optional array): Filter by tags.
- `idOffset` (optional number): Start listing from this monitor ID. Use with pageSize=1 to get a specific monitor.
- `page` (optional number): Page number for pagination (0-based).
- `pageSize` (optional number): Number of monitors per page (default: 100).
- **Returns**: Monitors data and a summary of their statuses.

4. `get_logs`
Expand Down
36 changes: 33 additions & 3 deletions src/tools/monitors/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,37 @@ export const GetMonitorsZodSchema = z.object({
groupStates: z
.array(z.enum(['alert', 'warn', 'no data', 'ok']))
.optional()
.describe('Filter monitors by their states'),
name: z.string().optional().describe('Filter monitors by name'),
tags: z.array(z.string()).optional().describe('Filter monitors by tags'),
.describe(
'Filter monitors by their states (e.g., alert, warn, no data, ok)',
),
name: z
.string()
.optional()
.describe('Filter monitors by name (case-sensitive)'),
tags: z
.array(z.string())
.optional()
.describe('Filter monitors by tags (e.g., ["env:prod", "service:api"])'),
idOffset: z
.number()
.int()
.optional()
.describe(
'Start listing from this monitor ID. Use with pageSize=1 to get a specific monitor. For pagination, use the last monitor ID from previous response.',
),
page: z
.number()
.int()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think it is better

Suggested change
.int()
.int()
.min(0)

.optional()
.describe(
'Page number for pagination (0-based). Use with pageSize parameter.',
),
pageSize: z
.number()
.int()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.int()
.int()
.positive()

.optional()
.default(100)
.describe(
'Number of monitors per page (default: 100). Set to 1 with idOffset to get a specific monitor.',
),
})
16 changes: 13 additions & 3 deletions src/tools/monitors/tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,16 @@ export const createMonitorsToolHandlers = (
): MonitorsToolHandlers => {
return {
get_monitors: async (request) => {
const { groupStates, name, tags } = GetMonitorsZodSchema.parse(
request.params.arguments,
)
const { groupStates, name, tags, idOffset, page, pageSize } =
GetMonitorsZodSchema.parse(request.params.arguments)

const response = await apiInstance.listMonitors({
groupStates: groupStates?.join(','),
name,
tags: tags?.join(','),
page,
pageSize,
idOffset,
})

if (response == null) {
Expand Down Expand Up @@ -105,6 +107,14 @@ export const createMonitorsToolHandlers = (
type: 'text',
text: `Summary of monitors: ${JSON.stringify(summary)}`,
},
...(monitors.length > 0
? [
{
type: 'text',
text: `Last monitor ID: ${monitors[monitors.length - 1].id}`,
},
]
: []),
Comment on lines +110 to +117
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think these logic can be more simplified

 monitors.length > 0 && {
    type: 'text',
    text: `Last monitor ID: 
  ${monitors[monitors.length - 1].id}`
  }

],
}
},
Expand Down
84 changes: 84 additions & 0 deletions tests/tools/monitors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,90 @@ describe('Monitors Tool', () => {
server.close()
})

it('should handle pagination', async () => {
const mockHandler = http.get(monitorsEndpoint, async (info) => {
const url = new URL(info.request.url)
const page = parseInt(url.searchParams.get('page') || '0')
const pageSize = parseInt(url.searchParams.get('page_size') || '100')

// Generate mock data based on pagination parameters
const monitors = Array.from({ length: pageSize }, (_, i) => ({
id: page * pageSize + i + 1,
name: `Monitor ${page * pageSize + i + 1}`,
type: 'metric alert',
overall_state: 'OK',
tags: ['env:test'],
query: 'avg(last_5m):avg:system.cpu.user{*} > 80',
}))

return HttpResponse.json(monitors)
})

const server = setupServer(mockHandler)

await server.boundary(async () => {
const request = createMockToolRequest('get_monitors', {
page: 1,
pageSize: 50,
})
const response = (await toolHandlers.get_monitors(
request,
)) as unknown as DatadogToolResponse

// Check pagination results
const monitors = JSON.parse(
response.content[0].text.replace('Monitors: ', ''),
)
expect(monitors.length).toBe(50)
expect(monitors[0].id).toBe(51) // First ID on page 1 with pageSize 50
expect(monitors[49].id).toBe(100) // Last ID on page 1 with pageSize 50
expect(response.content[2].text).toBe('Last monitor ID: 100')
})()

server.close()
})

it('should handle id offset', async () => {
const mockHandler = http.get(monitorsEndpoint, async (info) => {
const url = new URL(info.request.url)
const idOffset = parseInt(url.searchParams.get('id_offset') || '0')

// Generate mock data starting from id_offset
const monitors = Array.from({ length: 3 }, (_, i) => ({
id: idOffset + i + 1,
name: `Monitor ${idOffset + i + 1}`,
type: 'metric alert',
overall_state: 'OK',
tags: ['env:test'],
query: 'avg(last_5m):avg:system.cpu.user{*} > 80',
}))

return HttpResponse.json(monitors)
})

const server = setupServer(mockHandler)

await server.boundary(async () => {
const request = createMockToolRequest('get_monitors', {
idOffset: 100,
})
const response = (await toolHandlers.get_monitors(
request,
)) as unknown as DatadogToolResponse

// Check id offset results
const monitors = JSON.parse(
response.content[0].text.replace('Monitors: ', ''),
)
expect(monitors.length).toBe(3)
expect(monitors[0].id).toBe(101) // First ID after offset 100
expect(monitors[2].id).toBe(103) // Last ID
expect(response.content[2].text).toBe('Last monitor ID: 103')
})()

server.close()
})

it('should handle null response', async () => {
const mockHandler = http.get(monitorsEndpoint, async () => {
return HttpResponse.json(null)
Expand Down