## Summary
Closes
[PLEN-2451](https://linear.app/composio/issue/PLEN-2451/mastraprovider-dollardefsdollarref-schema-is-dangling-at-the-api).
Fixes https://github.com/ComposioHQ/composio/issues/3307.
The Composio API ships some tools (e.g. `GMAIL_FETCH_EMAILS`) with
`outputParameters` like:
```jsonc
{
"type": "object",
"properties": {
"data": { "$ref": "#/$defs/FetchEmailsResponse" }, // ← points into $defs
"error": { "type": "string" },
"successful": { "type": "boolean" }
},
"required": ["data", "successful"]
// ← no `$defs` block anywhere
}
```
Since [PLEN-2244](https://linear.app/composio/issue/PLEN-2244)
introduced strict `dereferenceJsonSchema`, this caused
`composio.tools.get(...)` to throw `JsonSchemaRefResolutionError: Cannot
resolve $ref #/$defs/FetchEmailsResponse` upfront via
`MastraProvider.wrapTool`, making every Gmail / Slack / Google Calendar
tool unusable through the Mastra integration.
### Approach
Two-tier fix that keeps the architectural boundary from PLEN-2244 (only
the provider that owns the validation contract pre-processes the
schema):
1. **`@composio/core`** — add an additive opt-in to
`dereferenceJsonSchema`:
```ts
dereferenceJsonSchema(schema, {
onUnresolved: 'sentinel', // default 'throw' unchanged
onReplace: (ref, reason) => { /* observe */ },
})
```
In `'sentinel'` mode the unresolvable branch is replaced with the same
`{ type: 'object', additionalProperties: true }` cycle-break sentinel
the resolver already uses for `$ref` cycles. Safety caps
(`MAX_REF_CHAIN_DEPTH`, `MAX_NODE_DEPTH`) keep throwing in both modes.
Default behavior is byte-for-byte unchanged — first-party / custom-tool
authors who typo a `$ref` still see a hard error.
2. **`@composio/mastra`** — opt `wrapTool` into sentinel mode for both
`inputParameters` and `outputParameters` and emit one `logger.warn` per
`(toolSlug, ref)` pair via a provider-scoped dedup `Set`. The warning
names the tool / toolkit / pointer and links to
https://github.com/ComposioHQ/composio/issues/3307.
### Why not centralize the dereferencing in `Tools.ts`?
- Mastra is the only provider that wraps `outputParameters`. Vercel /
OpenAI / Anthropic / Google bind tool input schemas to function-calling
APIs that ignore output shapes.
- Centralized dereferencing would silently mutate `tools.get()`'s public
output shape for callers iterating raw schemas.
- PLEN-2244 set the precedent — the provider that owns the validation
contract pre-processes its own schemas. This PR mirrors that boundary.
### API-side follow-up
The proper long-term fix is for the Composio API to emit a top-level
`$defs` block alongside any `$ref` it produces. That belongs in a
separate backend ticket — the SDK fix is intentionally independent so we
don't block on a backend rollout. The SDK now degrades gracefully
regardless of which API version it talks to.
## Test plan
Red-green TDD — every new test was confirmed red against the pre-fix
`dereferenceJsonSchema` / `MastraProvider`, then green after the fix.
- [x] **Core** — 9 new cases in
`ts/packages/core/test/utils/jsonSchema.test.ts` under `describe("with
onUnresolved: 'sentinel' lenient mode")` covering: missing-target,
missing-key, malformed-pointer (non-`#/` ref), mixed schema (resolvable
+ dangling siblings), `onReplace` reasons, safety-cap-still-throws
(chain & node depth), explicit `'throw'` matches default, `onReplace`
not called in strict mode.
- [x] **Mastra** — new file
`ts/packages/providers/mastra/test/mastra-dangling-defs.test.ts` (6
cases, un-mocked `@mastra/schema-compat`): replicates the exact
`GMAIL_FETCH_EMAILS.outputParameters` shape; verifies no throw, no
surviving `$ref` in the produced schema, exactly one warn per `(slug,
ref)` pair across repeat wraps, a separate warn per slug, symmetric
tolerance on `inputParameters`, and a regression guard for resolvable
`$defs` (PLEN-2244 contract).
- [x] **Regression** — all 908 core tests, all 39 existing
`mastra.test.ts` cases, and all 3 `mastra-ref.test.ts` cases continue to
pass. Full TS workspace test run: 14 packages green, 1322 tests total.
- [x] `pnpm typecheck` passes in both `@composio/core` and
`@composio/mastra`.
- [x] `pnpm lint` on touched files produces no new warnings.
- [ ] **Manual** (post-merge, against live API):
`composio.tools.get(userId, { tools: ['GMAIL_FETCH_EMAILS'] })` returns
a `MastraTool` whose `outputSchema` is defined; SDK logs exactly one
warning per process per `(slug, ref)` pair.
## Files
```
ts/packages/core/src/index.ts # new type exports
ts/packages/core/src/utils/jsonSchema.ts # opts + lenient branch
ts/packages/core/test/utils/jsonSchema.test.ts # 9 new cases
ts/packages/providers/mastra/src/index.ts # opt-in + dedup warn
ts/packages/providers/mastra/test/mastra-dangling-defs.test.ts # 6 new cases
.changeset/fix-mastra-dangling-defs-ref.md # core/mastra patch
plans/fix-mastra-dangling-defs-ref.md # design + acceptance log
```
---------
Co-authored-by: jkomyno <12381818+jkomyno@users.noreply.github.com>