Skip to content

fix(plugins): load_skill's outputSchema describes a loaded skill once - #1141

Merged
alexmercerpo merged 4 commits into
fix/cli-skilled-messagesfrom
fix/skilled-load-skill-output-schema
Nov 5, 2026
Merged

alexmercerpo merged 4 commits into
fix/cli-skilled-messagesfrom
fix/skilled-load-skill-output-schema

Conversation

@frontegg-david

Copy link
Copy Markdown
Contributor

Fixes #1126.

Stacks on #1140.

What and why

load_skill used one loaded-skill schema in two places of its output schema: skill (for skillId) and skills[].skill (for skillIds). tools/list wrote that object out in full both times, with no $ref. Every Skilled OpenAPI server sent the same 866 bytes twice. (It was 812 bytes in 1.11.1-rc.1; this stack's asksForConfirmation marker added 54.)

The fix takes the issue's first option, define once and reference, because it doesn't change what load_skill returns:

  • The loaded-skill schema is registered with an id: .meta({ id: 'LoadedSkill' }). zod's toJSONSchema then writes it once, under $defs.LoadedSkill, and both places point to it with { "$ref": "#/$defs/LoadedSkill" }.
  • The change is in the plugin only. The SDK already lists an output schema whose parts are $defs references, and it already has tests for that. Passing reused: 'ref' in the SDK instead would change the listed schema of every tool that reuses a sub-schema.
  • zod 4.6 doesn't throw when an id is registered twice, so loading the plugin twice in one process (CJS and ESM, or a test that resets modules) is safe.
  • The second option, one result shape for both inputs, isn't needed. It would change the result skillId callers get.

What reads the listed schema, and how it handles $defs / $ref:

  • The MCP client (Client from the upstream SDK, through @frontmcp/protocol) compiles each outputSchema with Ajv and checks structuredContent on every callTool. Ajv resolves the local $ref, and it rejects a nested skill that breaks the definition.
  • Elicitation wraps the schema in oneOf. extendOutputSchemaForElicitation already keeps $defs at the root, where #/$defs/... resolves.
  • The undeclaredKeys: 'keep' policy walks the whole schema, $defs included.
  • The answer budget's check (z.fromJSONSchema) resolves the reference too.
  • The 'description' output-schema mode renders skill as object, as it did before.
  • Whole-schema copies keep $defs with the rest: a lazy app's placeholder tools (built from the build manifest), codecall's describe and getTool, and the skills HTTP pages.
  • Readers that ignore output schemas: the generated CLI keeps only inputSchema from tools/list, and the webmcp bridge copies only inputSchema.

Scenarios covered:

  • the listed schema describes the skill once;
  • it is smaller than before;
  • skillId and skillIds answers validate, with and without elicitation;
  • a skill that breaks the referenced definition is refused.

User-facing change

  • What changed: In tools/list, load_skill's outputSchema describes a loaded skill once, under $defs.LoadedSkill. properties.skill and properties.skills.items.properties.skill are now { "$ref": "#/$defs/LoadedSkill" }. What load_skill returns doesn't change.
  • Before → after (bytes of JSON, with the issue's one-skill bundle):
    • load_skill's outputSchema: 2,118 → 1,337 (−781, −37%);
    • load_skill in tools/list: 3,725 → 2,944;
    • the whole tools/list: 9,723 → 8,942 (−8%).
    • The size doesn't depend on the bundle.
    • A client whose validator can't resolve a local $ref in an outputSchema would now fail to check load_skill's answers. The MCP TypeScript SDK's client resolves it, and $defs / $ref are core keywords of JSON Schema 2020-12, the dialect MCP assumes.
  • frontmcp.dev pages affected (if known): reference/plugins/skilled-openapi, if it shows load_skill's listed schema.
  • Skills catalog entries updated: none. No catalog entry shows load_skill's schema. The plugin README's load_skill row now says the skill is described once, under $defs.

Tests

Added load-skill.output-schema.spec.ts (8 tests). It connects the MCP client to a two-skill server over the in-memory transport and reads load_skill from tools/list:

  • skill and skills[].skill are the same $ref, $defs holds only LoadedSkill, and the action object is written once;
  • the schema is under 2,118 bytes and within a 1,337-byte budget;
  • with and without elicitation: { enabled: true }:
    • the skillId and skillIds answers pass the client's own check on callTool;
    • AjvJsonSchemaValidator from @frontmcp/protocol finds them valid;
    • a skill under skills[] with no instructions is refused, which shows the reference is followed.

The two schema tests fail on the old schema. The validation tests pass on both.

Run:

  • nx run-many -t build -p sdk,plugin-skilled-openapi,testing: passed.
  • nx test plugin-skilled-openapi: 54 suites, 852 tests passed.
  • nx test skills: 3 suites, 249 tests passed.
  • nx test demo-e2e-skilled-openapi: 11 suites, 99 tests passed.
  • nx run plugin-skilled-openapi:typecheck and tsc --noEmit -p plugins/plugin-skilled-openapi/tsconfig.lib.json: passed.
  • prettier and eslint on the changed files: clean.

Not run, since nothing they cover changed:

  • nx test sdk and the bundle-size e2e: the SDK is unchanged;
  • demo-e2e-cli-exec: the generated CLI doesn't read output schemas.

load_skill's output schema used the loaded-skill object for both `skill`
(skillId) and `skills[].skill` (skillIds), and tools/list wrote that
object out twice with no $ref: 866 bytes, twice, on every Skilled
OpenAPI server.

The loaded-skill schema is now registered with an id, so zod writes it
once under `$defs.LoadedSkill` and both places refer to it with `$ref`.
The SDK already lists such schemas, keeps `$defs` at the root when
elicitation wraps the schema in `oneOf`, and the MCP client's Ajv
resolves the reference. What load_skill returns does not change.

load_skill's outputSchema goes from 2,118 to 1,337 bytes, and tools/list
from 9,723 to 8,942 bytes with a one-skill bundle.
@coderabbitai

coderabbitai Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Your organization has reached its usage spending cap. Adjust your spending cap in the billing tab.

Next included review available in 28 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

Learn how review limits work.

Review configuration:

Run configuration

Commits

Reviewing files that changed from the base of the PR and between f9aa965 and 0c74867.

Files selected for processing: 3

  • plugins/plugin-skilled-openapi/README.md
  • plugins/plugin-skilled-openapi/src/__tests__/load-skill.output-schema.spec.ts
  • plugins/plugin-skilled-openapi/src/tools/load-skill.schema.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Performance Test Results

Status: ✅ All tests passed

Summary

Project Tests Passed Warnings Failed Leaks
✅ demo-e2e-agents 4 4 0 0 0
✅ demo-e2e-cache 11 11 0 0 0
✅ demo-e2e-codecall 4 4 0 0 0
✅ demo-e2e-config 4 4 0 0 0
✅ demo-e2e-direct 3 3 0 0 0
✅ demo-e2e-elicitation 1 1 0 0 0
✅ demo-e2e-errors 4 4 0 0 0
✅ demo-e2e-hooks 3 3 0 0 0
✅ demo-e2e-multiapp 4 4 0 0 0
✅ demo-e2e-notifications 3 3 0 0 0
✅ demo-e2e-openapi 2 2 0 0 0
✅ demo-e2e-providers 4 4 0 0 0
✅ demo-e2e-public 4 4 0 0 0
✅ demo-e2e-redis 15 15 0 0 0
✅ demo-e2e-remember 4 4 0 0 0
✅ demo-e2e-remote 5 5 0 0 0
✅ demo-e2e-serverless 2 2 0 0 0
✅ demo-e2e-skills 15 15 0 0 0
✅ demo-e2e-standalone 2 2 0 0 0
✅ demo-e2e-transport-recreation 3 3 0 0 0
✅ demo-e2e-ui 4 4 0 0 0

Total: 101 tests across 21 projects

📊 View full report in workflow run


Generated at: 2026-11-05T17:52:51.940Z
Commit: e9651606

@alexmercerpo
alexmercerpo merged commit 470010e into release/1.11.x Nov 5, 2026
46 checks passed
@alexmercerpo
alexmercerpo deleted the fix/skilled-load-skill-output-schema branch November 5, 2026 17:54
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Cherry-picked with its stack

This PR is part of stack #1121. Its cherry-pick to main is made as one PR for the whole stack when the stack's top PR, #1142, merges.

If #1142 does not merge, run the Cherry-pick to main workflow by hand with stack: 1121.

frontegg-david added a commit that referenced this pull request Nov 5, 2026
* Cherry-pick: fix(adapters,plugins,edge): an older saas bundle, pulled or pushed, doesn't roll the server back

Cherry-picked from #1119 (merged to release/1.11.x)
Original commit: d88f422

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk,plugins): an @app's Skilled OpenAPI credential resolver reaches execute_action and this.callTool() of an operation

Cherry-picked from #1120 (merged to release/1.11.x)
Original commit: 00a3b00

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(adapters,plugins,edge): a push token replayed to another replica or Worker isolate is refused

Cherry-picked from #1122 (merged to release/1.11.x)
Original commit: 1341026

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(plugins): a skill a skills:filter hook hides can't be run by naming its actions

Cherry-picked from #1123 (merged to release/1.11.x)
Original commit: a081fbf

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk): a plugin listed twice in one plugins list is refused at startup instead of keeping only its last entry

Cherry-picked from #1127 (merged to release/1.11.x)
Original commit: 3dbadd3

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(plugins,edge): a Cron run that doesn't apply its bundle fails, and /readyz says the bundle served is stale

Cherry-picked from #1128 (merged to release/1.11.x)
Original commit: 4938d21

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk): connect() with an authToken connects an anonymous caller again on a server without auth

Cherry-picked from #1129 (merged to release/1.11.x)
Original commit: 3d238da

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(auth,adapters,plugins,edge): operators can tell why a pull or push token was refused, while callers still get the generic answer

Cherry-picked from #1130 (merged to release/1.11.x)
Original commit: 913f4f5

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk,auth,plugins): this.authProviders works: the SDK wires the AuthProviders vault from a new authProvidersVault option

Cherry-picked from #1131 (merged to release/1.11.x)
Original commit: e14c30c

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk,auth): /oauth/connect connects a verified caller who has no vault yet, and asks an anonymous one to sign in

Cherry-picked from #1132 (merged to release/1.11.x)
Original commit: f46a87a

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk,plugins): the SDK skill surfaces say a plugin's skills haven't loaded yet, retryable, and carry bundleVersion

Cherry-picked from #1133 (merged to release/1.11.x)
Original commit: 0d1dc94

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk,auth,cli,plugins): the credentials-required error keeps its connect link out of the message, and asks through URL elicitation where the client supports it

Cherry-picked from #1134 (merged to release/1.11.x)
Original commit: 6d8ea24

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk): a health check registered after startup runs in /readyz, and registerHealthProbe returns a remover

Cherry-picked from #1135 (merged to release/1.11.x)
Original commit: 7f2f48f

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk,plugins): a bundle swap sends one list_changed per list it changed, not one per entry

Cherry-picked from #1136 (merged to release/1.11.x)
Original commit: 0296291

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(adapters,plugins,edge): a restarted server serves its cached bundle in place of an older or refused pull, and doesn't write it back

Cherry-picked from #1137 (merged to release/1.11.x)
Original commit: 358d114

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk,adapters,plugins,edge): a push token with a body_sha256 claim can't carry another bundle

Cherry-picked from #1138 (merged to release/1.11.x)
Original commit: b870325

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(plugins,edge): managed mode takes confirm, and the audit log says whether the user confirmed a call

Cherry-picked from #1139 (merged to release/1.11.x)
Original commit: 588289f

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(cli): the generated CLI says unknown command, prints an MCP error prefix once, and offers load_skill only the run lines it has

Cherry-picked from #1140 (merged to release/1.11.x)
Original commit: 4cfe9a2

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(plugins): load_skill's outputSchema describes a loaded skill once

Cherry-picked from #1141 (merged to release/1.11.x)
Original commit: 470010e

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

* Cherry-pick: fix(sdk,cli): skills/load's default answer sends formattedContent again, as 1.11.0 did; format: 'markdown' is the opt-in for the smaller one

Cherry-picked from #1142 (merged to release/1.11.x)
Original commit: a680fe7

Co-Authored-By: frontegg-david <69419539+frontegg-david@users.noreply.github.com>

---------

Co-authored-by: agentfront[bot] <agentfront[bot]@users.noreply.github.com>
Co-authored-by: frontegg-david <69419539+frontegg-david@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants