Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c9a8421
feat: Add ESM support with new loaders and record types
frontegg-david Mar 14, 2026
e44e907
feat: Implement browser E2E testing with dynamic ESM tool loading
frontegg-david Mar 14, 2026
74e8a2c
feat: Improve error handling and validation for ESM integration
frontegg-david Mar 15, 2026
72fc80a
feat: Enhance error handling and improve ESM server process management
frontegg-david Mar 15, 2026
188878a
feat: Enhance package specifier validation and improve error handling…
frontegg-david Mar 15, 2026
d46104e
feat: Add support for non-latest dist-tags in version range checks an…
frontegg-david Mar 15, 2026
1df9dcc
feat: Update ESM server port configuration for E2E tests and adjust r…
frontegg-david Mar 15, 2026
768d30f
feat: Update E2E tests to use actual ESM server port and improve envi…
frontegg-david Mar 15, 2026
7aacc41
Merge branch 'main' into support-remote-tools
frontegg-david Mar 15, 2026
f17a6bd
feat: Update E2E tests to use actual ESM server port and improve envi…
frontegg-david Mar 15, 2026
e118fd7
feat: Update E2E tests to use actual ESM server port and improve envi…
frontegg-david Mar 15, 2026
1b95469
feat: Enhance app normalization and test server port allocation with …
frontegg-david Mar 15, 2026
ee707d4
feat: Improve error handling during server startup and cleanup process
frontegg-david Mar 15, 2026
817373a
feat: Validate MCP URI and streamline file reading with utility funct…
frontegg-david Mar 15, 2026
7a1d093
feat: Refactor directory path handling to use path.sep for cross-plat…
frontegg-david Mar 15, 2026
844ca14
feat: Update E2E tests to use improved test structure and enhance err…
frontegg-david Mar 15, 2026
267ce6f
feat: Update esmShBaseUrl to use esmBaseUrl for consistency in E2E tests
frontegg-david Mar 15, 2026
3a3f7ea
feat: Rename esmShBaseUrl to esmBaseUrl for consistency across the co…
frontegg-david Mar 15, 2026
619f33d
feat: Enhance error handling and improve cache validation in E2E tests
frontegg-david Mar 15, 2026
7a07686
feat: Refactor package version handling and improve error responses i…
frontegg-david Mar 15, 2026
4051e58
feat: Improve request handling and enhance server startup error manag…
frontegg-david Mar 15, 2026
bcfa4c5
feat: Add documentation for ESM dynamic loading and update related se…
frontegg-david Mar 15, 2026
b0cc3fe
feat: Enhance server error handling and improve port validation
frontegg-david Mar 15, 2026
f36593e
feat: Add support for remote tools and enhance app filtering configur…
frontegg-david Mar 16, 2026
e9a0721
feat: Add support for remote tools and enhance app filtering configur…
frontegg-david Mar 16, 2026
33b8b14
feat: Enhance remote tools support with URL validation and new decora…
frontegg-david Mar 16, 2026
8c88e37
feat: Implement remote URL validation and enhance decorator types for…
frontegg-david Mar 16, 2026
d746e3b
feat: Improve remote URL validation and enhance job normalization logic
frontegg-david Mar 16, 2026
aa3b352
feat: Improve remote URL validation and enhance job normalization logic
frontegg-david Mar 16, 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
11 changes: 11 additions & 0 deletions apps/e2e/demo-e2e-esm/browser-app/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
<title>FrontMCP Browser ESM E2E</title>
</head>
<body>
<div id="app">Loading...</div>
<script type="module" src="./main.ts"></script>
</body>
</html>
116 changes: 116 additions & 0 deletions apps/e2e/demo-e2e-esm/browser-app/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/**
* @file main.ts
* @description Full FrontMCP instance running in the browser with dynamic ESM tool loading.
*
* This is the browser entry point for the ESM E2E test. It boots a real FrontMCP
* DirectClient (no HTTP server) that loads ESM tool packages on the fly from
* a local ESM package server.
*
* Playwright tests read results from window.__ESM_RESULTS__ after the page loads.
*/
import 'reflect-metadata';
import { connect, App, LogLevel } from '@frontmcp/sdk';

// Read ESM server URL from query params (set by Playwright test)
const params = new URLSearchParams(location.search);
const esmServerUrl = params.get('esmServer') ?? 'http://127.0.0.1:50413';

interface EsmTestResults {
success: boolean;
toolNames?: string[];
echoResult?: unknown;
addResult?: unknown;
greetResult?: unknown;
resourceUris?: string[];
promptNames?: string[];
error?: string;
}

declare global {
interface Window {
__ESM_RESULTS__?: EsmTestResults;
}
}

async function main(): Promise<void> {
const app = document.getElementById('app');

try {
if (!app) {
throw new Error('Missing #app root element');
}
app.textContent = 'Connecting to FrontMCP...';

// Boot a FULL FrontMCP instance in the browser via DirectClient
const client = await connect(
{
info: { name: 'Browser ESM E2E', version: '0.1.0' },
loader: { url: esmServerUrl },
apps: [
App.esm('@test/esm-tools@^1.0.0', { namespace: 'esm' }),
App.esm('@test/esm-multi@^1.0.0', { namespace: 'multi' }),
],
logging: { level: LogLevel.Warn },
},
{ mode: 'cli' },
);

app.textContent = 'Loading tools...';

// List tools
const { tools } = await client.listTools();
const toolNames = tools.map((t) => t.name);

// Call tools
const echoResult = await client.callTool({
name: 'esm:echo',
arguments: { message: 'browser-hello' },
});

const addResult = await client.callTool({
name: 'esm:add',
arguments: { a: 5, b: 7 },
});

const greetResult = await client.callTool({
name: 'multi:greet',
arguments: { name: 'Browser' },
});

// List resources
const { resources } = await client.listResources();
const resourceUris = resources.map((r) => r.uri);

// List prompts
const { prompts } = await client.listPrompts();
const promptNames = prompts.map((p) => p.name);

// Report results
const results: EsmTestResults = {
success: true,
toolNames,
echoResult,
addResult,
greetResult,
resourceUris,
promptNames,
};

window.__ESM_RESULTS__ = results;
app.textContent = JSON.stringify(results, null, 2);
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
const stack = err instanceof Error ? err.stack : undefined;
const results: EsmTestResults = {
success: false,
error: message + (stack ? '\n' + stack : ''),
};
window.__ESM_RESULTS__ = results;
if (app) {
app.textContent = 'Error: ' + message;
}
console.error('FrontMCP browser ESM error:', err);
}
}

main();
42 changes: 42 additions & 0 deletions apps/e2e/demo-e2e-esm/browser-app/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { defineConfig } from 'vite';
import { resolve } from 'path';

const root = resolve(__dirname, '../../../..');

export default defineConfig({
root: __dirname,
resolve: {
conditions: ['browser', 'development', 'import'],
alias: [
// @frontmcp/* → source files for local development (matches demo-e2e-browser-bundle pattern)
{ find: '@frontmcp/sdk', replacement: resolve(root, 'libs/sdk/src/index.ts') },
{ find: '@frontmcp/utils', replacement: resolve(root, 'libs/utils/src/index.ts') },
{ find: '@frontmcp/auth', replacement: resolve(root, 'libs/auth/src/index.ts') },
{ find: '@frontmcp/di', replacement: resolve(root, 'libs/di/src/index.ts') },
{ find: '@frontmcp/protocol', replacement: resolve(root, 'libs/protocol/src/index.ts') },
],
},
define: {
'process.env.NODE_ENV': JSON.stringify('development'),
},
server: {
port: 4402,
},
preview: {
port: 4402,
},
build: {
outDir: resolve(root, 'dist/apps/e2e/demo-e2e-esm-browser'),
emptyOutDir: true,
rollupOptions: {
shimMissingExports: true,
onwarn(warning, warn) {
// Suppress circular dependency warnings from reflect-metadata
if (warning.code === 'CIRCULAR_DEPENDENCY') return;
// Suppress missing export warnings (type-only re-exports erased by esbuild)
if (warning.code === 'MISSING_EXPORT') return;
warn(warning);
},
},
},
});
151 changes: 151 additions & 0 deletions apps/e2e/demo-e2e-esm/e2e/browser/esm-browser.pw.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* Browser E2E Tests for Full FrontMCP with Dynamic ESM Tool Loading
*
* Uses Playwright to verify that a complete FrontMCP DirectClient runs in the browser,
* loading ESM tool packages on the fly from a local ESM package server.
*
* The browser app (browser-app/main.ts) boots a real FrontMCP instance via connect()
* with loadFrom() ESM apps, then reports results to window.__ESM_RESULTS__.
*
* Prerequisites:
* - ESM package server must be running on the configured port
* - Vite preview server must be serving the browser app
*/
import { test, expect } from '@playwright/test';
import { spawn, type ChildProcess } from 'node:child_process';
import { ESM_SERVER_PORT } from './helpers';

let esmServerProcess: ChildProcess | null = null;

// Start local ESM package server before all tests
test.beforeAll(async () => {
await new Promise<void>((resolve, reject) => {
esmServerProcess = spawn('npx', ['tsx', 'apps/e2e/demo-e2e-esm/src/esm-package-server/main.ts'], {
env: { ...process.env, ESM_SERVER_PORT: String(ESM_SERVER_PORT) },
stdio: 'pipe',
cwd: process.cwd(),
});

let started = false;
const timeout = setTimeout(() => {
if (!started) {
esmServerProcess?.kill('SIGTERM');
reject(new Error('ESM server startup timeout'));
}
}, 30000);

esmServerProcess.stdout?.on('data', (data: Buffer) => {
if (data.toString().includes('ESM Package Server started') && !started) {
started = true;
clearTimeout(timeout);
resolve();
}
});

esmServerProcess.stderr?.on('data', (data: Buffer) => {
console.error('[esm-server]', data.toString());
});

esmServerProcess.on('error', (err) => {
clearTimeout(timeout);
esmServerProcess?.kill('SIGTERM');
reject(err);
});
});
});

// Stop ESM package server after all tests
test.afterAll(async () => {
if (esmServerProcess) {
try {
esmServerProcess.kill('SIGTERM');
} catch {
// Process may have already exited
}
esmServerProcess = null;
}
});

test.describe('Full FrontMCP in Browser with ESM Loading', () => {
test('loads and reports ESM tools successfully', async ({ page }) => {
await page.goto(`/?esmServer=http://127.0.0.1:${ESM_SERVER_PORT}`);

// Wait for FrontMCP to finish loading (window.__ESM_RESULTS__ becomes defined)
await page.waitForFunction(() => (window as unknown as { __ESM_RESULTS__?: unknown }).__ESM_RESULTS__, {
timeout: 60000,
});

const results = await page.evaluate(() => (window as unknown as { __ESM_RESULTS__: unknown }).__ESM_RESULTS__);
const r = results as { success: boolean; error?: string; toolNames: string[] };

expect(r.success).toBe(true);
expect(r.toolNames).toContain('esm:echo');
expect(r.toolNames).toContain('esm:add');
expect(r.toolNames).toContain('multi:greet');
});

test('ESM echo tool executes correctly in browser', async ({ page }) => {
await page.goto(`/?esmServer=http://127.0.0.1:${ESM_SERVER_PORT}`);
await page.waitForFunction(() => (window as unknown as { __ESM_RESULTS__?: unknown }).__ESM_RESULTS__, {
timeout: 60000,
});

const results = await page.evaluate(() => (window as unknown as { __ESM_RESULTS__: unknown }).__ESM_RESULTS__);
const r = results as { success: boolean; echoResult: { content: Array<{ text: string }> } };

expect(r.success).toBe(true);
expect(r.echoResult.content[0].text).toContain('browser-hello');
});

test('ESM add tool computes correctly in browser', async ({ page }) => {
await page.goto(`/?esmServer=http://127.0.0.1:${ESM_SERVER_PORT}`);
await page.waitForFunction(() => (window as unknown as { __ESM_RESULTS__?: unknown }).__ESM_RESULTS__, {
timeout: 60000,
});

const results = await page.evaluate(() => (window as unknown as { __ESM_RESULTS__: unknown }).__ESM_RESULTS__);
const r = results as { success: boolean; addResult: { content: Array<{ text: string }> } };

expect(r.success).toBe(true);
expect(r.addResult.content[0].text).toBe('12'); // 5 + 7
});

test('ESM multi-package greet tool works in browser', async ({ page }) => {
await page.goto(`/?esmServer=http://127.0.0.1:${ESM_SERVER_PORT}`);
await page.waitForFunction(() => (window as unknown as { __ESM_RESULTS__?: unknown }).__ESM_RESULTS__, {
timeout: 60000,
});

const results = await page.evaluate(() => (window as unknown as { __ESM_RESULTS__: unknown }).__ESM_RESULTS__);
const r = results as { success: boolean; greetResult: { content: Array<{ text: string }> } };

expect(r.success).toBe(true);
expect(r.greetResult.content[0].text).toContain('Browser');
});

test('ESM resources are discoverable in browser', async ({ page }) => {
await page.goto(`/?esmServer=http://127.0.0.1:${ESM_SERVER_PORT}`);
await page.waitForFunction(() => (window as unknown as { __ESM_RESULTS__?: unknown }).__ESM_RESULTS__, {
timeout: 60000,
});

const results = await page.evaluate(() => (window as unknown as { __ESM_RESULTS__: unknown }).__ESM_RESULTS__);
const r = results as { success: boolean; resourceUris: string[] };

expect(r.success).toBe(true);
expect(r.resourceUris).toContain('esm://status');
});

test('ESM prompts are discoverable in browser', async ({ page }) => {
await page.goto(`/?esmServer=http://127.0.0.1:${ESM_SERVER_PORT}`);
await page.waitForFunction(() => (window as unknown as { __ESM_RESULTS__?: unknown }).__ESM_RESULTS__, {
timeout: 60000,
});

const results = await page.evaluate(() => (window as unknown as { __ESM_RESULTS__: unknown }).__ESM_RESULTS__);
const r = results as { success: boolean; promptNames: string[] };

expect(r.success).toBe(true);
expect(r.promptNames).toContain('multi:greeting-prompt');
});
});
13 changes: 13 additions & 0 deletions apps/e2e/demo-e2e-esm/e2e/browser/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* @file helpers.ts
* @description Playwright test helpers for ESM browser E2E tests.
*
* The browser app is served by Vite (configured in playwright.config.ts webServer).
* These helpers provide shared constants and utilities for the test suite.
*/

/** Port for the local ESM package server */
export const ESM_SERVER_PORT = 50413;

/** Port for the Vite preview server */
export const VITE_PORT = 4402;
Loading
Loading