Skip to content

feat(seer): Thread short-lived API token to Explorer MCP tools#112179

Merged
azulus merged 12 commits intomasterfrom
jstanley/feat-explorer-tools
Apr 10, 2026
Merged

feat(seer): Thread short-lived API token to Explorer MCP tools#112179
azulus merged 12 commits intomasterfrom
jstanley/feat-explorer-tools

Conversation

@azulus
Copy link
Copy Markdown
Member

@azulus azulus commented Apr 2, 2026

Create a short-lived (1-hour), org-scoped API token when the user opens
the Seer Explorer and pass it to Seer via user_auth_token on the
ExplorerChatRequest. This enables the new MCP sentry_api_execute
tool to call the Sentry API on behalf of the user.

What

  • Create ApiToken in collect_user_org_context() with scopes:
    org:read/write, project:read/write, event:read/write,
    alerts:read/write, member:read, team:read
  • Resolve RpcUser → real User model for token creation
  • Pass request=request through to start_run() so the token
    can be created with the authenticated user
  • Add user_auth_token field to ExplorerChatRequest TypedDict
  • Add instrumentation logging at each stage

Security

  • Token scoped to single org via scoping_organization_id
  • 1-hour expiry
  • Created per-session, not reused

Companion PR

Seer-side changes (tool registration + threading): getsentry/seer#5626

@github-actions github-actions bot added the Scope: Backend Automatically applied to PRs that change backend components label Apr 2, 2026
@azulus azulus force-pushed the jstanley/feat-explorer-tools branch from 950f3b1 to 33ad867 Compare April 6, 2026 21:30
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 6, 2026

Backend Test Failures

Failures on 08c689c in this run:

tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py::OrganizationSeerExplorerChatEndpointTest::test_post_new_conversation_calls_clientlog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py:74: in test_post_new_conversation_calls_client
    mock_client.start_run.assert_called_once_with(
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:989: in assert_called_once_with
    return self.assert_called_with(*args, **kwargs)
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:977: in assert_called_with
    raise AssertionError(_error_message()) from cause
E   AssertionError: expected call not found.
E   Expected: start_run(prompt='What is this error about?', on_page_context=None, page_name=None, override_ce_enable=True)
E     Actual: start_run(prompt='What is this error about?', on_page_context=None, page_name=None, override_ce_enable=True, request=<rest_framework.request.Request: POST '/api/0/organizations/baz/seer/explorer-chat/'>)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

Backend Test Failures

Failures on d8f68b8 in this run:

tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py::OrganizationSeerExplorerChatEndpointTest::test_post_new_conversation_calls_clientlog
[gw0] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py:71: in test_post_new_conversation_calls_client
    mock_client_class.assert_called_once_with(
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:989: in assert_called_once_with
    return self.assert_called_with(*args, **kwargs)
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:977: in assert_called_with
    raise AssertionError(_error_message()) from cause
E   AssertionError: expected call not found.
E   Expected: SeerExplorerClient(<Organization at 0x7f3bd76f15a0: id=4557934902575120, owner_id=None, name='baz', slug='baz'>, <ANY>, is_interactive=True, enable_coding=False)
E     Actual: SeerExplorerClient(<Organization at 0x7f3bd76f1700: id=4557934902575120, owner_id=None, name='baz', slug='baz'>, <SimpleLazyObject: RpcUser(id=459, pk=459, name='', email='admin@localhost', username='admin@localhost', actor_id=None, display_name='admin@localhost', label='admin@localhost', is_superuser=True, is_authenticated=True, is_anonymous=False, is_active=True, is_staff=True, is_unclaimed=False, last_active=datetime.datetime(2026, 4, 8, 22, 55, 3, 67974, tzinfo=datetime.timezone.utc), is_sentry_app=False, password_usable=True, is_password_expired=False, roles=frozenset(), permissions=frozenset(), avatar=None, emails=frozenset({'admin@localhost'}), useremails=[RpcUserEmail(id=459, email='admin@localhost', is_verified=True)], authenticators=[])>, is_interactive=True, enable_coding=False, enable_mcp_tools=False)
tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py::OrganizationSeerExplorerChatEndpointTest::test_post_new_conversation_enable_codinglog
[gw0] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py:103: in test_post_new_conversation_enable_coding
    mock_client_class.assert_called_with(
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:977: in assert_called_with
    raise AssertionError(_error_message()) from cause
E   AssertionError: expected call not found.
E   Expected: SeerExplorerClient(<Organization at 0x7fe1885aa410: id=4557934903361552, owner_id=None, name='baz', slug='baz'>, <ANY>, is_interactive=True, enable_coding=True)
E     Actual: SeerExplorerClient(<Organization at 0x7fe18862d440: id=4557934903361552, owner_id=None, name='baz', slug='baz'>, <SimpleLazyObject: RpcUser(id=429, pk=429, name='', email='admin@localhost', username='admin@localhost', actor_id=None, display_name='admin@localhost', label='admin@localhost', is_superuser=True, is_authenticated=True, is_anonymous=False, is_active=True, is_staff=True, is_unclaimed=False, last_active=datetime.datetime(2026, 4, 8, 22, 55, 15, 880605, tzinfo=datetime.timezone.utc), is_sentry_app=False, password_usable=True, is_password_expired=False, roles=frozenset(), permissions=frozenset(), avatar=None, emails=frozenset({'admin@localhost'}), useremails=[RpcUserEmail(id=431, email='admin@localhost', is_verified=True)], authenticators=[])>, is_interactive=True, enable_coding=True, enable_mcp_tools=False)
tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py::OrganizationSeerExplorerChatEndpointTest::test_post_continue_conversation_enable_codinglog
[gw0] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py:151: in test_post_continue_conversation_enable_coding
    mock_client_class.assert_called_with(
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:977: in assert_called_with
    raise AssertionError(_error_message()) from cause
E   AssertionError: expected call not found.
E   Expected: SeerExplorerClient(<Organization at 0x7f3b216345f0: id=4557934904344592, owner_id=None, name='baz', slug='baz'>, <ANY>, is_interactive=True, enable_coding=True)
E     Actual: SeerExplorerClient(<Organization at 0x7f3b212bd8b0: id=4557934904344592, owner_id=None, name='baz', slug='baz'>, <SimpleLazyObject: RpcUser(id=446, pk=446, name='', email='admin@localhost', username='admin@localhost', actor_id=None, display_name='admin@localhost', label='admin@localhost', is_superuser=True, is_authenticated=True, is_anonymous=False, is_active=True, is_staff=True, is_unclaimed=False, last_active=datetime.datetime(2026, 4, 8, 22, 55, 30, 234970, tzinfo=datetime.timezone.utc), is_sentry_app=False, password_usable=True, is_password_expired=False, roles=frozenset(), permissions=frozenset(), avatar=None, emails=frozenset({'admin@localhost'}), useremails=[RpcUserEmail(id=449, email='admin@localhost', is_verified=True)], authenticators=[])>, is_interactive=True, enable_coding=True, enable_mcp_tools=False)
tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py::OrganizationSeerExplorerChatEndpointTest::test_post_continue_conversation_calls_clientlog
[gw0] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py:124: in test_post_continue_conversation_calls_client
    mock_client_class.assert_called_once_with(
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:989: in assert_called_once_with
    return self.assert_called_with(*args, **kwargs)
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:977: in assert_called_with
    raise AssertionError(_error_message()) from cause
E   AssertionError: expected call not found.
E   Expected: SeerExplorerClient(<Organization at 0x7f58ce0d4680: id=4557934906507280, owner_id=None, name='baz', slug='baz'>, <ANY>, is_interactive=True, enable_coding=False)
E     Actual: SeerExplorerClient(<Organization at 0x7f58cdeaf750: id=4557934906507280, owner_id=None, name='baz', slug='baz'>, <SimpleLazyObject: RpcUser(id=444, pk=444, name='', email='admin@localhost', username='admin@localhost', actor_id=None, display_name='admin@localhost', label='admin@localhost', is_superuser=True, is_authenticated=True, is_anonymous=False, is_active=True, is_staff=True, is_unclaimed=False, last_active=datetime.datetime(2026, 4, 8, 22, 56, 3, 30163, tzinfo=datetime.timezone.utc), is_sentry_app=False, password_usable=True, is_password_expired=False, roles=frozenset(), permissions=frozenset(), avatar=None, emails=frozenset({'admin@localhost'}), useremails=[RpcUserEmail(id=446, email='admin@localhost', is_verified=True)], authenticators=[])>, is_interactive=True, enable_coding=False, enable_mcp_tools=False)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 8, 2026

Backend Test Failures

Failures on b4a1ec2 in this run:

tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py::OrganizationSeerExplorerChatEndpointTest::test_post_continue_conversation_calls_clientlog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py:127: in test_post_continue_conversation_calls_client
    mock_client.continue_run.assert_called_once_with(
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:989: in assert_called_once_with
    return self.assert_called_with(*args, **kwargs)
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:977: in assert_called_with
    raise AssertionError(_error_message()) from cause
E   AssertionError: expected call not found.
E   Expected: continue_run(run_id=789, prompt='Follow up question', insert_index=2, on_page_context=None, page_name=None)
E     Actual: continue_run(run_id=789, prompt='Follow up question', insert_index=2, on_page_context=None, page_name=None, request=<rest_framework.request.Request: POST '/api/0/organizations/baz/seer/explorer-chat/789/'>)
tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py::OrganizationSeerExplorerChatEndpointTest::test_post_new_conversation_enable_codinglog
[gw1] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py:103: in test_post_new_conversation_enable_coding
    mock_client_class.assert_called_with(
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:977: in assert_called_with
    raise AssertionError(_error_message()) from cause
E   AssertionError: expected call not found.
E   Expected: SeerExplorerClient(<Organization at 0x7f90cfea0d70: id=4557935000354848, owner_id=None, name='baz', slug='baz'>, <ANY>, is_interactive=True, enable_coding=True)
E     Actual: SeerExplorerClient(<Organization at 0x7f90cd776710: id=4557935000354848, owner_id=None, name='baz', slug='baz'>, <SimpleLazyObject: RpcUser(id=446, pk=446, name='', email='admin@localhost', username='admin@localhost', actor_id=None, display_name='admin@localhost', label='admin@localhost', is_superuser=True, is_authenticated=True, is_anonymous=False, is_active=True, is_staff=True, is_unclaimed=False, last_active=datetime.datetime(2026, 4, 8, 23, 19, 55, 106523, tzinfo=datetime.timezone.utc), is_sentry_app=False, password_usable=True, is_password_expired=False, roles=frozenset(), permissions=frozenset(), avatar=None, emails=frozenset({'admin@localhost'}), useremails=[RpcUserEmail(id=448, email='admin@localhost', is_verified=True)], authenticators=[])>, is_interactive=True, enable_coding=True, enable_mcp_tools=False)
tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py::OrganizationSeerExplorerChatEndpointTest::test_post_continue_conversation_enable_codinglog
[gw0] linux -- Python 3.13.1 /home/runner/work/sentry/sentry/.venv/bin/python3
tests/sentry/seer/endpoints/test_organization_seer_explorer_chat.py:151: in test_post_continue_conversation_enable_coding
    mock_client_class.assert_called_with(
/opt/hostedtoolcache/Python/3.13.1/x64/lib/python3.13/unittest/mock.py:977: in assert_called_with
    raise AssertionError(_error_message()) from cause
E   AssertionError: expected call not found.
E   Expected: SeerExplorerClient(<Organization at 0x7f33247cc550: id=4557935002386448, owner_id=None, name='baz', slug='baz'>, <ANY>, is_interactive=True, enable_coding=True)
E     Actual: SeerExplorerClient(<Organization at 0x7f332457f570: id=4557935002386448, owner_id=None, name='baz', slug='baz'>, <SimpleLazyObject: RpcUser(id=424, pk=424, name='', email='admin@localhost', username='admin@localhost', actor_id=None, display_name='admin@localhost', label='admin@localhost', is_superuser=True, is_authenticated=True, is_anonymous=False, is_active=True, is_staff=True, is_unclaimed=False, last_active=datetime.datetime(2026, 4, 8, 23, 20, 26, 416041, tzinfo=datetime.timezone.utc), is_sentry_app=False, password_usable=True, is_password_expired=False, roles=frozenset(), permissions=frozenset(), avatar=None, emails=frozenset({'admin@localhost'}), useremails=[RpcUserEmail(id=427, email='admin@localhost', is_verified=True)], authenticators=[])>, is_interactive=True, enable_coding=True, enable_mcp_tools=False)

Comment thread src/sentry/features/temporary.py Outdated
Comment thread src/sentry/seer/explorer/client_utils.py Outdated
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 3 issues found in the latest run.

  • ✅ Fixed: Token created unconditionally for all callers including unrelated endpoints
    • I moved token creation into a dedicated helper and only invoke it in Explorer chat paths when enable_code_mode_tools is enabled.
  • ✅ Fixed: Auth token leaked inside user_org_context sent to Seer
    • collect_user_org_context now returns only non-sensitive context fields and no longer includes user_auth_token.
  • ✅ Fixed: Token scopes missing write permissions described in PR spec
    • The Explorer auth token scopes now include the required write permissions for org, project, event, and alerts APIs.

Create PR

Or push these changes by commenting:

@cursor push 4c79f312fb
Preview (4c79f312fb)
diff --git a/src/sentry/seer/explorer/client.py b/src/sentry/seer/explorer/client.py
--- a/src/sentry/seer/explorer/client.py
+++ b/src/sentry/seer/explorer/client.py
@@ -20,6 +20,7 @@
     ExplorerRunsRequest,
     ExplorerUpdateRequest,
     collect_user_org_context,
+    create_explorer_user_auth_token,
     fetch_run_status,
     make_explorer_chat_request,
     make_explorer_runs_request,
@@ -269,6 +270,9 @@
             raise ValueError("artifact_key and artifact_schema must be provided together")
 
         user_org_context = collect_user_org_context(self.user, self.organization, request=request)
+        user_auth_token = None
+        if self.enable_code_mode_tools:
+            user_auth_token = create_explorer_user_auth_token(self.user, self.organization)
 
         chat_body: ExplorerChatRequest = ExplorerChatRequest(
             organization_id=self.organization.id,
@@ -282,7 +286,7 @@
             is_interactive=self.is_interactive,
             enable_coding=self.enable_coding,
             enable_code_mode_tools=self.enable_code_mode_tools,
-            user_auth_token=user_org_context.get("user_auth_token"),
+            user_auth_token=user_auth_token,
         )
 
         if self.max_iterations is not None:
@@ -371,7 +375,9 @@
         if bool(artifact_schema) != bool(artifact_key):
             raise ValueError("artifact_key and artifact_schema must be provided together")
 
-        user_org_context = collect_user_org_context(self.user, self.organization, request=request)
+        user_auth_token = None
+        if self.enable_code_mode_tools:
+            user_auth_token = create_explorer_user_auth_token(self.user, self.organization)
 
         chat_body: ExplorerChatRequest = ExplorerChatRequest(
             organization_id=self.organization.id,
@@ -383,7 +389,7 @@
             is_interactive=self.is_interactive,
             enable_coding=self.enable_coding,
             enable_code_mode_tools=self.enable_code_mode_tools,
-            user_auth_token=user_org_context.get("user_auth_token"),
+            user_auth_token=user_auth_token,
         )
 
         if prompt_metadata:

diff --git a/src/sentry/seer/explorer/client_utils.py b/src/sentry/seer/explorer/client_utils.py
--- a/src/sentry/seer/explorer/client_utils.py
+++ b/src/sentry/seer/explorer/client_utils.py
@@ -287,8 +287,25 @@
     # Get IP address from http request, if provided
     user_ip: str | None = request.META.get("REMOTE_ADDR") if request else None
 
-    # Create a short-lived API token for Seer to call back into Sentry on behalf of the user
-    user_auth_token: str | None = None
+    return {
+        "org_slug": organization.slug,
+        "user_id": user.id,
+        "user_ip": user_ip,
+        "user_name": user_name,
+        "user_email": user.email,
+        "user_timezone": user_timezone,
+        "user_teams": user_teams,
+        "user_projects": user_projects,
+        "all_org_projects": all_org_projects,
+    }
+
+
+def create_explorer_user_auth_token(
+    user: SentryUser | RpcUser | AnonymousUser | None, organization: Organization
+) -> str | None:
+    if user is None or isinstance(user, AnonymousUser):
+        return None
+
     try:
         from sentry.models.apitoken import ApiToken
         from sentry.types.token import AuthTokenType
@@ -303,15 +320,18 @@
             scoping_organization_id=organization.id,
             scope_list=[
                 "org:read",
+                "org:write",
                 "project:read",
+                "project:write",
                 "event:read",
+                "event:write",
                 "alerts:read",
+                "alerts:write",
                 "member:read",
                 "team:read",
             ],
             expires_at=timezone.now() + timedelta(hours=1),
         )
-        user_auth_token = token.plaintext_token
         logger.info(
             "seer_explorer.auth_token_created",
             extra={
@@ -320,23 +340,12 @@
                 "expires_at": str(token.expires_at),
             },
         )
+        return token.plaintext_token
     except Exception:
         logger.exception("Failed to create short-lived API token for Seer Explorer")
+        return None
 
-    return {
-        "org_slug": organization.slug,
-        "user_id": user.id,
-        "user_ip": user_ip,
-        "user_name": user_name,
-        "user_email": user.email,
-        "user_timezone": user_timezone,
-        "user_teams": user_teams,
-        "user_projects": user_projects,
-        "all_org_projects": all_org_projects,
-        "user_auth_token": user_auth_token,
-    }
 
-
 def fetch_run_status(run_id: int, organization: Organization) -> SeerRunState:
     """Fetch current run status from Seer."""
     body = ExplorerStateRequest(run_id=run_id, organization_id=organization.id)

diff --git a/tests/sentry/seer/explorer/test_client_utils.py b/tests/sentry/seer/explorer/test_client_utils.py
--- a/tests/sentry/seer/explorer/test_client_utils.py
+++ b/tests/sentry/seer/explorer/test_client_utils.py
@@ -1,6 +1,7 @@
 from sentry.models.organizationmember import OrganizationMember
 from sentry.seer.explorer.client_utils import (
     collect_user_org_context,
+    create_explorer_user_auth_token,
     has_seer_explorer_access_with_detail,
     snapshot_to_markdown,
 )
@@ -186,7 +187,24 @@
         assert context is not None
         assert context.get("user_ip") == request.META.get("REMOTE_ADDR")
 
+    def test_collect_context_does_not_include_user_auth_token(self) -> None:
+        context = collect_user_org_context(self.user, self.organization)
 
+        assert "user_auth_token" not in context
+
+    def test_create_explorer_user_auth_token_includes_write_scopes(self) -> None:
+        from sentry.models.apitoken import ApiToken
+
+        token = create_explorer_user_auth_token(self.user, self.organization)
+
+        assert token is not None
+        api_token = ApiToken.objects.get(user_id=self.user.id, token=token)
+        assert "org:write" in api_token.scope_list
+        assert "project:write" in api_token.scope_list
+        assert "event:write" in api_token.scope_list
+        assert "alerts:write" in api_token.scope_list
+
+
 class SnapshotToMarkdownTest(TestCase):
     def test_single_node(self) -> None:
         snapshot = {

diff --git a/tests/sentry/seer/explorer/test_explorer_client.py b/tests/sentry/seer/explorer/test_explorer_client.py
--- a/tests/sentry/seer/explorer/test_explorer_client.py
+++ b/tests/sentry/seer/explorer/test_explorer_client.py
@@ -79,6 +79,30 @@
     @patch("sentry.seer.explorer.client.has_seer_access_with_detail")
     @patch("sentry.seer.explorer.client.make_explorer_chat_request")
     @patch("sentry.seer.explorer.client.collect_user_org_context")
+    @patch("sentry.seer.explorer.client.create_explorer_user_auth_token")
+    def test_start_run_only_creates_auth_token_when_code_mode_tools_enabled(
+        self, mock_create_token, mock_collect_context, mock_post, mock_access
+    ):
+        mock_access.return_value = (True, None)
+        mock_collect_context.return_value = {"user_id": self.user.id}
+        mock_create_token.return_value = "auth-token"
+        mock_response = MagicMock()
+        mock_response.json.return_value = {"run_id": 123}
+        mock_response.status = 200
+        mock_post.return_value = mock_response
+
+        client = SeerExplorerClient(self.organization, self.user, enable_code_mode_tools=False)
+        client.start_run("Test query")
+        mock_create_token.assert_not_called()
+
+        client = SeerExplorerClient(self.organization, self.user, enable_code_mode_tools=True)
+        client.start_run("Test query")
+        assert mock_create_token.call_count == 1
+        mock_create_token.assert_called_with(self.user, self.organization)
+
+    @patch("sentry.seer.explorer.client.has_seer_access_with_detail")
+    @patch("sentry.seer.explorer.client.make_explorer_chat_request")
+    @patch("sentry.seer.explorer.client.collect_user_org_context")
     def test_start_run_with_request(self, mock_collect_context, mock_post, mock_access):
         """Test starting a new run passes request object to collect_user_org_context"""
         mock_access.return_value = (True, None)

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Comment thread src/sentry/seer/explorer/client_utils.py
Comment thread src/sentry/seer/explorer/client.py Outdated
Comment thread src/sentry/seer/explorer/client_utils.py
Comment thread src/sentry/seer/explorer/client_utils.py Outdated
Comment thread src/sentry/seer/explorer/client.py
Comment thread src/sentry/seer/explorer/client.py Outdated
Copy link
Copy Markdown
Contributor

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit a190362. Configure here.

Comment thread src/sentry/seer/explorer/client.py
azulus and others added 4 commits April 9, 2026 16:20
Create a 1-hour scoped API token (org:read, project:read, event:read)
when starting an Explorer session and pass it to Seer via the
user_auth_token field on ExplorerChatRequest. This enables Seer's new
sentry_api_execute tool to call the Sentry API on behalf of the user.

Includes instrumentation logging at each stage to trace token flow.

Co-Authored-By: Claude <noreply@anthropic.com>
Add org:write, project:write, event:write, alerts:read, alerts:write,
member:read, and team:read scopes to the short-lived token. The agent
needs write scopes to create dashboards, alerts, and other resources
on behalf of the user. Token remains org-scoped and 1-hour expiry.

Co-Authored-By: Claude <noreply@anthropic.com>
The endpoint now passes request= to start_run() for auth token
creation. Update the mock assertion to expect it.

Co-Authored-By: Claude <noreply@anthropic.com>
…inue

1. Register organizations:seer-explorer-mcp-tools feature flag to gate
   general-purpose MCP tools (sentry_api_search/execute) in Explorer.

2. Thread enable_mcp_tools from the chat endpoint through
   SeerExplorerClient to the Seer request body.

3. Fix auth token expiry on multi-turn sessions: continue_run now calls
   collect_user_org_context to create a fresh token on every request,
   rather than relying on the token from start_run which has a 1-hour
   TTL.

Co-Authored-By: Claude <noreply@anthropic.com>
azulus and others added 7 commits April 9, 2026 16:20
Co-Authored-By: Claude <noreply@anthropic.com>
Add enable_mcp_tools=False to remaining assert_called_with calls and
request=ANY to continue_run assertion.

Co-Authored-By: Claude <noreply@anthropic.com>
Rename feature flag and field to reflect the code mode project naming
rather than the generic MCP implementation detail.

Co-Authored-By: Claude <noreply@anthropic.com>
Remove token_prefix from log lines to avoid persisting credential
material in logs. Keep the token creation log for debugging but
without sensitive fields.

Co-Authored-By: Claude <noreply@anthropic.com>
Remove write scopes (org:write, project:write, event:write,
alerts:write) from the short-lived token created for Seer Explorer.
Read-only is sufficient for code mode tools for now.

Co-Authored-By: Claude <noreply@anthropic.com>
- Set api_expose=False on code mode feature flag (not needed by frontend)
- Strip user_auth_token from user_org_context before sending to Seer
  to avoid sending it twice (once in dict, once as top-level field)

Co-Authored-By: Claude <noreply@anthropic.com>
Extract token creation into standalone create_explorer_api_token()
function and only call it when enable_code_mode_tools is True. This
avoids creating unnecessary tokens when the feature is off, and
eliminates expensive collect_user_org_context queries in continue_run
that were only needed for the token.

Co-Authored-By: Claude <noreply@anthropic.com>
create_explorer_api_token expects a real user, but self.user can be
None or AnonymousUser. Skip token creation in those cases.

Co-Authored-By: Claude <noreply@anthropic.com>
@azulus azulus merged commit b62f69f into master Apr 10, 2026
77 checks passed
@azulus azulus deleted the jstanley/feat-explorer-tools branch April 10, 2026 01:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Backend Automatically applied to PRs that change backend components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants