Skip to content

Commit c94e2a0

Browse files
dcramerclaude
andauthored
fix: improve 404 error handling to show all relevant parameters (#372)
Instead of trying to guess which resource caused a 404, now we list all path parameters that were used in the API call. This provides clearer error messages that help users identify which parameter might be incorrect. Example error message: ``` Resource not found. Please verify these parameters are correct: - organizationSlug: 'my-org' - issueId: 'PROJ-123' ``` Changes: - Simplified error handling to remove operation tracking - Tools now only pass path parameters that could cause 404s - Error messages list all relevant parameters for better debugging Fixes MCP-SERVER-EA1, MCP-SERVER-E9B 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <[email protected]>
1 parent 56aff50 commit c94e2a0

File tree

8 files changed

+109
-98
lines changed

8 files changed

+109
-98
lines changed

packages/mcp-server/src/api-client/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,7 @@ export class SentryApiService {
264264
throw new Error(friendlyMessage, { cause: error });
265265
}
266266

267+
// Handle error responses generically
267268
if (!response.ok) {
268269
const errorText = await response.text();
269270
let parsed: unknown | undefined;

packages/mcp-server/src/tools/find-errors.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { z } from "zod";
22
import { setTag } from "@sentry/core";
33
import { defineTool } from "./utils/defineTool";
4-
import { apiServiceFromContext } from "./utils/api-utils";
4+
import { apiServiceFromContext, withApiErrorHandling } from "./utils/api-utils";
55
import type { ServerContext } from "../types";
66
import {
77
ParamOrganizationSlug,
@@ -73,14 +73,21 @@ export default defineTool({
7373
setTag("organization.slug", organizationSlug);
7474
if (params.projectSlug) setTag("project.slug", params.projectSlug);
7575

76-
const eventList = await apiService.searchErrors({
77-
organizationSlug,
78-
projectSlug: params.projectSlug,
79-
filename: params.filename,
80-
query: params.query,
81-
transaction: params.transaction,
82-
sortBy: params.sortBy as "last_seen" | "count" | undefined,
83-
});
76+
const eventList = await withApiErrorHandling(
77+
() =>
78+
apiService.searchErrors({
79+
organizationSlug,
80+
projectSlug: params.projectSlug,
81+
filename: params.filename,
82+
query: params.query,
83+
transaction: params.transaction,
84+
sortBy: params.sortBy as "last_seen" | "count" | undefined,
85+
}),
86+
{
87+
organizationSlug,
88+
projectSlug: params.projectSlug,
89+
},
90+
);
8491
let output = `# Errors in **${organizationSlug}${params.projectSlug ? `/${params.projectSlug}` : ""}**\n\n`;
8592
if (params.query)
8693
output += `These errors match the query \`${params.query}\`\n`;

packages/mcp-server/src/tools/find-issues.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { z } from "zod";
22
import { setTag } from "@sentry/core";
33
import { defineTool } from "./utils/defineTool";
4-
import { apiServiceFromContext } from "./utils/api-utils";
4+
import { apiServiceFromContext, withApiErrorHandling } from "./utils/api-utils";
55
import { UserInputError } from "../errors";
66
import type { ServerContext } from "../types";
77
import {
@@ -73,14 +73,21 @@ export default defineTool({
7373
count: "freq" as const,
7474
userCount: "user" as const,
7575
};
76-
const issues = await apiService.listIssues({
77-
organizationSlug,
78-
projectSlug: params.projectSlug,
79-
query: params.query,
80-
sortBy: params.sortBy
81-
? sortByMap[params.sortBy as keyof typeof sortByMap]
82-
: undefined,
83-
});
76+
const issues = await withApiErrorHandling(
77+
() =>
78+
apiService.listIssues({
79+
organizationSlug,
80+
projectSlug: params.projectSlug,
81+
query: params.query,
82+
sortBy: params.sortBy
83+
? sortByMap[params.sortBy as keyof typeof sortByMap]
84+
: undefined,
85+
}),
86+
{
87+
organizationSlug,
88+
projectSlug: params.projectSlug,
89+
},
90+
);
8491
let output = `# Issues in **${organizationSlug}${params.projectSlug ? `/${params.projectSlug}` : ""}**\n\n`;
8592
if (issues.length === 0) {
8693
output += "No issues found.\n";

packages/mcp-server/src/tools/find-organizations.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { z } from "zod";
22
import { defineTool } from "./utils/defineTool";
3-
import { apiServiceFromContext } from "./utils/api-utils";
3+
import { apiServiceFromContext, withApiErrorHandling } from "./utils/api-utils";
44
import type { ServerContext } from "../types";
55

66
export default defineTool({
@@ -17,7 +17,10 @@ export default defineTool({
1717
// User data endpoints (like /users/me/regions/) should never use regionUrl
1818
// as they must always query the main API server, not region-specific servers
1919
const apiService = apiServiceFromContext(context);
20-
const organizations = await apiService.listOrganizations();
20+
const organizations = await withApiErrorHandling(
21+
() => apiService.listOrganizations(),
22+
{}, // No params for this endpoint
23+
);
2124

2225
let output = "# Organizations\n\n";
2326

packages/mcp-server/src/tools/get-issue-details.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,10 @@ export default defineTool({
130130
organizationSlug: orgSlug,
131131
issueId: parsedIssueId!,
132132
}),
133-
{ operation: "getIssue", resourceId: parsedIssueId },
133+
{
134+
organizationSlug: orgSlug,
135+
issueId: parsedIssueId,
136+
},
134137
);
135138

136139
const event = await apiService.getLatestEventForIssue({

packages/mcp-server/src/tools/update-issue.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,10 @@ export default defineTool({
109109
organizationSlug: orgSlug,
110110
issueId: parsedIssueId!,
111111
}),
112-
{ operation: "getIssue", resourceId: parsedIssueId },
112+
{
113+
organizationSlug: orgSlug,
114+
issueId: parsedIssueId,
115+
},
113116
);
114117

115118
// Update the issue
@@ -121,7 +124,10 @@ export default defineTool({
121124
status: params.status,
122125
assignedTo: params.assignedTo,
123126
}),
124-
{ operation: "updateIssue", resourceId: parsedIssueId },
127+
{
128+
organizationSlug: orgSlug,
129+
issueId: parsedIssueId,
130+
},
125131
);
126132

127133
let output = `# Issue ${updatedIssue.shortId} Updated in **${orgSlug}**\n\n`;

packages/mcp-server/src/tools/utils/api-utils.test.ts

Lines changed: 36 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,78 +4,81 @@ import { UserInputError } from "../../errors";
44
import { handleApiError, withApiErrorHandling } from "./api-utils";
55

66
describe("handleApiError", () => {
7-
it("converts 404 errors for issues to UserInputError", () => {
7+
it("converts 404 errors with params to list all parameters", () => {
88
const error = new ApiError("Not Found", 404);
99

1010
expect(() =>
11-
handleApiError(error, { operation: "getIssue", resourceId: "PROJ-123" }),
11+
handleApiError(error, {
12+
organizationSlug: "my-org",
13+
issueId: "PROJ-123",
14+
}),
1215
).toThrow(UserInputError);
1316

1417
expect(() =>
15-
handleApiError(error, { operation: "getIssue", resourceId: "PROJ-123" }),
18+
handleApiError(error, {
19+
organizationSlug: "my-org",
20+
issueId: "PROJ-123",
21+
}),
1622
).toThrow(
17-
"Issue 'PROJ-123' not found. Please verify the issue ID is correct.",
23+
"Resource not found. Please verify these parameters are correct:\n - organizationSlug: 'my-org'\n - issueId: 'PROJ-123'",
1824
);
1925
});
2026

21-
it("converts 404 errors for other resources appropriately", () => {
27+
it("converts 404 errors with multiple params including nullish values", () => {
2228
const error = new ApiError("Not Found", 404);
2329

2430
expect(() =>
2531
handleApiError(error, {
26-
operation: "getOrganization",
27-
resourceId: "my-org",
32+
organizationSlug: "my-org",
33+
projectSlug: "my-project",
34+
query: undefined,
35+
sortBy: null,
36+
limit: 0,
37+
emptyString: "",
2838
}),
2939
).toThrow(
30-
"Organization 'my-org' not found. Please verify the organization slug is correct.",
40+
"Resource not found. Please verify these parameters are correct:\n - organizationSlug: 'my-org'\n - projectSlug: 'my-project'\n - limit: '0'",
3141
);
42+
});
3243

33-
expect(() =>
34-
handleApiError(error, {
35-
operation: "getProject",
36-
resourceId: "my-project",
37-
}),
38-
).toThrow(
39-
"Project 'my-project' not found. Please verify the project slug is correct.",
44+
it("converts 404 errors with no params to generic message", () => {
45+
const error = new ApiError("Not Found", 404);
46+
47+
expect(() => handleApiError(error, {})).toThrow(
48+
"Resource not found (404). Please verify that all provided identifiers are correct and you have access to the requested resources.",
4049
);
4150
});
4251

4352
it("converts 400 errors to UserInputError", () => {
4453
const error = new ApiError("Invalid parameters", 400);
4554

46-
expect(() =>
47-
handleApiError(error, { operation: "getIssue", resourceId: "PROJ-123" }),
48-
).toThrow(UserInputError);
55+
expect(() => handleApiError(error)).toThrow(UserInputError);
4956

50-
expect(() =>
51-
handleApiError(error, { operation: "getIssue", resourceId: "PROJ-123" }),
52-
).toThrow("Invalid request: Invalid parameters");
57+
expect(() => handleApiError(error)).toThrow(
58+
"Invalid request: Invalid parameters",
59+
);
5360
});
5461

5562
it("converts 403 errors to UserInputError with access message", () => {
5663
const error = new ApiError("Forbidden", 403);
5764

58-
expect(() =>
59-
handleApiError(error, { operation: "getIssue", resourceId: "PROJ-123" }),
60-
).toThrow(
65+
expect(() => handleApiError(error)).toThrow(
6166
"Access denied: Forbidden. Please verify you have access to this resource.",
6267
);
6368
});
6469

6570
it("re-throws non-API errors unchanged", () => {
6671
const error = new Error("Network error");
6772

68-
expect(() =>
69-
handleApiError(error, { operation: "getIssue", resourceId: "PROJ-123" }),
70-
).toThrow(error);
73+
expect(() => handleApiError(error)).toThrow(error);
7174
});
7275
});
7376

7477
describe("withApiErrorHandling", () => {
7578
it("returns successful results unchanged", async () => {
7679
const result = await withApiErrorHandling(
7780
async () => ({ id: "123", title: "Test Issue" }),
78-
{ operation: "getIssue", resourceId: "PROJ-123" },
81+
{ issueId: "PROJ-123" },
7982
);
8083

8184
expect(result).toEqual({ id: "123", title: "Test Issue" });
@@ -89,10 +92,13 @@ describe("withApiErrorHandling", () => {
8992
async () => {
9093
throw error;
9194
},
92-
{ operation: "getIssue", resourceId: "PROJ-123" },
95+
{
96+
organizationSlug: "my-org",
97+
issueId: "PROJ-123",
98+
},
9399
),
94100
).rejects.toThrow(
95-
"Issue 'PROJ-123' not found. Please verify the issue ID is correct.",
101+
"Resource not found. Please verify these parameters are correct:\n - organizationSlug: 'my-org'\n - issueId: 'PROJ-123'",
96102
);
97103
});
98104
});

packages/mcp-server/src/tools/utils/api-utils.ts

Lines changed: 23 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -53,58 +53,36 @@ export function apiServiceFromContext(
5353
/**
5454
* Maps API errors to user-friendly errors based on context
5555
* @param error - The error to handle
56-
* @param context - Context about what was being attempted
56+
* @param params - The parameters that were used in the API call
5757
* @returns Never - always throws an error
5858
* @throws {UserInputError} For errors that are clearly user input issues
5959
* @throws {Error} For other errors
6060
*/
6161
export function handleApiError(
6262
error: unknown,
63-
context: {
64-
operation:
65-
| "getIssue"
66-
| "updateIssue"
67-
| "getEvent"
68-
| "getOrganization"
69-
| "getProject"
70-
| "getTeam";
71-
resourceId?: string;
72-
resourceType?: string;
73-
},
63+
params?: Record<string, unknown>,
7464
): never {
7565
if (error instanceof ApiError && error.status === 404) {
76-
const resourceType =
77-
context.resourceType ||
78-
context.operation.replace(/^get|update/, "").toLowerCase();
79-
const resourceId = context.resourceId || "unknown";
66+
// Build a list of all provided parameters
67+
const paramsList: string[] = [];
68+
if (params) {
69+
for (const [key, value] of Object.entries(params)) {
70+
if (value !== undefined && value !== null && value !== "") {
71+
paramsList.push(`${key}: '${value}'`);
72+
}
73+
}
74+
}
8075

81-
switch (context.operation) {
82-
case "getIssue":
83-
case "updateIssue":
84-
throw new UserInputError(
85-
`Issue '${resourceId}' not found. Please verify the issue ID is correct.`,
86-
);
87-
case "getEvent":
88-
throw new UserInputError(
89-
`Event '${resourceId}' not found. Please verify the event ID is correct.`,
90-
);
91-
case "getOrganization":
92-
throw new UserInputError(
93-
`Organization '${resourceId}' not found. Please verify the organization slug is correct.`,
94-
);
95-
case "getProject":
96-
throw new UserInputError(
97-
`Project '${resourceId}' not found. Please verify the project slug is correct.`,
98-
);
99-
case "getTeam":
100-
throw new UserInputError(
101-
`Team '${resourceId}' not found. Please verify the team slug is correct.`,
102-
);
103-
default:
104-
throw new UserInputError(
105-
`${resourceType} '${resourceId}' not found. Please verify the ID is correct.`,
106-
);
76+
if (paramsList.length > 0) {
77+
throw new UserInputError(
78+
`Resource not found. Please verify these parameters are correct:\n${paramsList.map((p) => ` - ${p}`).join("\n")}`,
79+
);
10780
}
81+
82+
// Fallback to generic message if no params provided
83+
throw new UserInputError(
84+
`Resource not found (404). Please verify that all provided identifiers are correct and you have access to the requested resources.`,
85+
);
10886
}
10987

11088
// For other API errors, check if they're likely user input issues
@@ -129,18 +107,18 @@ export function handleApiError(
129107
/**
130108
* Wraps an async API call with automatic error handling
131109
* @param fn - The async function to execute
132-
* @param context - Context about what operation is being performed
110+
* @param params - The parameters that were used in the API call
133111
* @returns The result of the function
134112
* @throws {UserInputError} For user input errors
135113
* @throws {Error} For other errors
136114
*/
137115
export async function withApiErrorHandling<T>(
138116
fn: () => Promise<T>,
139-
context: Parameters<typeof handleApiError>[1],
117+
params?: Record<string, unknown>,
140118
): Promise<T> {
141119
try {
142120
return await fn();
143121
} catch (error) {
144-
handleApiError(error, context);
122+
handleApiError(error, params);
145123
}
146124
}

0 commit comments

Comments
 (0)