From 96a18d4034cf235862bfc1cdca785736a8b72a19 Mon Sep 17 00:00:00 2001 From: Nikola Prijovic Date: Tue, 28 Apr 2026 15:25:55 +0200 Subject: [PATCH] refactor: enhance tool descriptions and add optional filters for logs, devices, crashes, and issues --- src/tools/apps.ts | 2 +- src/tools/crashes.ts | 18 ++++++-- src/tools/devices.ts | 27 ++++++++--- src/tools/issues.ts | 47 +++++++++---------- src/tools/logs.ts | 106 +++++++++++++++++++++++++------------------ 5 files changed, 120 insertions(+), 80 deletions(-) diff --git a/src/tools/apps.ts b/src/tools/apps.ts index 195824e..b695fe0 100644 --- a/src/tools/apps.ts +++ b/src/tools/apps.ts @@ -34,7 +34,7 @@ export function registerAppTools(server: McpServer, context: ServerContext): voi "get_app_summary", { app_id: z.string().describe("The public app ID (e.g. 5X3c4veRGV) from list_apps"), - date_range_start: z.string().optional(), + date_range_start: z.string().optional().describe("The date to summarize, as YYYY-MM-DD or ISO 8601. Returns stats for that single day only — not a range. Defaults to yesterday."), }, ({ app_id, date_range_start }) => handleTool(context, async () => { diff --git a/src/tools/crashes.ts b/src/tools/crashes.ts index f4dabbe..66756ff 100644 --- a/src/tools/crashes.ts +++ b/src/tools/crashes.ts @@ -8,13 +8,14 @@ import { normalizeEndDate, normalizeStartDate } from "../utils/date.js"; export function registerCrashTools(server: McpServer, context: ServerContext): void { server.tool( "get_crashes", + "Returns a list of crash groups for an app, optionally filtered by date range. Does not support filtering by status — use list_issues with type=crash and issue_status if you need open/resolved/closed crashes only.", { app_id: z.string().describe("The public app ID (e.g. 5X3c4veRGV) from list_apps"), - limit: z.number().int().positive().optional(), - date_range_start: z.string().optional(), - date_range_end: z.string().optional(), + limit: z.number().int().positive().optional().default(20).describe("Max number of crashes to return. Defaults to 20."), + date_range_start: z.string().optional().describe("ISO 8601 datetime string (e.g. 2026-04-21T00:00:00Z)."), + date_range_end: z.string().optional().describe("ISO 8601 datetime string (e.g. 2026-04-28T23:59:59Z)."), }, - ({ app_id, limit, date_range_start, date_range_end }) => + ({ app_id, limit = 20, date_range_start, date_range_end }) => handleTool(context, async () => { const result = await context.client.get<{ data?: unknown[]; @@ -24,7 +25,14 @@ export function registerCrashTools(server: McpServer, context: ServerContext): v date_range_end: normalizeEndDate(date_range_end), page_size: typeof limit === "number" ? Math.min(limit, MAX_PAGE_SIZE) : MAX_PAGE_SIZE, }); - const crashes = result.data ?? []; + const crashes = (result.data ?? []).map((crash) => { + if (typeof crash !== "object" || crash === null) return crash; + const c = crash as Record; + if (typeof c.body === "string" && c.body.length > 300) { + return { ...c, body: c.body.slice(0, 300) + "… (truncated, use get_crash_details for full stack trace)" }; + } + return c; + }); return ok(typeof limit === "number" ? crashes.slice(0, limit) : crashes); }), ); diff --git a/src/tools/devices.ts b/src/tools/devices.ts index fb6da49..ec1064c 100644 --- a/src/tools/devices.ts +++ b/src/tools/devices.ts @@ -9,17 +9,26 @@ export function registerDeviceTools(server: McpServer, context: ServerContext): "search_devices", { app_id: z.string().describe("The public app ID (e.g. 5X3c4veRGV) from list_apps"), - filters: z.record(z.string(), z.union([z.string(), z.number()])).optional(), + date_range_start: z.string().optional().describe("ISO 8601 datetime. Filter devices active from this date."), + date_range_end: z.string().optional().describe("ISO 8601 datetime. Filter devices active up to this date."), + name: z.string().optional().describe("Filter by device name. Use * as suffix wildcard (e.g. iPhone*)"), + model: z.string().optional().describe("Filter by device model."), + os_name: z.string().optional().describe("Filter by operating system name (e.g. iOS, Android)."), + os_version: z.string().optional().describe("Filter by OS version string."), + current_app_version: z.string().optional().describe("Filter devices currently running this app version."), + log_text: z.string().optional().describe("Filter devices that have logs matching this text."), + log_level: z.number().int().optional().describe("Filter devices that have logs at this level. 0=Debug, 1=Warning, 2=Error, 3=Trace, 4=Info, 5=Fatal"), + enabled: z.boolean().optional().describe("Filter by enabled (true) or disabled (false) devices."), + order: z.enum(["last_active", "name_asc", "name_desc"]).optional().default("last_active").describe("Sort order. Defaults to last_active."), page_size: z.number().int().positive().optional(), - next_cursor: z.string().optional(), + next_cursor: z.string().optional().describe("Cursor for next page, from pagination.next_cursor in previous response."), }, - ({ app_id, filters, page_size, next_cursor }) => + ({ app_id, page_size, ...filters }) => handleTool(context, async () => { const result = await context.client.get<{ devices?: unknown[]; next_cursor?: string }>(`/app/${app_id}/devices`, { ...filters, format: "json", page_size: clampPageSize(page_size), - next_cursor, }); return ok(result.devices ?? result, { next_cursor: result.next_cursor }); }), @@ -29,9 +38,15 @@ export function registerDeviceTools(server: McpServer, context: ServerContext): "count_devices", { app_id: z.string().describe("The public app ID (e.g. 5X3c4veRGV) from list_apps"), - filters: z.record(z.string(), z.union([z.string(), z.number()])).optional(), + date_range_start: z.string().optional().describe("ISO 8601 datetime. Count devices active from this date."), + date_range_end: z.string().optional().describe("ISO 8601 datetime. Count devices active up to this date."), + name: z.string().optional().describe("Filter by device name."), + model: z.string().optional().describe("Filter by device model."), + os_name: z.string().optional().describe("Filter by operating system name."), + os_version: z.string().optional().describe("Filter by OS version string."), + enabled: z.boolean().optional().describe("Filter by enabled (true) or disabled (false) devices."), }, - ({ app_id, filters }) => + ({ app_id, ...filters }) => handleTool(context, async () => ok( await context.client.get(`/app/${app_id}/devices/count`, { diff --git a/src/tools/issues.ts b/src/tools/issues.ts index 0137e17..b5edbe9 100644 --- a/src/tools/issues.ts +++ b/src/tools/issues.ts @@ -52,15 +52,16 @@ function buildIssuesAggregationQuery(args: { export function registerIssueTools(server: McpServer, context: ServerContext): void { server.tool( "list_issues", + "Lists issue groups for an app. Supports filtering by type (issue, crash, feedback) and status (open, resolved, closed). Use this instead of get_crashes or get_feedback when you need status filtering or combined results.", { app_id: z.string().describe("The public app ID (e.g. 5X3c4veRGV) from list_apps"), - type: z.string().optional(), - issue_status: z.string().optional(), - date_range_start: z.string().optional(), - date_range_end: z.string().optional(), - query: z.string().optional(), - content: z.string().optional(), - version: z.number().int().optional(), + type: z.string().optional().describe("Filter by type: issue, crash, feedback"), + issue_status: z.string().optional().describe("Filter by status: new, open, in_progress, resolved, closed, muted"), + date_range_start: z.string().optional().describe("ISO 8601 datetime string (e.g. 2026-04-28T00:00:00Z)"), + date_range_end: z.string().optional().describe("ISO 8601 datetime string (e.g. 2026-04-28T23:59:59Z)"), + query: z.string().optional().describe("Filter by issue title text."), + content: z.string().optional().describe("Filter by issue body/content text."), + version: z.number().int().optional().describe("Filter by app version ID."), page_size: z.number().int().positive().optional(), page: z.number().int().positive().optional(), }, @@ -156,14 +157,14 @@ export function registerIssueTools(server: McpServer, context: ServerContext): v "get_issue_stats", { app_id: z.string().describe("The public app ID (e.g. 5X3c4veRGV) from list_apps"), - date_range_start: z.string(), - date_range_end: z.string(), - type: z.string().optional(), - issue_status: z.string().optional(), - query: z.string().optional(), - content: z.string().optional(), - version: z.number().int().optional(), - hash: z.string().optional(), + date_range_start: z.string().describe("ISO 8601 datetime string (e.g. 2026-04-21T00:00:00Z)"), + date_range_end: z.string().describe("ISO 8601 datetime string (e.g. 2026-04-28T23:59:59Z)"), + type: z.string().optional().describe("Filter by type: issue, crash, feedback"), + issue_status: z.string().optional().describe("Filter by status: new, open, in_progress, resolved, closed, muted"), + query: z.string().optional().describe("Filter by issue title text."), + content: z.string().optional().describe("Filter by issue body/content text."), + version: z.number().int().optional().describe("Filter by app version ID."), + hash: z.string().optional().describe("Filter to a specific issue hash."), }, (args) => handleTool(context, async () => { @@ -181,14 +182,14 @@ export function registerIssueTools(server: McpServer, context: ServerContext): v "get_issue_device_stats", { app_id: z.string().describe("The public app ID (e.g. 5X3c4veRGV) from list_apps"), - date_range_start: z.string(), - date_range_end: z.string(), - type: z.string().optional(), - issue_status: z.string().optional(), - query: z.string().optional(), - content: z.string().optional(), - version: z.number().int().optional(), - hash: z.string().optional(), + date_range_start: z.string().describe("ISO 8601 datetime string (e.g. 2026-04-21T00:00:00Z)"), + date_range_end: z.string().describe("ISO 8601 datetime string (e.g. 2026-04-28T23:59:59Z)"), + type: z.string().optional().describe("Filter by type: issue, crash, feedback"), + issue_status: z.string().optional().describe("Filter by status: new, open, in_progress, resolved, closed, muted"), + query: z.string().optional().describe("Filter by issue title text."), + content: z.string().optional().describe("Filter by issue body/content text."), + version: z.number().int().optional().describe("Filter by app version ID."), + hash: z.string().optional().describe("Filter to a specific issue hash."), }, (args) => handleTool(context, async () => { diff --git a/src/tools/logs.ts b/src/tools/logs.ts index 6cfea97..75e78e3 100644 --- a/src/tools/logs.ts +++ b/src/tools/logs.ts @@ -5,57 +5,71 @@ import type { ServerContext } from "../server-context.js"; import { normalizeEndDate, normalizeStartDate } from "../utils/date.js"; import { clampPageSize } from "../utils/pagination.js"; -export function registerLogTools(server: McpServer, context: ServerContext): void { - server.tool( - "search_logs", - { - app_id: z.string().describe("The public app ID (e.g. 5X3c4veRGV) from list_apps"), - text: z.string().optional(), - device_udid: z.string().optional(), - date_range_start: z.string().optional(), - date_range_end: z.string().optional(), - page_size: z.number().int().positive().optional(), - cursor: z.string().optional(), - level: z.number().int().optional(), - tags: z.array(z.string()).optional(), - app_version: z.number().int().optional(), - }, - (args) => - handleTool(context, async () => { - const { app_id, date_range_start, date_range_end, page_size, ...filters } = args; - const result = await context.client.get<{ - data?: unknown[]; - previous?: string; - next?: string; - last?: string; - query_id?: string; - }>(`/app/${app_id}/logs/paginated`, { - ...filters, - page_size: clampPageSize(page_size), - date_range_start: normalizeStartDate(date_range_start), - date_range_end: normalizeEndDate(date_range_end), - }); +export const searchLogsSchema = { + app_id: z + .string() + .describe("The public app ID (e.g. 5X3c4veRGV) from list_apps"), + text: z.string().optional().describe("Full-text search across log messages."), + device_udid: z.string().optional().describe("Filter to a specific device by its UDID from search_devices."), + date_range_start: z.string().optional().describe("ISO 8601 datetime string (e.g. 2026-04-28T00:00:00Z). Date-only strings like 2026-04-28 are also accepted."), + date_range_end: z.string().optional().describe("ISO 8601 datetime string (e.g. 2026-04-28T23:59:59Z). Date-only strings like 2026-04-28 are also accepted."), + page_size: z.number().int().positive().optional().describe("Number of results per page. Defaults to 100, max 200. Use smaller values (e.g. 25) to avoid truncation."), + cursor: z.string().optional().describe("Pagination cursor from pagination.next in a previous response."), + level: z + .number() + .int() + .optional() + .describe("Filter by log level. 0=Debug, 1=Warning, 2=Error, 3=Trace, 4=Info, 5=Fatal"), + tags: z.array(z.string()).optional().describe("Filter by log tags (e.g. [\"ERROR\", \"NETWORK\"]). Tags are user-defined string labels."), + app_version: z.number().int().optional().describe("Filter by app version ID (integer). Get version IDs from list_app_versions."), +}; - return ok(result.data ?? result, { - previous: result.previous, - next: result.next, - last: result.last, - query_id: result.query_id, - }); - }), +export function registerLogTools( + server: McpServer, + context: ServerContext, +): void { + server.tool("search_logs", searchLogsSchema, (args) => + handleTool(context, async () => { + const { + app_id, + date_range_start, + date_range_end, + page_size, + ...filters + } = args; + const result = await context.client.get<{ + data?: unknown[]; + previous?: string; + next?: string; + last?: string; + query_id?: string; + }>(`/app/${app_id}/logs/paginated`, { + ...filters, + page_size: clampPageSize(page_size), + date_range_start: normalizeStartDate(date_range_start), + date_range_end: normalizeEndDate(date_range_end), + }); + + return ok(result.data ?? result, { + previous: result.previous, + next: result.next, + last: result.last, + query_id: result.query_id, + }); + }), ); server.tool( "count_logs", { app_id: z.string().describe("The public app ID (e.g. 5X3c4veRGV) from list_apps"), - text: z.string().optional(), - device_udid: z.string().optional(), - date_range_start: z.string().optional(), - date_range_end: z.string().optional(), - level: z.number().int().optional(), - tags: z.array(z.string()).optional(), - app_version: z.number().int().optional(), + text: z.string().optional().describe("Full-text search across log messages."), + device_udid: z.string().optional().describe("Filter to a specific device by its UDID."), + date_range_start: z.string().optional().describe("ISO 8601 datetime string (e.g. 2026-04-28T00:00:00Z)."), + date_range_end: z.string().optional().describe("ISO 8601 datetime string (e.g. 2026-04-28T23:59:59Z)."), + level: z.number().int().optional().describe("Filter by log level. 0=Debug, 1=Warning, 2=Error, 3=Trace, 4=Info, 5=Fatal"), + tags: z.array(z.string()).optional().describe("Filter by log tags (e.g. [\"ERROR\", \"NETWORK\"])."), + app_version: z.number().int().optional().describe("Filter by app version ID from list_app_versions."), }, (args) => handleTool(context, async () => { @@ -73,7 +87,9 @@ export function registerLogTools(server: McpServer, context: ServerContext): voi server.tool( "count_devices_with_logs", { - app_id: z.string().describe("The public app ID (e.g. 5X3c4veRGV) from list_apps"), + app_id: z + .string() + .describe("The public app ID (e.g. 5X3c4veRGV) from list_apps"), text: z.string(), date_range_start: z.string().optional(), date_range_end: z.string().optional(),