Skip to main content

sentry_mcp/tools/
search_issue_events.rs

1use crate::api_client::{Event, EventsQuery, SentryApi};
2use rmcp::{ErrorData as McpError, model::CallToolResult};
3use schemars::JsonSchema;
4use serde::Deserialize;
5
6#[derive(Debug, Deserialize, JsonSchema)]
7pub struct SearchIssueEventsInput {
8    #[schemars(description = "Organization slug")]
9    pub organization_slug: String,
10    #[schemars(description = "Issue ID like 'PROJECT-123' or numeric ID")]
11    pub issue_id: String,
12    #[schemars(description = "Sentry search query. Syntax: key:value pairs with optional raw text. \
13        Operators: > < >= <= for numbers, ! for negation, * for wildcard, OR/AND for logic. \
14        Event properties: environment, release, platform, message, user.id, user.email, \
15        device.family, browser.name, os.name, server_name, transaction. \
16        Examples: 'server_name:web-1', 'environment:production', '!user.email:*@test.com', \
17        'browser.name:Chrome OR browser.name:Firefox'")]
18    pub query: Option<String>,
19    #[schemars(description = "Maximum number of events to return (default: 10, max: 100)")]
20    pub limit: Option<i32>,
21    #[schemars(description = "Sort order: 'newest' (default) or 'oldest'")]
22    pub sort: Option<String>,
23}
24
25pub fn format_events_output(issue_id: &str, query: Option<&str>, events: &[Event]) -> String {
26    let mut output = String::new();
27    output.push_str("# Issue Events\n\n");
28    output.push_str(&format!("**Issue:** {}\n", issue_id));
29    if let Some(q) = query {
30        output.push_str(&format!("**Query:** {}\n", q));
31    }
32    output.push_str(&format!("**Found:** {} events\n\n", events.len()));
33    for (i, event) in events.iter().enumerate() {
34        output.push_str(&format!("## Event {} - {}\n\n", i + 1, event.event_id));
35        if let Some(date) = &event.date_created {
36            output.push_str(&format!("**Date:** {}\n", date));
37        }
38        if let Some(platform) = &event.platform {
39            output.push_str(&format!("**Platform:** {}\n", platform));
40        }
41        if let Some(msg) = &event.message
42            && !msg.is_empty()
43        {
44            output.push_str(&format!("**Message:** {}\n", msg));
45        }
46        if !event.tags.is_empty() {
47            output.push_str("**Tags:** ");
48            let tags: Vec<String> = event
49                .tags
50                .iter()
51                .map(|t| format!("{}={}", t.key, t.value))
52                .collect();
53            output.push_str(&tags.join(", "));
54            output.push('\n');
55        }
56        for entry in &event.entries {
57            if entry.entry_type == "exception"
58                && let Some(values) = entry.data.get("values").and_then(|v| v.as_array())
59            {
60                for exc in values {
61                    let exc_type = exc.get("type").and_then(|v| v.as_str()).unwrap_or("?");
62                    let exc_value = exc.get("value").and_then(|v| v.as_str()).unwrap_or("?");
63                    output.push_str(&format!("**Exception:** {} - {}\n", exc_type, exc_value));
64                }
65            }
66        }
67        output.push('\n');
68    }
69    if events.is_empty() {
70        output.push_str("No events found matching the query.\n");
71    }
72    output
73}
74
75pub async fn execute(
76    client: &impl SentryApi,
77    input: SearchIssueEventsInput,
78) -> Result<CallToolResult, McpError> {
79    let limit = input.limit.unwrap_or(10).min(100);
80    let sort = input.sort.unwrap_or_else(|| "newest".to_string());
81    let query = EventsQuery {
82        query: input.query.clone(),
83        limit: Some(limit),
84        sort: Some(sort),
85    };
86    let events = client
87        .list_events_for_issue(&input.organization_slug, &input.issue_id, &query)
88        .await
89        .map_err(|e| McpError::internal_error(e.to_string(), None))?;
90    let output = format_events_output(&input.issue_id, input.query.as_deref(), &events);
91    Ok(CallToolResult::success(vec![rmcp::model::Content::text(output)]))
92}