Skip to content
Open
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,14 @@ If a config file exists but is invalid, the server exits with an error instead o

`list-items(status?, level?, environment?, page?, limit?, query?, project?)`: List items filtered by status, environment, and search query. Optional `project` when multiple projects are configured.

`list-occurrences(counter, limit?, page?, last_id?, project?)`: List occurrences (instances) for a Rollbar item by its counter. `limit` defaults to 3, max 100. Use `last_id` (the `id` of the last occurrence from a previous page) for reliable cursor-based pagination; it takes precedence over `page` when both are provided. Optional `project` when multiple projects are configured. (Deprecated alias: `lastId` is accepted but `last_id` is preferred and sent to the API.) Example prompt: `Show me the last 3 occurrences of item #24265`

`get-replay(environment, sessionId, replayId, delivery?, project?)`: Retrieve session replay metadata and payload for a specific session. By default the tool writes the replay JSON to a temporary file (under your system temp directory) and returns the path. Set `delivery="resource"` to receive a `rollbar://replay/<environment>/<sessionId>/<replayId>` link for MCP-aware clients. Optional `project` when multiple projects are configured. `delivery="resource"` is only supported in single-project mode; when multiple projects are configured, use `delivery="file"` with a `project` parameter instead. Example prompt: `Fetch the replay 789 from session abc in staging`.

`update-item(itemId, status?, level?, title?, assignedUserId?, resolvedInVersion?, snoozed?, teamId?, project?)`: Update an item's properties including status, level, title, assignment, and more. Optional `project` when multiple projects are configured. Example prompt: `Mark Rollbar item #123456 as resolved` or `Assign item #123456 to user ID 789`. (Requires `write` scope)

Note: When using `get-replay` with `delivery=\"file\"`, you can enable automatic cleanup of the temporary JSON file with `cleanup=true` (deletes after ~10 minutes) or provide a custom TTL using `cleanup_ttl_seconds` (range 5–86400 seconds).

## How to Use

Tested with node 20 and 22 (`nvm use 22`).
Expand Down
19 changes: 9 additions & 10 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import "./load-env.js";
import { readFileSync, existsSync } from "node:fs";
import path from "node:path";
import { homedir } from "node:os";
import packageJson from "../package.json" with { type: "json" };
import packageJson from "../package.json" assert { type: "json" };
import { z } from "zod";

const DEFAULT_ROLLBAR_API_BASE = "https://api.rollbar.com/api/1";
Expand Down Expand Up @@ -123,10 +123,9 @@ function loadProjectsFromFile(filePath: string): ProjectConfig[] | null {
);
}

function exitWithError(message: string): never {
function throwConfigError(message: string): never {
console.error(message);
process.exit(1);
return undefined as never;
throw new Error(message);
}

function loadConfig(): ProjectConfig[] {
Expand All @@ -142,11 +141,11 @@ function loadConfig(): ProjectConfig[] {
return projects;
}
} catch (error) {
return exitWithError(
return throwConfigError(
error instanceof Error ? error.message : "Invalid Rollbar config file",
);
}
return exitWithError(
return throwConfigError(
`Error: ROLLBAR_CONFIG_FILE="${configFileEnv}" was not found.`,
);
}
Expand All @@ -157,7 +156,7 @@ function loadConfig(): ProjectConfig[] {
const fromCwd = loadProjectsFromFile(cwdPath);
if (fromCwd) return fromCwd;
} catch (error) {
return exitWithError(
return throwConfigError(
error instanceof Error ? error.message : "Invalid Rollbar config file",
);
}
Expand All @@ -168,7 +167,7 @@ function loadConfig(): ProjectConfig[] {
const fromHome = loadProjectsFromFile(homePath);
if (fromHome) return fromHome;
} catch (error) {
return exitWithError(
return throwConfigError(
error instanceof Error ? error.message : "Invalid Rollbar config file",
);
}
Expand All @@ -178,7 +177,7 @@ function loadConfig(): ProjectConfig[] {
if (token && token.length > 0) {
const apiBase = resolveApiBaseFromEnv();
if (apiBase === null) {
return exitWithError(
return throwConfigError(
"Error: ROLLBAR_API_BASE must be a valid HTTP(S) URL when using ROLLBAR_ACCESS_TOKEN.",
);
}
Expand All @@ -191,7 +190,7 @@ function loadConfig(): ProjectConfig[] {
];
}

return exitWithError(
return throwConfigError(
"Error: No Rollbar configuration found. Set ROLLBAR_ACCESS_TOKEN, or create .rollbar-mcp.json (in cwd or home), or set ROLLBAR_CONFIG_FILE.",
);
}
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { registerAllTools } from "./tools/index.js";
import { registerAllResources } from "./resources/index.js";
import packageJson from "../package.json" assert { type: "json" };

// Create server instance
const server = new McpServer(
{
name: "rollbar",
version: "0.0.1",
version: packageJson.version,
},
{
capabilities: {
Expand Down
6 changes: 5 additions & 1 deletion src/tools/get-item-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@ export function registerGetItemDetailsTool(server: McpServer) {
"get-item-details",
"Get item details for a Rollbar item",
{
counter: z.number().int().describe("Rollbar item counter"),
counter: z
.number()
.int()
.min(1)
.describe("Rollbar item counter (must be >= 1)"),
max_tokens: z
.number()
.int()
Expand Down
46 changes: 42 additions & 4 deletions src/tools/get-replay.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { mkdir, writeFile } from "node:fs/promises";
import { mkdir, writeFile, unlink } from "node:fs/promises";
import path from "node:path";
import { tmpdir } from "node:os";
import { z } from "zod";
Expand Down Expand Up @@ -49,7 +49,7 @@ async function writeReplayToFile(
.concat(".json");

const filePath = path.join(REPLAY_FILE_DIRECTORY, fileName);
await writeFile(filePath, JSON.stringify(replayData, null, 2), "utf8");
await writeFile(filePath, JSON.stringify(replayData, null, 2), { encoding: "utf8", mode: 0o600 });
return filePath;
}

Expand All @@ -70,9 +70,32 @@ export function registerGetReplayTool(server: McpServer) {
delivery: DELIVERY_MODE.optional().describe(
"How to return the replay payload. Defaults to 'file' (writes JSON to a temp file); 'resource' returns a rollbar:// link.",
),
cleanup: z
.boolean()
.optional()
.describe(
"When delivery='file', if true, schedules automatic deletion of the file after a default TTL (10 minutes).",
),
cleanup_ttl_seconds: z
.number()
.int()
.min(5)
.max(86400)
.optional()
.describe(
"When delivery='file', optional TTL in seconds to delete the file automatically. Overrides 'cleanup'.",
),
project: buildProjectParam(),
},
async ({ environment, sessionId, replayId, delivery, project }) => {
async ({
environment,
sessionId,
replayId,
delivery,
cleanup,
cleanup_ttl_seconds,
project,
}) => {
const deliveryMode = delivery ?? "file";
const { token, apiBase } = resolveProject(project);

Expand Down Expand Up @@ -105,12 +128,27 @@ export function registerGetReplayTool(server: McpServer) {
sessionId,
replayId,
);
const ttl =
typeof cleanup_ttl_seconds === "number"
? cleanup_ttl_seconds
: cleanup
? 600
: undefined;
if (ttl) {
// Best-effort cleanup; ignore errors
setTimeout(() => {
void unlink(filePath).catch(() => {});
}, ttl * 1000);
}

return {
content: [
{
type: "text",
text: `Replay ${replayId} for session ${sessionId} in ${environment} saved to ${filePath}. This file is not automatically deleted—remove it when finished or rerun with delivery="resource" for a rollbar:// link.`,
text:
ttl !== undefined
? `Replay ${replayId} for session ${sessionId} in ${environment} saved to ${filePath}. This file is scheduled for automatic deletion in ${ttl} seconds.`
: `Replay ${replayId} for session ${sessionId} in ${environment} saved to ${filePath}. This file is not automatically deleted—remove it when finished or rerun with delivery="resource" for a rollbar:// link.`,
},
],
};
Expand Down
2 changes: 2 additions & 0 deletions src/tools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { registerGetDeploymentsTool } from "./get-deployments.js";
import { registerGetVersionTool } from "./get-version.js";
import { registerGetTopItemsTool } from "./get-top-items.js";
import { registerListItemsTool } from "./list-items.js";
import { registerListOccurrencesTool } from "./list-occurrences.js";
import { registerUpdateItemTool } from "./update-item.js";
import { registerGetReplayTool } from "./get-replay.js";
import { registerListProjectsTool } from "./list-projects.js";
Expand All @@ -14,6 +15,7 @@ export function registerAllTools(server: McpServer) {
registerGetVersionTool(server);
registerGetTopItemsTool(server);
registerListItemsTool(server);
registerListOccurrencesTool(server);
registerUpdateItemTool(server);
registerGetReplayTool(server);
registerListProjectsTool(server);
Expand Down
145 changes: 145 additions & 0 deletions src/tools/list-occurrences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { z } from "zod";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { resolveProject } from "../config.js";
import { makeRollbarRequest } from "../utils/api.js";
import { buildProjectParam } from "../utils/project-params.js";
import {
RollbarApiResponse,
RollbarItemResponse,
RollbarListOccurrencesResponse,
} from "../types/index.js";

export function registerListOccurrencesTool(server: McpServer) {
server.tool(
"list-occurrences",
"List all occurrences for a Rollbar item",
{
counter: z
.number()
.int()
.min(1)
.describe("Rollbar item counter (must be >= 1)"),
limit: z
.number()
.int()
.min(1)
.max(100)
.default(3)
.describe(
"Number of occurrences to return (default: 3, max: 100)",
),
page: z
.number()
.int()
.min(1)
.default(1)
.describe("Page number for pagination (default: 1)"),
// Preferred API param name
last_id: z
.number()
.int()
.min(1)
.optional()
.describe(
"ID of last occurrence from previous page. Use for reliable pagination. Overrides page if both provided.",
),
// Backward-compatible alias (deprecated) — will be ignored if last_id is provided
lastId: z
.number()
.int()
.min(1)
.optional()
.describe(
"Deprecated: use last_id instead. If both last_id and lastId are provided, last_id takes precedence.",
),
project: buildProjectParam(),
},
async ({ counter, limit, page, last_id, lastId, project }) => {
const { token, apiBase } = resolveProject(project);

const counterUrl = `${apiBase}/item_by_counter/${counter}`;
const itemResponse = await makeRollbarRequest<
RollbarApiResponse<RollbarItemResponse>
>(counterUrl, "list-occurrences", token);

if (
!itemResponse ||
typeof itemResponse !== "object" ||
typeof (itemResponse as RollbarApiResponse<RollbarItemResponse>).err !==
"number"
) {
throw new Error(
`Invalid API response while fetching item: ${counterUrl}`,
);
}

if (itemResponse.err !== 0) {
const errorMessage =
itemResponse.message || `Unknown error (code: ${itemResponse.err})`;
throw new Error(`Rollbar API returned error: ${errorMessage}`);
}

const item = itemResponse.result;
if (!item || typeof item !== "object" || typeof item.id !== "number") {
throw new Error(`Invalid API response from ${counterUrl}: missing item`);
}

const params = new URLSearchParams();
params.set("limit", String(limit));
const effectiveLastId = last_id ?? lastId;
if (effectiveLastId !== undefined) {
params.set("last_id", String(effectiveLastId));
} else {
params.set("page", String(page));
}
const occurrencesUrl = `${apiBase}/item/${item.id}/instances?${params.toString()}`;
const occurrencesResponse = await makeRollbarRequest<
RollbarApiResponse<RollbarListOccurrencesResponse>
>(occurrencesUrl, "list-occurrences", token);

if (
!occurrencesResponse ||
typeof occurrencesResponse !== "object" ||
typeof (
occurrencesResponse as RollbarApiResponse<RollbarListOccurrencesResponse>
).err !== "number"
) {
throw new Error(
`Invalid API response while listing occurrences: ${occurrencesUrl}`,
);
}

if (occurrencesResponse.err !== 0) {
const errorMessage =
occurrencesResponse.message ||
`Unknown error (code: ${occurrencesResponse.err})`;
throw new Error(`Rollbar API returned error: ${errorMessage}`);
}

const occurrences = occurrencesResponse.result;
if (
!occurrences ||
typeof occurrences !== "object" ||
!Array.isArray(occurrences.instances)
) {
throw new Error(
`Invalid API response from ${occurrencesUrl}: missing instances`,
);
}

const responseData = {
page: occurrences.page,
instances: occurrences.instances,
};

return {
content: [
{
type: "text",
text: JSON.stringify(responseData),
},
],
};
},
);
}
8 changes: 7 additions & 1 deletion src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export interface RollbarOccurrenceResponse {
id: number;
item_id: number;
timestamp: number;
version: number;
version?: number | string;
data: {
body: any;
level: string;
Expand Down Expand Up @@ -157,3 +157,9 @@ export interface RollbarListItemsResponse {
items: RollbarListItemResponse[];
[key: string]: any; // Allow for any other properties
}

export interface RollbarListOccurrencesResponse {
page: number;
instances: RollbarOccurrenceResponse[];
[key: string]: any; // Allow for any other properties
}
Loading