Repository navigation
feat(tools): Type handler results from outputSchema - #2035
Merged
Merged
Conversation
Make the output schema a compile-time contract. `ToolConfig` and `defineTool` are now generic over `outputSchema`, and a handler on a tool that declares one must return `structuredResult(...)` typed as the schema's `z.output`. Markdown and raw content only type-check on tools without a schema. `structuredResult` now takes its type from where the result is returned instead of inferring it from the payload, so object literals get excess-property checks against the schema. The change exposed two drifts on main, both fixed here: - search_errors: the shared search builders typed row links as a loose record, not the dataset's links schema. The builders and `runSearchEvents` are now generic over the dataset, and the content type is derived from `buildSearchEventsOutputSchema`. - search_sentry_tools: its result helper had no return type. Co-Authored-By: David Cramer <23610+dcramer@users.noreply.github.com>
Name the schema-derived per-dataset payload DatasetSearchStructuredContent and keep SearchEventsStructuredContent as the loose shared shape. Dataset configs that read rows through it would otherwise make the derived type circular. Co-Authored-By: David Cramer <23610+dcramer@users.noreply.github.com>
Build both the shared and the per-dataset search output schemas from one factory. The builders return the shared payload, which is now schema-derived, and runSearchEvents narrows it to the locked dataset's schema. This keeps the builders non-generic, so adding datasets to STRUCTURED_DATASETS needs no casts. Co-Authored-By: David Cramer <23610+dcramer@users.noreply.github.com>
A schema tool could return its own `content` next to a valid payload, and the server passes a CallToolResult through unchanged. Mark `content` as never on StructuredToolOutput and cover it in the type test. Also state that excess-property checks only cover fresh literals, that output schemas must not use transforms, and that the dataset search overload is an unchecked narrowing. Co-Authored-By: David Cramer <23610+dcramer@users.noreply.github.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
sentry-junior Bot
added a commit
that referenced
this pull request
Nov 5, 2026
Resolve the runSearchEvents overload conflict with #2035: keep the replays overload and use the generic runSearchEvents<TDataset> one in place of the PublicEventsDataset overload.
sentry-junior Bot
added a commit
that referenced
this pull request
Nov 5, 2026
Resolve the ToolConfig conflict with #2035: keep the _meta field and the outputSchema-typed handler.
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Make
outputSchemaa compile-time contract instead of a runtime-only check. Before this,ToolConfig.outputSchemawas a plainz.ZodType,structuredResult<T>inferredTfrom whatever payload it got, and nothing tied a handler's return type to its schema. Tools that wanted a check addedsatisfies z.infer<...>by hand, and some didn't.What changes
ToolConfig<TSchema, TOutputSchema>anddefineToolare generic overoutputSchema. When a tool sets one, its handler must returnStructuredToolOutput<z.output<TOutputSchema>>. Strings, markdown and raw MCP content only type-check on tools without a schema. Tool definition sites need no annotations, because the schema type is inferred from theoutputSchemaproperty.StructuredToolOutputhascontent?: never, so a schema tool can't ship its owncontentnext to the payload. The server passes aCallToolResultthrough unchanged, so that content would otherwise reach clients as is.execute_sentry_tool,get_sentry_resource).ToolOutputFordistributes over a union, so theToolConfig<any>catalog and registry types compile unchanged.structuredResult(structuredContent: NoInfer<T>)now takesTfrom the return context, not from the payload. Missing and mistyped fields fail tsc. Extra fields are only caught in fresh object literals; spread or variable payloads (structuredResult({ ...upstream }),structuredResult(payload)) are not checked for them, so map fields explicitly. Shared helpers that build results now declare their return type.Why
z.outputand notz.inputIn
@modelcontextprotocol/server2.4.0,validateToolOutputvalidatesstructuredContentagainst the schema and then sends the raw payload, not the parse result. The schema is advertised to clients withstandardSchemaToJsonSchema(schema, "output"). So the payload has to already be in the schema's output form.z.inputwould accept payloads that rely on defaults, and clients would receive something that doesn't match the advertised schema. Output schemas must not use transforms, because the SDK can't advertise them as JSON Schema.Shared helpers typed from the schema
search_sentry_tools:createSearchToolsResultnow declaresStructuredToolOutput<SearchToolsOutput>.support/search-events/structured.ts): the hand-writtenSearchEventsStructuredContentinterface is replaced by a type derived from the same schema factory that builds each dataset'soutputSchema, so the builders can't drift from it.DatasetSearchStructuredContent<TDataset>is the per-dataset payload.runSearchEvents<TDataset>overload narrows to it. The overload is documented as an unchecked narrowing: the builders return loose row links, and it holds because structured runs lock the dataset and each row's links come from that dataset'sdefineDatasetConfig.STRUCTURED_DATASETSneeds no casts.Type test
src/internal/tool-helpers/define.test.tsusesexpectTypeOffor the happy path and six@ts-expect-errorcases:contentI checked that each one fails tsc for the intended reason when its directive is removed.
Docs
docs/contributing/tool-responses.mddescribes the guarantee, the fresh-literal caveat, and the rule that shared builders declare their return type. It also says not to add per-toolsatisfieschecks.Checks (all with
SENTRY_AUTH_TOKENunset, package-scoped, after merging main964342c):generate-definitions(no diff)Review
An independent reviewer passed this branch after its one must-fix (the
content?: neverabove) was applied.Open PR compatibility
I merged an earlier head of this branch locally into each open outputSchema / structured-output PR and ran mcp-core tsc. All of them passed except one conflict:
search.tson therunSearchEventsoverloads. To resolve it, keep feat(mcp): Return structured output from search_replays #2025'sdataset: "replays"overload and replace itsPublicEventsDatasetoverload with the genericrunSearchEvents<TDataset>one from this PR. With that, tsc passes.SearchReplaysStructuredContentis already derived from its schema, sobuildStructuredReplayResultsneeds no change.Follow-ups (not in this PR)
satisfieschecks are now redundant and can be removed later:get-replay-details.ts(satisfies GetReplayDetailsPayload) andupdate-issue.ts(twosatisfies UpdateIssueOutput).value || null), and nullable user/geo mapping (null when every field is empty). Both repeat across the replay, issue-event and profile tools.via @dcramer.
--
View Session [Sentry]