From 0b5869ee30369c2dc333dfbb7fb570ad582eebe1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Josh=20Goldberg=20=E2=9C=A8?= Date: Mon, 9 Nov 2026 16:22:33 -0500 Subject: [PATCH] ref(lint): Enable no-unnecessary-condition in mcp-core Turn on typescript/no-unnecessary-condition for packages/mcp-core as the first step of a per-package rollout. Checks the types already guarantee are removed. Where a guard was load-bearing but the type hid it (array and record index access, Object.entries inference, event contexts), the type is made accurate instead so the guard stays. Co-Authored-By: Claude Opus 5.5 --- .oxlintrc.json | 1 + .../mcp-core/scripts/generate-definitions.ts | 4 +-- .../scripts/generate-otel-namespaces.ts | 2 +- .../mcp-core/scripts/measure-token-cost.ts | 2 +- packages/mcp-core/src/api-client/client.ts | 2 +- packages/mcp-core/src/api-client/schema.ts | 2 +- .../src/internal/agents/callEmbeddedAgent.ts | 13 ++++------ .../internal/agents/tools/otel-semantics.ts | 2 +- packages/mcp-core/src/internal/blob-utils.ts | 4 +-- packages/mcp-core/src/internal/formatting.ts | 26 ++++++------------- .../mcp-core/src/internal/issue-helpers.ts | 5 ---- .../tool-helpers/credential-routing.ts | 3 --- .../tool-helpers/default-organization.test.ts | 4 +-- .../tool-helpers/default-organization.ts | 4 +-- packages/mcp-core/src/server.metadata.test.ts | 7 +---- packages/mcp-core/src/server.test.ts | 18 ++++++------- packages/mcp-core/src/telem/logging.ts | 5 ---- .../mcp-core/src/telem/unknown-tool.test.ts | 2 +- .../src/test-utils/dataset-search-tool.ts | 2 +- .../src/test-utils/structured-content.ts | 2 +- .../src/tools/catalog-runtime/availability.ts | 8 +++--- .../tools/catalog/find-alert-rules.test.ts | 6 ++--- .../src/tools/catalog/find-dashboards.test.ts | 6 ++--- .../src/tools/catalog/find-monitors.test.ts | 12 ++++----- .../tools/catalog/find-organizations.test.ts | 2 +- .../catalog/find-uptime-monitors.test.ts | 4 +-- .../src/tools/catalog/get-alert-rule.test.ts | 8 +++--- .../catalog/get-dashboard-details.test.ts | 2 +- .../src/tools/catalog/get-issue-details.ts | 9 ++++--- .../tools/catalog/get-monitor-details.test.ts | 16 ++++++------ .../src/tools/catalog/get-replay-details.ts | 6 ++--- .../src/tools/catalog/get-trace-details.ts | 26 +++++++++---------- .../catalog/search-agent-conversations.ts | 2 +- .../tools/catalog/search-issue-events.test.ts | 4 +-- .../src/tools/catalog/search-issue-events.ts | 2 +- .../src/tools/catalog/search-issues.ts | 2 +- .../catalog/search-replays.structured.test.ts | 16 ++++++------ .../tools/catalog/support/uptime-monitors.ts | 11 +++----- .../src/tools/catalog/update-issue.ts | 23 ++++++++-------- .../src/tools/support/code-location.ts | 4 +-- .../src/tools/support/event-stacktrace.ts | 4 +-- .../src/tools/support/issue-linking/app.ts | 4 +-- .../src/tools/support/issue-linking/index.ts | 6 ++--- .../tools/support/metric-monitor-config.ts | 4 ++- .../src/tools/support/profile/analyzer.ts | 2 +- .../tools/support/profile/details-output.ts | 2 +- .../support/profile/flamegraph-output.test.ts | 10 ++++--- .../support/profile/flamegraph-output.ts | 4 +-- .../src/tools/support/search-events/agent.ts | 2 +- .../src/tools/support/search-events/config.ts | 2 +- .../tools/support/search-events/formatters.ts | 10 +++---- .../tools/support/search-events/replays.ts | 2 +- .../src/tools/support/search-events/search.ts | 9 +++---- .../src/tools/support/search-events/utils.ts | 9 +++---- .../support/search-issue-events/agent.ts | 2 +- .../support/search-issue-events/results.ts | 2 +- .../tools/support/search-issues/results.ts | 4 +-- packages/mcp-core/src/utils/url-utils.ts | 2 +- packages/mcp-core/src/version.ts | 2 +- 59 files changed, 164 insertions(+), 197 deletions(-) diff --git a/.oxlintrc.json b/.oxlintrc.json index 6a86d7b0d..a6b78a17d 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -56,6 +56,7 @@ { "files": ["packages/mcp-core/**/*.{ts,tsx,js,mjs,cjs}"], "rules": { + "typescript/no-unnecessary-condition": "error", "no-restricted-imports": [ "error", { diff --git a/packages/mcp-core/scripts/generate-definitions.ts b/packages/mcp-core/scripts/generate-definitions.ts index 1c3bb2777..e48cd681e 100644 --- a/packages/mcp-core/scripts/generate-definitions.ts +++ b/packages/mcp-core/scripts/generate-definitions.ts @@ -30,7 +30,7 @@ function ensureDirExists(dir: string) { function zodFieldMapToJsonSchema( fieldMap: Record, ): unknown { - if (!fieldMap || Object.keys(fieldMap).length === 0) return {}; + if (Object.keys(fieldMap).length === 0) return {}; const obj = z.object(fieldMap); const { $schema: _, ...jsonSchema } = z.toJSONSchema(obj, { io: "input", @@ -156,7 +156,7 @@ function generateToolDefinitions({ if (!Array.isArray(t.requiredScopes)) { throw new Error(`Tool '${t.name}' is missing requiredScopes array`); } - const jsonSchema = zodFieldMapToJsonSchema(t.inputSchema || {}); + const jsonSchema = zodFieldMapToJsonSchema(t.inputSchema); const surface: ToolSurface = isDirect ? "direct" : "catalog"; return { name: t.name, diff --git a/packages/mcp-core/scripts/generate-otel-namespaces.ts b/packages/mcp-core/scripts/generate-otel-namespaces.ts index 4066240c1..af2b19aa7 100644 --- a/packages/mcp-core/scripts/generate-otel-namespaces.ts +++ b/packages/mcp-core/scripts/generate-otel-namespaces.ts @@ -281,7 +281,7 @@ function convertYamlToJson( } // Handle enums/members from the type object or explicit members - if (typeof attr.type === "object" && attr.type.members) { + if (typeof attr.type === "object") { jsonAttr.examples = attr.type.members.map((m) => String(m.value)); } else if (attr.members) { jsonAttr.examples = attr.members.map((m) => String(m.value)); diff --git a/packages/mcp-core/scripts/measure-token-cost.ts b/packages/mcp-core/scripts/measure-token-cost.ts index 57aab0c2b..40d1cc7a7 100644 --- a/packages/mcp-core/scripts/measure-token-cost.ts +++ b/packages/mcp-core/scripts/measure-token-cost.ts @@ -76,7 +76,7 @@ type ToolDefinition = { */ function formatToolsForMCP(tools: Record) { return Object.entries(tools).map(([_key, tool]) => { - const inputSchema = tool.inputSchema || {}; + const inputSchema = tool.inputSchema; const zodObject = Object.keys(inputSchema).length > 0 ? z.object(inputSchema) diff --git a/packages/mcp-core/src/api-client/client.ts b/packages/mcp-core/src/api-client/client.ts index acc0d2629..c78ef29f3 100644 --- a/packages/mcp-core/src/api-client/client.ts +++ b/packages/mcp-core/src/api-client/client.ts @@ -3261,7 +3261,7 @@ export class SentryApiService { query: { limit, sort: sortBy, - statsPeriod: statsPeriod ?? DEFAULT_SEARCH_ISSUES_PERIOD, + statsPeriod, // Always send `query`, even empty: Sentry applies `is:unresolved` // only when the parameter is missing. query: query ?? "", diff --git a/packages/mcp-core/src/api-client/schema.ts b/packages/mcp-core/src/api-client/schema.ts index e2605bf25..c41a61840 100644 --- a/packages/mcp-core/src/api-client/schema.ts +++ b/packages/mcp-core/src/api-client/schema.ts @@ -1070,7 +1070,7 @@ const BaseEventSchema = z.object({ ]), ), contexts: z - .record( + .partialRecord( z.string(), z .object({ diff --git a/packages/mcp-core/src/internal/agents/callEmbeddedAgent.ts b/packages/mcp-core/src/internal/agents/callEmbeddedAgent.ts index 53b77a708..70e6a1f45 100644 --- a/packages/mcp-core/src/internal/agents/callEmbeddedAgent.ts +++ b/packages/mcp-core/src/internal/agents/callEmbeddedAgent.ts @@ -162,13 +162,11 @@ export async function callEmbeddedAgent< // See: https://github.com/getsentry/sentry-mcp/issues/623 providerOptions: provider.getProviderOptions(), onStepFinish: (event) => { - if (event.toolCalls && event.toolCalls.length > 0) { - for (const toolCall of event.toolCalls) { - capturedToolCalls.push({ - toolName: toolCall.toolName, - args: toolCall.input, - }); - } + for (const toolCall of event.toolCalls) { + capturedToolCalls.push({ + toolName: toolCall.toolName, + args: toolCall.input, + }); } }, }); @@ -184,7 +182,6 @@ export async function callEmbeddedAgent< if ( typeof rawOutput === "object" && - rawOutput !== null && "error" in rawOutput && typeof (rawOutput as { error?: unknown }).error === "string" ) { diff --git a/packages/mcp-core/src/internal/agents/tools/otel-semantics.ts b/packages/mcp-core/src/internal/agents/tools/otel-semantics.ts index 1f3773409..3d89c9b89 100644 --- a/packages/mcp-core/src/internal/agents/tools/otel-semantics.ts +++ b/packages/mcp-core/src/internal/agents/tools/otel-semantics.ts @@ -82,7 +82,7 @@ import webengine from "./data/webengine.json"; import zos from "./data/zos.json"; // Create the namespaceData object -const namespaceData: Record = { +const namespaceData: Partial> = { android, app, artifact, diff --git a/packages/mcp-core/src/internal/blob-utils.ts b/packages/mcp-core/src/internal/blob-utils.ts index bf176b0ed..f867fce3d 100644 --- a/packages/mcp-core/src/internal/blob-utils.ts +++ b/packages/mcp-core/src/internal/blob-utils.ts @@ -156,7 +156,7 @@ interface PreviewCodec { encode: (image: RgbaImage, options: { jpegQuality: number }) => Uint8Array; } -const PREVIEW_CODECS: Record = { +const PREVIEW_CODECS: Partial> = { "image/png": { contentType: "image/png", readDimensions: readPngDimensions, @@ -245,7 +245,7 @@ function readJpegDimensions(bytes: Uint8Array): ImageDimensions | null { offset++; } - const marker = bytes[offset]; + const marker = bytes.at(offset); offset++; if (marker === undefined || marker === 0xd9 || marker === 0xda) { diff --git a/packages/mcp-core/src/internal/formatting.ts b/packages/mcp-core/src/internal/formatting.ts index 2da18eb80..2556ad2b8 100644 --- a/packages/mcp-core/src/internal/formatting.ts +++ b/packages/mcp-core/src/internal/formatting.ts @@ -258,16 +258,6 @@ export function formatEventOutput( }); } - // Check if entries exist (may be undefined for unsupported event types) - if (!eventToRender.entries || !Array.isArray(eventToRender.entries)) { - // For unsupported event types, just show tags and contexts - output += formatEventUser(eventUser); - output += formatTags(eventToRender.tags); - output += formatContext(eventToRender.context); - output += formatContexts(eventToRender.contexts); - return output; - } - // Look for the primary error information const messageEntry = eventToRender.entries.find((e) => e.type === "message"); const exceptionEntry = eventToRender.entries.find( @@ -441,7 +431,7 @@ function formatExceptionInterfaceOutput( parts.push(""); } - if (!exception.stacktrace || !exception.stacktrace.frames) { + if (!exception.stacktrace) { parts.push("**Stacktrace:**"); parts.push("```"); parts.push("No stacktrace available"); @@ -692,7 +682,7 @@ function formatThreadTable( "| Thread ID | Name | State | Flags | Frames |", "| --- | --- | --- | --- | ---: |", ...threads.map((thread) => { - const frameCount = thread.stacktrace?.frames?.length ?? 0; + const frameCount = thread.stacktrace?.frames.length ?? 0; return `| ${getThreadDisplayValue(thread.id)} | ${getThreadDisplayValue(thread.name)} | ${getThreadDisplayValue(thread.state)} | ${getThreadFlags(thread)} | ${frameCount} |`; }), ]; @@ -1085,7 +1075,7 @@ function buildTraceSpanTree( if (spanId && spanId.length > 0) { spanMap.set(spanId, span); } - for (const child of span.children ?? []) { + for (const child of span.children) { if (isTraceSpan(child)) { indexSpan(child); } @@ -1176,7 +1166,7 @@ function convertTraceSpanToPerformanceSpan( level, }; - for (const child of span.children ?? []) { + for (const child of span.children) { if (!isTraceSpan(child)) { continue; } @@ -1275,8 +1265,8 @@ function renderPerformanceSpanTree(spans: PerformanceSpan[]): string[] { function renderSpan(span: PerformanceSpan, prefix = "", isLast = true): void { const connector = prefix === "" ? "" : isLast ? "└─ " : "├─ "; - const displayName = span.description?.trim() || span.op || "unnamed"; - const spanId = span.span_id ?? "unknown"; + const displayName = span.description.trim() || span.op || "unnamed"; + const spanId = span.span_id; const durationDisplay = span.duration > 0 ? `${Math.round(span.duration)}ms` : "unknown"; @@ -2150,7 +2140,7 @@ function formatContexts(contexts: z.infer["contexts"]) { ) .map( ([name, data]) => - `**${name}**\n${Object.entries(data) + `**${name}**\n${Object.entries(data ?? {}) .filter(([key, _]) => key !== "type") .map(([key, value]) => { return `${key}: ${JSON.stringify(value, undefined, 2)}`; @@ -2585,7 +2575,7 @@ export function buildIssueResponseNotes({ fallbackInstruction: "Issue event search is not available in this session", }); notes.push(`Issue event search: ${issueEventSearchInstruction}`); - const hasMultipleThreads = event.entries?.some((entry) => { + const hasMultipleThreads = event.entries.some((entry) => { if (entry.type !== "threads") { return false; } diff --git a/packages/mcp-core/src/internal/issue-helpers.ts b/packages/mcp-core/src/internal/issue-helpers.ts index 1404e10f1..6f65d67fe 100644 --- a/packages/mcp-core/src/internal/issue-helpers.ts +++ b/packages/mcp-core/src/internal/issue-helpers.ts @@ -136,11 +136,6 @@ export function parseIssueParams({ } { if (issueUrl) { const resolved = extractIssueId(issueUrl); - if (!resolved) { - throw new Error( - "Invalid Sentry issue URL. Path should contain '/issues/{issue_id}'", - ); - } return { organizationSlug: resolveScopedOrganizationSlug({ resourceLabel: "Issue", diff --git a/packages/mcp-core/src/internal/tool-helpers/credential-routing.ts b/packages/mcp-core/src/internal/tool-helpers/credential-routing.ts index c7f844e4d..e054bc2d2 100644 --- a/packages/mcp-core/src/internal/tool-helpers/credential-routing.ts +++ b/packages/mcp-core/src/internal/tool-helpers/credential-routing.ts @@ -195,9 +195,6 @@ export async function mapCredentialContexts( let firstError: unknown; for (const [index, result] of settled.entries()) { const entry = entries[index]; - if (!entry) { - continue; - } if (result.status === "fulfilled") { results.push({ entry, value: result.value }); continue; diff --git a/packages/mcp-core/src/internal/tool-helpers/default-organization.test.ts b/packages/mcp-core/src/internal/tool-helpers/default-organization.test.ts index a8a995bbc..8996783ba 100644 --- a/packages/mcp-core/src/internal/tool-helpers/default-organization.test.ts +++ b/packages/mcp-core/src/internal/tool-helpers/default-organization.test.ts @@ -88,8 +88,8 @@ describe("applyDefaultOrganization", () => { requiredOrgTool, getServerContext(), ); - expect(unscoped.organizationSlug?.safeParse(undefined).success).toBe(true); - expect(unscoped.organizationSlug?.safeParse("").success).toBe(false); + expect(unscoped.organizationSlug.safeParse(undefined).success).toBe(true); + expect(unscoped.organizationSlug.safeParse("").success).toBe(false); const scoped = getFilteredInputSchema( requiredOrgTool, diff --git a/packages/mcp-core/src/internal/tool-helpers/default-organization.ts b/packages/mcp-core/src/internal/tool-helpers/default-organization.ts index 391c0d039..7c1272e26 100644 --- a/packages/mcp-core/src/internal/tool-helpers/default-organization.ts +++ b/packages/mcp-core/src/internal/tool-helpers/default-organization.ts @@ -161,7 +161,7 @@ function formatRetry( ): string { const { organizationSlug: _omitted, ...rest } = rawParams; const args = Object.fromEntries( - Object.entries({ organizationSlug, ...rest }).filter( + Object.entries({ organizationSlug, ...rest }).filter( ([key, value]) => value !== undefined && !isDefaultValue(tool, key, value), ), @@ -260,7 +260,7 @@ export async function applyDefaultOrganization({ ); } - const [only] = reachable.organizations; + const only = reachable.organizations.at(0); // Default only from a complete list, so a login that failed to answer // cannot hide a second organization. if ( diff --git a/packages/mcp-core/src/server.metadata.test.ts b/packages/mcp-core/src/server.metadata.test.ts index a5edaf656..8d9b5e300 100644 --- a/packages/mcp-core/src/server.metadata.test.ts +++ b/packages/mcp-core/src/server.metadata.test.ts @@ -157,9 +157,6 @@ describe.each(Object.keys(MODES))("%s mode", (mode) => { if (!/^[A-Za-z0-9_.-]{1,128}$/.test(tool.name)) { return "name must match ^[A-Za-z0-9_.-]{1,128}$"; } - if (tool.inputSchema.type !== "object") { - return "inputSchema root must be an object"; - } // The server implements no MCP tasks, so no tool may advertise them. return tool.execution?.taskSupport === undefined ? undefined @@ -219,9 +216,7 @@ describe.each(Object.keys(MODES))("%s mode", (mode) => { it("declares an object outputSchema unless the tool is skipped", () => { const problems = failures(surface().tools, (tool) => { if (tool.outputSchema) { - return tool.outputSchema.type === "object" - ? undefined - : "outputSchema root must be an object"; + return undefined; } return tool.name in OUTPUT_SCHEMA_EXEMPT || tool.name in OUTPUT_SCHEMA_PENDING diff --git a/packages/mcp-core/src/server.test.ts b/packages/mcp-core/src/server.test.ts index 5e8019607..584608bb8 100644 --- a/packages/mcp-core/src/server.test.ts +++ b/packages/mcp-core/src/server.test.ts @@ -503,7 +503,7 @@ describe("buildServer", () => { expect(registeredTools.v2_tool).toBeDefined(); await expect( - registeredTools.v2_tool?.handler({ value: "ok" }), + registeredTools.v2_tool.handler({ value: "ok" }), ).resolves.toMatchObject({ content: [{ type: "text", text: "v2:ok" }], }); @@ -757,7 +757,7 @@ describe("buildServer", () => { } )._registeredTools; - await registeredTools.example_tool?.handler({}, {}); + await registeredTools.example_tool.handler({}, {}); expect(setUser).toHaveBeenCalledWith({ id: "user-123", @@ -1525,8 +1525,8 @@ describe("buildServer", () => { ); expect(getTextContent(result)).not.toContain("# Tool Search Results"); expect(getTextContent(result)).not.toContain("```json"); - expect(firstResult?.name).toBe("get_replay_details"); - expect(Object.keys(firstResult ?? {}).sort()).toEqual([ + expect(firstResult.name).toBe("get_replay_details"); + expect(Object.keys(firstResult).sort()).toEqual([ "annotations", "description", "inputSchema", @@ -1535,7 +1535,7 @@ describe("buildServer", () => { expect(firstResult).not.toHaveProperty("requiredSkills"); expect(firstResult).not.toHaveProperty("requiredScopes"); expect(firstResult).not.toHaveProperty("skills"); - expect(firstResult?.inputSchema.properties).not.toHaveProperty( + expect(firstResult.inputSchema.properties).not.toHaveProperty( "organizationSlug", ); }); @@ -3007,9 +3007,9 @@ describe("buildServer", () => { expect.any(Function), ); const span = startedSpans[0]; - expect(span?.setStatus).toHaveBeenCalledWith({ code: 2 }); + expect(span.setStatus).toHaveBeenCalledWith({ code: 2 }); // Expected validation failures mark the span failed without recording an exception. - expect(span?.recordException).not.toHaveBeenCalled(); + expect(span.recordException).not.toHaveBeenCalled(); }); it("execute_sentry_tool injects constrained arguments for catalog-only tools", async () => { @@ -3092,9 +3092,9 @@ describe("buildServer", () => { }>(result); const replayTool = payload.results[0]; - expect(replayTool?.name).toBe("get_replay_details"); + expect(replayTool.name).toBe("get_replay_details"); expect( - Object.keys(replayTool?.inputSchema.properties ?? {}).sort(), + Object.keys(replayTool.inputSchema.properties ?? {}).sort(), ).toEqual([ "categories", "endOffsetSeconds", diff --git a/packages/mcp-core/src/telem/logging.ts b/packages/mcp-core/src/telem/logging.ts index c0a5262f1..848906a7b 100644 --- a/packages/mcp-core/src/telem/logging.ts +++ b/packages/mcp-core/src/telem/logging.ts @@ -78,11 +78,6 @@ function resolveLowestLevel(): LogLevel { */ function createSentryLogsSink(): Sink { return (record: LogRecord) => { - // Check if sentryLogger is available (may not be in all environments) - if (!sentryLogger) { - return; - } - // Extract message from LogRecord let message = ""; for (let i = 0; i < record.message.length; i++) { diff --git a/packages/mcp-core/src/telem/unknown-tool.test.ts b/packages/mcp-core/src/telem/unknown-tool.test.ts index 65135f0ad..c43a85756 100644 --- a/packages/mcp-core/src/telem/unknown-tool.test.ts +++ b/packages/mcp-core/src/telem/unknown-tool.test.ts @@ -132,6 +132,6 @@ it("leaves registered tool spans alone", async () => { const span = await callToolSpan("find_projects"); expect(span.status).toBe("ok"); - expect(span.attributes?.["error.type"]).toBeUndefined(); + expect(span.attributes["error.type"]).toBeUndefined(); expect(span.attributes).toMatchObject({ "app.transport": "http" }); }); diff --git a/packages/mcp-core/src/test-utils/dataset-search-tool.ts b/packages/mcp-core/src/test-utils/dataset-search-tool.ts index 4ec2ec7cd..24146ae36 100644 --- a/packages/mcp-core/src/test-utils/dataset-search-tool.ts +++ b/packages/mcp-core/src/test-utils/dataset-search-tool.ts @@ -32,7 +32,7 @@ export async function inspectDatasetSearchTool

(tool: { const run = vi.mocked(runSearchEvents); run.mockClear(); await tool.handler(params as P, createTestContext()); - const [handlerParams, , options] = run.mock.calls[0] ?? []; + const [handlerParams, , options] = run.mock.calls.at(0) ?? []; return { dataset: handlerParams?.dataset, options, diff --git a/packages/mcp-core/src/test-utils/structured-content.ts b/packages/mcp-core/src/test-utils/structured-content.ts index 02c5f23ea..ab22292d3 100644 --- a/packages/mcp-core/src/test-utils/structured-content.ts +++ b/packages/mcp-core/src/test-utils/structured-content.ts @@ -4,7 +4,7 @@ export function getTextContent(result: unknown): string { if (!Array.isArray(content)) { return ""; } - return content?.find((item) => typeof item.text === "string")?.text ?? ""; + return content.find((item) => typeof item.text === "string")?.text ?? ""; } /** Returns structuredContent after checking the result has an object payload. */ diff --git a/packages/mcp-core/src/tools/catalog-runtime/availability.ts b/packages/mcp-core/src/tools/catalog-runtime/availability.ts index 3931d6fb1..ebba91844 100644 --- a/packages/mcp-core/src/tools/catalog-runtime/availability.ts +++ b/packages/mcp-core/src/tools/catalog-runtime/availability.ts @@ -144,9 +144,11 @@ export function getFilteredInputSchema( ([key]) => !constraintKeysToFilter.has(key), ), ) as Record; - const organizationSlug = schema.organizationSlug; - if (organizationSlug && isOrganizationSlugRequired(organizationSlug)) { - schema.organizationSlug = organizationSlug.optional(); + if ( + "organizationSlug" in schema && + isOrganizationSlugRequired(schema.organizationSlug) + ) { + schema.organizationSlug = schema.organizationSlug.optional(); } return schema; } diff --git a/packages/mcp-core/src/tools/catalog/find-alert-rules.test.ts b/packages/mcp-core/src/tools/catalog/find-alert-rules.test.ts index c13f0a0d5..db65ae7c5 100644 --- a/packages/mcp-core/src/tools/catalog/find-alert-rules.test.ts +++ b/packages/mcp-core/src/tools/catalog/find-alert-rules.test.ts @@ -75,7 +75,7 @@ function useAlertRuleHandlers() { describe("find_alert_rules", () => { it("serializes project-scoped issue and metric searches", async () => { useAlertRuleHandlers(); - let issueRequestUrl: string | null = null; + let issueRequestUrl: string | undefined; const metricRequestUrls: string[] = []; mswServer.use( http.get( @@ -162,7 +162,7 @@ describe("find_alert_rules", () => { }); it("resolves project redirects before listing metric alerts", async () => { - let metricRequestUrl: string | null = null; + let metricRequestUrl: string | undefined; mswServer.use( http.get( "https://sentry.io/api/0/projects/sentry-mcp-evals/legacy-cloudflare-mcp/", @@ -182,7 +182,7 @@ describe("find_alert_rules", () => { context, ); - expect(metricRequestUrl).not.toBeNull(); + expect(metricRequestUrl).toBeDefined(); expect(new URL(metricRequestUrl ?? "").searchParams.get("project")).toBe( project.id, ); diff --git a/packages/mcp-core/src/tools/catalog/find-dashboards.test.ts b/packages/mcp-core/src/tools/catalog/find-dashboards.test.ts index c4c397ae9..fe8d67054 100644 --- a/packages/mcp-core/src/tools/catalog/find-dashboards.test.ts +++ b/packages/mcp-core/src/tools/catalog/find-dashboards.test.ts @@ -96,7 +96,7 @@ describe("find_dashboards", () => { }); it("filters explicit dashboard project IDs in project-constrained sessions", async () => { - let requestUrl: string | null = null; + let requestUrl: string | undefined; const paths: string[] = []; mswServer.use( http.get( @@ -372,7 +372,7 @@ describe("find_dashboards", () => { }); it("resumes a project cursor within the first API page", async () => { - let requestUrl: string | null = null; + let requestUrl: string | undefined; const firstPageCursor = `mcp-dashboard-project:${Buffer.from( JSON.stringify({ v: 1, @@ -514,7 +514,7 @@ describe("find_dashboards", () => { }); it("passes search, sort, cursor, and limit query parameters", async () => { - let requestUrl: string | null = null; + let requestUrl: string | undefined; mswServer.use( http.get( "https://sentry.io/api/0/organizations/sentry-mcp-evals/dashboards/", diff --git a/packages/mcp-core/src/tools/catalog/find-monitors.test.ts b/packages/mcp-core/src/tools/catalog/find-monitors.test.ts index df06be31f..735bb4c6b 100644 --- a/packages/mcp-core/src/tools/catalog/find-monitors.test.ts +++ b/packages/mcp-core/src/tools/catalog/find-monitors.test.ts @@ -75,7 +75,7 @@ describe("find_monitors", () => { }); it("filters by projectSlug instead of numeric project for project slugs", async () => { - let requestUrl: string | null = null; + let requestUrl: string | undefined; mswServer.use( http.get( "https://sentry.io/api/0/organizations/sentry-mcp-evals/monitors/", @@ -99,14 +99,14 @@ describe("find_monitors", () => { context, ); - expect(requestUrl).not.toBeNull(); + expect(requestUrl).toBeDefined(); const params = new URL(requestUrl ?? "").searchParams; expect(params.get("projectSlug")).toBe("backend"); expect(params.get("project")).toBeNull(); }); it("sends monitor list filters to Sentry", async () => { - let requestUrl: string | null = null; + let requestUrl: string | undefined; mswServer.use( http.get( "https://sentry.io/api/0/organizations/sentry-mcp-evals/monitors/", @@ -130,7 +130,7 @@ describe("find_monitors", () => { context, ); - expect(requestUrl).not.toBeNull(); + expect(requestUrl).toBeDefined(); const params = new URL(requestUrl ?? "").searchParams; expect(params.get("projectSlug")).toBe("backend"); expect(params.get("environment")).toBe("production"); @@ -140,7 +140,7 @@ describe("find_monitors", () => { }); it("uses the active project constraint as the monitor list project", async () => { - let requestUrl: string | null = null; + let requestUrl: string | undefined; mswServer.use( http.get( "https://sentry.io/api/0/organizations/sentry-mcp-evals/monitors/", @@ -170,7 +170,7 @@ describe("find_monitors", () => { }, ); - expect(requestUrl).not.toBeNull(); + expect(requestUrl).toBeDefined(); const params = new URL(requestUrl ?? "").searchParams; expect(params.get("projectSlug")).toBe("backend"); }); diff --git a/packages/mcp-core/src/tools/catalog/find-organizations.test.ts b/packages/mcp-core/src/tools/catalog/find-organizations.test.ts index f4fc053e7..a8a5f25e1 100644 --- a/packages/mcp-core/src/tools/catalog/find-organizations.test.ts +++ b/packages/mcp-core/src/tools/catalog/find-organizations.test.ts @@ -212,7 +212,7 @@ describe("find_organizations", () => { /** Each token's org pages; a cursor names the page after the first. */ function mockAccountOrganizations( - pages: Record>, + pages: Partial>>>, next: Record> = {}, ) { const requests: Array<{ token: string; cursor: string | null }> = []; diff --git a/packages/mcp-core/src/tools/catalog/find-uptime-monitors.test.ts b/packages/mcp-core/src/tools/catalog/find-uptime-monitors.test.ts index bb3da292d..2b9a40766 100644 --- a/packages/mcp-core/src/tools/catalog/find-uptime-monitors.test.ts +++ b/packages/mcp-core/src/tools/catalog/find-uptime-monitors.test.ts @@ -56,7 +56,7 @@ describe("find_uptime_monitors", () => { }); it("resolves projectSlug to project id query param", async () => { - let requestUrl: string | null = null; + let requestUrl: string | undefined; mswServer.use( http.get( "https://sentry.io/api/0/organizations/sentry-mcp-evals/uptime/", @@ -80,7 +80,7 @@ describe("find_uptime_monitors", () => { context, ); - expect(requestUrl).not.toBeNull(); + expect(requestUrl).toBeDefined(); const params = new URL(requestUrl ?? "").searchParams; expect(params.get("project")).toBe("4509109104082945"); expect(params.get("environment")).toBe("production"); diff --git a/packages/mcp-core/src/tools/catalog/get-alert-rule.test.ts b/packages/mcp-core/src/tools/catalog/get-alert-rule.test.ts index 0482c6548..524ad61ea 100644 --- a/packages/mcp-core/src/tools/catalog/get-alert-rule.test.ts +++ b/packages/mcp-core/src/tools/catalog/get-alert-rule.test.ts @@ -362,7 +362,7 @@ describe("get_alert_rule", () => { }); it("fetches issue alert details after resolving an exact name", async () => { - let listRequestUrl: string | null = null; + let listRequestUrl: string | undefined; mswServer.use( http.get(`${organizationApi}/workflows/`, ({ request }) => { listRequestUrl = request.url; @@ -381,7 +381,7 @@ describe("get_alert_rule", () => { ruleIdOrName: "Notify backend team", }); - expect(listRequestUrl).not.toBeNull(); + expect(listRequestUrl).toBeDefined(); const listParams = new URL(listRequestUrl ?? "").searchParams; expect(listParams.get("query")).toBe('name:"*Notify backend team*"'); expect(listParams.get("projectSlug")).toBe("cloudflare-mcp"); @@ -391,7 +391,7 @@ describe("get_alert_rule", () => { }); it("quotes issue alert name lookups for workflow query syntax", async () => { - let listRequestUrl: string | null = null; + let listRequestUrl: string | undefined; mswServer.use( http.get(`${organizationApi}/workflows/`, ({ request }) => { listRequestUrl = request.url; @@ -406,7 +406,7 @@ describe("get_alert_rule", () => { ruleIdOrName: "Critical: backend", }); - expect(listRequestUrl).not.toBeNull(); + expect(listRequestUrl).toBeDefined(); expect(new URL(listRequestUrl ?? "").searchParams.get("query")).toBe( 'name:"*Critical: backend*"', ); diff --git a/packages/mcp-core/src/tools/catalog/get-dashboard-details.test.ts b/packages/mcp-core/src/tools/catalog/get-dashboard-details.test.ts index 83cb3eb06..a0f041afc 100644 --- a/packages/mcp-core/src/tools/catalog/get-dashboard-details.test.ts +++ b/packages/mcp-core/src/tools/catalog/get-dashboard-details.test.ts @@ -180,7 +180,7 @@ describe("get_dashboard_details", () => { }); it("resolves dashboard titles after applying the active project constraint", async () => { - let requestUrl: string | null = null; + let requestUrl: string | undefined; mswServer.use( http.get( "https://sentry.io/api/0/organizations/sentry-mcp-evals/dashboards/", diff --git a/packages/mcp-core/src/tools/catalog/get-issue-details.ts b/packages/mcp-core/src/tools/catalog/get-issue-details.ts index 5163a6dbd..59b0fdcb3 100644 --- a/packages/mcp-core/src/tools/catalog/get-issue-details.ts +++ b/packages/mcp-core/src/tools/catalog/get-issue-details.ts @@ -251,8 +251,8 @@ function buildIssueDetailsPayload({ culprit: issue.culprit, firstSeen: issue.firstSeen, lastSeen: issue.lastSeen, - occurrences: issue.count == null ? null : Number(issue.count), - usersImpacted: issue.userCount == null ? null : Number(issue.userCount), + occurrences: Number(issue.count), + usersImpacted: Number(issue.userCount), status: issue.status, substatus: issue.substatus, assignedTo: @@ -266,7 +266,7 @@ function buildIssueDetailsPayload({ ? null : getSeerActionabilityLabel(issue.seerFixabilityScore), platform: issue.platform, - project: issue.project?.name, + project: issue.project.name, url: apiService.getIssueUrl(organizationSlug, issue.shortId), // metadata.value is a query pattern only for a performance issue; on an error it is the // exception message, so reading it unconditionally would misname the error text @@ -424,10 +424,11 @@ export default defineTool({ issueId: params.issueId, }); } else { - const [found] = await apiService.listIssues({ + const issues = await apiService.listIssues({ organizationSlug: orgSlug, query: eventId, }); + const found = issues.at(0); if (!found) { throw new UserInputError(`No issue found for Event ID: ${eventId}`); } diff --git a/packages/mcp-core/src/tools/catalog/get-monitor-details.test.ts b/packages/mcp-core/src/tools/catalog/get-monitor-details.test.ts index 01ba89335..1a82940bb 100644 --- a/packages/mcp-core/src/tools/catalog/get-monitor-details.test.ts +++ b/packages/mcp-core/src/tools/catalog/get-monitor-details.test.ts @@ -373,8 +373,8 @@ describe("get_monitor_details", () => { }); it("uses StatsMixin time parameters for monitor stats", async () => { - let checkInsRequestUrl: string | null = null; - let statsRequestUrl: string | null = null; + let checkInsRequestUrl: string | undefined; + let statsRequestUrl: string | undefined; const monitorResponse = { id: "4509100000000001", slug: "nightly-import", @@ -430,14 +430,14 @@ describe("get_monitor_details", () => { context, ); - expect(checkInsRequestUrl).not.toBeNull(); + expect(checkInsRequestUrl).toBeDefined(); const checkInsParams = new URL(checkInsRequestUrl ?? "").searchParams; expect(checkInsParams.get("environment")).toBe("production"); expect(checkInsParams.get("start")).toBe("2025-04-14T02:00:00.000Z"); expect(checkInsParams.get("end")).toBe("2025-04-14T03:00:00.000Z"); expect(checkInsParams.get("statsPeriod")).toBeNull(); - expect(statsRequestUrl).not.toBeNull(); + expect(statsRequestUrl).toBeDefined(); const params = new URL(statsRequestUrl ?? "").searchParams; expect(params.get("environment")).toBe("production"); expect(params.get("since")).toBe("1744596000"); @@ -489,8 +489,8 @@ describe("get_monitor_details", () => { }); it("defaults null period to a 24h monitor window", async () => { - let checkInsRequestUrl: string | null = null; - let statsRequestUrl: string | null = null; + let checkInsRequestUrl: string | undefined; + let statsRequestUrl: string | undefined; const monitorResponse = { id: "4509100000000001", slug: "nightly-import", @@ -546,11 +546,11 @@ describe("get_monitor_details", () => { context, ); - expect(checkInsRequestUrl).not.toBeNull(); + expect(checkInsRequestUrl).toBeDefined(); const checkInsParams = new URL(checkInsRequestUrl ?? "").searchParams; expect(checkInsParams.get("statsPeriod")).toBe("24h"); - expect(statsRequestUrl).not.toBeNull(); + expect(statsRequestUrl).toBeDefined(); const statsParams = new URL(statsRequestUrl ?? "").searchParams; expect(statsParams.get("statsPeriod")).toBeNull(); expect(statsParams.get("since")).not.toBeNull(); diff --git a/packages/mcp-core/src/tools/catalog/get-replay-details.ts b/packages/mcp-core/src/tools/catalog/get-replay-details.ts index d9288d8f9..5d29f4823 100644 --- a/packages/mcp-core/src/tools/catalog/get-replay-details.ts +++ b/packages/mcp-core/src/tools/catalog/get-replay-details.ts @@ -500,12 +500,12 @@ async function fetchReplayIssues({ return await Promise.all( ids.map(async (eventId) => { try { - const [issue] = await apiService.listIssues({ + const issues = await apiService.listIssues({ organizationSlug, query: eventId, limit: 1, }); - return { eventId, issue: issue ?? null }; + return { eventId, issue: issues.at(0) ?? null }; } catch { return { eventId, issue: null }; } @@ -625,7 +625,7 @@ function buildTimeline({ }): Extract { const { events } = recording; const startedAtMs = parseTimestampMs(replay.started_at); - const replayStartMs = startedAtMs ?? events[0]?.timestampMs ?? 0; + const replayStartMs = startedAtMs ?? events.at(0)?.timestampMs ?? 0; const replayEndMs = parseTimestampMs(replay.finished_at) ?? (startedAtMs !== null && replay.duration != null diff --git a/packages/mcp-core/src/tools/catalog/get-trace-details.ts b/packages/mcp-core/src/tools/catalog/get-trace-details.ts index 4146f3b2e..fd9f7ec63 100644 --- a/packages/mcp-core/src/tools/catalog/get-trace-details.ts +++ b/packages/mcp-core/src/tools/catalog/get-trace-details.ts @@ -600,18 +600,20 @@ function calculateOperationStats(trace: Trace): Record< } > { const allSpans = getAllSpansFlattened(trace); - const operationSpans: Record = {}; + const operationSpans = new Map(); // Group leaf spans by operation type (only spans with no children) for (const span of allSpans) { // Only consider leaf nodes - spans that have no children - if (!span.children || span.children.length === 0) { + if (span.children.length === 0) { // Use span.op if available, otherwise extract from span.name, fallback to "unknown" const op = span.op || (span.name ? span.name.split(" ")[0] : "unknown"); - if (!operationSpans[op]) { - operationSpans[op] = []; + const bucket = operationSpans.get(op); + if (bucket) { + bucket.push(span); + } else { + operationSpans.set(op, [span]); } - operationSpans[op].push(span); } } @@ -621,7 +623,7 @@ function calculateOperationStats(trace: Trace): Record< > = {}; // Calculate stats for each operation - for (const [op, opSpans] of Object.entries(operationSpans)) { + for (const [op, opSpans] of operationSpans) { const durations = opSpans .map((span) => span.duration || 0) .filter((duration) => duration > 0) @@ -896,7 +898,7 @@ function formatFocusedSpanOutput({ sections.push(""); sections.push("## Summary"); sections.push(""); - sections.push(`**Project**: ${focusedSpan.project_slug ?? "unknown"}`); + sections.push(`**Project**: ${focusedSpan.project_slug}`); sections.push(`**Operation**: ${focusedSpan.op ?? "unknown"}`); sections.push(`**Description**: ${formatTraceSpanDescription(focusedSpan)}`); sections.push(`**Duration**: ${formatDuration(focusedSpan.duration)}`); @@ -1073,20 +1075,18 @@ function formatSpanAttributeSections( span.additional_attributes ?? {}, ), ); - sections.push(...formatJsonSection("### Errors", span.errors ?? [])); - sections.push( - ...formatJsonSection("### Occurrences", span.occurrences ?? []), - ); + sections.push(...formatJsonSection("### Errors", span.errors)); + sections.push(...formatJsonSection("### Occurrences", span.occurrences)); return sections; } -function formatJsonSection(title: string, value: unknown): string[] { +function formatJsonSection(title: string, value: object): string[] { return [ title, "", "```json", - JSON.stringify(sortJsonValue(value), null, 2) ?? "null", + JSON.stringify(sortJsonValue(value), null, 2), "```", "", ]; diff --git a/packages/mcp-core/src/tools/catalog/search-agent-conversations.ts b/packages/mcp-core/src/tools/catalog/search-agent-conversations.ts index 4ff9b5296..5040913b8 100644 --- a/packages/mcp-core/src/tools/catalog/search-agent-conversations.ts +++ b/packages/mcp-core/src/tools/catalog/search-agent-conversations.ts @@ -264,7 +264,7 @@ export default defineTool({ if ((params.start && !params.end) || (!params.start && params.end)) { throw new UserInputError("`start` and `end` must be provided together."); } - const requestedPeriod = params.period ?? "30d"; + const requestedPeriod = params.period; const apiService = apiServiceFromContext(context, { regionUrl: params.regionUrl ?? undefined, diff --git a/packages/mcp-core/src/tools/catalog/search-issue-events.test.ts b/packages/mcp-core/src/tools/catalog/search-issue-events.test.ts index 789ce3f6c..4f6a0f4bb 100644 --- a/packages/mcp-core/src/tools/catalog/search-issue-events.test.ts +++ b/packages/mcp-core/src/tools/catalog/search-issue-events.test.ts @@ -515,8 +515,8 @@ describe("search_issue_events", () => { ); const [emptyUser, emptyGeo] = sc(result).events; - expect(emptyUser?.user).toBeNull(); - expect(emptyGeo?.user).toEqual({ + expect(emptyUser.user).toBeNull(); + expect(emptyGeo.user).toEqual({ id: "user-1", email: null, username: null, diff --git a/packages/mcp-core/src/tools/catalog/search-issue-events.ts b/packages/mcp-core/src/tools/catalog/search-issue-events.ts index bb81eb466..8ff800e6e 100644 --- a/packages/mcp-core/src/tools/catalog/search-issue-events.ts +++ b/packages/mcp-core/src/tools/catalog/search-issue-events.ts @@ -222,7 +222,7 @@ export default defineTool({ sortParam = parsed.sort; explanation = parsed.explanation; - const requestedFields = parsed.fields || []; + const requestedFields = parsed.fields; fields = requestedFields.length > 0 ? requestedFields : RECOMMENDED_FIELDS; diff --git a/packages/mcp-core/src/tools/catalog/search-issues.ts b/packages/mcp-core/src/tools/catalog/search-issues.ts index 8364cc994..482e0b3c2 100644 --- a/packages/mcp-core/src/tools/catalog/search-issues.ts +++ b/packages/mcp-core/src/tools/catalog/search-issues.ts @@ -239,7 +239,7 @@ export default defineTool({ ).result, }); - query = translatedQuery.query ?? params.query; + query = translatedQuery.query; sort = translatedQuery.sort || params.sort; explanation = translatedQuery.explanation; } else { diff --git a/packages/mcp-core/src/tools/catalog/search-replays.structured.test.ts b/packages/mcp-core/src/tools/catalog/search-replays.structured.test.ts index 9e4b9e11f..2a9fc1d1c 100644 --- a/packages/mcp-core/src/tools/catalog/search-replays.structured.test.ts +++ b/packages/mcp-core/src/tools/catalog/search-replays.structured.test.ts @@ -184,12 +184,12 @@ describe("search_replays structured output", () => { const structuredContent = await runSearch({ query: "count_errors:>0" }); const [replay] = structuredContent.replays; - expect(replay?.urls).toHaveLength(3); - expect(replay?.urlCount).toBe(8); - expect(replay?.traceIds).toHaveLength(3); - expect(replay?.traceCount).toBe(7); - expect(replay?.releases).toHaveLength(3); - expect(replay?.releaseCount).toBe(5); + expect(replay.urls).toHaveLength(3); + expect(replay.urlCount).toBe(8); + expect(replay.traceIds).toHaveLength(3); + expect(replay.traceCount).toBe(7); + expect(replay.releases).toHaveLength(3); + expect(replay.releaseCount).toBe(5); }); it("omits the placeholder user on archived replays", async () => { @@ -204,8 +204,8 @@ describe("search_replays structured output", () => { const structuredContent = await runSearch({ query: "count_errors:>0" }); const [replay] = structuredContent.replays; - expect(replay?.isArchived).toBe(true); - expect(replay?.user).toBeNull(); + expect(replay.isArchived).toBe(true); + expect(replay.user).toBeNull(); }); it("keeps the unknown-environment warning in notes", async () => { diff --git a/packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts b/packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts index 7c8cf5570..e009e3ef9 100644 --- a/packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts +++ b/packages/mcp-core/src/tools/catalog/support/uptime-monitors.ts @@ -232,19 +232,16 @@ export function toUptimeMonitorSummary( return { ...toUptimeMonitorListItem(monitor, webUrl), timeoutMs: monitor.timeoutMs, - ...(monitor.recoveryThreshold !== undefined && - monitor.recoveryThreshold !== null + ...(monitor.recoveryThreshold !== undefined ? { recoveryThreshold: monitor.recoveryThreshold } : {}), - ...(monitor.downtimeThreshold !== undefined && - monitor.downtimeThreshold !== null + ...(monitor.downtimeThreshold !== undefined ? { downtimeThreshold: monitor.downtimeThreshold } : {}), - ...(monitor.traceSampling !== undefined && monitor.traceSampling !== null + ...(monitor.traceSampling !== undefined ? { traceSampling: monitor.traceSampling } : {}), - ...(monitor.responseCaptureEnabled !== undefined && - monitor.responseCaptureEnabled !== null + ...(monitor.responseCaptureEnabled !== undefined ? { responseCaptureEnabled: monitor.responseCaptureEnabled } : {}), }; diff --git a/packages/mcp-core/src/tools/catalog/update-issue.ts b/packages/mcp-core/src/tools/catalog/update-issue.ts index 96832756c..d49ac1f78 100644 --- a/packages/mcp-core/src/tools/catalog/update-issue.ts +++ b/packages/mcp-core/src/tools/catalog/update-issue.ts @@ -657,7 +657,7 @@ function getReasonComment( reason: string | undefined, result: ReasonCommentResult, ): UpdateIssueOutput["reasonComment"] { - if (!reason || ("skipped" in result && result.skipped)) { + if (!reason || "skipped" in result) { return null; } if (result.posted) { @@ -679,7 +679,7 @@ function getReasonComment( * still count as different assignees. */ function getAssigneeKey(assignedTo: AssignedTo): string { - if (assignedTo && typeof assignedTo === "object" && assignedTo.id != null) { + if (assignedTo && typeof assignedTo === "object") { return `${assignedTo.type}:${assignedTo.id}`; } return `name:${formatAssignedTo(assignedTo)}`; @@ -917,16 +917,15 @@ export default defineTool({ status: statusChanged ? { from: currentStatusDisplay, to: updatedStatusDisplay } : null, - ignoreBehavior: - showIgnoreChange && updatedIgnoreState - ? { - from: - currentIgnoreState && ignoreBehaviorChanged - ? currentIgnoreState.behavior - : null, - to: updatedIgnoreState.behavior, - } - : null, + ignoreBehavior: showIgnoreChange + ? { + from: + currentIgnoreState && ignoreBehaviorChanged + ? currentIgnoreState.behavior + : null, + to: updatedIgnoreState.behavior, + } + : null, assignedTo: updateAssignedTo !== undefined && assignmentChanged ? { diff --git a/packages/mcp-core/src/tools/support/code-location.ts b/packages/mcp-core/src/tools/support/code-location.ts index c93d1159b..8f32f6f25 100644 --- a/packages/mcp-core/src/tools/support/code-location.ts +++ b/packages/mcp-core/src/tools/support/code-location.ts @@ -126,7 +126,7 @@ function findRelevantFrame(event: Event): Frame | undefined { parsed.data.values ?? (parsed.data.value ? [parsed.data.value] : []); const rootException = [...exceptions] .reverse() - .find((exception) => exception?.stacktrace?.frames?.length); + .find((exception) => exception?.stacktrace?.frames.length); if (rootException?.stacktrace?.frames) { return findMostRelevantInAppFrame(rootException.stacktrace.frames); } @@ -143,7 +143,7 @@ function findRelevantFrame(event: Event): Frame | undefined { } const crashedThread = parsed.data.values?.find( - (thread) => thread.crashed && thread.stacktrace?.frames?.length, + (thread) => thread.crashed && thread.stacktrace?.frames.length, ); if (crashedThread?.stacktrace?.frames) { return findMostRelevantInAppFrame(crashedThread.stacktrace.frames); diff --git a/packages/mcp-core/src/tools/support/event-stacktrace.ts b/packages/mcp-core/src/tools/support/event-stacktrace.ts index f54fdf503..dbbb2058b 100644 --- a/packages/mcp-core/src/tools/support/event-stacktrace.ts +++ b/packages/mcp-core/src/tools/support/event-stacktrace.ts @@ -71,7 +71,7 @@ export async function fetchAndFormatEventStacktrace({ } function getThreads(event: Event): Thread[] { - const threadsEntry = event.entries?.find((entry) => entry.type === "threads"); + const threadsEntry = event.entries.find((entry) => entry.type === "threads"); const result = ThreadsEntrySchema.safeParse(threadsEntry?.data); return result.success ? (result.data.values ?? []) : []; } @@ -138,7 +138,7 @@ function selectSentryDefaultThread(threads: Thread[]): Thread { } function hasFrames(thread: Thread): boolean { - return Boolean(thread.stacktrace?.frames?.length); + return Boolean(thread.stacktrace?.frames.length); } function selectUnique( diff --git a/packages/mcp-core/src/tools/support/issue-linking/app.ts b/packages/mcp-core/src/tools/support/issue-linking/app.ts index ce193039c..ebc66f639 100644 --- a/packages/mcp-core/src/tools/support/issue-linking/app.ts +++ b/packages/mcp-core/src/tools/support/issue-linking/app.ts @@ -150,7 +150,7 @@ async function resolveFields( form: LinkForm, fields: Map, ): Promise { - const supplied = params.fields ?? {}; + const supplied: Partial = params.fields ?? {}; const candidates = [...fields.values()].filter((field) => TARGET_FIELD.test(field.name), ); @@ -400,7 +400,7 @@ export async function unlinkAppIssue( throw new UserInputError( "Multiple App links match this URL. Provide appSlug to select one.", ); - const match = matches[0]; + const match = matches.at(0); if (!match) return { url: params.externalIssueUrl, diff --git a/packages/mcp-core/src/tools/support/issue-linking/index.ts b/packages/mcp-core/src/tools/support/issue-linking/index.ts index e3e0f7cb2..aea12ddf1 100644 --- a/packages/mcp-core/src/tools/support/issue-linking/index.ts +++ b/packages/mcp-core/src/tools/support/issue-linking/index.ts @@ -147,7 +147,7 @@ function matchesIntegration(url: URL, integration: IssueIntegration): boolean { const provider = integration.provider.key; const domain = integrationDomain(integration); const path = pathOf(url); - const owner = path.split("/")[1]; + const owner = path.split("/").at(1); switch (provider) { case "github": @@ -165,7 +165,7 @@ function matchesIntegration(url: URL, integration: IssueIntegration): boolean { // Older personal installations store only the username as domainName. const installedOwner = domain?.host === "bitbucket.org" - ? pathOf(domain).split("/")[1] + ? pathOf(domain).split("/").at(1) : integration.name; return owner?.toLowerCase() === installedOwner?.toLowerCase(); } @@ -316,7 +316,7 @@ export async function unlinkExternalIssue( `Multiple linked issues match this URL. Provide integrationId to select one: ${describeIntegrations(matches.map(({ integration }) => integration))}.`, ); } - const match = matches[0]; + const match = matches.at(0); if (!match) { // Custom Apps can own any URL, including one with a native provider shape. if (!params.integrationId) return await unlinkAppIssue(apiService, params); diff --git a/packages/mcp-core/src/tools/support/metric-monitor-config.ts b/packages/mcp-core/src/tools/support/metric-monitor-config.ts index 1c40923ce..c739ea10f 100644 --- a/packages/mcp-core/src/tools/support/metric-monitor-config.ts +++ b/packages/mcp-core/src/tools/support/metric-monitor-config.ts @@ -135,7 +135,9 @@ export function toMetricMonitorUpdate( ): MetricMonitorUpdate { const { status, query, config, conditionGroup, ...metadata } = params; const body: MetricMonitorUpdate = Object.fromEntries( - Object.entries(metadata).filter(([, value]) => value !== undefined), + Object.entries(metadata).filter( + ([, value]) => value !== undefined, + ), ); if (status !== undefined) body.enabled = status === "active"; if (query !== undefined) { diff --git a/packages/mcp-core/src/tools/support/profile/analyzer.ts b/packages/mcp-core/src/tools/support/profile/analyzer.ts index 6d54eb994..5c4166320 100644 --- a/packages/mcp-core/src/tools/support/profile/analyzer.ts +++ b/packages/mcp-core/src/tools/support/profile/analyzer.ts @@ -89,7 +89,7 @@ export function analyzeHotPathsFromFlamegraph( flamegraph: Flamegraph, options: { focusOnUserCode: boolean; maxHotPaths: number }, ): HotPath[] { - const profile = flamegraph.profiles[0]; // Main thread + const profile = flamegraph.profiles.at(0); // Main thread if (!profile) { return []; } diff --git a/packages/mcp-core/src/tools/support/profile/details-output.ts b/packages/mcp-core/src/tools/support/profile/details-output.ts index 13062213f..d7cfac680 100644 --- a/packages/mcp-core/src/tools/support/profile/details-output.ts +++ b/packages/mcp-core/src/tools/support/profile/details-output.ts @@ -147,7 +147,7 @@ function buildSampleAnalysis( const topFrames = [...frameCounts.entries()] .flatMap(([frameIndex, count]) => { - const frame = profile.frames[frameIndex]; + const frame = profile.frames.at(frameIndex); if (!frame || (options.focusOnUserCode && !frame.in_app)) { return []; } diff --git a/packages/mcp-core/src/tools/support/profile/flamegraph-output.test.ts b/packages/mcp-core/src/tools/support/profile/flamegraph-output.test.ts index bba696664..7b071deff 100644 --- a/packages/mcp-core/src/tools/support/profile/flamegraph-output.test.ts +++ b/packages/mcp-core/src/tools/support/profile/flamegraph-output.test.ts @@ -162,10 +162,12 @@ describe("buildFlamegraphAnalysis", () => { }); const [topPath] = analysis.hotPaths; - expect(topPath?.stack.map((index) => analysis.frames[index]?.file)).toEqual( - ["main.py", "utils.py", "psycopg2/pool.py"], - ); - expect(topPath?.stack.at(-1)).toBe(topPath?.leaf); + expect(topPath.stack.map((index) => analysis.frames[index]?.file)).toEqual([ + "main.py", + "utils.py", + "psycopg2/pool.py", + ]); + expect(topPath.stack.at(-1)).toBe(topPath.leaf); expect(analysis.topFunctions).toHaveLength(3); }); diff --git a/packages/mcp-core/src/tools/support/profile/flamegraph-output.ts b/packages/mcp-core/src/tools/support/profile/flamegraph-output.ts index db8d78975..859f283aa 100644 --- a/packages/mcp-core/src/tools/support/profile/flamegraph-output.ts +++ b/packages/mcp-core/src/tools/support/profile/flamegraph-output.ts @@ -169,7 +169,7 @@ export function buildFlamegraphAnalysis( flamegraph: Flamegraph, options: { focusOnUserCode: boolean; maxHotPaths: number }, ): FlamegraphAnalysis { - const mainProfile = flamegraph.profiles[0]; + const mainProfile = flamegraph.profiles.at(0); const sampleCount = sumOf(mainProfile?.sample_counts); // Split the main profile's sampled time by the leaf frame of each sample @@ -224,7 +224,7 @@ export function buildFlamegraphAnalysis( const hotPaths = analyzeHotPathsFromFlamegraph(flamegraph, options).flatMap( (path) => { // callStack is leaf-to-root; output stacks read root-to-leaf. - const leaf = path.callStack[0]; + const leaf = path.callStack.at(0); if (!leaf) { return []; } diff --git a/packages/mcp-core/src/tools/support/search-events/agent.ts b/packages/mcp-core/src/tools/support/search-events/agent.ts index 8c727cf14..b96cb6a76 100644 --- a/packages/mcp-core/src/tools/support/search-events/agent.ts +++ b/packages/mcp-core/src/tools/support/search-events/agent.ts @@ -103,7 +103,7 @@ export const searchEventsAgentOutputSchema = z } // Only validate if both sort and fields are present - if (!data.sort || !data.fields || data.fields.length === 0) { + if (!data.sort || data.fields.length === 0) { return true; } diff --git a/packages/mcp-core/src/tools/support/search-events/config.ts b/packages/mcp-core/src/tools/support/search-events/config.ts index 2e26e3af5..92d27aa42 100644 --- a/packages/mcp-core/src/tools/support/search-events/config.ts +++ b/packages/mcp-core/src/tools/support/search-events/config.ts @@ -294,7 +294,7 @@ export const BASE_COMMON_FIELDS = { }; // Known numeric fields for each dataset -export const NUMERIC_FIELDS: Record> = { +export const NUMERIC_FIELDS: Partial>> = { spans: new Set([ "span.duration", "span.self_time", diff --git a/packages/mcp-core/src/tools/support/search-events/formatters.ts b/packages/mcp-core/src/tools/support/search-events/formatters.ts index cdcb6a92f..06ae5de80 100644 --- a/packages/mcp-core/src/tools/support/search-events/formatters.ts +++ b/packages/mcp-core/src/tools/support/search-events/formatters.ts @@ -290,7 +290,7 @@ export function formatErrorResults(params: FormatEventResultsParams): string { value !== null && value !== undefined ) { - if (key === "user" && typeof value === "object" && value !== null) { + if (key === "user" && typeof value === "object") { for (const line of formatUserFieldLines( value as Record, )) { @@ -448,7 +448,7 @@ export function formatLogResults(params: FormatEventResultsParams): string { value !== null && value !== undefined ) { - if (key === "user" && typeof value === "object" && value !== null) { + if (key === "user" && typeof value === "object") { for (const line of formatUserFieldLines( value as Record, { prefix: "- " }, @@ -582,7 +582,7 @@ export function formatSpanResults(params: FormatEventResultsParams): string { value !== null && value !== undefined ) { - if (key === "user" && typeof value === "object" && value !== null) { + if (key === "user" && typeof value === "object") { for (const line of formatUserFieldLines( value as Record, )) { @@ -777,7 +777,7 @@ export function formatProfileResults(params: FormatEventResultsParams): string { value !== null && value !== undefined ) { - if (key === "user" && typeof value === "object" && value !== null) { + if (key === "user" && typeof value === "object") { for (const line of formatUserFieldLines( value as Record, )) { @@ -922,7 +922,7 @@ export function formatTraceMetricsResults( value !== null && value !== undefined ) { - if (key === "user" && typeof value === "object" && value !== null) { + if (key === "user" && typeof value === "object") { for (const line of formatUserFieldLines( value as Record, )) { diff --git a/packages/mcp-core/src/tools/support/search-events/replays.ts b/packages/mcp-core/src/tools/support/search-events/replays.ts index 7559d10f0..a88da787d 100644 --- a/packages/mcp-core/src/tools/support/search-events/replays.ts +++ b/packages/mcp-core/src/tools/support/search-events/replays.ts @@ -236,7 +236,7 @@ function buildReplayActions( availableToolNames: params.availableToolNames, directToolNames: params.directToolNames, }; - const firstReplay = replays[0]; + const firstReplay = replays.at(0); const traceId = replays.find((replay) => replay.traceIds[0])?.traceIds[0]; const actions = [ firstReplay diff --git a/packages/mcp-core/src/tools/support/search-events/search.ts b/packages/mcp-core/src/tools/support/search-events/search.ts index 2f070b83f..b0dba83d9 100644 --- a/packages/mcp-core/src/tools/support/search-events/search.ts +++ b/packages/mcp-core/src/tools/support/search-events/search.ts @@ -187,10 +187,7 @@ function formatEnvironmentFilter( return undefined; } if (environments.length === 1) { - const environmentValue = environments[0]; - return environmentValue === undefined - ? undefined - : `environment:${formatSearchValue(environmentValue)}`; + return `environment:${formatSearchValue(environments[0])}`; } return `environment:[${environments.map(formatSearchValue).join(",")}]`; } @@ -897,7 +894,7 @@ async function executeSearchEvents( // Time series requests use yAxis/interval, so sort is not required. if ( !timeSeries && - !parsed.sort?.trim() && + !parsed.sort.trim() && !(shouldTrustExplicitSearchParams && hasExplicitSort) ) { throw new UserInputError( @@ -928,7 +925,7 @@ async function executeSearchEvents( sortParam = shouldTrustExplicitSearchParams && explicitSort ? explicitSort - : parsed.sort?.trim() || defaultSortForDataset(dataset); + : parsed.sort.trim() || defaultSortForDataset(dataset); explanation = parsed.explanation; environment = params.environment ?? parsed.environment; diff --git a/packages/mcp-core/src/tools/support/search-events/utils.ts b/packages/mcp-core/src/tools/support/search-events/utils.ts index 5226b68d7..b1daef3fa 100644 --- a/packages/mcp-core/src/tools/support/search-events/utils.ts +++ b/packages/mcp-core/src/tools/support/search-events/utils.ts @@ -380,7 +380,7 @@ function searchFilterOccurrences(query: string): SearchFilterOccurrence[] { // so a structured filter rewritten into one is still a downgrade. for (const match of masked.matchAll(SENTRY_SEARCH_TOKEN_WITH_REGEX_PATTERN)) { const key = (match.groups?.regexKey ?? match.groups?.key)?.toLowerCase(); - if (!key || match.index === undefined) { + if (!key) { continue; } @@ -862,10 +862,7 @@ export async function fetchCustomAttributes( for (const attr of attributesResponse) { if (attr.key && !attr.key.startsWith("sentry:")) { customAttributes[attr.key] = { ...attr, name: attr.name || attr.key }; - // Track field type from the attribute response with validation - if (attr.type) { - fieldTypes[attr.key] = attr.type; - } + fieldTypes[attr.key] = attr.type; } } } @@ -1360,7 +1357,7 @@ export function createDatasetAttributesTool(options: { const recommendedFields = RECOMMENDED_FIELDS[normalizedDataset]; // Combine field types from both static config and dynamic API - const allFieldTypes: Record = { + const allFieldTypes: Partial> = { ...updatedFieldTypes, }; const staticNumericFields = diff --git a/packages/mcp-core/src/tools/support/search-issue-events/agent.ts b/packages/mcp-core/src/tools/support/search-issue-events/agent.ts index 37991efcf..f1f15fb84 100644 --- a/packages/mcp-core/src/tools/support/search-issue-events/agent.ts +++ b/packages/mcp-core/src/tools/support/search-issue-events/agent.ts @@ -43,7 +43,7 @@ export const searchIssueEventsAgentOutputSchema = z .refine( (data) => { // Only validate if both sort and fields are present - if (!data.sort || !data.fields || data.fields.length === 0) { + if (!data.sort || data.fields.length === 0) { return true; } diff --git a/packages/mcp-core/src/tools/support/search-issue-events/results.ts b/packages/mcp-core/src/tools/support/search-issue-events/results.ts index 0a7e9a5c7..97165da00 100644 --- a/packages/mcp-core/src/tools/support/search-issue-events/results.ts +++ b/packages/mcp-core/src/tools/support/search-issue-events/results.ts @@ -186,7 +186,7 @@ export function buildIssueEventResults( }); } - const first = events[0]; + const first = events.at(0); const action = first ? buildSuggestedToolAction({ toolName: "get_sentry_resource", diff --git a/packages/mcp-core/src/tools/support/search-issues/results.ts b/packages/mcp-core/src/tools/support/search-issues/results.ts index 8ae5eeab3..5c6d0d52d 100644 --- a/packages/mcp-core/src/tools/support/search-issues/results.ts +++ b/packages/mcp-core/src/tools/support/search-issues/results.ts @@ -157,7 +157,7 @@ export function buildIssueResults( issue.seerFixabilityScore == null ? null : getSeerActionabilityLabel(issue.seerFixabilityScore), - projectSlug: issue.project?.slug ?? null, + projectSlug: issue.project.slug, })); return structuredResult({ @@ -187,7 +187,7 @@ function buildIssueActions( issues: SearchIssuesStructuredContent["issues"], params: BuildIssueResultsParams, ): SuggestedToolAction[] { - const first = issues[0]; + const first = issues.at(0); const feedback = issues.find((issue) => issue.issueCategory === "feedback"); const targets = [ first && first !== feedback diff --git a/packages/mcp-core/src/utils/url-utils.ts b/packages/mcp-core/src/utils/url-utils.ts index 6bbfb0885..4592507ef 100644 --- a/packages/mcp-core/src/utils/url-utils.ts +++ b/packages/mcp-core/src/utils/url-utils.ts @@ -674,7 +674,7 @@ export function extractConversationIdFromSearchQuery( return undefined; } - const conversationId = matches[0][1] ?? matches[0][2]; + const conversationId = matches[0].at(1) ?? matches[0][2]; if (conversationId.startsWith("[")) { return undefined; } diff --git a/packages/mcp-core/src/version.ts b/packages/mcp-core/src/version.ts index 7fbf5b582..eba47fe32 100644 --- a/packages/mcp-core/src/version.ts +++ b/packages/mcp-core/src/version.ts @@ -1,5 +1,5 @@ export const LIB_VERSION = - (typeof process !== "undefined" && process.env?.npm_package_version) || + (typeof process !== "undefined" && process.env.npm_package_version) || "0.0.0"; let hostVersion: string | undefined;