fix: MCP server entrypoint not found in packaged builds

In packaged Electron apps process.cwd() does not point to the app
directory so the mcp-server bundle was never found. Additionally the
mcp-server dist was not included in the package at all.

Changes:
- Bundle mcp-server into a single self-contained ESM file (tsup
  noExternal + createRequire banner for CJS compat)
- Ship mcp-server/dist/index.js and package.json via extraResources
- Resolve entry via process.resourcesPath when app.isPackaged is true
- Build controller + mcp-server in prebuild so dist exists before
  electron-builder runs
- Add mcp-server/dist/index.js to CI verify steps on all platforms
- Improve error message to list checked paths for easier debugging
This commit is contained in:
iliya 2026-03-20 13:21:51 +02:00
parent 24a398bccf
commit 1d15f5f4d9
4 changed files with 72 additions and 4 deletions

View file

@ -113,6 +113,7 @@ jobs:
test -f dist-electron/main/index.cjs
test -f dist-electron/preload/index.js
test -f out/renderer/index.html
test -f mcp-server/dist/index.js
- name: Package (macOS ${{ matrix.arch }})
env:
@ -180,6 +181,7 @@ jobs:
test -f dist-electron/main/index.cjs
test -f dist-electron/preload/index.js
test -f out/renderer/index.html
test -f mcp-server/dist/index.js
- name: Package (Windows)
env:
@ -246,6 +248,7 @@ jobs:
test -f dist-electron/main/index.cjs
test -f dist-electron/preload/index.js
test -f out/renderer/index.html
test -f mcp-server/dist/index.js
- name: Package (Linux)
env:

View file

@ -4,8 +4,28 @@ export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
target: 'node20',
platform: 'node',
outDir: 'dist',
clean: true,
sourcemap: true,
dts: false,
// Bundle all dependencies into a single self-contained file so the packaged
// Electron app can run the MCP server without a node_modules tree.
noExternal: [/.*/],
splitting: false,
// Provide a real `require` function for CJS dependencies (e.g. undici)
// that use require() for Node built-in modules.
banner: {
js: `import { createRequire as __bundled_createRequire } from 'module';\nconst require = __bundled_createRequire(import.meta.url);`,
},
esbuildOptions(options) {
// Optional peer deps of xsschema (pulled in by fastmcp) — we only use zod.
// Mark as external at the esbuild level to avoid resolution errors.
options.external = [
...(options.external ?? []),
'sury',
'@valibot/to-json-schema',
'effect',
];
},
});

View file

@ -20,7 +20,7 @@
"scripts": {
"dev": "electron-vite dev",
"dev:kill": "node bin/kill-dev.js",
"prebuild": "tsx scripts/fetch-pricing-data.ts",
"prebuild": "tsx scripts/fetch-pricing-data.ts && pnpm --filter agent-teams-controller build && pnpm --filter agent-teams-mcp build",
"build": "electron-vite build",
"dist": "electron-builder --mac --win --linux",
"dist:mac": "electron-builder --mac",
@ -223,6 +223,14 @@
{
"from": "resources/pricing.json",
"to": "pricing.json"
},
{
"from": "mcp-server/dist/index.js",
"to": "mcp-server/index.js"
},
{
"from": "mcp-server/package.json",
"to": "mcp-server/package.json"
}
],
"npmRebuild": false,

View file

@ -23,6 +23,24 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return !!value && typeof value === 'object' && !Array.isArray(value);
}
function isPackagedApp(): boolean {
try {
const { app } = require('electron') as typeof import('electron');
return app.isPackaged;
} catch {
return false;
}
}
/**
* In a packaged Electron build the mcp-server bundle lives under
* `process.resourcesPath/mcp-server/index.js` (copied via extraResources).
* In dev mode we resolve relative to the workspace root (process.cwd()).
*/
function getPackagedServerEntry(): string {
return path.join(process.resourcesPath, 'mcp-server', 'index.js');
}
function getWorkspaceRoot(): string {
return process.cwd();
}
@ -82,17 +100,34 @@ async function resolveNodePath(): Promise<string> {
}
async function resolveMcpLaunchSpec(): Promise<McpLaunchSpec> {
const checked: string[] = [];
// 1. Packaged Electron app — use extraResources bundle
if (isPackagedApp()) {
const packagedEntry = getPackagedServerEntry();
checked.push(packagedEntry);
if (await pathExists(packagedEntry)) {
return {
command: await resolveNodePath(),
args: [packagedEntry],
};
}
logger.warn(`Packaged MCP entry not found at ${packagedEntry}, falling back to workspace`);
}
// 2. Dev mode — prefer source for hot changes
const sourceEntry = getSourceServerEntry();
checked.push(sourceEntry);
if (await pathExists(sourceEntry)) {
// Prefer source in workspace/dev runs so newly added MCP tools are available
// immediately and we do not accidentally serve a stale built dist bundle.
return {
command: 'pnpm',
args: ['--dir', getMcpServerDir(), 'exec', 'tsx', sourceEntry],
};
}
// 3. Dev mode — built dist
const builtEntry = getBuiltServerEntry();
checked.push(builtEntry);
if (await pathExists(builtEntry)) {
return {
command: await resolveNodePath(),
@ -100,7 +135,9 @@ async function resolveMcpLaunchSpec(): Promise<McpLaunchSpec> {
};
}
throw new Error('agent-teams-mcp entrypoint not found in mcp-server package');
throw new Error(
`agent-teams-mcp entrypoint not found. Checked paths:\n${checked.map((p) => ` - ${p}`).join('\n')}`
);
}
export class TeamMcpConfigBuilder {