Skip to content

Initial work on stdio #31

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Apr 6, 2025
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
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@ It is based on [Cloudflare's work towards remote MCPs](https://blog.cloudflare.c

## Getting Started

### Stdio vs Remote

While this repository is primarily servicing a remote MCP use-case, we also support a stdio transport.

Note: This is currently a draft and not available via a distribution.

You will need to ensure your token is provisioned with the necessary scopes. As of writing this is:

```
org:read project:read project:write team:read team:write event:read
```

You can find the canonical reference to the needed scopes in the [source code](https://github.com/getsentry/sentry-mcp/blob/main/src/routes/auth.ts).

Launching the stdio transport will just require you to bind `SENTRY_AUTH_TOKEN` and run the provided script:

```shell
SENTRY_AUTH_TOKEN= npm run start:stdio
```

### Self-Hosted Sentry

You can override the `SENTRY_URL` env variable to set your base Sentry url:
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
"version": "0.0.1",
"private": true,
"scripts": {
"build": "tsc",
"deploy": "wrangler deploy",
"dev": "wrangler dev",
"format": "biome format --write",
"lint": "biome lint",
"lint:fix": "biome lint --fix",
"inspector": "pnpx @modelcontextprotocol/inspector@latest",
"start": "wrangler dev",
"start:stdio": "tsx dist/mcp/start-stdio.js",
"cf-typegen": "wrangler types",
"postinstall": "simple-git-hooks",
"test": "vitest",
Expand Down Expand Up @@ -43,7 +45,7 @@
"pre-commit": "pnpm exec lint-staged --concurrent false"
},
"lint-staged": {
"*": ["biome format --write"],
"**/*.{ts,tsx,json}": ["biome format --write"],
"**/*.{ts,tsx}": ["biome lint --fix"]
}
}
6 changes: 3 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { withSentry } from "@sentry/cloudflare";
import OAuthProvider from "@cloudflare/workers-oauth-provider";
import app from "./app";
import SentryMCP from "./mcp/server";
import SentryMCPWorker from "./mcp/transports/cloudflare-worker";

Check warning on line 4 in src/index.ts

View check run for this annotation

Codecov / codecov/patch

src/index.ts#L4

Added line #L4 was not covered by tests
import { SCOPES } from "./routes/auth";

// required for Durable Objects
export { default as SentryMCP } from "./mcp/server";
export { SentryMCPWorker };

const oAuthProvider = new OAuthProvider({
apiRoute: "/sse",
// @ts-ignore
apiHandler: SentryMCP.mount("/sse"),
apiHandler: SentryMCPWorker.mount("/sse"),

Check warning on line 13 in src/index.ts

View check run for this annotation

Codecov / codecov/patch

src/index.ts#L13

Added line #L13 was not covered by tests
// @ts-ignore
defaultHandler: app,
authorizeEndpoint: "/authorize",
Expand Down
88 changes: 41 additions & 47 deletions src/mcp/server.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,49 @@
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { Props } from "../types";
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import type { ServerContext } from "../types";
import { logError } from "../lib/logging";
import { TOOL_DEFINITIONS, TOOL_HANDLERS } from "./tools";

// Context from the auth process, encrypted & stored in the auth token
// and provided to the DurableMCP as this.props
export default class SentryMCP extends McpAgent<Env, unknown, Props> {
server = new McpServer({
name: "Sentry MCP",
version: "0.1.0",
});
export function configureServer(server: McpServer, context: ServerContext) {
server.server.onerror = (error) => {
logError(error);
};

Check warning on line 9 in src/mcp/server.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/server.ts#L6-L9

Added lines #L6 - L9 were not covered by tests

async init() {
for (const tool of TOOL_DEFINITIONS) {
const handler = TOOL_HANDLERS[tool.name];
for (const tool of TOOL_DEFINITIONS) {
const handler = TOOL_HANDLERS[tool.name];

Check warning on line 12 in src/mcp/server.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/server.ts#L11-L12

Added lines #L11 - L12 were not covered by tests

this.server.tool(
tool.name as string,
tool.description,
tool.paramsSchema ? tool.paramsSchema : {},
async (...args) => {
try {
// TODO(dcramer): I'm too dumb to figure this out
// @ts-ignore
const output = await handler(this.props, ...args);
server.tool(
tool.name as string,
tool.description,
tool.paramsSchema ? tool.paramsSchema : {},
async (...args) => {
try {

Check warning on line 19 in src/mcp/server.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/server.ts#L14-L19

Added lines #L14 - L19 were not covered by tests
// TODO(dcramer): I'm too dumb to figure this out
// @ts-ignore
const output = await handler(context, ...args);

Check warning on line 22 in src/mcp/server.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/server.ts#L22

Added line #L22 was not covered by tests

return {
content: [
{
type: "text",
text: output,
},
],
};
} catch (error) {
logError(error);
return {
content: [
{
type: "text",
text: `**Error**\n\nIt looks like there was a problem communicating with the Sentry API:\n\n${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
};
}
},
);
}
return {
content: [
{
type: "text",
text: output,
},
],
};
} catch (error) {
logError(error);
return {
content: [
{
type: "text",
text: `**Error**\n\nIt looks like there was a problem communicating with the Sentry API:\n\n${
error instanceof Error ? error.message : String(error)
}`,
},
],
isError: true,
};
}
},
);

Check warning on line 47 in src/mcp/server.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/server.ts#L24-L47

Added lines #L24 - L47 were not covered by tests
}
}
26 changes: 26 additions & 0 deletions src/mcp/start-stdio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#!/usr/bin/env node

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { startStdio } from "./transports/stdio.js";

Check warning on line 4 in src/mcp/start-stdio.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/start-stdio.ts#L3-L4

Added lines #L3 - L4 were not covered by tests

const accessToken = process.env.SENTRY_AUTH_TOKEN;

Check warning on line 6 in src/mcp/start-stdio.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/start-stdio.ts#L6

Added line #L6 was not covered by tests

if (!accessToken) {
console.error("SENTRY_AUTH_TOKEN is not set");
process.exit(1);
}

Check warning on line 11 in src/mcp/start-stdio.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/start-stdio.ts#L8-L11

Added lines #L8 - L11 were not covered by tests

const server = new McpServer({
name: "Sentry MCP",
version: "0.1.0",
});

Check warning on line 16 in src/mcp/start-stdio.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/start-stdio.ts#L13-L16

Added lines #L13 - L16 were not covered by tests

// XXX: we could do what we're doing in routes/auth.ts and pass the context
// identically, but we don't really need userId and userName yet
startStdio(server, {
accessToken,
organizationSlug: null,
}).catch((err) => {
console.error("Server error:", err);
process.exit(1);
});

Check warning on line 26 in src/mcp/start-stdio.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/start-stdio.ts#L20-L26

Added lines #L20 - L26 were not covered by tests
77 changes: 52 additions & 25 deletions src/mcp/tools.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Props } from "../types";
import type { ServerContext } from "../types";
import { z } from "zod";
import type {
SentryErrorEntrySchema,
Expand Down Expand Up @@ -66,7 +66,7 @@
"- View all teams in a Sentry organization",
].join("\n"),
paramsSchema: {
organizationSlug: ParamOrganizationSlug,
organizationSlug: ParamOrganizationSlug.optional(),
},
},
{
Expand All @@ -78,7 +78,7 @@
"- View all projects in a Sentry organization",
].join("\n"),
paramsSchema: {
organizationSlug: ParamOrganizationSlug,
organizationSlug: ParamOrganizationSlug.optional(),
},
},
{
Expand Down Expand Up @@ -131,7 +131,7 @@
"- Create a new team in a Sentry organization",
].join("\n"),
paramsSchema: {
organizationSlug: ParamOrganizationSlug,
organizationSlug: ParamOrganizationSlug.optional(),
name: z.string().describe("The name of the team to create."),
},
},
Expand Down Expand Up @@ -178,7 +178,7 @@
) => Promise<string>;

export type ToolHandlerExtended<T extends ToolName> = (
props: Props,
context: ServerContext,
params: ToolParams<T>,
extra: RequestHandlerExtra,
) => Promise<string>;
Expand All @@ -188,20 +188,24 @@
};

export const TOOL_HANDLERS = {
list_organizations: async (props) => {
const apiService = new SentryApiService(props.accessToken);
list_organizations: async (context) => {
const apiService = new SentryApiService(context.accessToken);

Check warning on line 192 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L192

Added line #L192 was not covered by tests
const organizations = await apiService.listOrganizations();

let output = "# Organizations\n\n";
output += organizations.map((org) => `- ${org.slug}\n`).join("");

return output;
},
list_teams: async (props, { organizationSlug }) => {
const apiService = new SentryApiService(props.accessToken);
list_teams: async (context, { organizationSlug }) => {
const apiService = new SentryApiService(context.accessToken);

Check warning on line 201 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L201

Added line #L201 was not covered by tests

if (!organizationSlug && context.organizationSlug) {
organizationSlug = context.organizationSlug;
}

Check warning on line 205 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L203-L205

Added lines #L203 - L205 were not covered by tests

if (!organizationSlug) {
organizationSlug = props.organizationSlug;
throw new Error("Organization slug is required");

Check warning on line 208 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L208

Added line #L208 was not covered by tests
}

const teams = await apiService.listTeams(organizationSlug);
Expand All @@ -211,11 +215,15 @@

return output;
},
list_projects: async (props, { organizationSlug }) => {
const apiService = new SentryApiService(props.accessToken);
list_projects: async (context, { organizationSlug }) => {
const apiService = new SentryApiService(context.accessToken);

Check warning on line 219 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L219

Added line #L219 was not covered by tests

if (!organizationSlug && context.organizationSlug) {
organizationSlug = context.organizationSlug;
}

Check warning on line 223 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L221-L223

Added lines #L221 - L223 were not covered by tests

if (!organizationSlug) {
organizationSlug = props.organizationSlug;
throw new Error("Organization slug is required");

Check warning on line 226 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L226

Added line #L226 was not covered by tests
}

const projects = await apiService.listProjects(organizationSlug);
Expand All @@ -225,8 +233,11 @@

return output;
},
get_error_details: async (props, { issueId, issueUrl, organizationSlug }) => {
const apiService = new SentryApiService(props.accessToken);
get_error_details: async (
context,
{ issueId, issueUrl, organizationSlug },
) => {
const apiService = new SentryApiService(context.accessToken);

Check warning on line 240 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L237-L240

Added lines #L237 - L240 were not covered by tests

if (issueUrl) {
const resolved = extractIssueId(issueUrl);
Expand All @@ -241,8 +252,12 @@
throw new Error("Either issueId or issueUrl must be provided");
}

if (!organizationSlug && context.organizationSlug) {
organizationSlug = context.organizationSlug;
}

Check warning on line 257 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L255-L257

Added lines #L255 - L257 were not covered by tests

if (!organizationSlug) {
organizationSlug = props.organizationSlug;
throw new Error("Organization slug is required");

Check warning on line 260 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L260

Added line #L260 was not covered by tests
}

const event = await apiService.getLatestEventForIssue({
Expand All @@ -269,13 +284,17 @@
},

search_errors_in_file: async (
props,
context,

Check warning on line 287 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L287

Added line #L287 was not covered by tests
{ filename, sortBy, organizationSlug },
) => {
const apiService = new SentryApiService(props.accessToken);
const apiService = new SentryApiService(context.accessToken);

Check warning on line 290 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L290

Added line #L290 was not covered by tests

if (!organizationSlug && context.organizationSlug) {
organizationSlug = context.organizationSlug;
}

Check warning on line 294 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L292-L294

Added lines #L292 - L294 were not covered by tests

if (!organizationSlug) {
organizationSlug = props.organizationSlug;
throw new Error("Organization slug is required");

Check warning on line 297 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L297

Added line #L297 was not covered by tests
}

const eventList = await apiService.searchErrors({
Expand Down Expand Up @@ -305,11 +324,15 @@
return output;
},

create_team: async (props, { organizationSlug, name }) => {
const apiService = new SentryApiService(props.accessToken);
create_team: async (context, { organizationSlug, name }) => {
const apiService = new SentryApiService(context.accessToken);

Check warning on line 328 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L328

Added line #L328 was not covered by tests

if (!organizationSlug && context.organizationSlug) {
organizationSlug = context.organizationSlug;
}

Check warning on line 332 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L330-L332

Added lines #L330 - L332 were not covered by tests

if (!organizationSlug) {
organizationSlug = props.organizationSlug;
throw new Error("Organization slug is required");

Check warning on line 335 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L335

Added line #L335 was not covered by tests
}

const team = await apiService.createTeam({
Expand All @@ -326,13 +349,17 @@
},

create_project: async (
props,
context,

Check warning on line 352 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L352

Added line #L352 was not covered by tests
{ organizationSlug, teamSlug, name, platform },
) => {
const apiService = new SentryApiService(props.accessToken);
const apiService = new SentryApiService(context.accessToken);

Check warning on line 355 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L355

Added line #L355 was not covered by tests

if (!organizationSlug && context.organizationSlug) {
organizationSlug = context.organizationSlug;
}

Check warning on line 359 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L357-L359

Added lines #L357 - L359 were not covered by tests

if (!organizationSlug) {
organizationSlug = props.organizationSlug;
throw new Error("Organization slug is required");

Check warning on line 362 in src/mcp/tools.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/tools.ts#L362

Added line #L362 was not covered by tests
}

const [project, clientKey] = await apiService.createProject({
Expand Down
21 changes: 21 additions & 0 deletions src/mcp/transports/cloudflare-worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { McpAgent } from "agents/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

Check warning on line 2 in src/mcp/transports/cloudflare-worker.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/transports/cloudflare-worker.ts#L1-L2

Added lines #L1 - L2 were not covered by tests
import type { WorkerProps } from "../../types";
import { configureServer } from "../server";

Check warning on line 4 in src/mcp/transports/cloudflare-worker.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/transports/cloudflare-worker.ts#L4

Added line #L4 was not covered by tests

// Context from the auth process, encrypted & stored in the auth token
// and provided to the DurableMCP as this.props
export default class SentryMCPWorker extends McpAgent<

Check warning on line 8 in src/mcp/transports/cloudflare-worker.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/transports/cloudflare-worker.ts#L8

Added line #L8 was not covered by tests
Env,
unknown,
WorkerProps
> {
server = new McpServer({
name: "Sentry MCP",
version: "0.1.0",
});

Check warning on line 16 in src/mcp/transports/cloudflare-worker.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/transports/cloudflare-worker.ts#L12-L16

Added lines #L12 - L16 were not covered by tests

async init() {
configureServer(this.server, this.props);
}
}

Check warning on line 21 in src/mcp/transports/cloudflare-worker.ts

View check run for this annotation

Codecov / codecov/patch

src/mcp/transports/cloudflare-worker.ts#L18-L21

Added lines #L18 - L21 were not covered by tests
Loading