Skip to content

fix(plugins,edge): declare @enclave-vm/ast ^2.17.0 where the bundled ReDoS check imports it - #953

Merged
frontegg-david merged 1 commit into
release/1.10.xfrom
fix/1.10.x-declare-enclave-ast
Oct 21, 2026
Merged

frontegg-david merged 1 commit into
release/1.10.xfrom
fix/1.10.x-declare-enclave-ast

Conversation

@frontegg-david

@frontegg-david frontegg-david commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

What and why

In 1.10.9, @frontmcp/plugin-cache, @frontmcp/plugin-dashboard and @frontmcp/edge fail to load when a project's top-level @enclave-vm/ast is older than 2.17.0:

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './redos' is not defined by "exports" in node_modules/@enclave-vm/ast/package.json imported from node_modules/@frontmcp/plugin-cache/esm/index.mjs

frontmcp.dev hit it while moving to 1.10.9 (run 39652798919), because it pins Enclave ^2.16.0 itself.

Why:

  • They bundle utils' ReDoS code. These three packages bundle @frontmcp/utils into their own builds; their esbuild external lists don't include it. Since perf(utils): take the ReDoS analyzer from @enclave-vm/ast/redos, which loads none of the parser #921, utils' ReDoS check imports @enclave-vm/ast/redos, an entry point added in Enclave 2.17.0, so their builds import it too.
  • They don't declare @enclave-vm/ast. So Node resolves whatever copy sits at the top of node_modules.
  • Most installs work. On a fresh install that copy is 2.17.0, pulled in by @frontmcp/utils.
  • Some break. A project that also uses Enclave 2.16 gets 2.16.0 at the top level, which has no ./redos.

What changed

  • package.json of the three packages: @enclave-vm/ast ^2.17.0 is added to dependencies. plugin-dashboard had no dependencies block, so it gets one.
  • yarn.lock: the three workspace entries.

With the dependency declared, npm and Yarn install 2.17.0 inside the package's own node_modules when the project keeps 2.16.0 at the top level.

Tests

  • nx run-many -t build,lint for plugin-cache, plugin-dashboard and edge: pass. Each built dist/package.json declares @enclave-vm/ast ^2.17.0.
  • Reproduced with npm in a scratch project, with @enclave-vm/ast@2.16.0 installed alongside the three packages:
import require @enclave-vm/ast inside the package
Published 1.10.9 ERR_PACKAGE_PATH_NOT_EXPORTED ERR_PACKAGE_PATH_NOT_EXPORTED none
This PR's builds (npm pack of each dist) ok ok 2.17.0

Not in this PR

The same three packages import other dependencies of the bundled utils that they don't declare: @noble/ciphers, @noble/hashes, @upstash/redis and @vercel/kv. They work today only because @frontmcp/utils brings them in.

The cleaner fix is to add @frontmcp/utils to these packages' esbuild external lists and declare it, so they import utils instead of bundling it. That also removes the duplicated code. It changes more than a patch release should, so it's a follow-up.

Summary by CodeRabbit

  • Chores
    • Updated runtime package requirements for the edge library, cache plugin, and dashboard plugin.

…ReDoS check imports it

plugin-cache, plugin-dashboard and edge bundle @frontmcp/utils, whose ReDoS check imports @enclave-vm/ast/redos since #921, but didn't declare @enclave-vm/ast. They resolved whatever copy sat at the top of node_modules, and with 2.16.0 there (any project that pins Enclave 2.16) importing them failed with ERR_PACKAGE_PATH_NOT_EXPORTED. Declaring ^2.17.0 makes the package manager give them 2.17.0, nested when the project keeps 2.16.0.
@coderabbitai

coderabbitai Bot commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration
  • Configuration used: Repository: agentfront/frontmcp/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Essentials
  • Run ID: 9a8e5ee0-4bd7-41b1-bc83-6415ff8f6086
📥 Commits

Reviewing files that changed from the base of the PR and between 40051f9 and f1f345a.

⛔ Files ignored due to path filters: 1
  • yarn.lock is excluded by !**/yarn.lock, !**/*.lock
📒 Files selected for processing: 3
  • libs/edge/package.json
  • plugins/plugin-cache/package.json
  • plugins/plugin-dashboard/package.json
+---------------------------------+
| Ripgrepping your code for bugs. |
+---------------------------------+
  \
   \     /)/)
        ( '')
        /  |
      o(___)
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@frontegg-david
frontegg-david merged commit cef3cc4 into release/1.10.x Oct 21, 2026
18 of 19 checks passed
@frontegg-david
frontegg-david deleted the fix/1.10.x-declare-enclave-ast branch October 21, 2026 18:57
@github-actions

Copy link
Copy Markdown
Contributor

Cherry-pick Created

A cherry-pick PR to main has been automatically created.

Please review and merge if this change should also be in main.

If the cherry-pick is not needed, close the PR.

@github-actions

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-10-21T19:10:06.753Z
Commit: 5d606021

frontegg-david added a commit that referenced this pull request Oct 21, 2026
… the bundled ReDoS check imports it (#954)

Cherry-picked from #953 (merged to release/1.10.x)
Original commit: cef3cc4

Co-authored-by: agentfront[bot] <agentfront[bot]@users.noreply.github.com>
Co-authored-by: frontegg-david <69419539+frontegg-david@users.noreply.github.com>
alexmercerpo pushed a commit that referenced this pull request Nov 2, 2026
…dashboard and edge instead of bundling it

The three packages bundled @frontmcp/utils, so their builds imported utils'
own dependencies (@noble/ciphers, @noble/hashes, @enclave-vm/ast,
@upstash/redis, @vercel/kv, ioredis, zod, @frontmcp/lazy-zod) without
declaring them, and loaded a second copy of utils next to the SDK's.

- add @frontmcp/utils to the external list of the CommonJS and ESM builds
- declare @frontmcp/utils at the internal version in plugin-dashboard and edge
- drop the @enclave-vm/ast dependency added in #953; utils declares it
- add demo-e2e-published-imports: packs each package, fails on bundled utils
  code or an undeclared bare import, and loads all three under require and
  import in a fresh npm project with @enclave-vm/ast 2.16.0 at the top level

Closes #956
frontegg-david added a commit that referenced this pull request Nov 2, 2026
…ard and edge instead of bundling it (#1002)

* perf(plugins,edge): import @frontmcp/utils from plugin-cache, plugin-dashboard and edge instead of bundling it

The three packages bundled @frontmcp/utils, so their builds imported utils'
own dependencies (@noble/ciphers, @noble/hashes, @enclave-vm/ast,
@upstash/redis, @vercel/kv, ioredis, zod, @frontmcp/lazy-zod) without
declaring them, and loaded a second copy of utils next to the SDK's.

- add @frontmcp/utils to the external list of the CommonJS and ESM builds
- declare @frontmcp/utils at the internal version in plugin-dashboard and edge
- drop the @enclave-vm/ast dependency added in #953; utils declares it
- add demo-e2e-published-imports: packs each package, fails on bundled utils
  code or an undeclared bare import, and loads all three under require and
  import in a fresh npm project with @enclave-vm/ast 2.16.0 at the top level

Closes #956

* test(e2e): ignore a package's own function names in the bundled-utils scan and time out npm child processes

The scan reported any function or class named like a @frontmcp/utils export, so a local helper
such as escapeHtml failed it. Names the package's own sources define are now left out, and a
positive control checks that the remaining names still identify definitions in utils' build.

npm install and the load checks run synchronously, which blocks Jest's own timeouts; they now
time out on their own.
frontegg-david added a commit that referenced this pull request Nov 2, 2026
…endencies (#1007)

* perf(plugins,edge): import @frontmcp/utils from plugin-cache, plugin-dashboard and edge instead of bundling it

The three packages bundled @frontmcp/utils, so their builds imported utils'
own dependencies (@noble/ciphers, @noble/hashes, @enclave-vm/ast,
@upstash/redis, @vercel/kv, ioredis, zod, @frontmcp/lazy-zod) without
declaring them, and loaded a second copy of utils next to the SDK's.

- add @frontmcp/utils to the external list of the CommonJS and ESM builds
- declare @frontmcp/utils at the internal version in plugin-dashboard and edge
- drop the @enclave-vm/ast dependency added in #953; utils declares it
- add demo-e2e-published-imports: packs each package, fails on bundled utils
  code or an undeclared bare import, and loads all three under require and
  import in a fresh npm project with @enclave-vm/ast 2.16.0 at the top level

Closes #956

* test(sdk): pin how the Express host answers request bodies

Status, content type and body of the answers to JSON, form and other
bodies (charsets, encodings, malformed JSON, limits), and the error a later
error handler receives, before the JSON parser changes.

* perf(sdk): parse UTF-8 JSON bodies without loading every iconv-lite encoding

body-parser's json() decodes UTF-8 through iconv-lite, whose first decoder
loads all of its encodings, about 400 of them, on a server's first JSON
body. The Express host now reads application/json bodies with no charset
or charset=utf-8 through body-parser's raw(), so limits, inflation and read
errors stay its own, then decodes and parses them as json() does: byte
order mark dropped, empty body as {}, the same strict-mode and syntax
errors. Bodies in any other charset still go through json().

Refs #951

* perf(sdk): load the MCP client on first connect under ESM

The SDK's ESM bundle imported @frontmcp/protocol/client for the direct
client and remote apps, so an ESM process loaded the MCP SDK's client and
its transports at startup. Under require() that subpath was already lazy.

Both now take the client from #protocol-client. Its import variant, which
the ESM build resolves, is a dynamic import() of @frontmcp/protocol/client;
everywhere else it is the static module, so the CommonJS (kept external,
lazy under require()), browser and Worker builds load as before.

Refs #951

* perf(sdk): build the MCP server's Ajv the first time it validates

The MCP SDK's Server builds an Ajv instance with ajv-formats in its
constructor, to check the results of the elicitations it requests itself.
FrontMCP never calls it for that, yet built one per session (per request on
a stateless web-standard server), about 2.5 ms on a server's first request.

Every MCP server FrontMCP creates now comes from createMcpServer(), whose
validator builds the same AjvJsonSchemaValidator on its first use, so a
server that does validate gets the same answers. @frontmcp/protocol exports
AjvJsonSchemaValidator for it.

Refs #951

* test(e2e): ignore a package's own function names in the bundled-utils scan and time out npm child processes

The scan reported any function or class named like a @frontmcp/utils export, so a local helper
such as escapeHtml failed it. Names the package's own sources define are now left out, and a
positive control checks that the remaining names still identify definitions in utils' build.

npm install and the load checks run synchronously, which blocks Jest's own timeouts; they now
time out on their own.

* fix(sdk): leave alone a JSON body read before the UTF-8 parser

When a host hands over a request whose body was already read into a
Buffer, body-parser's raw() skips it and json() leaves it as it is. The
UTF-8 parser parsed that Buffer anyway; it now parses only a body its own
raw() read.

Refs #951

* perf(sdk): load the Streamable HTTP transport once an HTTP server listens

The transport, and through it @hono/node-server and Node's bundled undici,
loaded with the first Streamable HTTP session, so a server's first
initialize waited for it. A scope that serves Streamable HTTP (streamable,
stateless or JSON responses) now loads it right after the server starts
listening, which is not on the path to listening and is usually before a
client arrives. A stdio server, a serverless handler and a Worker never
start listening and load it as before; an SSE-only server still never
loads it.

Refs #951

* perf(protocol): export the MCP server's validator from the module its server comes from

`AjvJsonSchemaValidator` came straight from the MCP SDK while `McpServer` comes
through `#mcp-server`, which resolves to the SDK's CommonJS build under `import`.
An ESM server therefore loaded the validator module from both builds. Both now
come from `#mcp-server`.

* test(sdk): pin more request bodies and the entry points that load the transport early

The body pins add brotli, two byte order marks, a parameter other than charset
and a vendor +json type; they pass with and without the UTF-8 JSON parser.
A listening server loads the Streamable HTTP transport after start() returns
and reports ready; createHandler(), createFetchHandler() and runStdio() never do.

* fix(sdk): read only the selected remote transport from the lazily loaded MCP client

Destructuring both transports in createTransport read both lazy getters,
so a CommonJS remote connection loaded the SSE client even when Streamable
HTTP connected. The module-load e2e helper now prints every loaded module
and the spec filters them, so no pattern comes from the command line.
frontegg-david added a commit that referenced this pull request Nov 3, 2026
…er and OAuth flows on first use (#1016)

* perf(plugins,edge): import @frontmcp/utils from plugin-cache, plugin-dashboard and edge instead of bundling it

The three packages bundled @frontmcp/utils, so their builds imported utils'
own dependencies (@noble/ciphers, @noble/hashes, @enclave-vm/ast,
@upstash/redis, @vercel/kv, ioredis, zod, @frontmcp/lazy-zod) without
declaring them, and loaded a second copy of utils next to the SDK's.

- add @frontmcp/utils to the external list of the CommonJS and ESM builds
- declare @frontmcp/utils at the internal version in plugin-dashboard and edge
- drop the @enclave-vm/ast dependency added in #953; utils declares it
- add demo-e2e-published-imports: packs each package, fails on bundled utils
  code or an undeclared bare import, and loads all three under require and
  import in a fresh npm project with @enclave-vm/ast 2.16.0 at the top level

Closes #956

* test(sdk): pin how the Express host answers request bodies

Status, content type and body of the answers to JSON, form and other
bodies (charsets, encodings, malformed JSON, limits), and the error a later
error handler receives, before the JSON parser changes.

* perf(sdk): parse UTF-8 JSON bodies without loading every iconv-lite encoding

body-parser's json() decodes UTF-8 through iconv-lite, whose first decoder
loads all of its encodings, about 400 of them, on a server's first JSON
body. The Express host now reads application/json bodies with no charset
or charset=utf-8 through body-parser's raw(), so limits, inflation and read
errors stay its own, then decodes and parses them as json() does: byte
order mark dropped, empty body as {}, the same strict-mode and syntax
errors. Bodies in any other charset still go through json().

Refs #951

* perf(sdk): load the MCP client on first connect under ESM

The SDK's ESM bundle imported @frontmcp/protocol/client for the direct
client and remote apps, so an ESM process loaded the MCP SDK's client and
its transports at startup. Under require() that subpath was already lazy.

Both now take the client from #protocol-client. Its import variant, which
the ESM build resolves, is a dynamic import() of @frontmcp/protocol/client;
everywhere else it is the static module, so the CommonJS (kept external,
lazy under require()), browser and Worker builds load as before.

Refs #951

* perf(sdk): build the MCP server's Ajv the first time it validates

The MCP SDK's Server builds an Ajv instance with ajv-formats in its
constructor, to check the results of the elicitations it requests itself.
FrontMCP never calls it for that, yet built one per session (per request on
a stateless web-standard server), about 2.5 ms on a server's first request.

Every MCP server FrontMCP creates now comes from createMcpServer(), whose
validator builds the same AjvJsonSchemaValidator on its first use, so a
server that does validate gets the same answers. @frontmcp/protocol exports
AjvJsonSchemaValidator for it.

Refs #951

* test(e2e): ignore a package's own function names in the bundled-utils scan and time out npm child processes

The scan reported any function or class named like a @frontmcp/utils export, so a local helper
such as escapeHtml failed it. Names the package's own sources define are now left out, and a
positive control checks that the remaining names still identify definitions in utils' build.

npm install and the load checks run synchronously, which blocks Jest's own timeouts; they now
time out on their own.

* fix(sdk): leave alone a JSON body read before the UTF-8 parser

When a host hands over a request whose body was already read into a
Buffer, body-parser's raw() skips it and json() leaves it as it is. The
UTF-8 parser parsed that Buffer anyway; it now parses only a body its own
raw() read.

Refs #951

* perf(sdk): load the Streamable HTTP transport once an HTTP server listens

The transport, and through it @hono/node-server and Node's bundled undici,
loaded with the first Streamable HTTP session, so a server's first
initialize waited for it. A scope that serves Streamable HTTP (streamable,
stateless or JSON responses) now loads it right after the server starts
listening, which is not on the path to listening and is usually before a
client arrives. A stdio server, a serverless handler and a Worker never
start listening and load it as before; an SSE-only server still never
loads it.

Refs #951

* perf(protocol): export the MCP server's validator from the module its server comes from

`AjvJsonSchemaValidator` came straight from the MCP SDK while `McpServer` comes
through `#mcp-server`, which resolves to the SDK's CommonJS build under `import`.
An ESM server therefore loaded the validator module from both builds. Both now
come from `#mcp-server`.

* test(sdk): pin more request bodies and the entry points that load the transport early

The body pins add brotli, two byte order marks, a parameter other than charset
and a vendor +json type; they pass with and without the UTF-8 JSON parser.
A listening server loads the Streamable HTTP transport after start() returns
and reports ready; createHandler(), createFetchHandler() and runStdio() never do.

* test(bundle-size): pin the names and kinds of the SDK's root exports under require() and import

* test(bundle-size): set up the SDK's unused subsystems and OAuth flows on first use, with their hooks still running

* perf(sdk): set up jobs, workflows, channels, remote MCP and the ESM loader when a CommonJS process first reads their exports

* perf(sdk): register the OAuth flows by name and route, and load each one with the first request that reaches it

* fix(sdk): name the root exports the CommonJS build loads on first use, so an ESM import of that build still sees them

* test(bundle-size): hook the tools of a remote app and an ESM package under the SDK's CommonJS build

* fix(sdk): run a flow's route guards once per request when its class also has a static canActivate

* fix(sdk): answer a request to a deferred flow whose class fails to load with a 500

canActivate awaited the class load outside the Express middleware's error
handling and before the Web fetch handler's, so a load that failed escaped as
an unhandled rejection (Express 4) or a thrown fetch handler. The flow now
claims its route, and its run throws the load error, which both adapters
answer with a 500.

* test(e2e): pin the root exports main gained since this branch was cut

Regenerated from main's own build (91531eb): 42 names added by the feature stack, none removed, no kind changed. The merged build exports the same 966 names and kinds under require() and import.

* test(e2e): pin the root exports main gained from the feature stack

Regenerated from main's own build (998ca63): 76 names added, none removed, no kind changed. This branch's CommonJS and ESM builds export exactly the same names and kinds.
alexmercerpo pushed a commit that referenced this pull request Nov 3, 2026
…t runtime (#1027)

* perf(plugins,edge): import @frontmcp/utils from plugin-cache, plugin-dashboard and edge instead of bundling it

The three packages bundled @frontmcp/utils, so their builds imported utils'
own dependencies (@noble/ciphers, @noble/hashes, @enclave-vm/ast,
@upstash/redis, @vercel/kv, ioredis, zod, @frontmcp/lazy-zod) without
declaring them, and loaded a second copy of utils next to the SDK's.

- add @frontmcp/utils to the external list of the CommonJS and ESM builds
- declare @frontmcp/utils at the internal version in plugin-dashboard and edge
- drop the @enclave-vm/ast dependency added in #953; utils declares it
- add demo-e2e-published-imports: packs each package, fails on bundled utils
  code or an undeclared bare import, and loads all three under require and
  import in a fresh npm project with @enclave-vm/ast 2.16.0 at the top level

Closes #956

* test(sdk): pin how the Express host answers request bodies

Status, content type and body of the answers to JSON, form and other
bodies (charsets, encodings, malformed JSON, limits), and the error a later
error handler receives, before the JSON parser changes.

* perf(sdk): parse UTF-8 JSON bodies without loading every iconv-lite encoding

body-parser's json() decodes UTF-8 through iconv-lite, whose first decoder
loads all of its encodings, about 400 of them, on a server's first JSON
body. The Express host now reads application/json bodies with no charset
or charset=utf-8 through body-parser's raw(), so limits, inflation and read
errors stay its own, then decodes and parses them as json() does: byte
order mark dropped, empty body as {}, the same strict-mode and syntax
errors. Bodies in any other charset still go through json().

Refs #951

* perf(sdk): load the MCP client on first connect under ESM

The SDK's ESM bundle imported @frontmcp/protocol/client for the direct
client and remote apps, so an ESM process loaded the MCP SDK's client and
its transports at startup. Under require() that subpath was already lazy.

Both now take the client from #protocol-client. Its import variant, which
the ESM build resolves, is a dynamic import() of @frontmcp/protocol/client;
everywhere else it is the static module, so the CommonJS (kept external,
lazy under require()), browser and Worker builds load as before.

Refs #951

* perf(sdk): build the MCP server's Ajv the first time it validates

The MCP SDK's Server builds an Ajv instance with ajv-formats in its
constructor, to check the results of the elicitations it requests itself.
FrontMCP never calls it for that, yet built one per session (per request on
a stateless web-standard server), about 2.5 ms on a server's first request.

Every MCP server FrontMCP creates now comes from createMcpServer(), whose
validator builds the same AjvJsonSchemaValidator on its first use, so a
server that does validate gets the same answers. @frontmcp/protocol exports
AjvJsonSchemaValidator for it.

Refs #951

* test(e2e): ignore a package's own function names in the bundled-utils scan and time out npm child processes

The scan reported any function or class named like a @frontmcp/utils export, so a local helper
such as escapeHtml failed it. Names the package's own sources define are now left out, and a
positive control checks that the remaining names still identify definitions in utils' build.

npm install and the load checks run synchronously, which blocks Jest's own timeouts; they now
time out on their own.

* fix(sdk): leave alone a JSON body read before the UTF-8 parser

When a host hands over a request whose body was already read into a
Buffer, body-parser's raw() skips it and json() leaves it as it is. The
UTF-8 parser parsed that Buffer anyway; it now parses only a body its own
raw() read.

Refs #951

* perf(sdk): load the Streamable HTTP transport once an HTTP server listens

The transport, and through it @hono/node-server and Node's bundled undici,
loaded with the first Streamable HTTP session, so a server's first
initialize waited for it. A scope that serves Streamable HTTP (streamable,
stateless or JSON responses) now loads it right after the server starts
listening, which is not on the path to listening and is usually before a
client arrives. A stdio server, a serverless handler and a Worker never
start listening and load it as before; an SSE-only server still never
loads it.

Refs #951

* perf(protocol): export the MCP server's validator from the module its server comes from

`AjvJsonSchemaValidator` came straight from the MCP SDK while `McpServer` comes
through `#mcp-server`, which resolves to the SDK's CommonJS build under `import`.
An ESM server therefore loaded the validator module from both builds. Both now
come from `#mcp-server`.

* test(sdk): pin more request bodies and the entry points that load the transport early

The body pins add brotli, two byte order marks, a parameter other than charset
and a vendor +json type; they pass with and without the UTF-8 JSON parser.
A listening server loads the Streamable HTTP transport after start() returns
and reports ready; createHandler(), createFetchHandler() and runStdio() never do.

* test(bundle-size): pin the names and kinds of the SDK's root exports under require() and import

* test(bundle-size): set up the SDK's unused subsystems and OAuth flows on first use, with their hooks still running

* perf(sdk): set up jobs, workflows, channels, remote MCP and the ESM loader when a CommonJS process first reads their exports

* perf(sdk): register the OAuth flows by name and route, and load each one with the first request that reaches it

* fix(sdk): name the root exports the CommonJS build loads on first use, so an ESM import of that build still sees them

* test(bundle-size): hook the tools of a remote app and an ESM package under the SDK's CommonJS build

* fix(sdk): run a flow's route guards once per request when its class also has a static canActivate

* fix(sdk): answer a request to a deferred flow whose class fails to load with a 500

canActivate awaited the class load outside the Express middleware's error
handling and before the Web fetch handler's, so a load that failed escaped as
an unhandled rejection (Express 4) or a thrown fetch handler. The flow now
claims its route, and its run throws the load error, which both adapters
answer with a 500.

* test(e2e): pin the root exports main gained since this branch was cut

Regenerated from main's own build (91531eb): 42 names added by the feature stack, none removed, no kind changed. The merged build exports the same 966 names and kinds under require() and import.

* perf(lazy-zod): keep what a stand-in is built from in fields, not closures

internalZ made two closures and their contexts per stand-in, which cost more
than the stand-in itself. A stand-in now keeps zod's factory, its namespace and
the arguments (or the parent and method it was chained from) and drops the
arguments once built. The public z's stand-ins make one closure instead of two.

* fix(lazy-zod): drop a stand-in's arguments and parent only once it has built

A stand-in cleared its arguments before building, so a build that threw left
it empty: the next parse built the schema without them (an internalZ.object
whose build threw became z.object() and accepted any object). It now drops
them after a successful build, as the closure was dropped before.

A chained stand-in also kept its parent stand-in, and the parent's built
schema, for as long as it lived. It now drops the parent once built.

* test(e2e): pin the root exports main gained from the feature stack

Regenerated from main's own build (998ca63): 76 names added, none removed, no kind changed. This branch's CommonJS and ESM builds export exactly the same names and kinds.

* fix(sdk): ship the JavaScript of the transport, job/tools and workflow/tools subpath exports

The published exports map offers @frontmcp/sdk/transport, /job/tools and
/workflow/tools, but the build emitted only their declarations, so require()
threw MODULE_NOT_FOUND and import threw ERR_MODULE_NOT_FOUND.

The build now writes each subpath's require and import files as a re-export
of the package root, with the names its source barrel exports, so a subpath
hands back the root's own classes from whichever root build the importer
resolves, and never a second copy of the SDK. The root entry exports the
seven values only ./transport had: the recreateable and SSE transports and
the event stores, which the CommonJS build loads on first read, and
buildScopedServerOptions.

* fix(sdk): leave a type re-exported without export type out of a subpath's re-export

build-subpath-reexports read a barrel's export names without bundling it, so
esbuild could not tell a type re-exported with a plain export { } from a
value. The type was written into the subpath's files: importing the subpath
then failed with "does not provide an export named", and require() gave
undefined for it. The script now bundles the barrel with its packages
external and reads the names from the bundled code, which keeps only values;
a local export * is read the same way, and only an export * from another
package fails the build. Adds scripts/build-subpath-reexports.test.mjs.

* test(e2e): type the SDK subpaths and their root exports in the declaration consumer

No dts-consumer fixture imported @frontmcp/sdk/transport, /job/tools or
/workflow/tools, or the seven values the root now exports for them, so a
broken types target or a root export that types as something else went
unnoticed. subpath-exports-must-compile.ts compiles them under nodenext with
skipLibCheck off, checks each subpath value against the root's type and that
createEventStore is not any. It fails on the base, where the root lacks the
seven values.
frontegg-david added a commit that referenced this pull request Nov 3, 2026
… stores, which the server validates when it starts (#1042)

* perf(plugins,edge): import @frontmcp/utils from plugin-cache, plugin-dashboard and edge instead of bundling it

The three packages bundled @frontmcp/utils, so their builds imported utils'
own dependencies (@noble/ciphers, @noble/hashes, @enclave-vm/ast,
@upstash/redis, @vercel/kv, ioredis, zod, @frontmcp/lazy-zod) without
declaring them, and loaded a second copy of utils next to the SDK's.

- add @frontmcp/utils to the external list of the CommonJS and ESM builds
- declare @frontmcp/utils at the internal version in plugin-dashboard and edge
- drop the @enclave-vm/ast dependency added in #953; utils declares it
- add demo-e2e-published-imports: packs each package, fails on bundled utils
  code or an undeclared bare import, and loads all three under require and
  import in a fresh npm project with @enclave-vm/ast 2.16.0 at the top level

Closes #956

* test(sdk): pin how the Express host answers request bodies

Status, content type and body of the answers to JSON, form and other
bodies (charsets, encodings, malformed JSON, limits), and the error a later
error handler receives, before the JSON parser changes.

* perf(sdk): parse UTF-8 JSON bodies without loading every iconv-lite encoding

body-parser's json() decodes UTF-8 through iconv-lite, whose first decoder
loads all of its encodings, about 400 of them, on a server's first JSON
body. The Express host now reads application/json bodies with no charset
or charset=utf-8 through body-parser's raw(), so limits, inflation and read
errors stay its own, then decodes and parses them as json() does: byte
order mark dropped, empty body as {}, the same strict-mode and syntax
errors. Bodies in any other charset still go through json().

Refs #951

* perf(sdk): load the MCP client on first connect under ESM

The SDK's ESM bundle imported @frontmcp/protocol/client for the direct
client and remote apps, so an ESM process loaded the MCP SDK's client and
its transports at startup. Under require() that subpath was already lazy.

Both now take the client from #protocol-client. Its import variant, which
the ESM build resolves, is a dynamic import() of @frontmcp/protocol/client;
everywhere else it is the static module, so the CommonJS (kept external,
lazy under require()), browser and Worker builds load as before.

Refs #951

* perf(sdk): build the MCP server's Ajv the first time it validates

The MCP SDK's Server builds an Ajv instance with ajv-formats in its
constructor, to check the results of the elicitations it requests itself.
FrontMCP never calls it for that, yet built one per session (per request on
a stateless web-standard server), about 2.5 ms on a server's first request.

Every MCP server FrontMCP creates now comes from createMcpServer(), whose
validator builds the same AjvJsonSchemaValidator on its first use, so a
server that does validate gets the same answers. @frontmcp/protocol exports
AjvJsonSchemaValidator for it.

Refs #951

* test(e2e): ignore a package's own function names in the bundled-utils scan and time out npm child processes

The scan reported any function or class named like a @frontmcp/utils export, so a local helper
such as escapeHtml failed it. Names the package's own sources define are now left out, and a
positive control checks that the remaining names still identify definitions in utils' build.

npm install and the load checks run synchronously, which blocks Jest's own timeouts; they now
time out on their own.

* fix(sdk): leave alone a JSON body read before the UTF-8 parser

When a host hands over a request whose body was already read into a
Buffer, body-parser's raw() skips it and json() leaves it as it is. The
UTF-8 parser parsed that Buffer anyway; it now parses only a body its own
raw() read.

Refs #951

* perf(sdk): load the Streamable HTTP transport once an HTTP server listens

The transport, and through it @hono/node-server and Node's bundled undici,
loaded with the first Streamable HTTP session, so a server's first
initialize waited for it. A scope that serves Streamable HTTP (streamable,
stateless or JSON responses) now loads it right after the server starts
listening, which is not on the path to listening and is usually before a
client arrives. A stdio server, a serverless handler and a Worker never
start listening and load it as before; an SSE-only server still never
loads it.

Refs #951

* perf(protocol): export the MCP server's validator from the module its server comes from

`AjvJsonSchemaValidator` came straight from the MCP SDK while `McpServer` comes
through `#mcp-server`, which resolves to the SDK's CommonJS build under `import`.
An ESM server therefore loaded the validator module from both builds. Both now
come from `#mcp-server`.

* test(sdk): pin more request bodies and the entry points that load the transport early

The body pins add brotli, two byte order marks, a parameter other than charset
and a vendor +json type; they pass with and without the UTF-8 JSON parser.
A listening server loads the Streamable HTTP transport after start() returns
and reports ready; createHandler(), createFetchHandler() and runStdio() never do.

* test(bundle-size): pin the names and kinds of the SDK's root exports under require() and import

* test(bundle-size): set up the SDK's unused subsystems and OAuth flows on first use, with their hooks still running

* perf(sdk): set up jobs, workflows, channels, remote MCP and the ESM loader when a CommonJS process first reads their exports

* perf(sdk): register the OAuth flows by name and route, and load each one with the first request that reaches it

* fix(sdk): name the root exports the CommonJS build loads on first use, so an ESM import of that build still sees them

* test(bundle-size): hook the tools of a remote app and an ESM package under the SDK's CommonJS build

* fix(sdk): run a flow's route guards once per request when its class also has a static canActivate

* fix(sdk): answer a request to a deferred flow whose class fails to load with a 500

canActivate awaited the class load outside the Express middleware's error
handling and before the Web fetch handler's, so a load that failed escaped as
an unhandled rejection (Express 4) or a thrown fetch handler. The flow now
claims its route, and its run throws the load error, which both adapters
answer with a 500.

* test(e2e): pin the root exports main gained since this branch was cut

Regenerated from main's own build (91531eb): 42 names added by the feature stack, none removed, no kind changed. The merged build exports the same 966 names and kinds under require() and import.

* perf(lazy-zod): keep what a stand-in is built from in fields, not closures

internalZ made two closures and their contexts per stand-in, which cost more
than the stand-in itself. A stand-in now keeps zod's factory, its namespace and
the arguments (or the parent and method it was chained from) and drops the
arguments once built. The public z's stand-ins make one closure instead of two.

* fix(lazy-zod): drop a stand-in's arguments and parent only once it has built

A stand-in cleared its arguments before building, so a build that threw left
it empty: the next parse built the schema without them (an internalZ.object
whose build threw became z.object() and accepted any object). It now drops
them after a successful build, as the closure was dropped before.

A chained stand-in also kept its parent stand-in, and the parent's built
schema, for as long as it lived. It now drops the parent once built.

* test(e2e): pin the root exports main gained from the feature stack

Regenerated from main's own build (998ca63): 76 names added, none removed, no kind changed. This branch's CommonJS and ESM builds export exactly the same names and kinds.

* fix(sdk): ship the JavaScript of the transport, job/tools and workflow/tools subpath exports

The published exports map offers @frontmcp/sdk/transport, /job/tools and
/workflow/tools, but the build emitted only their declarations, so require()
threw MODULE_NOT_FOUND and import threw ERR_MODULE_NOT_FOUND.

The build now writes each subpath's require and import files as a re-export
of the package root, with the names its source barrel exports, so a subpath
hands back the root's own classes from whichever root build the importer
resolves, and never a second copy of the SDK. The root entry exports the
seven values only ./transport had: the recreateable and SSE transports and
the event stores, which the CommonJS build loads on first read, and
buildScopedServerOptions.

* feat(sdk): jobs.store: false, and no Redis or SQLite in the browser build

jobs.store: false keeps job runs and dynamic definitions in memory although
the server has a top-level redis or sqlite. tasks.store: false and
elicitation.store: false do the same for the two other stores that had no
memory switch; transport.persistence: false and
elicitation.persistent.storage: { type: 'memory' } stay as they were.

In the browser build a store that would open Redis over TCP or a SQLite
file, its own or the top-level one, fails startup before any store opens
with BrowserStoreUnavailableError, which names each store's switch. The
top-level sqlite is not resolved there (no default path, no mkdir).

* perf(sdk,utils,auth): keep server-only code out of browser builds

@frontmcp/utils and @frontmcp/auth now ship a browser build of one ES
module per source file, under a `browser` export condition, built like the
SDK's. A bundler keeps only the modules a page reaches, so the stdio
bridge, process supervision, the change feed, people sign-in, the auth
pages and the Redis session stores stay out of it. A `worker` condition
listed first keeps Workers on the ESM bundle; Node resolves as before.

utils' rsaVerify imports its WebCrypto path instead of require('./browser').
The SDK loads CliTaskRunner only with tasks.runner: 'cli', and the file log
transport only in createForCli() and runStdio().

create() + one tool: 1,339,977 -> 1,292,553 bytes (esbuild, minified);
with WebMcpPlugin 1,347,154 -> 1,299,736; Vite initial chunk
964,695 -> 761,067. The budgets follow, and the bundle-size e2e checks
that none of these modules reach either bundle or the initial chunk.

* fix(sdk): no unused-sqlite warning in the browser build, which opens no SQLite file

* fix(sdk): leave a type re-exported without export type out of a subpath's re-export

build-subpath-reexports read a barrel's export names without bundling it, so
esbuild could not tell a type re-exported with a plain export { } from a
value. The type was written into the subpath's files: importing the subpath
then failed with "does not provide an export named", and require() gave
undefined for it. The script now bundles the barrel with its packages
external and reads the names from the bundled code, which keeps only values;
a local export * is read the same way, and only an export * from another
package fails the build. Adds scripts/build-subpath-reexports.test.mjs.

* test(e2e): type the SDK subpaths and their root exports in the declaration consumer

No dts-consumer fixture imported @frontmcp/sdk/transport, /job/tools or
/workflow/tools, or the seven values the root now exports for them, so a
broken types target or a root export that types as something else went
unnoticed. subpath-exports-must-compile.ts compiles them under nodenext with
skipLibCheck off, checks each subpath value against the root's type and that
createEventStore is not any. It fails on the base, where the root lacks the
seven values.

* fix(sdk): the CLI host reads the tasks options, so tasks.store: false keeps its tasks off the top-level sqlite

The lite config parser of CLI mode had no tasks key, so every tasks option was dropped there: tasks.store: false,
tasks.sqlite, tasks.redis, tasks.runner and tasks.enabled. It now validates tasks with the full server schema.

* fix(sdk): a browser refuses jobs.store.redis on Vercel KV too, which the job stores open with the Redis client

The job stores take no provider: they open jobs.store.redis with ioredis. In the browser build a Vercel KV there was
not refused, and the stores fell back to memory with a warning instead of failing startup.

* fix(sdk): a page warns of no per-process requestState key for the redis its config keeps

A page is one instance and reads no VAULT_SECRET, yet with the store switches set and the top-level redis kept for its
Node server, it warned that multi-round tools restart on another instance. The browser build of create() now starts
with no warning. The catalog also says what a browser did up to 1.10: Redis stores fell back to memory with a warning,
SQLite stores and persistent questions failed.

* test(e2e): raise the browser bundle budgets by the 47 bytes the CLI tasks schema and the page's quiet start add

* docs(skills): the generated CLI reads the tasks options

* test(e2e,scripts): pin the CommonJS single-file utils and auth out of browser bundles

A require() of @frontmcp/utils or @frontmcp/auth in a bundler with the browser
condition and without `module` (the bundle-size e2e's esbuild config) resolves
dist/index.js. The never-bundled list only named dist/esm/index.mjs, so a second
copy pulled in that way went unnoticed. It now names both single-file builds.

The export-condition check also pins the require() side: a browser require()
without `module` (jest-environment-jsdom) keeps the CommonJS build, and a worker
gets the same files as Node.

* test(e2e): set the browser budgets from this branch's sizes on top of #1028

With #1028 merged in, create() + one tool is 1,294,971 bytes, with WebMcpPlugin 1,302,155, and Vite's initial chunk 761,771. Each budget is that size plus the headroom it was set with (536, 224 and 132 bytes): 1,295,507, 1,302,379 and 761,903.

* fix(sdk): CLI mode validates jobs and the top-level sqlite as the server does

The lite config parser of CLI mode passed jobs and sqlite through as z.any(), so the generated CLI took an
invalid jobs.store for unset and kept the runs in the top-level redis or sqlite, ignored a wrong runner,
defaultTimeoutMs or admission, took an empty sqlite.path for the default path and kept its tasks unencrypted on
a missing sqlite.encryption.secret. Both keys now use the server's own schemas, so the same configs fail at
startup with the same messages.

Fixes #1032

* fix(utils,auth,protocol): give a browser Web Worker the browser variants

webpack's target: 'webworker', Parcel's workers, and esbuild or Rollup
given the `worker` condition resolve `worker` and `browser`. @frontmcp/utils
mapped `worker` to its Node variants in its imports (#crypto-provider,
#async-context, #event-emitter, #env, #runtime-context, #path) and its root
export, @frontmcp/auth in its root export, and @frontmcp/protocol in
#mcp-streamable-http, #mcp-server and #server-types, so the SDK failed in a
browser Web Worker with `process is not defined` or a missing Node built-in.

The Node variants now take the `workerd` condition, which wrangler and
@cloudflare/vite-plugin pass with `worker` and `browser`, so a Cloudflare
Worker resolves the same files as before (a wrangler dry run bundles the
same 1,652 inputs into a byte-identical worker.js) and a browser Web Worker
gets the browser variants, as a page does. The `worker` keys that already
pointed at stubs stay.

Fixes #1031

* fix(sdk,plugins): refuse plugin stores on Redis or SQLite in a browser build

CachePlugin and RememberPlugin on global-store took the top-level redis
in a page, which has no TCP socket: the store fell back to memory with a
warning or failed on its first call. refusePluginStoreInBrowser(), from
@frontmcp/sdk, throws BrowserStoreUnavailableError in a browser for a
plugin store that would open Redis or SQLite, naming the plugin option
that keeps it in memory. CachePlugin, RememberPlugin, ApprovalPlugin and
ResultHandlesPlugin call it where they build their store. Node servers
are unchanged.

Fixes #1033

* fix(sdk): name a plugin's memory option when the plugin is named like an SDK store

* test(e2e): read the CLI's sqlite keys with a helper of demo-e2e-cli-exec, and build the CLI its tests drive

Importing the demo-e2e-jobs helper made demo-e2e-cli-exec depend on that app in the Nx graph, so its tests built it first.

* docs(skills): a jobs block needs enabled, which the server and now the CLI refuse without

One create-job example and its comment said enabled was implied, but the decorator refused the block.

* test(scripts): check that every package gives a browser Web Worker the files it gives a page

The guard listed the splits by hand, so it missed the SDK's #protocol-client and would not catch a new split
that maps `worker` to a Node file. It now resolves every imports and exports entry of every workspace package
with and without `worker`, for import and require.

* docs(skills): only utils and protocol gave `worker` their Node builds up to 1.10

@frontmcp/auth's root export had no `worker` key on release/1.10.x; it came with the browser builds.

* chore: name `workerd`, not `worker`, where comments and fixtures map the Node variants

* docs(skills): approval and result-handles fell back to memory in a page, they never opened a SQLite file

* test(e2e): type PluginStoreInBrowser and BrowserStoreUnavailableError's plugin memory options in a strict consumer

* fix(sdk): CLI mode keeps its tasks in the Redis a redis url names, as the server does

The lite config parser of CLI mode passed the top-level redis through as z.any(), so a redis: { url } reached
createTaskStore raw. The factory mapped redis into a storage config only when it had a host, and fell back to
REDIS_URL / REDIS_HOST or memory otherwise, so the generated CLI kept its tasks where its detached workers could not
find them. The lite parser now validates redis with the server's schema, and createTaskStore and
createElicitationStore normalize their redis option with that schema, so every form reaches the store with its
password, database and TLS, and an unusable one fails at startup. A build still reads a config whose redis url comes
from an unset variable.

Fixes #1035

* fix(sdk): keep Node built-ins out of the browser build

The SDK's browser build imported node:cluster, node:net, node:path,
node:http, node:stream and node:os from the shared local server and Unix
socket code, so a webpack 5 build of create() + one tool, for a page or a
Web Worker, stopped with UnhandledSchemeError unless the app stubbed Node
built-ins. Two chains reached them: duty.lifecycle and the persistent
question store import findReplaceableDirectory from unix-socket-guard, and
runShared() imports the shared server with import(), which webpack follows.

findReplaceableDirectory moves to its own module, which uses
@frontmcp/utils' dirname, so the socket guard is left to the Express host.
runShared() loads the shared server through a #shared-server subpath
import whose browser and worker variant is a stub: the browser builds no
longer contain the shared server, and runShared() in a page rejects with
BrowserEntryPointUnavailableError (BROWSER_ENTRY_POINT_UNAVAILABLE)
instead of "process is not defined". runUnixSocket() refuses in a browser
with the same error before it builds a server. Node builds are unchanged.

Fixes #1034

* test(e2e): close the Redis client the monitor connection was opened from

* fix(sdk): a build reads a server without any of its stores, so it opens no task store and needs none of their secrets

The manifest boot left out the top-level redis before validating the config, but still validated sqlite,
jobs.store and tasks.redis / tasks.sqlite, so a config object whose sqlite.encryption.secret or tasks.redis.url
comes from an unset variable stopped frontmcp build, and it opened tasks.redis and created the tasks.sqlite file.
Every store is now left out before the config is validated: redis, pubsub, sqlite, jobs.store, tasks.redis and
tasks.sqlite.

* test(e2e): type BrowserEntryPointUnavailableError in the declarations consumer

The new error and BrowserRefusedEntryPoint had no consumer coverage, unlike
BrowserStoreUnavailableError. The shared server fixture now narrows to the
error, reads its code and entryPoint, and rejects an entry point a browser
does not refuse.

* perf(sdk): load BrowserEntryPointUnavailableError only when runUnixSocket() refuses

runUnixSocket()'s browser check imported the error class statically, so every
create() in a page carried it. The class now loads on the refusal path, as
runShared()'s browser stub already does, and create() + one tool shrinks by
about 300 bytes. Budgets are the new sizes plus the same headroom.

* fix(sdk,cli): a build reads a @FrontMcp class without validating its stores, which the server validates when it starts

The decorator validated the whole config when the entry loaded, before the build could leave the stores out, so a
@FrontMcp class whose redis.url or sqlite.encryption.secret comes from a variable unset on the build machine failed
--target cli and --target mcpb, and the other targets wrote no manifest.

The build now loads the entry with FRONTMCP_BUILD_EXTRACT beside FRONTMCP_SCHEMA_EXTRACT. Under it the decorator
validates the config without its store settings (redis, pubsub, sqlite, jobs.store, tasks.redis, tasks.sqlite,
transport.persistence, throttle.storage) and records them as given; the manifest boot leaves them out with the same
list. The generated CLI sets only FRONTMCP_SCHEMA_EXTRACT while it loads the server, so it, and every other way of
running the server, validates them at startup as before.

Fixes #1041

* fix(sdk): leave a store setting's parent that is an array as given, for the schema to refuse

* fix(sdk): a CLI host run with FRONTMCP_BUILD_EXTRACT set validates the stores with the server's schema

A class loaded under the build flag records its store settings unvalidated, and the CLI host's lite schema passes
transport.persistence through, so a generated CLI whose environment had the flag opened a transport session store
the server would refuse, such as an SQLite file whose encryption secret was unset.

* fix(sdk): a build also reads a server without its event store, elicitation Redis and auth stores

transport.eventStore, elicitation.redis, auth.tokenStorage and auth.secureStore join the shared list of store
settings. A @FrontMcp class whose elicitation or event store Redis, or whose auth token storage, takes a variable the
build machine had not set stopped the build, and the manifest boot wrote the SQLite files of auth.tokenStorage and
auth.secureStore, for a config object too. The server validates them when it starts, as before.

* test(cli): setBuildExtractFlags puts back a flag's previous value

* test(e2e): raise the WebMcpPlugin bundle budget by the 143 bytes the review fixes add

The CLI host's check under FRONTMCP_BUILD_EXTRACT and the four added store settings grow both create() bundles by
143 bytes: create() + one tool is 1,295,491 (budget 1,295,679, unchanged), create() + WebMcpPlugin + one tool is
1,302,674 (budget 1,302,551 + 143 = 1,302,694).
frontegg-david added a commit that referenced this pull request Nov 3, 2026
…rowser store refusal, no stores opened by a build, steadier tests (#1043)

* perf(plugins,edge): import @frontmcp/utils from plugin-cache, plugin-dashboard and edge instead of bundling it

The three packages bundled @frontmcp/utils, so their builds imported utils'
own dependencies (@noble/ciphers, @noble/hashes, @enclave-vm/ast,
@upstash/redis, @vercel/kv, ioredis, zod, @frontmcp/lazy-zod) without
declaring them, and loaded a second copy of utils next to the SDK's.

- add @frontmcp/utils to the external list of the CommonJS and ESM builds
- declare @frontmcp/utils at the internal version in plugin-dashboard and edge
- drop the @enclave-vm/ast dependency added in #953; utils declares it
- add demo-e2e-published-imports: packs each package, fails on bundled utils
  code or an undeclared bare import, and loads all three under require and
  import in a fresh npm project with @enclave-vm/ast 2.16.0 at the top level

Closes #956

* test(sdk): pin how the Express host answers request bodies

Status, content type and body of the answers to JSON, form and other
bodies (charsets, encodings, malformed JSON, limits), and the error a later
error handler receives, before the JSON parser changes.

* perf(sdk): parse UTF-8 JSON bodies without loading every iconv-lite encoding

body-parser's json() decodes UTF-8 through iconv-lite, whose first decoder
loads all of its encodings, about 400 of them, on a server's first JSON
body. The Express host now reads application/json bodies with no charset
or charset=utf-8 through body-parser's raw(), so limits, inflation and read
errors stay its own, then decodes and parses them as json() does: byte
order mark dropped, empty body as {}, the same strict-mode and syntax
errors. Bodies in any other charset still go through json().

Refs #951

* perf(sdk): load the MCP client on first connect under ESM

The SDK's ESM bundle imported @frontmcp/protocol/client for the direct
client and remote apps, so an ESM process loaded the MCP SDK's client and
its transports at startup. Under require() that subpath was already lazy.

Both now take the client from #protocol-client. Its import variant, which
the ESM build resolves, is a dynamic import() of @frontmcp/protocol/client;
everywhere else it is the static module, so the CommonJS (kept external,
lazy under require()), browser and Worker builds load as before.

Refs #951

* perf(sdk): build the MCP server's Ajv the first time it validates

The MCP SDK's Server builds an Ajv instance with ajv-formats in its
constructor, to check the results of the elicitations it requests itself.
FrontMCP never calls it for that, yet built one per session (per request on
a stateless web-standard server), about 2.5 ms on a server's first request.

Every MCP server FrontMCP creates now comes from createMcpServer(), whose
validator builds the same AjvJsonSchemaValidator on its first use, so a
server that does validate gets the same answers. @frontmcp/protocol exports
AjvJsonSchemaValidator for it.

Refs #951

* test(e2e): ignore a package's own function names in the bundled-utils scan and time out npm child processes

The scan reported any function or class named like a @frontmcp/utils export, so a local helper
such as escapeHtml failed it. Names the package's own sources define are now left out, and a
positive control checks that the remaining names still identify definitions in utils' build.

npm install and the load checks run synchronously, which blocks Jest's own timeouts; they now
time out on their own.

* fix(sdk): leave alone a JSON body read before the UTF-8 parser

When a host hands over a request whose body was already read into a
Buffer, body-parser's raw() skips it and json() leaves it as it is. The
UTF-8 parser parsed that Buffer anyway; it now parses only a body its own
raw() read.

Refs #951

* perf(sdk): load the Streamable HTTP transport once an HTTP server listens

The transport, and through it @hono/node-server and Node's bundled undici,
loaded with the first Streamable HTTP session, so a server's first
initialize waited for it. A scope that serves Streamable HTTP (streamable,
stateless or JSON responses) now loads it right after the server starts
listening, which is not on the path to listening and is usually before a
client arrives. A stdio server, a serverless handler and a Worker never
start listening and load it as before; an SSE-only server still never
loads it.

Refs #951

* perf(protocol): export the MCP server's validator from the module its server comes from

`AjvJsonSchemaValidator` came straight from the MCP SDK while `McpServer` comes
through `#mcp-server`, which resolves to the SDK's CommonJS build under `import`.
An ESM server therefore loaded the validator module from both builds. Both now
come from `#mcp-server`.

* test(sdk): pin more request bodies and the entry points that load the transport early

The body pins add brotli, two byte order marks, a parameter other than charset
and a vendor +json type; they pass with and without the UTF-8 JSON parser.
A listening server loads the Streamable HTTP transport after start() returns
and reports ready; createHandler(), createFetchHandler() and runStdio() never do.

* test(bundle-size): pin the names and kinds of the SDK's root exports under require() and import

* test(bundle-size): set up the SDK's unused subsystems and OAuth flows on first use, with their hooks still running

* perf(sdk): set up jobs, workflows, channels, remote MCP and the ESM loader when a CommonJS process first reads their exports

* perf(sdk): register the OAuth flows by name and route, and load each one with the first request that reaches it

* fix(sdk): name the root exports the CommonJS build loads on first use, so an ESM import of that build still sees them

* test(bundle-size): hook the tools of a remote app and an ESM package under the SDK's CommonJS build

* fix(sdk): run a flow's route guards once per request when its class also has a static canActivate

* fix(sdk): answer a request to a deferred flow whose class fails to load with a 500

canActivate awaited the class load outside the Express middleware's error
handling and before the Web fetch handler's, so a load that failed escaped as
an unhandled rejection (Express 4) or a thrown fetch handler. The flow now
claims its route, and its run throws the load error, which both adapters
answer with a 500.

* test(e2e): pin the root exports main gained since this branch was cut

Regenerated from main's own build (91531eb): 42 names added by the feature stack, none removed, no kind changed. The merged build exports the same 966 names and kinds under require() and import.

* perf(lazy-zod): keep what a stand-in is built from in fields, not closures

internalZ made two closures and their contexts per stand-in, which cost more
than the stand-in itself. A stand-in now keeps zod's factory, its namespace and
the arguments (or the parent and method it was chained from) and drops the
arguments once built. The public z's stand-ins make one closure instead of two.

* fix(lazy-zod): drop a stand-in's arguments and parent only once it has built

A stand-in cleared its arguments before building, so a build that threw left
it empty: the next parse built the schema without them (an internalZ.object
whose build threw became z.object() and accepted any object). It now drops
them after a successful build, as the closure was dropped before.

A chained stand-in also kept its parent stand-in, and the parent's built
schema, for as long as it lived. It now drops the parent once built.

* test(e2e): pin the root exports main gained from the feature stack

Regenerated from main's own build (998ca63): 76 names added, none removed, no kind changed. This branch's CommonJS and ESM builds export exactly the same names and kinds.

* fix(sdk): ship the JavaScript of the transport, job/tools and workflow/tools subpath exports

The published exports map offers @frontmcp/sdk/transport, /job/tools and
/workflow/tools, but the build emitted only their declarations, so require()
threw MODULE_NOT_FOUND and import threw ERR_MODULE_NOT_FOUND.

The build now writes each subpath's require and import files as a re-export
of the package root, with the names its source barrel exports, so a subpath
hands back the root's own classes from whichever root build the importer
resolves, and never a second copy of the SDK. The root entry exports the
seven values only ./transport had: the recreateable and SSE transports and
the event stores, which the CommonJS build loads on first read, and
buildScopedServerOptions.

* feat(sdk): jobs.store: false, and no Redis or SQLite in the browser build

jobs.store: false keeps job runs and dynamic definitions in memory although
the server has a top-level redis or sqlite. tasks.store: false and
elicitation.store: false do the same for the two other stores that had no
memory switch; transport.persistence: false and
elicitation.persistent.storage: { type: 'memory' } stay as they were.

In the browser build a store that would open Redis over TCP or a SQLite
file, its own or the top-level one, fails startup before any store opens
with BrowserStoreUnavailableError, which names each store's switch. The
top-level sqlite is not resolved there (no default path, no mkdir).

* perf(sdk,utils,auth): keep server-only code out of browser builds

@frontmcp/utils and @frontmcp/auth now ship a browser build of one ES
module per source file, under a `browser` export condition, built like the
SDK's. A bundler keeps only the modules a page reaches, so the stdio
bridge, process supervision, the change feed, people sign-in, the auth
pages and the Redis session stores stay out of it. A `worker` condition
listed first keeps Workers on the ESM bundle; Node resolves as before.

utils' rsaVerify imports its WebCrypto path instead of require('./browser').
The SDK loads CliTaskRunner only with tasks.runner: 'cli', and the file log
transport only in createForCli() and runStdio().

create() + one tool: 1,339,977 -> 1,292,553 bytes (esbuild, minified);
with WebMcpPlugin 1,347,154 -> 1,299,736; Vite initial chunk
964,695 -> 761,067. The budgets follow, and the bundle-size e2e checks
that none of these modules reach either bundle or the initial chunk.

* fix(sdk): no unused-sqlite warning in the browser build, which opens no SQLite file

* fix(sdk): leave a type re-exported without export type out of a subpath's re-export

build-subpath-reexports read a barrel's export names without bundling it, so
esbuild could not tell a type re-exported with a plain export { } from a
value. The type was written into the subpath's files: importing the subpath
then failed with "does not provide an export named", and require() gave
undefined for it. The script now bundles the barrel with its packages
external and reads the names from the bundled code, which keeps only values;
a local export * is read the same way, and only an export * from another
package fails the build. Adds scripts/build-subpath-reexports.test.mjs.

* test(e2e): type the SDK subpaths and their root exports in the declaration consumer

No dts-consumer fixture imported @frontmcp/sdk/transport, /job/tools or
/workflow/tools, or the seven values the root now exports for them, so a
broken types target or a root export that types as something else went
unnoticed. subpath-exports-must-compile.ts compiles them under nodenext with
skipLibCheck off, checks each subpath value against the root's type and that
createEventStore is not any. It fails on the base, where the root lacks the
seven values.

* fix(sdk): the CLI host reads the tasks options, so tasks.store: false keeps its tasks off the top-level sqlite

The lite config parser of CLI mode had no tasks key, so every tasks option was dropped there: tasks.store: false,
tasks.sqlite, tasks.redis, tasks.runner and tasks.enabled. It now validates tasks with the full server schema.

* fix(sdk): a browser refuses jobs.store.redis on Vercel KV too, which the job stores open with the Redis client

The job stores take no provider: they open jobs.store.redis with ioredis. In the browser build a Vercel KV there was
not refused, and the stores fell back to memory with a warning instead of failing startup.

* fix(sdk): a page warns of no per-process requestState key for the redis its config keeps

A page is one instance and reads no VAULT_SECRET, yet with the store switches set and the top-level redis kept for its
Node server, it warned that multi-round tools restart on another instance. The browser build of create() now starts
with no warning. The catalog also says what a browser did up to 1.10: Redis stores fell back to memory with a warning,
SQLite stores and persistent questions failed.

* test(e2e): raise the browser bundle budgets by the 47 bytes the CLI tasks schema and the page's quiet start add

* docs(skills): the generated CLI reads the tasks options

* test(e2e,scripts): pin the CommonJS single-file utils and auth out of browser bundles

A require() of @frontmcp/utils or @frontmcp/auth in a bundler with the browser
condition and without `module` (the bundle-size e2e's esbuild config) resolves
dist/index.js. The never-bundled list only named dist/esm/index.mjs, so a second
copy pulled in that way went unnoticed. It now names both single-file builds.

The export-condition check also pins the require() side: a browser require()
without `module` (jest-environment-jsdom) keeps the CommonJS build, and a worker
gets the same files as Node.

* test(e2e): set the browser budgets from this branch's sizes on top of #1028

With #1028 merged in, create() + one tool is 1,294,971 bytes, with WebMcpPlugin 1,302,155, and Vite's initial chunk 761,771. Each budget is that size plus the headroom it was set with (536, 224 and 132 bytes): 1,295,507, 1,302,379 and 761,903.

* fix(sdk): CLI mode validates jobs and the top-level sqlite as the server does

The lite config parser of CLI mode passed jobs and sqlite through as z.any(), so the generated CLI took an
invalid jobs.store for unset and kept the runs in the top-level redis or sqlite, ignored a wrong runner,
defaultTimeoutMs or admission, took an empty sqlite.path for the default path and kept its tasks unencrypted on
a missing sqlite.encryption.secret. Both keys now use the server's own schemas, so the same configs fail at
startup with the same messages.

Fixes #1032

* fix(utils,auth,protocol): give a browser Web Worker the browser variants

webpack's target: 'webworker', Parcel's workers, and esbuild or Rollup
given the `worker` condition resolve `worker` and `browser`. @frontmcp/utils
mapped `worker` to its Node variants in its imports (#crypto-provider,
#async-context, #event-emitter, #env, #runtime-context, #path) and its root
export, @frontmcp/auth in its root export, and @frontmcp/protocol in
#mcp-streamable-http, #mcp-server and #server-types, so the SDK failed in a
browser Web Worker with `process is not defined` or a missing Node built-in.

The Node variants now take the `workerd` condition, which wrangler and
@cloudflare/vite-plugin pass with `worker` and `browser`, so a Cloudflare
Worker resolves the same files as before (a wrangler dry run bundles the
same 1,652 inputs into a byte-identical worker.js) and a browser Web Worker
gets the browser variants, as a page does. The `worker` keys that already
pointed at stubs stay.

Fixes #1031

* fix(sdk,plugins): refuse plugin stores on Redis or SQLite in a browser build

CachePlugin and RememberPlugin on global-store took the top-level redis
in a page, which has no TCP socket: the store fell back to memory with a
warning or failed on its first call. refusePluginStoreInBrowser(), from
@frontmcp/sdk, throws BrowserStoreUnavailableError in a browser for a
plugin store that would open Redis or SQLite, naming the plugin option
that keeps it in memory. CachePlugin, RememberPlugin, ApprovalPlugin and
ResultHandlesPlugin call it where they build their store. Node servers
are unchanged.

Fixes #1033

* fix(sdk): name a plugin's memory option when the plugin is named like an SDK store

* test(e2e): read the CLI's sqlite keys with a helper of demo-e2e-cli-exec, and build the CLI its tests drive

Importing the demo-e2e-jobs helper made demo-e2e-cli-exec depend on that app in the Nx graph, so its tests built it first.

* docs(skills): a jobs block needs enabled, which the server and now the CLI refuse without

One create-job example and its comment said enabled was implied, but the decorator refused the block.

* test(scripts): check that every package gives a browser Web Worker the files it gives a page

The guard listed the splits by hand, so it missed the SDK's #protocol-client and would not catch a new split
that maps `worker` to a Node file. It now resolves every imports and exports entry of every workspace package
with and without `worker`, for import and require.

* docs(skills): only utils and protocol gave `worker` their Node builds up to 1.10

@frontmcp/auth's root export had no `worker` key on release/1.10.x; it came with the browser builds.

* chore: name `workerd`, not `worker`, where comments and fixtures map the Node variants

* docs(skills): approval and result-handles fell back to memory in a page, they never opened a SQLite file

* test(e2e): type PluginStoreInBrowser and BrowserStoreUnavailableError's plugin memory options in a strict consumer

* fix(sdk): CLI mode keeps its tasks in the Redis a redis url names, as the server does

The lite config parser of CLI mode passed the top-level redis through as z.any(), so a redis: { url } reached
createTaskStore raw. The factory mapped redis into a storage config only when it had a host, and fell back to
REDIS_URL / REDIS_HOST or memory otherwise, so the generated CLI kept its tasks where its detached workers could not
find them. The lite parser now validates redis with the server's schema, and createTaskStore and
createElicitationStore normalize their redis option with that schema, so every form reaches the store with its
password, database and TLS, and an unusable one fails at startup. A build still reads a config whose redis url comes
from an unset variable.

Fixes #1035

* fix(sdk): keep Node built-ins out of the browser build

The SDK's browser build imported node:cluster, node:net, node:path,
node:http, node:stream and node:os from the shared local server and Unix
socket code, so a webpack 5 build of create() + one tool, for a page or a
Web Worker, stopped with UnhandledSchemeError unless the app stubbed Node
built-ins. Two chains reached them: duty.lifecycle and the persistent
question store import findReplaceableDirectory from unix-socket-guard, and
runShared() imports the shared server with import(), which webpack follows.

findReplaceableDirectory moves to its own module, which uses
@frontmcp/utils' dirname, so the socket guard is left to the Express host.
runShared() loads the shared server through a #shared-server subpath
import whose browser and worker variant is a stub: the browser builds no
longer contain the shared server, and runShared() in a page rejects with
BrowserEntryPointUnavailableError (BROWSER_ENTRY_POINT_UNAVAILABLE)
instead of "process is not defined". runUnixSocket() refuses in a browser
with the same error before it builds a server. Node builds are unchanged.

Fixes #1034

* test(e2e): close the Redis client the monitor connection was opened from

* fix(sdk): a build reads a server without any of its stores, so it opens no task store and needs none of their secrets

The manifest boot left out the top-level redis before validating the config, but still validated sqlite,
jobs.store and tasks.redis / tasks.sqlite, so a config object whose sqlite.encryption.secret or tasks.redis.url
comes from an unset variable stopped frontmcp build, and it opened tasks.redis and created the tasks.sqlite file.
Every store is now left out before the config is validated: redis, pubsub, sqlite, jobs.store, tasks.redis and
tasks.sqlite.

* test(e2e): type BrowserEntryPointUnavailableError in the declarations consumer

The new error and BrowserRefusedEntryPoint had no consumer coverage, unlike
BrowserStoreUnavailableError. The shared server fixture now narrows to the
error, reads its code and entryPoint, and rejects an entry point a browser
does not refuse.

* perf(sdk): load BrowserEntryPointUnavailableError only when runUnixSocket() refuses

runUnixSocket()'s browser check imported the error class statically, so every
create() in a page carried it. The class now loads on the refusal path, as
runShared()'s browser stub already does, and create() + one tool shrinks by
about 300 bytes. Budgets are the new sizes plus the same headroom.

* fix(sdk,cli): a build reads a @FrontMcp class without validating its stores, which the server validates when it starts

The decorator validated the whole config when the entry loaded, before the build could leave the stores out, so a
@FrontMcp class whose redis.url or sqlite.encryption.secret comes from a variable unset on the build machine failed
--target cli and --target mcpb, and the other targets wrote no manifest.

The build now loads the entry with FRONTMCP_BUILD_EXTRACT beside FRONTMCP_SCHEMA_EXTRACT. Under it the decorator
validates the config without its store settings (redis, pubsub, sqlite, jobs.store, tasks.redis, tasks.sqlite,
transport.persistence, throttle.storage) and records them as given; the manifest boot leaves them out with the same
list. The generated CLI sets only FRONTMCP_SCHEMA_EXTRACT while it loads the server, so it, and every other way of
running the server, validates them at startup as before.

Fixes #1041

* fix(sdk): leave a store setting's parent that is an array as given, for the schema to refuse

* fix(sdk): a CLI host run with FRONTMCP_BUILD_EXTRACT set validates the stores with the server's schema

A class loaded under the build flag records its store settings unvalidated, and the CLI host's lite schema passes
transport.persistence through, so a generated CLI whose environment had the flag opened a transport session store
the server would refuse, such as an SQLite file whose encryption secret was unset.

* fix(sdk): a build also reads a server without its event store, elicitation Redis and auth stores

transport.eventStore, elicitation.redis, auth.tokenStorage and auth.secureStore join the shared list of store
settings. A @FrontMcp class whose elicitation or event store Redis, or whose auth token storage, takes a variable the
build machine had not set stopped the build, and the manifest boot wrote the SQLite files of auth.tokenStorage and
auth.secureStore, for a config object too. The server validates them when it starts, as before.

* test(cli): setBuildExtractFlags puts back a flag's previous value

* test(sdk): mock @frontmcp/storage-sqlite without virtual: true, so a spec's mock holds whichever spec ran before it

Jest caches module IDs per worker. A virtual mock gives the module a
different ID than a non-virtual mock or a real load of it, so the store
factories' lazy require() took the real module, or missed the mock, when
another spec had required it first in the same worker. The module always
resolves through the pinned moduleNameMapper, so no spec needs the flag.

* test(cli): the cli-exec e2e names the fixture build's errors when it fails

The fixture build's TypeScript diagnostics go to the CLI's stdout, which
ensureBuild() captured and dropped, so a failed build read only as
'node exited with code 2'. It failed that way when another nx task in the
same worktree rebuilt libs/sdk/dist during the run, and tsc could not
resolve @frontmcp/sdk. The error now carries the build's output.

* build: declare webpack, which the bundle-size e2e imports, as a root devDependency

It came only through @nx/webpack. The range is the one the lockfile already
resolves (5.105.1), so the lockfile gains only the root's entry.

* fix(sdk,plugins): a browser refuses every Redis or SQLite store in one error, plugins' and explicit stores included

Plugins declare their stores with static browserStores; the SDK's startup check reads them for
the server's and its class apps' plugins, with questions, jobs, throttle.storage,
auth.tokenStorage and skillsConfig.cache.redis, before any store opens. The plugin registry
checks the rest (init({ useFactory }), nested plugins) before it builds them. Resource stores
and sqliteStorageProvider() are refused where they open. BrowserStoreUnavailableError requires
the memory options for plugin entries.

* perf(plugins): CachePlugin and RememberPlugin load ioredis when they build a Redis store

A page that uses them on memory or Vercel KV no longer bundles ioredis, and RememberPlugin
reads its secret path through @frontmcp/utils instead of importing Node's path. The bundle-size
model leaves out what utils and auth load with import(), as it does the SDK's on-demand
modules, and the helpers name inputs with / on every platform.

* fix(sdk): the CommonJS browser build requires @frontmcp/storage-sqlite on first use, and the guard finds a require() that runs on load

dist/browser/index.cjs no longer bundles @frontmcp/storage-sqlite and its node:events imports.
The browser guard also reports a require() or esbuild __require() of a Node built-in outside any
function, which throws while a page loads the module, and covers the cache and remember plugins.

* docs(skills): one browser refusal for every store, plugins' static browserStores, and REST tokens in a page

The catalog names the stores the browser build refuses and their switches, the plugin hook that
declares a plugin's stores, the CommonJS browser build's lazy Node built-ins, and warns that an
Upstash or Vercel KV token used from a page is readable by every visitor.

* test(sdk): start the lazy-app rate-limit tests mid-window

The guard's windows align to the wall clock, so two calls that straddle a
minute boundary let the second through, which failed the test under load.
useMidRateLimitWindow puts the boundary 30 s away, as the other
rate-limit specs do.

* fix(sdk): CLI mode validates every option the server validates, and runs with it as given

The lite config parser of CLI mode passed http, pubsub, transport, pagination, fetch, elicitation, extApps, ui,
throttle, health, metrics and authorities through as z.any(), and dropped session, clients, observability, registry
and channels, so a config object the server refuses ran in CLI mode; an invalid transport.persistence opened the
CLI's session store with it. Each of those keys is now checked with the server's schema, with the server's issues,
and kept as given: the server's defaults (transport, http, fetch origins, redis key prefixes) would change what CLI
mode does. The dropped keys are checked and still left out, so CLI mode turns none of them on. authorities keeps
its pass-through (#366): the value reaches the scope as given, keys the schema does not know included.

* fix(sdk): a redis with neither host nor url says so, instead of Invalid input

The redis union reported Zod's generic Invalid input when no branch matched, in server and CLI mode, so a redis whose
host and url both come from unset variables did not say what was missing. It now fails with `redis needs host or
url`, wherever a redis option is read (pubsub, jobs.store.redis, tasks.redis and the others). Every other message is
unchanged.

* fix(sdk): the job stores refuse a Vercel KV jobs.store.redis instead of connecting to localhost

The job stores open jobs.store.redis with a Redis client whatever its provider, so on Node a Vercel KV one connected
to a Redis on localhost. Both job store factories now refuse it with JobStoreNotSupportedError
(JOB_STORE_NOT_SUPPORTED), which stops startup in server and CLI mode. A top-level Vercel KV redis was already passed
over by resolveJobsStore; a test now pins that the runs stay in memory beside one.

* fix(cli): the manifest warning names each invalid field instead of printing the error as JSON

When the server's schema refused its config, `frontmcp.manifest.json not written: ...` printed the ZodError's
message, the issues as JSON. It now prints each invalid field as `path: message`, separated by `; `. A union that says
no more than Invalid input is read through its branch with the fewest issues, as the SDK's describeConfigIssues does;
a union with its own message, such as `redis needs host or url`, keeps it.

* fix(auth): an auth.secureStore backing it cannot use stops startup instead of falling back to memory

The bare-object variant of secureStoreConfigSchema matched a config whose sqlite, redis or backend failed its own
variant and stripped the backing, so a broken backing (an empty sqlite.path, a redis without host such as a url, a
backend missing a method) kept the secrets in memory with no error. That variant now takes no backing, so the config
is refused, with a message that names the backing, such as `secureStore.backend must implement get/set/delete/list`.
createSecureStore() validates its config the same way and refuses an unusable one with SecureStoreConfigError
(SECURE_STORE_CONFIG_INVALID) before it opens a store. 'memory', a config without a backing, and a backing left
undefined still select the memory store.

* test(e2e): fix the three type errors in the demo-e2e-cli-exec specs

* test(e2e): the generated CLI of a config object validates its options as the server does

Builds a CLI whose server is a config object and runs it with an invalid transport.persistence (refused, no session
file), a valid one (runs), a redis with neither host nor url, and a Vercel KV jobs.store.redis; then builds the node
target of a config the schema refuses and reads the manifest warning.

* fix(sdk): skip the browser store check for a hand-written plugin value record, whose options are not recorded

* test(sdk): set the browser bundle budgets to the measured size plus headroom

* test(e2e): pin JobStoreNotSupportedError and raise the browser budgets by the measured growth

The SDK root exports JobStoreNotSupportedError. The browser bundles grow with the CLI-mode checks (+344 bytes in
front-mcp.metadata), the redis message (+197) and the secureStore refusal (+484): create() + one tool by 1,030 bytes
(1,295,491 -> 1,296,521), with WebMcpPlugin by 1,033 (1,302,674 -> 1,303,707), and the Vite initial chunk by 605
(761,200 -> 761,805). Each budget rises by that growth; the WebMcpPlugin one also carries the +143 of the base's
next commit (3a450fd), which the base was already over.

* test(plugin-codecall): time simple scripts after a warm-up call, on the fastest of three

The first execute() in a process pays one-time module and JIT costs, so
the test failed its 100 ms bound when it ran first, alone or under load
(106-184 ms in 5 of 24 loaded runs). Warm calls take 3-33 ms under the
same load. The bound now applies to the calls after a warm-up, and to the
fastest of three, so one descheduled call does not fail it while a slow
execute() still does.

* test(e2e): set the browser budgets from the combined follow-ups' measured sizes

On main 5c8f654 the bundles are 1,295,491 / 1,302,674 bytes and Vite's initial chunk 761,200. With the follow-ups: 1,280,950 / 1,288,132 (most of the drop is the bundle model now leaving out what utils and auth load on demand), budgets set to size plus their headroom, 1,281,486 / 1,288,356; Vite's initial chunk grows by the measured 1,086 bytes (the single browser startup check and the CLI-schema checks), so its budget rises by exactly that, 761,267 -> 762,353.

* fix(sdk): disposing a scope destroys its transport service and closes its session store

Only the direct server destroyed the transport service. A scope disposed by
FrontMcpInstance.shutdown() or connect() kept its session store: one that
failed at startup kept retrying, and an SQLite one kept its file open.

* fix(plugins): the servers that share one CachePlugin or RememberPlugin init() on redis share its client

Building the type: 'redis' store at server start, rather than at init(),
gave every server its own ioredis client, none of which is closed. The
store is now built once, when the first server that installs it starts.

* fix(sdk): a browser refuses an auth.secureStore on Redis or SQLite in its one startup refusal

The startup check named auth.tokenStorage but not auth.secureStore, so a
local auth in a page with a secure store on SQLite or Redis failed with
the storage layer's module or connection error.

* docs(skills): graceful shutdown closes the session store of transport.persistence

* test(e2e): set the browser budgets from the measured sizes after the review fixes

The scope's transport-service disposal and the secure store's browser refusal
add 156 bytes to each esbuild bundle and 74 to Vite's initial chunk.

* test(scripts): the CommonJS browser build keeps @frontmcp/storage-sqlite external, beside the ESM build's externals

a70ec4a left @frontmcp/storage-sqlite out of dist/browser/index.cjs, so the
check that the two builds keep the same packages external failed.

* test(plugin-remember): give the shared-client test's redis config the port its type requires

* fix(sdk,plugins): a build reads a server without building its plugins' stores

The build's manifest boot leaves the top-level redis out, so a CachePlugin
or RememberPlugin on global-store failed a --target cli or mcpb build with
'requires global "redis" configuration', and a plugin store on Redis or
SQLite connected, or wrote its file, during the build.

storeProvider() marks a factory provider that opens a store. In a manifest
boot the provider registry does not build it and gives what injects it a
stand-in that throws StoreNotOpenedInBuildError when called; any other boot
builds it as before. The cache, remember, approval and result-handles
stores and sqliteStorageProvider() are made with it.

* test(e2e): build a CLI and an MCPB of a server whose plugins keep their stores on Redis and SQLite

Both builds succeed without the redis the build leaves out, connect to no
plugin Redis and write no plugin SQLite file, and the shipped server still
checks the redis when it starts.

* test(e2e): set the browser budgets from the measured sizes with storeProvider()

The provider registry's build-boot check, storeProvider() and
StoreNotOpenedInBuildError add 796 and 798 bytes to the esbuild bundles and
179 to Vite's initial chunk.

* test(sdk,plugins): type the build-store specs' configs and providers for their spec configs

* fix(sdk,auth): open the secure-store backing the schema validated, and refuse a publicAccess guard on Redis or SQLite in a page

- createSecureStore() selects the backing from the schema's parsed config, so a config naming two backings opens the
  one that validated instead of the raw other one.
- In a browser, a throttle.storage on Redis or SQLite is refused at startup also with throttle.enabled: false when
  auth.publicAccess.rateLimit builds its guard on it.
- demo-e2e-mcpb builds plugin-approval and plugin-cache before its tests, which its plugin-stores spec imports, and
  that spec shows the build's output when the build fails.
- Browser budgets re-fitted to the measured sizes.

* fix(auth,sdk): refuse a secure store that names more than one backing, and count an app's publicAccess before its defaults

- auth.secureStore takes one of sqlite, redis or backend: a config naming more stops startup with
  "secureStore takes one backing, not sqlite and redis". createSecureStore() selects from the config as given again,
  so a direct call keeps its Redis key prefix and its custom backend.
- The browser check counts auth.publicAccess whose rateLimit is not yet defaulted (an app's own auth) as rate limited.
- demo-e2e-mcpb builds the CLI before its tests too.
- Browser budgets re-fitted to the measured sizes.
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.

1 participant