Skip to content

Commit fd87ae7

Browse files
DominikB2014claude
andcommitted
fix(search-events): Suggest Seer's wider project scope instead of applying it
In the UI, Seer's broader project scope is a suggestion the user accepts. Here the caller chose the project, or the session is scoped to it, so applying it silently overrode that choice and could escape a project constraint. Keep the requested project and note the suggested project IDs instead, except in project-scoped sessions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 822e6dc commit fd87ae7

6 files changed

Lines changed: 119 additions & 93 deletions

File tree

‎packages/mcp-core/src/api-client/client.ts‎

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@ import {
2828
getTraceMetricsExploreUrl,
2929
getTraceUrl as getTraceUrlUtil,
3030
getUptimeMonitorUrl as getUptimeMonitorUrlUtil,
31-
appendProjectParams,
3231
isPublicSentryHost,
33-
type ProjectIdParam,
3432
type TraceMetricIdentifier,
3533
} from "../utils/url-utils";
3634
import { USER_AGENT } from "../version";
@@ -190,7 +188,7 @@ type ExplorerAggregateParams = {
190188
type ExplorerUrlParams = ExplorerAggregateParams & {
191189
organizationSlug: string;
192190
query: string;
193-
projectId?: ProjectIdParam;
191+
projectId?: string;
194192
sort?: string;
195193
statsPeriod?: string;
196194
start?: string;
@@ -1222,7 +1220,7 @@ export class SentryApiService {
12221220
private buildDiscoverUrl(params: {
12231221
organizationSlug: string;
12241222
query: string;
1225-
projectId?: ProjectIdParam;
1223+
projectId?: string;
12261224
fields?: string[];
12271225
sort?: string;
12281226
statsPeriod?: string;
@@ -1251,7 +1249,9 @@ export class SentryApiService {
12511249
urlParams.set("queryDataset", "error-events");
12521250
urlParams.set("query", query);
12531251

1254-
appendProjectParams(urlParams, projectId);
1252+
if (projectId) {
1253+
urlParams.set("project", projectId);
1254+
}
12551255

12561256
// Discover API includes aggregate functions directly in field list
12571257
if (fields && fields.length > 0) {
@@ -1387,7 +1387,9 @@ export class SentryApiService {
13871387
const urlParams = new URLSearchParams();
13881388
urlParams.set("query", query);
13891389

1390-
appendProjectParams(urlParams, projectId);
1390+
if (projectId) {
1391+
urlParams.set("project", projectId);
1392+
}
13911393

13921394
const isAggregateQuery = this.isAggregateExplorerQuery(params);
13931395

@@ -1421,7 +1423,7 @@ export class SentryApiService {
14211423
const urlParams = new URLSearchParams();
14221424

14231425
urlParams.set("logsQuery", query);
1424-
appendProjectParams(urlParams, projectId ?? "-1");
1426+
urlParams.set("project", projectId ?? "-1");
14251427

14261428
const isAggregateQuery = this.isAggregateExplorerQuery(params);
14271429
if (isAggregateQuery) {
@@ -1495,7 +1497,7 @@ export class SentryApiService {
14951497
getEventsExplorerUrl(
14961498
organizationSlug: string,
14971499
query: string,
1498-
projectId?: ProjectIdParam,
1500+
projectId?: string,
14991501
dataset: EventsDataset = "spans",
15001502
fields?: string[],
15011503
sort?: string,
@@ -3670,7 +3672,7 @@ export class SentryApiService {
36703672
fields?: string[];
36713673
query?: string;
36723674
orderby?: string[];
3673-
project?: ProjectIdParam;
3675+
project?: string;
36743676
environment?: string | string[];
36753677
statsPeriod?: string;
36763678
start?: string;
@@ -3680,7 +3682,9 @@ export class SentryApiService {
36803682
): Promise<EventsValidationResult> {
36813683
const queryParams = new URLSearchParams();
36823684
queryParams.set("dataset", normalizeEventsDataset(dataset));
3683-
appendProjectParams(queryParams, project);
3685+
if (project) {
3686+
queryParams.set("project", project);
3687+
}
36843688
if (query) {
36853689
queryParams.set("query", query);
36863690
}
@@ -4696,7 +4700,7 @@ export class SentryApiService {
46964700
query: string;
46974701
fields: string[];
46984702
limit: number;
4699-
projectId?: ProjectIdParam;
4703+
projectId?: string;
47004704
dataset?: "errors" | "tracemetrics" | "profiles";
47014705
statsPeriod?: string;
47024706
start?: string;
@@ -4717,7 +4721,9 @@ export class SentryApiService {
47174721
params.end,
47184722
);
47194723

4720-
appendProjectParams(queryParams, params.projectId);
4724+
if (params.projectId) {
4725+
queryParams.set("project", params.projectId);
4726+
}
47214727

47224728
queryParams.set("sort", params.sort);
47234729

@@ -4740,7 +4746,7 @@ export class SentryApiService {
47404746
query: string;
47414747
fields: string[];
47424748
limit: number;
4743-
projectId?: ProjectIdParam;
4749+
projectId?: string;
47444750
dataset: "spans" | "logs";
47454751
statsPeriod?: string;
47464752
start?: string;
@@ -4761,7 +4767,9 @@ export class SentryApiService {
47614767
params.end,
47624768
);
47634769

4764-
appendProjectParams(queryParams, params.projectId);
4770+
if (params.projectId) {
4771+
queryParams.set("project", params.projectId);
4772+
}
47654773

47664774
// Dataset-specific parameters
47674775
if (params.dataset === "spans") {
@@ -4805,7 +4813,7 @@ export class SentryApiService {
48054813
query: string;
48064814
fields: string[];
48074815
limit?: number;
4808-
projectId?: ProjectIdParam;
4816+
projectId?: string;
48094817
dataset?: EventsDataset;
48104818
statsPeriod?: string;
48114819
start?: string;
@@ -4878,7 +4886,7 @@ export class SentryApiService {
48784886
query: string;
48794887
yAxis: string;
48804888
interval?: string;
4881-
projectId?: ProjectIdParam;
4889+
projectId?: string;
48824890
dataset?: EventsDataset;
48834891
statsPeriod?: string;
48844892
start?: string;
@@ -4895,7 +4903,9 @@ export class SentryApiService {
48954903
queryParams.set("interval", interval);
48964904
}
48974905
this.applyTimeParams(queryParams, statsPeriod, start, end);
4898-
appendProjectParams(queryParams, projectId);
4906+
if (projectId) {
4907+
queryParams.set("project", projectId);
4908+
}
48994909
// partial=1 keeps the current (in-progress) bucket, matching Sentry's charts.
49004910
queryParams.set("partial", "1");
49014911
queryParams.set("referrer", SENTRY_MCP_SEARCH_EVENTS_REFERRER);

‎packages/mcp-core/src/tools/catalog/search-events.test.ts‎

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3566,32 +3566,50 @@ describe("search_events", () => {
35663566
expect(mockGenerateText).not.toHaveBeenCalled();
35673567
});
35683568

3569-
it("should search the projects Seer broadened the query to", async () => {
3570-
mswServer.use(
3571-
mockOrganization(["gen-ai-features", "gen-ai-search-agent-translate"]),
3572-
mockSeerState({
3573-
status: "completed",
3574-
final_response: {
3575-
responses: [seerQuery],
3576-
unsupported_reason: null,
3577-
project_ids: [42, 43],
3578-
},
3579-
}),
3580-
http.get(
3581-
"https://sentry.io/api/0/organizations/test-org/events/",
3582-
({ request }) => {
3583-
const url = new URL(request.url);
3584-
expect(url.searchParams.getAll("project")).toEqual(["42", "43"]);
3585-
return HttpResponse.json({ data: [] });
3586-
},
3587-
),
3588-
);
3569+
it.each([
3570+
["suggest", context, true],
3571+
[
3572+
"not suggest in a project-scoped session",
3573+
{
3574+
...context,
3575+
constraints: { ...context.constraints, projectSlug: "test-project" },
3576+
},
3577+
false,
3578+
],
3579+
])(
3580+
"should keep the requested project and %s Seer's wider scope",
3581+
async (_, handlerContext, expectNote) => {
3582+
mswServer.use(
3583+
mockOrganization([
3584+
"gen-ai-features",
3585+
"gen-ai-search-agent-translate",
3586+
]),
3587+
mockSeerState({
3588+
status: "completed",
3589+
final_response: {
3590+
responses: [seerQuery],
3591+
unsupported_reason: null,
3592+
project_ids: [42, 43],
3593+
},
3594+
}),
3595+
http.get(
3596+
"https://sentry.io/api/0/organizations/test-org/events/",
3597+
({ request }) => {
3598+
const url = new URL(request.url);
3599+
expect(url.searchParams.getAll("project")).toEqual(["42"]);
3600+
return HttpResponse.json({ data: [] });
3601+
},
3602+
),
3603+
);
35893604

3590-
const result = await searchEvents.handler(seerParams, context);
3605+
const result = await searchEvents.handler(seerParams, handlerContext);
35913606

3592-
expect(mockSeerStart).toHaveBeenCalled();
3593-
expect(result).toContain("Seer broadened the search to 2 projects.");
3594-
});
3607+
expect(mockSeerStart).toHaveBeenCalled();
3608+
expect(
3609+
result.includes("Seer suggested also searching project IDs 43"),
3610+
).toBe(expectNote);
3611+
},
3612+
);
35953613

35963614
it("should keep the requested project when Seer does not broaden it", async () => {
35973615
mswServer.use(
@@ -3617,7 +3635,7 @@ describe("search_events", () => {
36173635
const result = await searchEvents.handler(seerParams, context);
36183636

36193637
expect(mockSeerStart).toHaveBeenCalled();
3620-
expect(result).not.toContain("Seer broadened the search");
3638+
expect(result).not.toContain("Seer suggested also searching");
36213639
});
36223640

36233641
it("should search all accessible projects without a projectSlug", async () => {

‎packages/mcp-core/src/tools/catalog/search-events.ts‎

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -723,8 +723,20 @@ export default defineTool({
723723
unknownEnvironments.length > 0
724724
? formatUnknownEnvironmentNote(unknownEnvironments, environmentNames)
725725
: "";
726-
const withEnvironmentNote = (text: string): string =>
727-
environmentNote ? `${environmentNote}\n\n${text}` : text;
726+
// The caller chose the project (or the session is scoped to it), so Seer's
727+
// wider scope is only suggested. Scoped sessions can't change the project.
728+
const suggestedProjectIds = context.constraints.projectSlug
729+
? []
730+
: (seerTranslation?.suggestedProjectIds ?? []);
731+
const projectSuggestionNote =
732+
suggestedProjectIds.length > 0
733+
? `**Note:** Seer suggested also searching project IDs ${suggestedProjectIds.join(", ")}, for example other services in the same trace. Omit \`projectSlug\` to search all accessible projects.`
734+
: "";
735+
const leadingNote = [environmentNote, projectSuggestionNote]
736+
.filter(Boolean)
737+
.join("\n\n");
738+
const withLeadingNote = (text: string): string =>
739+
leadingNote ? `${leadingNote}\n\n${text}` : text;
728740

729741
if (dataset === "replays") {
730742
const replaySort = sortParam || DEFAULT_REPLAY_SORT;
@@ -793,11 +805,9 @@ export default defineTool({
793805
availableToolNames: context.availableToolNames,
794806
directToolNames: context.directToolNames,
795807
});
796-
return withEnvironmentNote(replayOutput);
808+
return withLeadingNote(replayOutput);
797809
}
798810

799-
const eventsProjectId = seerTranslation?.projectIds ?? projectId;
800-
801811
if (timeSeries) {
802812
const timeSeriesQuery = applyEnvironmentToEventsQuery(
803813
dataset,
@@ -813,14 +823,14 @@ export default defineTool({
813823
query: timeSeriesQuery,
814824
yAxis: timeSeries.yAxis,
815825
interval: timeSeries.interval ?? undefined,
816-
projectId: eventsProjectId,
826+
projectId,
817827
dataset,
818828
...timeParams,
819829
});
820830
const statsUrl = apiService.getEventsExplorerUrl(
821831
organizationSlug,
822832
timeSeriesQuery,
823-
eventsProjectId,
833+
projectId,
824834
dataset,
825835
[timeSeries.yAxis],
826836
`-${timeSeries.yAxis}`,
@@ -830,7 +840,7 @@ export default defineTool({
830840
timeParams.start,
831841
timeParams.end,
832842
);
833-
return withEnvironmentNote(
843+
return withLeadingNote(
834844
formatTimeSeriesResults({
835845
series,
836846
yAxis: timeSeries.yAxis,
@@ -872,7 +882,7 @@ export default defineTool({
872882
fields: requestFields,
873883
query: sentryQuery,
874884
sort: sortParam,
875-
projectId: eventsProjectId,
885+
projectId,
876886
environment: environment ?? undefined,
877887
...timeParams,
878888
});
@@ -902,7 +912,7 @@ export default defineTool({
902912
query: sentryQuery,
903913
fields: finalRequestFields,
904914
limit: params.limit,
905-
projectId: eventsProjectId,
915+
projectId,
906916
dataset,
907917
sort: sortParam,
908918
...timeParams,
@@ -950,7 +960,7 @@ export default defineTool({
950960
: apiService.getEventsExplorerUrl(
951961
organizationSlug,
952962
sentryQuery,
953-
eventsProjectId,
963+
projectId,
954964
dataset,
955965
fields,
956966
sortParam,
@@ -986,15 +996,15 @@ export default defineTool({
986996

987997
switch (dataset) {
988998
case "errors":
989-
return withEnvironmentNote(formatErrorResults(formatParams));
999+
return withLeadingNote(formatErrorResults(formatParams));
9901000
case "logs":
991-
return withEnvironmentNote(formatLogResults(formatParams));
1001+
return withLeadingNote(formatLogResults(formatParams));
9921002
case "spans":
993-
return withEnvironmentNote(formatSpanResults(formatParams));
1003+
return withLeadingNote(formatSpanResults(formatParams));
9941004
case "profiles":
995-
return withEnvironmentNote(formatProfileResults(formatParams));
1005+
return withLeadingNote(formatProfileResults(formatParams));
9961006
default:
997-
return withEnvironmentNote(formatTraceMetricsResults(formatParams));
1007+
return withLeadingNote(formatTraceMetricsResults(formatParams));
9981008
}
9991009
},
10001010
});

0 commit comments

Comments
 (0)