Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1c72d48
perf(plugins,edge): import @frontmcp/utils from plugin-cache, plugin-…
frontegg-david Nov 2, 2026
5fb2e07
test(sdk): pin how the Express host answers request bodies
frontegg-david Nov 2, 2026
a7e0d1a
perf(sdk): parse UTF-8 JSON bodies without loading every iconv-lite e…
frontegg-david Nov 2, 2026
de56afb
perf(sdk): load the MCP client on first connect under ESM
frontegg-david Nov 2, 2026
f9992d1
perf(sdk): build the MCP server's Ajv the first time it validates
frontegg-david Nov 2, 2026
00dcb4c
test(e2e): ignore a package's own function names in the bundled-utils…
frontegg-david Nov 2, 2026
95f5e08
fix(sdk): leave alone a JSON body read before the UTF-8 parser
frontegg-david Nov 2, 2026
e9f66fd
perf(sdk): load the Streamable HTTP transport once an HTTP server lis…
frontegg-david Nov 2, 2026
f44b363
perf(protocol): export the MCP server's validator from the module its…
frontegg-david Nov 2, 2026
151249e
test(sdk): pin more request bodies and the entry points that load the…
frontegg-david Nov 2, 2026
aae2933
Merge remote-tracking branch 'origin/perf/p01-external-utils' into pe…
frontegg-david Nov 2, 2026
b8bbaf0
test(bundle-size): pin the names and kinds of the SDK's root exports …
frontegg-david Nov 2, 2026
196cba0
test(bundle-size): set up the SDK's unused subsystems and OAuth flows…
frontegg-david Nov 2, 2026
79b15bb
perf(sdk): set up jobs, workflows, channels, remote MCP and the ESM l…
frontegg-david Nov 2, 2026
2d815d9
perf(sdk): register the OAuth flows by name and route, and load each …
frontegg-david Nov 2, 2026
ac6708d
fix(sdk): name the root exports the CommonJS build loads on first use…
frontegg-david Nov 2, 2026
6db38c4
test(bundle-size): hook the tools of a remote app and an ESM package …
frontegg-david Nov 2, 2026
7e02769
fix(sdk): run a flow's route guards once per request when its class a…
frontegg-david Nov 2, 2026
95b33fb
fix(sdk): answer a request to a deferred flow whose class fails to lo…
frontegg-david Nov 2, 2026
314dae8
Merge remote-tracking branch 'origin/main' into perf/p04-lazy-sdk-sub…
frontegg-david Nov 2, 2026
1a4d98a
test(e2e): pin the root exports main gained since this branch was cut
frontegg-david Nov 2, 2026
61e67ef
perf(lazy-zod): keep what a stand-in is built from in fields, not clo…
frontegg-david Nov 2, 2026
f43ad8c
fix(lazy-zod): drop a stand-in's arguments and parent only once it ha…
frontegg-david Nov 2, 2026
50f5881
Merge origin/main into perf/p04-lazy-sdk-subsystems
frontegg-david Nov 3, 2026
dae7555
test(e2e): pin the root exports main gained from the feature stack
frontegg-david Nov 3, 2026
3b92e82
Merge perf/p04-lazy-sdk-subsystems (with main) into perf/p05-startup-…
frontegg-david Nov 3, 2026
701508f
fix(sdk): ship the JavaScript of the transport, job/tools and workflo…
frontegg-david Nov 3, 2026
e54e741
fix(sdk): leave a type re-exported without export type out of a subpa…
frontegg-david Nov 3, 2026
1d66a51
test(e2e): type the SDK subpaths and their root exports in the declar…
frontegg-david Nov 3, 2026
ca05375
Merge origin/main into fix/f01-sdk-subpath-exports
frontegg-david Nov 3, 2026
4764e07
Merge remote-tracking branch 'origin/main' into fix/f01-sdk-subpath-e…
frontegg-david Nov 3, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@
"McpStatelessClient": "class",
"McpStatelessClientAdapter": "class",
"McpStatelessError": "class",
"MemoryEventStore": "class",
"MemorySkillProvider": "class",
"MethodNotImplementedError": "class",
"MissingClientCapabilityError": "class",
Expand Down Expand Up @@ -407,6 +408,9 @@
"RUN_TASK_ENV_VAR": "string",
"RUN_TASK_SUBCOMMAND": "string",
"RateLimitError": "class",
"RecreateableSSEServerTransport": "class",
"RecreateableStreamableHTTPServerTransport": "class",
"RedisEventStore": "class",
"RegisterJobTool": "class",
"RegisterWorkflowTool": "class",
"RegistryDefinitionNotFoundError": "class",
Expand Down Expand Up @@ -450,6 +454,7 @@
"SCHEMA_DESCRIPTION_FORMATS": "array",
"SERVER_MANIFEST_FILE": "string",
"SERVER_MANIFEST_VERSION": "number",
"SSEServerTransport": "class",
"STATELESS_SESSION_ID": "string",
"SURFACE_META_KEY": "string",
"SamplingNotAvailableError": "class",
Expand Down Expand Up @@ -639,6 +644,7 @@
"buildRemoteResourceRecord": "function",
"buildRemoteResourceTemplateRecord": "function",
"buildRemoteToolRecord": "function",
"buildScopedServerOptions": "function",
"buildSkillsCatalogSummary": "function",
"buildStorageKey": "function",
"canDeliverNotifications": "function",
Expand Down Expand Up @@ -680,6 +686,7 @@
"createEsmResourceInstance": "function",
"createEsmToolContextClass": "function",
"createEsmToolInstance": "function",
"createEventStore": "function",
"createExtAppsMessageHandler": "function",
"createGuardManager": "function",
"createInMemoryServer": "function",
Expand Down
12 changes: 12 additions & 0 deletions apps/e2e/demo-e2e-bundle-size/e2e/helpers/subpath-exports.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Loads @frontmcp/sdk and one of its subpaths from the project this file is copied into, with require() or import(), and
// prints, as JSON, the subpath's export names and those not the root's: node subpath-exports.mjs <subpath> <require|import>
import { createRequire } from 'node:module';

const [subpath, loader] = process.argv.slice(2);
const load = loader === 'require' ? createRequire(import.meta.url) : (specifier) => import(specifier);
const root = await load('@frontmcp/sdk');
const subpathExports = await load(`@frontmcp/sdk/${subpath}`);

const names = Object.keys(subpathExports).sort();
const notFromRoot = names.filter((name) => subpathExports[name] === undefined || subpathExports[name] !== root[name]);
process.stdout.write(JSON.stringify({ names, notFromRoot }));
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import { workspaceRoot } from './helpers/browser-bundle';

type Step = { step: string; initialized: string[] };

const SUBSYSTEM_MODULE = /^libs\/sdk\/src\/(job|workflow|channel|remote-mcp|esm-loader)\//;
const SUBSYSTEM_MODULE =
/^libs\/sdk\/src\/((job|workflow|channel|remote-mcp|esm-loader)\/|transport\/(event-stores\/|adapters\/(base-sse|sse|streamable-http)-transport\.ts))/;
// Helpers the startup path itself uses: the decorators parse `Tool.esm()` specifiers, the scope normalizes app entries
const STARTUP_HELPERS = [
'libs/sdk/src/esm-loader/package-specifier.ts',
Expand Down Expand Up @@ -48,6 +49,9 @@ describe('the subsystems of the SDK under require()', () => {
['ChannelRegistry', 'libs/sdk/src/channel/channel.registry.ts'],
['McpClientService', 'libs/sdk/src/remote-mcp/mcp-client.service.ts'],
['EsmModuleLoader', 'libs/sdk/src/esm-loader/esm-module-loader.ts'],
['RecreateableStreamableHTTPServerTransport', 'libs/sdk/src/transport/adapters/streamable-http-transport.ts'],
['SSEServerTransport', 'libs/sdk/src/transport/adapters/base-sse-transport.ts'],
['createEventStore', 'libs/sdk/src/transport/event-stores/event-store.factory.ts'],
])('are set up when %s is read', (exportName, source) => {
const [, read] = initializedModules(`read:${exportName}`);

Expand Down
89 changes: 89 additions & 0 deletions apps/e2e/demo-e2e-bundle-size/e2e/sdk-subpath-exports.e2e.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* The published exports map of @frontmcp/sdk names only files its build emits, and each subpath hands back the root
* entry's own classes and functions: under require() and import in Node, and in a browser bundle.
*
* Needs the packages built: `nx run-many -t build -p sdk plugin-webmcp`.
*/
import { execFileSync } from 'node:child_process';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';

import { copyFile, fileExists, mkdir, mkdtemp, readFileSync, rm, symlink } from '@frontmcp/utils';

import { bundleForBrowser, fixtureDir, inputSizes, SERVER_MODULES, workspaceRoot } from './helpers/browser-bundle';

type Loader = 'require' | 'import';

const sdkDist = resolve(workspaceRoot, 'libs/sdk/dist');
const exportsMap: Record<string, unknown> = JSON.parse(readFileSync(join(sdkDist, 'package.json'))).exports;
const SUBPATHS = Object.keys(exportsMap)
.filter((key) => key !== '.' && key !== './package.json' && exportsMap[key] !== null)
.map((key) => key.slice('./'.length));

function exportTargets(entry: unknown): string[] {
if (typeof entry === 'string') return [entry];
if (entry === null || typeof entry !== 'object') return [];
return Object.values(entry).flatMap(exportTargets);
}

describe('the exports map of the published @frontmcp/sdk', () => {
it('names only files its build emits', async () => {
const targets = exportTargets(exportsMap);
const missing: string[] = [];
for (const target of targets) {
if (!(await fileExists(join(sdkDist, target)))) missing.push(target);
}

expect(targets.length).toBeGreaterThan(0);
expect(missing).toEqual([]);
});
});

describe('a subpath of @frontmcp/sdk installed in a project', () => {
let project: string;

beforeAll(async () => {
project = await mkdtemp(join(tmpdir(), 'frontmcp-sdk-subpaths-'));
await mkdir(join(project, 'node_modules', '@frontmcp'), { recursive: true });
await symlink(sdkDist, join(project, 'node_modules', '@frontmcp', 'sdk'), 'dir');
await copyFile(resolve(__dirname, 'helpers', 'subpath-exports.mjs'), join(project, 'subpath-exports.mjs'));
});

afterAll(async () => {
if (project) await rm(project, { recursive: true, force: true });
});

function subpathExports(subpath: string, loader: Loader): { names: string[]; notFromRoot: string[] } {
return JSON.parse(
execFileSync(process.execPath, [join(project, 'subpath-exports.mjs'), subpath, loader], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
}),
);
}

describe.each(SUBPATHS)('@frontmcp/sdk/%s', (subpath) => {
it.each<Loader>(['require', 'import'])("hands back the root entry's own values under %s", (loader) => {
const { names, notFromRoot } = subpathExports(subpath, loader);

expect(names.length).toBeGreaterThan(0);
expect(notFromRoot).toEqual([]);
});

it('exports the same names under require and import', () => {
expect(subpathExports(subpath, 'import').names).toEqual(subpathExports(subpath, 'require').names);
});
});
});

describe('a browser bundle of the subpaths of @frontmcp/sdk', () => {
it('takes their values from the browser build the root entry comes from', async () => {
const bundle = await bundleForBrowser(resolve(fixtureDir, 'subpath-imports.mjs'), SERVER_MODULES);
const sdkInputs = inputSizes(bundle.metafile)
.map(([input]) => input)
.filter((input) => input.startsWith('libs/sdk/dist/'));

expect(sdkInputs).toContain('libs/sdk/dist/browser/job/tools/execute-job.tool.mjs');
expect(sdkInputs.filter((input) => !input.startsWith('libs/sdk/dist/browser/'))).toEqual([]);
});
});
4 changes: 4 additions & 0 deletions apps/e2e/demo-e2e-bundle-size/fixture/subpath-imports.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export { ExecuteJobTool } from '@frontmcp/sdk/job/tools';
export { ExecuteWorkflowTool } from '@frontmcp/sdk/workflow/tools';
export { createEventStore } from '@frontmcp/sdk/transport';
export { ToolContext } from '@frontmcp/sdk';
5 changes: 5 additions & 0 deletions apps/e2e/demo-e2e-dts-consumer/e2e/dts-consumer.e2e.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const fixtureFiles = [
'build-manifest-must-compile.ts',
'persistent-questions-must-compile.ts',
'lazy-apps-must-compile.ts',
'subpath-exports-must-compile.ts',
].map((fileName) => path.join(fixtureDir, fileName));

function compileFixture(withoutExpectErrors?: string): { diagnostics: FixtureDiagnostic[]; sourceFiles: string[] } {
Expand Down Expand Up @@ -255,6 +256,10 @@ describe('@frontmcp/sdk declarations in a strict consumer project', () => {
expect(describeEach(inFixtureFile('lazy-apps-must-compile.ts'))).toEqual([]);
});

it('types the transport, job/tools and workflow/tools subpaths as the root exports they re-export', () => {
expect(describeEach(inFixtureFile('subpath-exports-must-compile.ts'))).toEqual([]);
});

it('ships declaration files that type-check with skipLibCheck disabled', () => {
const sdkDeclarationDiagnostics = diagnostics.filter((diagnostic) =>
diagnostic.fileName.startsWith(sdkDeclarationsDir + path.sep),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
createEventStore,
type buildScopedServerOptions,
type ExecuteJobTool,
type ExecuteWorkflowTool,
type MemoryEventStore,
type RecreateableSSEServerTransport,
type RecreateableStreamableHTTPServerTransport,
type RedisEventStore,
type SSEServerTransport,
} from '@frontmcp/sdk';
import { ExecuteJobTool as SubpathExecuteJobTool } from '@frontmcp/sdk/job/tools';
import * as transport from '@frontmcp/sdk/transport';
import { ExecuteWorkflowTool as SubpathExecuteWorkflowTool } from '@frontmcp/sdk/workflow/tools';

export const jobTool: typeof ExecuteJobTool = SubpathExecuteJobTool;
export const workflowTool: typeof ExecuteWorkflowTool = SubpathExecuteWorkflowTool;
export const streamableTransport: typeof RecreateableStreamableHTTPServerTransport =
transport.RecreateableStreamableHTTPServerTransport;
export const sseTransport: typeof RecreateableSSEServerTransport = transport.RecreateableSSEServerTransport;
export const baseSseTransport: typeof SSEServerTransport = transport.SSEServerTransport;
export const memoryStore: typeof MemoryEventStore = transport.MemoryEventStore;
export const redisStore: typeof RedisEventStore = transport.RedisEventStore;
export const serverOptions: typeof buildScopedServerOptions = transport.buildScopedServerOptions;

export function eventStoreOf(config: transport.EventStoreConfig): transport.EventStoreResult['eventStore'] {
return createEventStore(config).eventStore;
}

export function notAConfig(): void {
// @ts-expect-error createEventStore takes an EventStoreConfig
transport.createEventStore('memory');
}
1 change: 1 addition & 0 deletions apps/e2e/demo-e2e-dts-consumer/fixture/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"http-routes-must-compile.ts",
"persistent-questions-must-compile.ts",
"lazy-apps-must-compile.ts",
"subpath-exports-must-compile.ts",
"http-routes-must-compile.ts"
]
}
2 changes: 2 additions & 0 deletions libs/sdk/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,12 @@
"build": {
"executor": "nx:run-commands",
"dependsOn": ["build-cjs", "build-esm", "build-browser", "build-browser-esm"],
"inputs": ["production", "^production", "{workspaceRoot}/scripts/build-subpath-reexports.mjs"],
"options": {
"commands": [
"npx esbuild libs/sdk/src/esm-loader/import-computed.ts --format=cjs --platform=node --outfile=libs/sdk/dist/esm-loader/import-computed.js",
"npx esbuild libs/sdk/src/esm-loader/import-computed.ts --format=esm --platform=node --outfile=libs/sdk/dist/esm/esm-loader/import-computed.mjs",
"node scripts/build-subpath-reexports.mjs libs/sdk",
"node scripts/strip-dist-from-pkg.js libs/sdk/dist/package.json",
"node scripts/inject-esm-require-banner.js libs/sdk/dist/esm libs/sdk/dist/browser",
"node scripts/escape-non-ascii.mjs libs/sdk/dist"
Expand Down
15 changes: 15 additions & 0 deletions libs/sdk/src/__tests__/subpath-exports.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// The build writes each subpath export of the package as a re-export of the root entry, so the root exports all its values
describe.each([
['./transport', () => import('../transport')],
['./job/tools', () => import('../job/tools')],
['./workflow/tools', () => import('../workflow/tools')],
])('the %s subpath', (_subpath, loadSubpath) => {
it('exports only values the root entry exports', async () => {
const root: Record<string, unknown> = await import('../index');
const subpathExports: Record<string, unknown> = await loadSubpath();

const names = Object.keys(subpathExports);
expect(names.length).toBeGreaterThan(0);
expect(names.filter((name) => root[name] !== subpathExports[name])).toEqual([]);
});
});
11 changes: 11 additions & 0 deletions libs/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,17 @@ export type { PersistentSessionOwnerStore } from './transport/persistent-session
// Transport types
export type { TransportType, TransportKey } from './transport/transport.types';
export { STATELESS_SESSION_ID } from './transport/transport.types';
export { buildScopedServerOptions } from './transport/build-scoped-server-options';

// The transports and SSE event stores `@frontmcp/sdk/transport` also exports, which the CommonJS build loads on first use
export {
RecreateableStreamableHTTPServerTransport,
RecreateableSSEServerTransport,
SSEServerTransport,
createEventStore,
MemoryEventStore,
RedisEventStore,
} from './subsystems';

// ─────────────────────────────────────────────────────────────────────────────
// MCP Apps (ext-apps) Support
Expand Down
10 changes: 10 additions & 0 deletions libs/sdk/src/lazy-subsystems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,16 @@
() => require('./workflow/tools'),
['ExecuteWorkflowTool', 'GetWorkflowStatusTool', 'ListWorkflowsTool', 'RegisterWorkflowTool', 'RemoveWorkflowTool'],
);
defineLazyExports(
() => require('./transport/adapters/streamable-http-transport'),
['RecreateableStreamableHTTPServerTransport'],
);
defineLazyExports(() => require('./transport/adapters/sse-transport'), ['RecreateableSSEServerTransport']);
defineLazyExports(() => require('#sse-transport'), ['SSEServerTransport']);
defineLazyExports(
() => require('./transport/event-stores'),
['createEventStore', 'MemoryEventStore', 'RedisEventStore'],
);
defineLazyExports(
() => require('./channel'),
[
Expand Down
4 changes: 4 additions & 0 deletions libs/sdk/src/subsystems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ export {
RegisterWorkflowTool,
RemoveWorkflowTool,
} from './workflow/tools';
export { RecreateableStreamableHTTPServerTransport } from './transport/adapters/streamable-http-transport';
export { RecreateableSSEServerTransport } from './transport/adapters/sse-transport';
export { SSEServerTransport } from '#sse-transport';
export { createEventStore, MemoryEventStore, RedisEventStore } from './transport/event-stores';
export {
ChannelInstance,
ChannelRegistry,
Expand Down
4 changes: 2 additions & 2 deletions libs/sdk/src/transport/event-stores/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*
* @example Memory EventStore (single-node)
* ```typescript
* import { createEventStore } from '@frontmcp/sdk/transport/event-stores';
* import { createEventStore } from '@frontmcp/sdk';
*
* const { eventStore, type } = createEventStore({
* enabled: true,
Expand All @@ -27,7 +27,7 @@
*
* @example Redis EventStore (distributed)
* ```typescript
* import { createEventStore } from '@frontmcp/sdk/transport/event-stores';
* import { createEventStore } from '@frontmcp/sdk';
*
* const { eventStore, type } = createEventStore({
* enabled: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ transport: {

From 1.7.2 each session gets a view over the shared store scoped to its own session id: an event id belonging to another session replays nothing.

Code that builds its own transport can import the stores and transports: `createEventStore`, `MemoryEventStore`, `RedisEventStore`, `RecreateableStreamableHTTPServerTransport`, `RecreateableSSEServerTransport` and `SSEServerTransport` come from `@frontmcp/sdk`, and from `@frontmcp/sdk/transport`, which re-exports them from the root entry. Up to 1.10 neither loaded them at runtime: the root did not export them, and the subpath shipped type declarations only (`MODULE_NOT_FOUND` under `require`, `ERR_MODULE_NOT_FOUND` under `import`).

## Target-Specific Recommendations

| Target | Recommended Preset | Persistence | Event Store |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -644,7 +644,7 @@ import { App, ExecuteJobTool, GetJobStatusTool, ListJobsTool } from '@frontmcp/s
class DataApp {}
```

The same classes are also reachable via the subpath `@frontmcp/sdk/job/tools` and `@frontmcp/sdk/workflow/tools` for bundlers that prefer narrower imports.
The subpaths `@frontmcp/sdk/job/tools` and `@frontmcp/sdk/workflow/tools` export the same classes: they re-export them from the root entry, so `ExecuteJobTool` from either is the class `@frontmcp/sdk` exports, and a subpath import does not make a bundle smaller. Up to 1.10 the subpaths shipped type declarations only, so loading one failed with `MODULE_NOT_FOUND` under `require` and `ERR_MODULE_NOT_FOUND` under `import`; on those versions import the classes from `@frontmcp/sdk`.

## Nx Generator

Expand Down
Loading
Loading