Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ After saving the server, restart the app before testing `who_am_i` or `list_apps
- `count_devices`
- `list_issues`
- `get_issue`
- `update_issue_status`
- `get_issue_stats`
- `get_issue_device_stats`
- `get_issue_devices`
Expand Down
10 changes: 10 additions & 0 deletions src/__tests__/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,16 @@ describe("BugfenderClient", () => {
expect(init.method).toBe("POST");
expect(init.body).toBe(JSON.stringify({ key: "value" }));
});

it("makes PUT requests with JSON body", async () => {
fetchSpy.mockResolvedValueOnce(jsonResponse({ ok: true }));
const client = makeClient();
await client.put("/app/app-1/issues-aggregation/hash-1", { status: 3 });
const [url, init] = fetchSpy.mock.calls[0];
expect(url).toBe("https://api.test/app/app-1/issues-aggregation/hash-1");
expect(init.method).toBe("PUT");
expect(init.body).toBe(JSON.stringify({ status: 3 }));
});
});

describe("buildHeaders", () => {
Expand Down
11 changes: 11 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,17 @@ export class BugfenderClient {
);
}

async put<T>(path: string, body?: unknown, requireAuth = true): Promise<T> {
return this.request<T>(
path,
{
method: "PUT",
body: body ? JSON.stringify(body) : undefined,
},
requireAuth,
);
}

private async request<T>(path: string, init: RequestInit, requireAuth: boolean): Promise<T> {
if (requireAuth && !this.apiToken && !this.refreshToken) {
throw new BugfenderApiError("Missing Bugfender token", 401, null);
Expand Down
43 changes: 43 additions & 0 deletions src/tools/issues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,30 @@ const issueTypeByName: Record<string, string> = {
"user feedback": "2",
};

const issueStatusByName: Record<string, number> = {
new: 0,
open: 1,
in_progress: 2,
resolved: 3,
closed: 4,
muted: 5,
invalid: 6,
};

function normalizeIssueStatus(value: string): number {
const trimmed = value.trim().toLowerCase();
if (/^\d+$/.test(trimmed)) {
return Number(trimmed);
}

const status = issueStatusByName[trimmed];
if (status === undefined) {
throw new Error(`Invalid issue status: ${value}. Use one of: ${Object.keys(issueStatusByName).join(", ")}`);
}

return status;
}

function normalizeIssueType(value?: string): string | undefined {
if (!value) {
return undefined;
Expand Down Expand Up @@ -111,6 +135,25 @@ export function registerIssueTools(server: McpServer, context: ServerContext): v
),
);

server.tool(
"update_issue_status",
"Updates the status of an issue group (issues aggregation). Use this to mark issues as resolved, closed, in progress, etc.",
{
app_id: z.string().describe("The public app ID (e.g. 5X3c4veRGV) from list_apps"),
issue_id: z.string().describe("The issue group hash from list_issues or get_issue"),
status: z
.string()
.describe("New status: new, open, in_progress, resolved, closed, muted, or invalid"),
},
({ app_id, issue_id, status }) =>
handleTool(context, async () => {
await context.client.put(`/app/${app_id}/issues-aggregation/${issue_id}`, {
status: normalizeIssueStatus(status),
});
return ok({ app_id, issue_id, status: status.trim().toLowerCase() });
}),
);

server.tool(
"get_feedback",
{
Expand Down
Loading