Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
{
"files": ["packages/mcp-core/**/*.{ts,tsx,js,mjs,cjs}"],
"rules": {
"typescript/no-unnecessary-condition": "error",
"no-restricted-imports": [
"error",
{
Expand Down
4 changes: 2 additions & 2 deletions packages/mcp-core/scripts/generate-definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function ensureDirExists(dir: string) {
function zodFieldMapToJsonSchema(
fieldMap: Record<string, ZodTypeAny>,
): 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",
Expand Down Expand Up @@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-core/scripts/generate-otel-namespaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-core/scripts/measure-token-cost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ type ToolDefinition = {
*/
function formatToolsForMCP(tools: Record<string, ToolDefinition>) {
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)
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-core/src/api-client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? "",
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-core/src/api-client/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1070,7 +1070,7 @@ const BaseEventSchema = z.object({
]),
),
contexts: z
.record(
.partialRecord(
z.string(),
z
.object({
Expand Down
13 changes: 5 additions & 8 deletions packages/mcp-core/src/internal/agents/callEmbeddedAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
}
},
});
Expand All @@ -184,7 +182,6 @@ export async function callEmbeddedAgent<

if (
typeof rawOutput === "object" &&
rawOutput !== null &&
"error" in rawOutput &&
typeof (rawOutput as { error?: unknown }).error === "string"
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ import webengine from "./data/webengine.json";
import zos from "./data/zos.json";

// Create the namespaceData object
const namespaceData: Record<string, NamespaceData> = {
const namespaceData: Partial<Record<string, NamespaceData>> = {
android,
app,
artifact,
Expand Down
4 changes: 2 additions & 2 deletions packages/mcp-core/src/internal/blob-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ interface PreviewCodec {
encode: (image: RgbaImage, options: { jpegQuality: number }) => Uint8Array;
}

const PREVIEW_CODECS: Record<string, PreviewCodec> = {
const PREVIEW_CODECS: Partial<Record<string, PreviewCodec>> = {
"image/png": {
contentType: "image/png",
readDimensions: readPngDimensions,
Expand Down Expand Up @@ -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) {
Expand Down
26 changes: 8 additions & 18 deletions packages/mcp-core/src/internal/formatting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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} |`;
}),
];
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -1176,7 +1166,7 @@ function convertTraceSpanToPerformanceSpan(
level,
};

for (const child of span.children ?? []) {
for (const child of span.children) {
if (!isTraceSpan(child)) {
continue;
}
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -2150,7 +2140,7 @@ function formatContexts(contexts: z.infer<typeof EventSchema>["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)}`;
Expand Down Expand Up @@ -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;
}
Expand Down
5 changes: 0 additions & 5 deletions packages/mcp-core/src/internal/issue-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,6 @@ export async function mapCredentialContexts<T>(
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ function formatRetry(
): string {
const { organizationSlug: _omitted, ...rest } = rawParams;
const args = Object.fromEntries(
Object.entries({ organizationSlug, ...rest }).filter(
Object.entries<unknown>({ organizationSlug, ...rest }).filter(
([key, value]) =>
value !== undefined && !isDefaultValue(tool, key, value),
),
Expand Down Expand Up @@ -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 (
Expand Down
7 changes: 1 addition & 6 deletions packages/mcp-core/src/server.metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
18 changes: 9 additions & 9 deletions packages/mcp-core/src/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" }],
});
Expand Down Expand Up @@ -757,7 +757,7 @@ describe("buildServer", () => {
}
)._registeredTools;

await registeredTools.example_tool?.handler({}, {});
await registeredTools.example_tool.handler({}, {});

expect(setUser).toHaveBeenCalledWith({
id: "user-123",
Expand Down Expand Up @@ -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",
Expand All @@ -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",
);
});
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 0 additions & 5 deletions packages/mcp-core/src/telem/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-core/src/telem/unknown-tool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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" });
});
2 changes: 1 addition & 1 deletion packages/mcp-core/src/test-utils/dataset-search-tool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export async function inspectDatasetSearchTool<P>(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,
Expand Down
2 changes: 1 addition & 1 deletion packages/mcp-core/src/test-utils/structured-content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
8 changes: 5 additions & 3 deletions packages/mcp-core/src/tools/catalog-runtime/availability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,11 @@ export function getFilteredInputSchema(
([key]) => !constraintKeysToFilter.has(key),
),
) as Record<string, z.ZodType>;
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;
}
Expand Down
Loading
Loading