Why ESM Packages?
Zero Build Step
Load community or internal packages at runtime without bundling them into your server build
Auto-Updates
Background version polling with semver-aware hot-reload — tools update without restarts
Private Registries
Token-based authentication for private npm registries and custom CDN endpoints
Cross-Platform
Works in Node.js (disk + memory cache) and browser environments (memory-only cache)
Quick Start
App.esm() method creates an app entry that loads the npm package @acme/mcp-tools (any version matching ^1.0.0) at server startup. Its tools, resources, and prompts are automatically registered.
How It Works
Parse Specifier
App.esm('@acme/tools@^1.0.0') parses the npm package specifier into scope, name, and semver range.Resolve Version
The npm registry is queried to resolve the semver range (e.g.,
^1.0.0) to a concrete version (e.g., 1.2.3).Check Cache
The two-tier cache is checked — first in-memory, then disk. On a hit, the cached bundle is used directly.
Fetch Bundle
On a cache miss, the bundle is fetched from the esm.sh CDN (or a custom loader URL) and stored in both cache tiers.
Evaluate Module
The bundle is evaluated (ESM via native
import(), CJS via bridge wrapper) and the default export is extracted.Normalize Manifest
The export is normalized into a
FrontMcpPackageManifest — supporting plain objects, decorated classes, or named exports.App.esm() API
Parameters
npm package specifier in the format
@scope/name@range or name@range.
The range defaults to latest if omitted.Options
Override the auto-derived app name (defaults to the package’s full name).
Prefix for all tools, resources, and prompts from this package. For example,
namespace: 'acme' turns a tool named echo into acme:echo.Human-readable description for the app entry.
Isolation mode.
true creates a separate scope; 'includeInParent' lists it under the parent while keeping isolation.Per-app loader override. Takes precedence over the gateway-level
loader. See Gateway-Level Loader for fields.Enable background version polling. When a new version matching the semver range is published, the package is automatically reloaded. Default interval: 300,000ms (5 minutes).
Local cache time-to-live in milliseconds. Default: 86,400,000ms (24 hours).
Import map overrides for ESM resolution. Maps package names to alternative URLs.
Include/exclude filter for selectively importing primitives. See Per-Primitive Loading for details.
Examples
Package Specifier Format
| Pattern | Example | Description |
|---|---|---|
@scope/name@range | @acme/tools@^1.0.0 | Scoped package with semver range |
@scope/name@tag | @acme/tools@latest | Scoped package with dist-tag |
name@range | my-tools@~2.0.0 | Unscoped package with semver range |
name | my-tools | Unscoped package, defaults to latest |
^1.0.0, ~1.0.0, >=1.0.0 <2.0.0, exact versions like 1.2.3, and dist-tags like latest, next, beta.
Gateway-Level Loader
Set a default loader at the server level that applies to all npm apps:loader option in App.esm().
PackageLoader Fields
| Field | Type | Default | Description |
|---|---|---|---|
url | string | https://esm.sh (bundles), https://registry.npmjs.org (registry) | Base URL for both registry API and bundle fetching |
registryUrl | string | Same as url | Separate registry URL for version resolution (if different from bundle URL) |
token | string | — | Bearer token for authentication |
tokenEnvVar | string | — | Environment variable name containing the bearer token |
When
url is set but registryUrl is not, both the registry API and bundle downloads use url. When registryUrl is also set, the registry uses registryUrl while bundles use url.Auto-Update & Hot-Reload
Enable background version polling to automatically reload packages when new versions are published:- The new bundle is fetched and cached
- All tools, resources, and prompts from the old version are unregistered
- The new manifest is registered
- MCP clients receive
tools/list_changedandresources/list_changednotifications
CLI Update
You can also check for and apply updates via the CLI:Caching
ESM packages use a two-tier cache for fast startup and offline resilience:| Tier | Environment | Persistence | Speed |
|---|---|---|---|
| Memory | All | Process lifetime | Instant |
| Disk | Node.js only | Survives restarts | Fast (local I/O) |
Cache Locations (Node.js)
- Project mode:
node_modules/.cache/frontmcp-esm/(relative to project root) - CLI mode:
~/.frontmcp/esm-cache/(user home directory)
.mjs or .cjs) and a meta.json with version, etag, and timestamp.
Cache TTL
Configure how long cached bundles remain valid:Browser Mode
In browser environments, only the in-memory cache is used. Bundles are evaluated viaBlob + URL.createObjectURL and stored in a Map. See Browser Compatibility for details.
Private Registry Authentication
Authorization header for both registry API calls (version resolution) and bundle fetching.
Comparison: ESM vs Remote vs Local Apps
| Aspect | Local Apps | ESM Packages | Remote Apps |
|---|---|---|---|
| Declaration | @App class | App.esm() | App.remote() |
| Execution | In-process | In-process | Out-of-process (HTTP) |
| Transport | None | None | Streamable HTTP / SSE |
| Caching | N/A | Two-tier (memory + disk) | Optional via CachePlugin |
| Auth | N/A | npm registry auth | remoteAuth config |
| Hot-Reload | Requires restart | Version polling | N/A |
| Plugins/Adapters | Full support | Not supported | Not supported |
| Best For | First-party code | Community/npm packages | External MCP servers |
Error Handling
ESM loading can fail at several stages. FrontMCP provides specific error classes for each:| Error | When | HTTP |
|---|---|---|
EsmInvalidSpecifierError | Package specifier format is invalid | 400 |
EsmVersionResolutionError | npm registry query fails or no matching version | 500 |
EsmRegistryAuthError | Private registry authentication fails | 401 |
EsmPackageLoadError | Bundle fetch or evaluation fails | 500 |
EsmManifestInvalidError | Package export doesn’t match manifest contract | 400 |
EsmCacheError | Cache read/write operation fails | 500 |
Best Practices
Do:- Use
namespaceto avoid naming conflicts between packages - Use
tokenEnvVarinstead of inline tokens for private registries - Set
autoUpdatewith conservative intervals in production (5+ minutes) - Use
cacheTTLto balance freshness vs. startup speed - Test packages locally before deploying to production
- Commit registry tokens in source code
- Use very short polling intervals in production (avoids registry rate limits)
- Load untrusted packages without reviewing their manifest and code
- Rely on ESM packages for plugins or adapters (use local
@Appclasses instead)
Next Steps
Publishing ESM Packages
Create and publish npm packages loadable by FrontMCP servers
CLI Reference
Use
frontmcp package esm-update to manage ESM packagesApps
Full guide to local and remote app configuration
ESM Errors
Error classes for ESM loading, caching, and authentication