-
Notifications
You must be signed in to change notification settings - Fork 6
feat: support dynamic loading remote tools #277
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 e44e907
feat: Implement browser E2E testing with dynamic ESM tool loading
frontegg-david 74e8a2c
feat: Improve error handling and validation for ESM integration
frontegg-david 72fc80a
feat: Enhance error handling and improve ESM server process management
frontegg-david 188878a
feat: Enhance package specifier validation and improve error handling…
frontegg-david d46104e
feat: Add support for non-latest dist-tags in version range checks an…
frontegg-david 1df9dcc
feat: Update ESM server port configuration for E2E tests and adjust r…
frontegg-david 768d30f
feat: Update E2E tests to use actual ESM server port and improve envi…
frontegg-david 7aacc41
Merge branch 'main' into support-remote-tools
frontegg-david f17a6bd
feat: Update E2E tests to use actual ESM server port and improve envi…
frontegg-david e118fd7
feat: Update E2E tests to use actual ESM server port and improve envi…
frontegg-david 1b95469
feat: Enhance app normalization and test server port allocation with …
frontegg-david ee707d4
feat: Improve error handling during server startup and cleanup process
frontegg-david 817373a
feat: Validate MCP URI and streamline file reading with utility funct…
frontegg-david 7a1d093
feat: Refactor directory path handling to use path.sep for cross-plat…
frontegg-david 844ca14
feat: Update E2E tests to use improved test structure and enhance err…
frontegg-david 267ce6f
feat: Update esmShBaseUrl to use esmBaseUrl for consistency in E2E tests
frontegg-david 3a3f7ea
feat: Rename esmShBaseUrl to esmBaseUrl for consistency across the co…
frontegg-david 619f33d
feat: Enhance error handling and improve cache validation in E2E tests
frontegg-david 7a07686
feat: Refactor package version handling and improve error responses i…
frontegg-david 4051e58
feat: Improve request handling and enhance server startup error manag…
frontegg-david bcfa4c5
feat: Add documentation for ESM dynamic loading and update related se…
frontegg-david b0cc3fe
feat: Enhance server error handling and improve port validation
frontegg-david f36593e
feat: Add support for remote tools and enhance app filtering configur…
frontegg-david e9a0721
feat: Add support for remote tools and enhance app filtering configur…
frontegg-david 33b8b14
feat: Enhance remote tools support with URL validation and new decora…
frontegg-david 8c88e37
feat: Implement remote URL validation and enhance decorator types for…
frontegg-david d746e3b
feat: Improve remote URL validation and enhance job normalization logic
frontegg-david aa3b352
feat: Improve remote URL validation and enhance job normalization logic
frontegg-david File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| 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> |
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
| 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(); |
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
| 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
151
apps/e2e/demo-e2e-esm/e2e/browser/esm-browser.pw.spec.ts
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
| 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'); | ||
| }); | ||
| }); | ||
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
| 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; |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.