Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/tools/apps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
18 changes: 13 additions & 5 deletions src/tools/crashes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand All @@ -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<string, unknown>;
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);
}),
);
Expand Down
27 changes: 21 additions & 6 deletions src/tools/devices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}),
Expand All @@ -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`, {
Expand Down
47 changes: 24 additions & 23 deletions src/tools/issues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
},
Expand Down Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand Down
106 changes: 61 additions & 45 deletions src/tools/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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(),
Expand Down
Loading