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
2 changes: 2 additions & 0 deletions packages/backend/src/adapters/catalog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ import * as sapConcur from './intl/sap-concur.json';
import * as sapS4hanaCloud from './intl/sap-s4hana-cloud.json';
import * as savvycal from './intl/savvycal.json';
import * as sendgrid from './intl/sendgrid.json';
import * as sentry from './intl/sentry.json';
import * as signwell from './intl/signwell.json';
import * as slab from './intl/slab.json';
import * as snov from './intl/snov.json';
Expand Down Expand Up @@ -444,6 +445,7 @@ const RAW_ADAPTERS: AdapterDefinition[] = [
sapS4hanaCloud as unknown as AdapterDefinition,
savvycal as unknown as AdapterDefinition,
sendgrid as unknown as AdapterDefinition,
sentry as unknown as AdapterDefinition,
signwell as unknown as AdapterDefinition,
slab as unknown as AdapterDefinition,
snov as unknown as AdapterDefinition,
Expand Down
174 changes: 174 additions & 0 deletions packages/backend/src/adapters/intl/sentry.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
{
"slug": "sentry",
"name": "Sentry",
"description": "Inspect Sentry projects, issues, and error events.",
"instructions": "Connect to Sentry's hosted REST API to investigate projects, issues, and error events using four read-only tools.\n\nSetup: Create a Sentry personal authentication token under User settings > Personal Tokens with org:read and event:read scopes. Supply it as SENTRY_AUTH_TOKEN when configuring this connector. org:read permits listing organization projects; event:read permits reading issues and their events. Access remains limited by the token owner's permissions. Authentication uses the Authorization: Bearer header. This adapter targets https://sentry.io/api/0.\n\nWorkflow: Supply your organization ID or URL slug, such as acme. Use sentry_list_projects to discover project IDs and slugs, then sentry_list_issues to find issues. Pass the returned issue id, rather than shortId, to sentry_get_issue or sentry_list_issue_events. An issue groups related errors; events are individual occurrences. Set full=true when listing events to include full event bodies and stack traces; Sentry then caps the page size at 10.\n\nFiltering: sentry_list_issues defaults to unresolved issues when query is omitted. Use project to select one project and statsPeriod to narrow the time range, for example 24h or 14d.\n\nPagination: List tools return one page. Sentry supplies pagination cursors in HTTP Link headers, but AnythingMCP's REST engine returns only the response body. These tools cannot discover the next cursor. The cursor input accepts an externally obtained value; do not invent cursors or assume a returned page contains every result.",
"region": "intl",
"category": "monitoring",
"icon": "sentry",
"docsUrl": "https://docs.sentry.io/api/",
"requiredEnvVars": ["SENTRY_AUTH_TOKEN"],
"connector": {
"name": "Sentry API",
"type": "REST",
"baseUrl": "https://sentry.io/api/0",
"authType": "BEARER_TOKEN",
"authConfig": {
"token": "{{SENTRY_AUTH_TOKEN}}"
}
},
"tools": [
{
"name": "sentry_list_projects",
"description": "List one page of projects in a Sentry organization. Returns project IDs, slugs, names, and platforms. Optionally filter by project name or slug.",
"parameters": {
"type": "object",
"properties": {
"organization_id_or_slug": {
"type": "string",
"description": "Organization ID or URL slug, for example 'acme'."
},
"query": {
"type": "string",
"description": "Filter projects by name or slug, for example 'backend'."
},
"per_page": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"description": "Number of projects per page. Sentry defaults to 100; maximum 100."
},
"cursor": {
"type": "string",
"description": "Externally supplied Sentry pagination cursor. Omit for the first page; this connector does not return pagination headers."
}
},
"required": ["organization_id_or_slug"]
},
"endpointMapping": {
"method": "GET",
"path": "/organizations/{organization_id_or_slug}/projects/",
"queryParams": {
"query": "$query",
"per_page": "$per_page",
"cursor": "$cursor"
}
}
},
{
"name": "sentry_list_issues",
"description": "List one page of issues in a Sentry organization. Returns issue IDs, titles, statuses, and occurrence counts. Use an issue ID with sentry_get_issue to investigate further.",
"parameters": {
"type": "object",
"properties": {
"organization_id_or_slug": {
"type": "string",
"description": "Organization ID or URL slug, for example 'acme'."
},
"project": {
"type": "string",
"description": "Filter to one project ID or slug from sentry_list_projects. Omit for all accessible projects."
},
"query": {
"type": "string",
"description": "Sentry issue search, for example 'is:unresolved'. Omit to use Sentry's default unresolved-issue filter."
},
"statsPeriod": {
"type": "string",
"description": "Relative time range, for example '24h' or '14d'."
},
"limit": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"description": "Maximum number of issues to return, up to 100."
},
"cursor": {
"type": "string",
"description": "Externally supplied Sentry pagination cursor. Omit for the first page; this connector does not return pagination headers."
}
},
"required": ["organization_id_or_slug"]
},
"endpointMapping": {
"method": "GET",
"path": "/organizations/{organization_id_or_slug}/issues/",
"queryParams": {
"project": "$project",
"query": "$query",
"statsPeriod": "$statsPeriod",
"limit": "$limit",
"cursor": "$cursor"
}
}
},
{
"name": "sentry_get_issue",
"description": "Retrieve details for a Sentry issue, including status, occurrence statistics, and a summary of the latest event.",
"parameters": {
"type": "object",
"properties": {
"organization_id_or_slug": {
"type": "string",
"description": "Organization ID or URL slug, for example 'acme'."
},
"issue_id": {
"type": "string",
"description": "Issue ID from the 'id' field returned by sentry_list_issues, for example '123456'. Use the ID, not the shortId such as 'BACKEND-42'."
}
},
"required": ["organization_id_or_slug", "issue_id"]
},
"endpointMapping": {
"method": "GET",
"path": "/organizations/{organization_id_or_slug}/issues/{issue_id}/"
}
},
{
"name": "sentry_list_issue_events",
"description": "List one page of error events belonging to a Sentry issue. Set full to true to include full event bodies and stack traces.",
"parameters": {
"type": "object",
"properties": {
"organization_id_or_slug": {
"type": "string",
"description": "Organization ID or URL slug, for example 'acme'."
},
"issue_id": {
"type": "string",
"description": "Issue ID from sentry_list_issues, for example '123456'."
},
"statsPeriod": {
"type": "string",
"description": "Relative time range, for example '24h' or '14d'."
},
"full": {
"type": "boolean",
"description": "Include full event bodies and stack traces. When true, Sentry caps the page size at 10."
},
"per_page": {
"type": "integer",
"minimum": 1,
"maximum": 100,
"description": "Events per page. Default and maximum 100, capped at 10 when full is true."
},
"cursor": {
"type": "string",
"description": "Externally supplied Sentry pagination cursor. Omit for the first page; this connector does not return pagination headers."
}
},
"required": ["organization_id_or_slug", "issue_id"]
},
"endpointMapping": {
"method": "GET",
"path": "/organizations/{organization_id_or_slug}/issues/{issue_id}/events/",
"queryParams": {
"statsPeriod": "$statsPeriod",
"full": "$full",
"per_page": "$per_page",
"cursor": "$cursor"
}
}
}
]
}
155 changes: 155 additions & 0 deletions packages/backend/src/adapters/intl/sentry.live.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/**
* Opt-in live tests using a disposable Sentry project.
*
* Required environment variables:
* SENTRY_AUTH_TOKEN — personal token with org:read and event:read
* SENTRY_TEST_ORG — organization ID or slug
* SENTRY_TEST_PROJECT_ID — project containing the test issue
* SENTRY_TEST_ISSUE_ID — unresolved issue in that project
* SENTRY_TEST_EVENT_ID — exception event with a stack trace in that issue
*
* Use a small test project: list assertions inspect only the first page.
* The event must be among the first ten events returned for the issue.
*
* From the repository root:
* RUN_SENTRY_LIVE=1 npm test --workspace=packages/backend -- --runInBand --watch=false adapters/intl/sentry.live.spec.ts
*
* These tests only read existing data; they do not create test fixtures.
*/

import { AxiosError } from "axios";
import * as adapter from "./sentry.json";
import { RestEngine } from "../../connectors/engines/rest.engine";
import { OAuth2TokenService } from "../../connectors/engines/oauth2-token.service";
import { LoginTokenService } from "../../connectors/engines/login-token.service";

// Normal test runs skip requests to Sentry.
const describeLive =
process.env.RUN_SENTRY_LIVE === "1" ? describe : describe.skip;

describeLive("Sentry adapter — live API", () => {
const engine = new RestEngine(
{} as OAuth2TokenService,
{} as LoginTokenService,
);

beforeAll(() => {
for (const name of [
"SENTRY_AUTH_TOKEN",
"SENTRY_TEST_ORG",
"SENTRY_TEST_PROJECT_ID",
"SENTRY_TEST_ISSUE_ID",
"SENTRY_TEST_EVENT_ID",
]) {
if (!process.env[name]?.trim()) {
throw new Error(`Set ${name} before running Sentry live tests`);
}
}
});

async function callTool(
name: string,
params: Record<string, unknown>,
): Promise<unknown> {
const tool = adapter.tools.find((item) => item.name === name);
if (!tool) throw new Error(`Missing adapter tool: ${name}`);

try {
return await engine.execute(
{
...adapter.connector,
authConfig: { token: process.env.SENTRY_AUTH_TOKEN },
},
tool.endpointMapping,
params,
);
} catch (error) {
// Axios errors contain request headers, including the real token.
const status =
error instanceof AxiosError ? error.response?.status : undefined;
throw new Error(
`${name} failed: ${status ? `HTTP ${status}` : "request error"}`,
);
}
}

it("finds the configured test project", async () => {
const result = await callTool("sentry_list_projects", {
organization_id_or_slug: process.env.SENTRY_TEST_ORG,
per_page: 100,
});

expect(Array.isArray(result)).toBe(true);
expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: process.env.SENTRY_TEST_PROJECT_ID,
}),
]),
);
}, 60000);

it("finds the configured issue within its project", async () => {
const result = await callTool("sentry_list_issues", {
organization_id_or_slug: process.env.SENTRY_TEST_ORG,
project: process.env.SENTRY_TEST_PROJECT_ID,
query: "is:unresolved",
limit: 100,
});

expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: process.env.SENTRY_TEST_ISSUE_ID,
}),
]),
);
}, 60000);

it("retrieves the configured issue and its project", async () => {
const result = await callTool("sentry_get_issue", {
organization_id_or_slug: process.env.SENTRY_TEST_ORG,
issue_id: process.env.SENTRY_TEST_ISSUE_ID,
});

expect(result).toEqual(
expect.objectContaining({
id: process.env.SENTRY_TEST_ISSUE_ID,
project: expect.objectContaining({
id: process.env.SENTRY_TEST_PROJECT_ID,
}),
}),
);
}, 60000);

it("retrieves the sample event with exception details", async () => {
const result = await callTool("sentry_list_issue_events", {
organization_id_or_slug: process.env.SENTRY_TEST_ORG,
issue_id: process.env.SENTRY_TEST_ISSUE_ID,
full: true,
per_page: 10,
});

expect(result).toEqual(
expect.arrayContaining([
expect.objectContaining({
eventID: process.env.SENTRY_TEST_EVENT_ID,
entries: expect.arrayContaining([
expect.objectContaining({
type: "exception",
data: expect.objectContaining({
values: expect.arrayContaining([
expect.objectContaining({
stacktrace: expect.objectContaining({
frames: expect.any(Array),
}),
}),
]),
}),
}),
]),
}),
]),
);
}, 60000);
});
Loading