Skip to content
Draft
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
74 changes: 51 additions & 23 deletions packages/mcp-server/src/internal/formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -683,44 +683,72 @@ export function formatIssueOutput({
event,
apiService,
autofixState,
responseType = "md",
}: {
organizationSlug: string;
issue: Issue;
event: Event;
apiService: SentryApiService;
autofixState?: AutofixRunState;
responseType?: "md" | "json";
}) {
let output = `# Issue ${issue.shortId} in **${organizationSlug}**\n\n`;
output += `**Description**: ${issue.title}\n`;
output += `**Culprit**: ${issue.culprit}\n`;
output += `**First Seen**: ${new Date(issue.firstSeen).toISOString()}\n`;
output += `**Last Seen**: ${new Date(issue.lastSeen).toISOString()}\n`;
output += `**Occurrences**: ${issue.count}\n`;
output += `**Users Impacted**: ${issue.userCount}\n`;
output += `**Status**: ${issue.status}\n`;
output += `**Platform**: ${issue.platform}\n`;
output += `**Project**: ${issue.project.name}\n`;
output += `**URL**: ${apiService.getIssueUrl(organizationSlug, issue.shortId)}\n`;
output += "\n";
output += "## Event Details\n\n";
output += `**Event ID**: ${event.id}\n`;
let mdOutput = `# Issue ${issue.shortId} in **${organizationSlug}**\n\n`;
const issueUrl = apiService.getIssueUrl(organizationSlug, issue.shortId);
const firstSeen = new Date(issue.firstSeen).toISOString();
const lastSeen = new Date(issue.lastSeen).toISOString();
const jsonOutput: Record<string, unknown> = {
issue: {
...issue,
firstSeen,
lastSeen,
url: issueUrl,
},
event: {
id: event.id,
},
};
mdOutput += `**Description**: ${issue.title}\n`;
mdOutput += `**Culprit**: ${issue.culprit}\n`;
mdOutput += `**First Seen**: ${firstSeen}\n`;
mdOutput += `**Last Seen**: ${lastSeen}\n`;
mdOutput += `**Occurrences**: ${issue.count}\n`;
mdOutput += `**Users Impacted**: ${issue.userCount}\n`;
mdOutput += `**Status**: ${issue.status}\n`;
mdOutput += `**Platform**: ${issue.platform}\n`;
mdOutput += `**Project**: ${issue.project.name}\n`;
mdOutput += `**URL**: ${issueUrl}\n`;
mdOutput += "\n";
mdOutput += "## Event Details\n\n";
mdOutput += `**Event ID**: ${event.id}\n`;
if (event.type === "error") {
output += `**Occurred At**: ${new Date((event as z.infer<typeof ErrorEventSchema>).dateCreated).toISOString()}\n`;
const occurredAt = new Date(
(event as z.infer<typeof ErrorEventSchema>).dateCreated,
).toISOString();
mdOutput += `**Occurred At**: ${occurredAt}\n`;
(jsonOutput.event as Record<string, unknown>).occurredAt = occurredAt;
}
if (event.message) {
output += `**Message**:\n${event.message}\n`;
mdOutput += `**Message**:\n${event.message}\n`;
(jsonOutput.event as Record<string, unknown>).message = event.message;
}
output += "\n";
output += formatEventOutput(event);
mdOutput += "\n";
mdOutput += formatEventOutput(event);

// Add Seer context if available
if (autofixState) {
output += formatSeerContext(autofixState);
const formattedSeerContext = formatSeerContext(autofixState);
mdOutput += formattedSeerContext;
jsonOutput.autofixState = formattedSeerContext;
}

output += "# Using this information\n\n";
output += `- You can reference the IssueID in commit messages (e.g. \`Fixes ${issue.shortId}\`) to automatically close the issue when the commit is merged.\n`;
output +=
mdOutput += "# Using this information\n\n";
mdOutput += `- You can reference the IssueID in commit messages (e.g. \`Fixes ${issue.shortId}\`) to automatically close the issue when the commit is merged.\n`;
mdOutput +=
"- The stacktrace includes both first-party application code as well as third-party code, its important to triage to first-party code.\n";
return output;

if (responseType === "json") {
return jsonOutput;
}

return mdOutput;
}
5 changes: 5 additions & 0 deletions packages/mcp-server/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,8 @@ export const ParamAttachmentId = z
.string()
.trim()
.describe("The ID of the attachment to download.");

export const ResponseType = z
.enum(["md", "json"])
.default("md")
.describe("The format of the tool's response. Defaults to markdown.");
11 changes: 11 additions & 0 deletions packages/mcp-server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,17 @@ export async function configureServer({
content: output,
};
}
// if the tool returns a plain object, convert to JSON text
if (typeof output === "object" && output !== null) {
return {
content: [
{
type: "text" as const,
text: JSON.stringify(output),
},
],
};
}
throw new Error(`Invalid tool output: ${output}`);
} catch (error) {
span.setStatus({
Expand Down
22 changes: 22 additions & 0 deletions packages/mcp-server/src/tools/create-team.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,26 @@ describe("create_team", () => {
"
`);
});
it("returns json", async () => {
const result = await createTeam.handler(
{
organizationSlug: "sentry-mcp-evals",
name: "the-goats",
responseType: "json",
},
{
accessToken: "access-token",
userId: "1",
organizationSlug: null,
},
);
expect(result).toMatchObject({
organizationSlug: "sentry-mcp-evals",
team: {
id: "4509109078196224",
slug: "the-goats",
name: "the-goats",
},
});
});
});
25 changes: 17 additions & 8 deletions packages/mcp-server/src/tools/create-team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { setTag } from "@sentry/core";
import { defineTool } from "./utils/defineTool";
import { apiServiceFromContext } from "./utils/api-utils";
import type { ServerContext } from "../types";
import { ParamOrganizationSlug, ParamRegionUrl } from "../schema";
import { ParamOrganizationSlug, ParamRegionUrl, ResponseType } from "../schema";

export default defineTool({
name: "create_team",
Expand Down Expand Up @@ -32,6 +32,7 @@ export default defineTool({
organizationSlug: ParamOrganizationSlug,
regionUrl: ParamRegionUrl.optional(),
name: z.string().trim().describe("The name of the team to create."),
responseType: ResponseType.optional(),
},
async handler(params, context: ServerContext) {
const apiService = apiServiceFromContext(context, {
Expand All @@ -45,12 +46,20 @@ export default defineTool({
organizationSlug,
name: params.name,
});
let output = `# New Team in **${organizationSlug}**\n\n`;
output += `**ID**: ${team.id}\n`;
output += `**Slug**: ${team.slug}\n`;
output += `**Name**: ${team.name}\n`;
output += "# Using this information\n\n";
output += `- You should always inform the user of the Team Slug value.\n`;
return output;
let mdOutput = `# New Team in **${organizationSlug}**\n\n`;
mdOutput += `**ID**: ${team.id}\n`;
mdOutput += `**Slug**: ${team.slug}\n`;
mdOutput += `**Name**: ${team.name}\n`;
mdOutput += "# Using this information\n\n";
mdOutput += `- You should always inform the user of the Team Slug value.\n`;

if (params.responseType === "json") {
return {
organizationSlug,
team,
};
}

return mdOutput;
},
});
33 changes: 33 additions & 0 deletions packages/mcp-server/src/tools/get-issue-details.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,4 +294,37 @@ describe("get_issue_details", () => {
expect(result).toContain("## Seer AI Analysis");
expect(result).toContain("Consolidate bottle and price data fetching");
});

it("returns json", async () => {
const result = await getIssueDetails.handler(
{
responseType: "json",
organizationSlug: "sentry-mcp-evals",
issueId: "CLOUDFLARE-MCP-41",
eventId: undefined,
issueUrl: undefined,
regionUrl: undefined,
},
{
accessToken: "access-token",
userId: "1",
organizationSlug: null,
},
);
expect(result).toMatchObject({
autofixState: expect.any(String),
event: {
id: "7ca573c0f4814912aaa9bdc77d1a7d51",
occurredAt: "2025-04-08T21:15:04.000Z",
},
issue: {
count: "25",
culprit: "Object.fetch(index)",
firstSeen: "2025-04-03T22:51:19.403Z",
id: "6507376925",
lastSeen: "2025-04-12T11:34:11.000Z",
url: expect.any(String),
},
});
});
});
4 changes: 4 additions & 0 deletions packages/mcp-server/src/tools/get-issue-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
ParamRegionUrl,
ParamIssueShortId,
ParamIssueUrl,
ResponseType,
} from "../schema";

export default defineTool({
Expand Down Expand Up @@ -55,6 +56,7 @@ export default defineTool({
issueId: ParamIssueShortId.optional(),
eventId: z.string().trim().describe("The ID of the event.").optional(),
issueUrl: ParamIssueUrl.optional(),
responseType: ResponseType.optional(),
},
async handler(params, context: ServerContext) {
const apiService = apiServiceFromContext(context, {
Expand Down Expand Up @@ -103,6 +105,7 @@ export default defineTool({
event,
apiService,
autofixState,
responseType: params.responseType,
});
}

Expand Down Expand Up @@ -165,6 +168,7 @@ export default defineTool({
event,
apiService,
autofixState,
responseType: params.responseType,
});
},
});
6 changes: 5 additions & 1 deletion packages/mcp-server/src/tools/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ export interface ToolConfig<
handler: (
params: z.infer<z.ZodObject<TSchema>>,
context: ServerContext,
) => Promise<string | (TextContent | ImageContent | EmbeddedResource)[]>;
) => Promise<
| string
| Record<string, unknown>
| (TextContent | ImageContent | EmbeddedResource)[]
>;
}

/**
Expand Down
27 changes: 27 additions & 0 deletions packages/mcp-server/src/tools/update-project.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,31 @@ describe("update_project", () => {
"
`);
});
it("returns json", async () => {
const result = await updateProject.handler(
{
organizationSlug: "sentry-mcp-evals",
projectSlug: "cloudflare-mcp",
name: undefined,
slug: undefined,
platform: undefined,
teamSlug: "backend-team",
regionUrl: undefined,
responseType: "json",
},
{
accessToken: "access-token",
userId: "1",
organizationSlug: null,
},
);
expect(result).toMatchObject({
organizationSlug: "sentry-mcp-evals",
project: {
id: "4509106749636608",
slug: "cloudflare-mcp",
name: "cloudflare-mcp",
},
});
});
});
34 changes: 22 additions & 12 deletions packages/mcp-server/src/tools/update-project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
ParamProjectSlug,
ParamPlatform,
ParamTeamSlug,
ResponseType,
} from "../schema";

export default defineTool({
Expand Down Expand Up @@ -69,6 +70,7 @@ export default defineTool({
teamSlug: ParamTeamSlug.optional().describe(
"The team to assign this project to. Note: this will replace the current team assignment.",
),
responseType: ResponseType.optional(),
},
async handler(params, context: ServerContext) {
const apiService = apiServiceFromContext(context, {
Expand Down Expand Up @@ -124,12 +126,12 @@ export default defineTool({
}
}

let output = `# Updated Project in **${organizationSlug}**\n\n`;
output += `**ID**: ${project.id}\n`;
output += `**Slug**: ${project.slug}\n`;
output += `**Name**: ${project.name}\n`;
let mdOutput = `# Updated Project in **${organizationSlug}**\n\n`;
mdOutput += `**ID**: ${project.id}\n`;
mdOutput += `**Slug**: ${project.slug}\n`;
mdOutput += `**Name**: ${project.name}\n`;
if (project.platform) {
output += `**Platform**: ${project.platform}\n`;
mdOutput += `**Platform**: ${project.platform}\n`;
}

// Display what was updated
Expand All @@ -141,16 +143,24 @@ export default defineTool({
updates.push(`team assignment to "${params.teamSlug}"`);

if (updates.length > 0) {
output += `\n## Updates Applied\n`;
output += updates.map((update) => `- Updated ${update}`).join("\n");
output += `\n`;
mdOutput += `\n## Updates Applied\n`;
mdOutput += updates.map((update) => `- Updated ${update}`).join("\n");
mdOutput += `\n`;
}

output += "\n# Using this information\n\n";
output += `- The project is now accessible at slug: \`${project.slug}\`\n`;
mdOutput += "\n# Using this information\n\n";
mdOutput += `- The project is now accessible at slug: \`${project.slug}\`\n`;
if (params.teamSlug) {
output += `- The project is now assigned to the \`${params.teamSlug}\` team\n`;
mdOutput += `- The project is now assigned to the \`${params.teamSlug}\` team\n`;
}
return output;

if (params.responseType === "json") {
return {
organizationSlug,
project,
};
}

return mdOutput;
},
});
19 changes: 19 additions & 0 deletions packages/mcp-server/src/tools/whoami.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,23 @@ describe("whoami", () => {
`,
);
});
it("returns json", async () => {
const result = await whoami.handler(
{ responseType: "json" },
{
accessToken: "access-token",
userId: "1",
organizationSlug: null,
},
);
expect(result).toMatchInlineSnapshot(`
{
"user": {
"email": "[email protected]",
"id": "1",
"name": "John Doe",
},
}
`);
});
});
Loading