Repository navigation
fix(plugins,edge): declare @enclave-vm/ast ^2.17.0 where the bundled ReDoS check imports it - #953
Merged
Conversation
…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.
Contributor
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
⛔ Files ignored due to path filters: 1
📒 Files selected for processing: 3
✨ Finishing Touches
Comment |
Contributor
Cherry-pick CreatedA cherry-pick PR to Please review and merge if this change should also be in If the cherry-pick is not needed, close the PR. |
Contributor
Performance Test ResultsStatus: ✅ All tests passed Summary
Total: 101 tests across 21 projects 📊 View full report in workflow run Generated at: 2026-10-21T19:10:06.753Z |
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What and why
In 1.10.9,
@frontmcp/plugin-cache,@frontmcp/plugin-dashboardand@frontmcp/edgefail to load when a project's top-level@enclave-vm/astis older than 2.17.0:frontmcp.dev hit it while moving to 1.10.9 (run 39652798919), because it pins Enclave
^2.16.0itself.Why:
utils' ReDoS code. These three packages bundle@frontmcp/utilsinto their own builds; their esbuildexternallists 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.@enclave-vm/ast. So Node resolves whatever copy sits at the top ofnode_modules.@frontmcp/utils../redos.What changed
package.jsonof the three packages:@enclave-vm/ast^2.17.0is added todependencies.plugin-dashboardhad nodependenciesblock, 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_moduleswhen the project keeps 2.16.0 at the top level.Tests
nx run-many -t build,lintforplugin-cache,plugin-dashboardandedge: pass. Each builtdist/package.jsondeclares@enclave-vm/ast^2.17.0.@enclave-vm/ast@2.16.0installed alongside the three packages:importrequire@enclave-vm/astinside the packageERR_PACKAGE_PATH_NOT_EXPORTEDERR_PACKAGE_PATH_NOT_EXPORTEDnpm packof eachdist)Not in this PR
The same three packages import other dependencies of the bundled
utilsthat they don't declare:@noble/ciphers,@noble/hashes,@upstash/redisand@vercel/kv. They work today only because@frontmcp/utilsbrings them in.The cleaner fix is to add
@frontmcp/utilsto these packages' esbuildexternallists and declare it, so they importutilsinstead 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