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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 37 additions & 3 deletions docs/contributing/tool-responses.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,32 @@ The event API's legacy `context` field (extra data) can be `null`. Issue details
must still load; omit the `Extra Data` section for null or empty values while
preserving the event's error, stacktrace, and structured `contexts` data.

In experimental mode `get_issue_details` returns `structuredContent` for every
event type, including types the markdown path does not support, and declares
`outputSchema` to experimental sessions only (`experimentalOutputSchema`).
`event` maps the entries into fields instead of rendering markdown: the
exception chain (the raised exception first), up to 20 thread summaries with a
`threadCount`, the crashed thread when there is no exception, request method and
URL, CSP report, N+1 evidence and spans, metric alert, evidence, user (including
email and IP), tags, extra data, and contexts. Frames use the shared shape in
`tools/support/structured-stacktrace.ts`: most recent call last, with only the
fields that have a value (`inApp` only when true, `absPath` only when it differs
from `filename`). Every frame gets its `contextLine`. Only the most relevant
frame (the last in-app frame of the raised exception or the thread) gets
`preContext`, `postContext` (3 lines each) and `vars`, with each variable cut
to 200 characters of JSON. Request headers, cookies and body, frame addresses, symbols
and source links, tag search queries, and external issue link ids stay out.

### Event Stacktrace

`get_event_stacktrace` returns one thread under `thread`, using the same frame
and thread shapes as `get_issue_details`, with `selection.status` saying how the
`thread` selector resolved. When the selector matches no thread or several,
`thread` is null, `selection.message` says why, and `availableThreads` lists
every thread summary. Only the thread's most relevant frame (its last in-app
frame) gets pre/post context and variables. Held locks,
registers and the raw (unsymbolicated) stacktrace stay out.

### Profile Details

`get_profile_details` returns one profile summary under `profile`,
Expand Down Expand Up @@ -209,10 +235,15 @@ Policy (encoded in `packages/mcp-core/src/internal/tool-helpers/results.ts`):
`content` as compact JSON (no indentation) of that same payload. Most
clients that read the text send it to a model, and indentation adds tokens
without adding data.
- Markdown-only tools (for example issue and trace details today) should return
- Markdown-only tools (for example trace details today) should return
markdown and omit `structuredContent` until they have a real structured
payload. Do not attach sparse structured side-channels such as
`suggestedActions` alone on top of markdown.
- A tool whose structured payload is still behind experimental mode declares
`experimentalOutputSchema` instead. The server advertises it to experimental
sessions only, and tsc cannot check the handler against it. The test guard
checks every `structuredContent` result against it, and the tool's tests
must cover every structured path.
- If a tool declares `outputSchema`, every successful `structuredContent` result
must conform to that schema. `defineTool` enforces this at compile time: the
handler must return `structuredResult(...)` typed as the schema's `z.output`,
Expand Down Expand Up @@ -280,8 +311,11 @@ reason:
- `OUTPUT_SCHEMA_PENDING` for tools whose `outputSchema` has not landed, with
the PR, for example `"pending getsentry/toolkit#2004"`.

An entry fails once its tool has an `outputSchema`, so the PR that adds one
also deletes the entry. To add a `_meta` key, add it to `ALLOWED_TOOL_META`
An `OUTPUT_SCHEMA_EXEMPT` entry fails once any session lists its tool with an
`outputSchema`. An `OUTPUT_SCHEMA_PENDING` entry fails once every session that
lists the tool has one, so a tool whose `experimentalOutputSchema` reaches
experimental sessions first stays pending until it graduates. The PR that
makes an entry stale also deletes it. To add a `_meta` key, add it to `ALLOWED_TOOL_META`
with a comment that says which client reads it.

## Suggested Actions
Expand Down
31 changes: 14 additions & 17 deletions packages/mcp-core/src/api-client/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,18 @@ export const FrameInterface = z
})
.partial();

const StacktraceSchema = z
.object({
frames: z.array(FrameInterface),
framesOmitted: z.array(z.unknown()).nullable().optional(),
registers: z.record(z.string(), z.unknown()).nullable().optional(),
hasSystemFrames: z.boolean().nullable().optional(),
})
.partial()
.extend({
frames: z.array(FrameInterface),
});

// XXX: Sentry's schema generally speaking is "assume all user input is missing"
// so we need to handle effectively every field being optional or nullable.
export const ExceptionInterface = z
Expand All @@ -920,11 +932,8 @@ export const ExceptionInterface = z
.nullable(),
type: z.string().nullable(),
value: z.string().nullable(),
stacktrace: z
.object({
frames: z.array(FrameInterface),
})
.nullable(),
module: z.string().nullable(),
stacktrace: StacktraceSchema.nullable(),
})
.partial();

Expand Down Expand Up @@ -955,18 +964,6 @@ export const MessageEntrySchema = z
})
.partial();

const StacktraceSchema = z
.object({
frames: z.array(FrameInterface),
framesOmitted: z.array(z.unknown()).nullable().optional(),
registers: z.record(z.string(), z.unknown()).nullable().optional(),
hasSystemFrames: z.boolean().nullable().optional(),
})
.partial()
.extend({
frames: z.array(FrameInterface),
});

export const ThreadEntrySchema = z
.object({
id: z.union([z.number(), z.string()]).nullable(),
Expand Down
Loading
Loading