fix: launch dev mcp server via node
This commit is contained in:
parent
27a38034cc
commit
7fa71019b2
2 changed files with 81 additions and 28 deletions
|
|
@ -75,13 +75,24 @@ function getSourceServerEntry(): string {
|
|||
return path.join(getWorkspaceMcpServerDir(), 'src', 'index.ts');
|
||||
}
|
||||
|
||||
function getWorkspaceTsxBinCandidates(): string[] {
|
||||
function getWorkspaceTsxPackageJsonCandidates(): string[] {
|
||||
return [
|
||||
path.join(getWorkspaceMcpServerDir(), 'node_modules', '.bin', 'tsx'),
|
||||
path.join(getWorkspaceRoot(), 'node_modules', '.bin', 'tsx'),
|
||||
path.join(getWorkspaceMcpServerDir(), 'node_modules', 'tsx', 'package.json'),
|
||||
path.join(getWorkspaceRoot(), 'node_modules', 'tsx', 'package.json'),
|
||||
];
|
||||
}
|
||||
|
||||
function resolvePackageBin(
|
||||
packageJsonPath: string,
|
||||
binName: string,
|
||||
packageJsonRaw: string
|
||||
): string | null {
|
||||
const packageJson = JSON.parse(packageJsonRaw) as { bin?: string | Record<string, string> };
|
||||
const bin = typeof packageJson.bin === 'string' ? packageJson.bin : packageJson.bin?.[binName];
|
||||
if (!bin) return null;
|
||||
return path.resolve(path.dirname(packageJsonPath), bin);
|
||||
}
|
||||
|
||||
async function pathExists(targetPath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.promises.access(targetPath, fs.constants.F_OK);
|
||||
|
|
@ -91,6 +102,40 @@ async function pathExists(targetPath: string): Promise<boolean> {
|
|||
}
|
||||
}
|
||||
|
||||
async function resolveWorkspaceTsxCli(checked: string[]): Promise<string | null> {
|
||||
for (const packageJsonPath of getWorkspaceTsxPackageJsonCandidates()) {
|
||||
checked.push(packageJsonPath);
|
||||
if (!(await pathExists(packageJsonPath))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const tsxCli = resolvePackageBin(
|
||||
packageJsonPath,
|
||||
'tsx',
|
||||
await fs.promises.readFile(packageJsonPath, 'utf8')
|
||||
);
|
||||
if (!tsxCli) {
|
||||
logger.warn(`tsx package has no bin.tsx entry at ${packageJsonPath}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
checked.push(tsxCli);
|
||||
if (await pathExists(tsxCli)) {
|
||||
return tsxCli;
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(
|
||||
`Failed to resolve tsx CLI from ${packageJsonPath}: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function shouldRetryMcpConfigRemoval(error: NodeJS.ErrnoException): boolean {
|
||||
return error.code === 'EPERM' || error.code === 'EBUSY';
|
||||
}
|
||||
|
|
@ -236,14 +281,12 @@ export async function resolveAgentTeamsMcpLaunchSpec(): Promise<McpLaunchSpec> {
|
|||
const sourceEntry = getSourceServerEntry();
|
||||
checked.push(sourceEntry);
|
||||
if (await pathExists(sourceEntry)) {
|
||||
for (const tsxBin of getWorkspaceTsxBinCandidates()) {
|
||||
checked.push(tsxBin);
|
||||
if (await pathExists(tsxBin)) {
|
||||
return {
|
||||
command: tsxBin,
|
||||
args: [sourceEntry],
|
||||
};
|
||||
}
|
||||
const tsxCli = await resolveWorkspaceTsxCli(checked);
|
||||
if (tsxCli) {
|
||||
return {
|
||||
command: await resolveNodePath(),
|
||||
args: [tsxCli, sourceEntry],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -80,7 +80,10 @@ describe('TeamMcpConfigBuilder', () => {
|
|||
): { command?: string; args?: string[]; env?: Record<string, string> } | undefined {
|
||||
const raw = fs.readFileSync(configPath, 'utf8');
|
||||
const parsed = JSON.parse(raw) as {
|
||||
mcpServers?: Record<string, { command?: string; args?: string[]; env?: Record<string, string> }>;
|
||||
mcpServers?: Record<
|
||||
string,
|
||||
{ command?: string; args?: string[]; env?: Record<string, string> }
|
||||
>;
|
||||
};
|
||||
return parsed.mcpServers?.['agent-teams'];
|
||||
}
|
||||
|
|
@ -93,12 +96,13 @@ describe('TeamMcpConfigBuilder', () => {
|
|||
expect(server?.command).toMatch(/(^node$|[\\/]node(?:\.exe)?$)/);
|
||||
}
|
||||
|
||||
function expectTsxEntry(
|
||||
function expectNodeTsxSourceEntry(
|
||||
server: { command?: string; args?: string[] } | undefined,
|
||||
entry: string
|
||||
tsxCli: string,
|
||||
sourceEntry: string
|
||||
): void {
|
||||
expect(server?.args).toEqual([entry]);
|
||||
expect(server?.command).toMatch(/[\\/]tsx(?:\.cmd)?$/);
|
||||
expect(server?.args).toEqual([tsxCli, sourceEntry]);
|
||||
expect(server?.command).toMatch(/(^node$|[\\/]node(?:\.exe)?$)/);
|
||||
}
|
||||
|
||||
function getBuiltWorkspaceEntry(): string {
|
||||
|
|
@ -109,8 +113,12 @@ describe('TeamMcpConfigBuilder', () => {
|
|||
return path.join(process.cwd(), 'mcp-server', 'src', 'index.ts');
|
||||
}
|
||||
|
||||
function getWorkspaceTsxBin(): string {
|
||||
return path.join(process.cwd(), 'mcp-server', 'node_modules', '.bin', 'tsx');
|
||||
function getWorkspaceTsxPackageJson(): string {
|
||||
return path.join(process.cwd(), 'mcp-server', 'node_modules', 'tsx', 'package.json');
|
||||
}
|
||||
|
||||
function getWorkspaceTsxCli(): string {
|
||||
return path.join(process.cwd(), 'mcp-server', 'node_modules', 'tsx', 'dist', 'cli.mjs');
|
||||
}
|
||||
|
||||
function mockPathExists(existingPaths: string[], options: { strict?: boolean } = {}): void {
|
||||
|
|
@ -138,14 +146,16 @@ describe('TeamMcpConfigBuilder', () => {
|
|||
|
||||
function mockSourceWorkspaceEntryAvailable(): {
|
||||
sourceEntry: string;
|
||||
tsxBin: string;
|
||||
tsxPackageJson: string;
|
||||
tsxCli: string;
|
||||
builtEntry: string;
|
||||
} {
|
||||
const sourceEntry = getSourceWorkspaceEntry();
|
||||
const tsxBin = getWorkspaceTsxBin();
|
||||
const tsxPackageJson = getWorkspaceTsxPackageJson();
|
||||
const tsxCli = getWorkspaceTsxCli();
|
||||
const builtEntry = getBuiltWorkspaceEntry();
|
||||
mockPathExists([sourceEntry, tsxBin, builtEntry], { strict: true });
|
||||
return { sourceEntry, tsxBin, builtEntry };
|
||||
mockPathExists([sourceEntry, tsxPackageJson, tsxCli, builtEntry], { strict: true });
|
||||
return { sourceEntry, tsxPackageJson, tsxCli, builtEntry };
|
||||
}
|
||||
|
||||
function mockBuiltWorkspaceEntryAvailable(): string {
|
||||
|
|
@ -225,7 +235,7 @@ describe('TeamMcpConfigBuilder', () => {
|
|||
});
|
||||
|
||||
it('prefers the source workspace MCP entry in dev mode when available', async () => {
|
||||
const { sourceEntry } = mockSourceWorkspaceEntryAvailable();
|
||||
const { sourceEntry, tsxCli } = mockSourceWorkspaceEntryAvailable();
|
||||
const builder = new TeamMcpConfigBuilder();
|
||||
|
||||
const configPath = await builder.writeConfigFile();
|
||||
|
|
@ -237,7 +247,7 @@ describe('TeamMcpConfigBuilder', () => {
|
|||
};
|
||||
|
||||
const server = parsed.mcpServers?.['agent-teams'];
|
||||
expectTsxEntry(server, sourceEntry);
|
||||
expectNodeTsxSourceEntry(server, tsxCli, sourceEntry);
|
||||
});
|
||||
|
||||
it('pins the MCP controller to the active Claude base path', async () => {
|
||||
|
|
@ -355,7 +365,7 @@ describe('TeamMcpConfigBuilder', () => {
|
|||
});
|
||||
|
||||
it('generated agent-teams server ignores same-named user MCP entry', async () => {
|
||||
const { sourceEntry } = mockSourceWorkspaceEntryAvailable();
|
||||
const { sourceEntry, tsxCli } = mockSourceWorkspaceEntryAvailable();
|
||||
const homeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-home-'));
|
||||
createdDirs.push(homeDir);
|
||||
mockHomeDir = homeDir;
|
||||
|
|
@ -381,7 +391,7 @@ describe('TeamMcpConfigBuilder', () => {
|
|||
mcpServers: Record<string, { command?: string; args?: string[] }>;
|
||||
};
|
||||
|
||||
expectTsxEntry(parsed.mcpServers['agent-teams'], sourceEntry);
|
||||
expectNodeTsxSourceEntry(parsed.mcpServers['agent-teams'], tsxCli, sourceEntry);
|
||||
});
|
||||
|
||||
it('passes the configured Claude root to the MCP server', async () => {
|
||||
|
|
@ -626,7 +636,7 @@ describe('TeamMcpConfigBuilder', () => {
|
|||
});
|
||||
|
||||
it('packaged mode falls back to the source workspace MCP entry when resourcesPath bundle is missing', async () => {
|
||||
const { sourceEntry } = mockSourceWorkspaceEntryAvailable();
|
||||
const { sourceEntry, tsxCli } = mockSourceWorkspaceEntryAvailable();
|
||||
setPackagedMode(true, '6.0.0');
|
||||
const resourcesDir = fs.mkdtempSync(path.join(os.tmpdir(), 'team-mcp-resources-'));
|
||||
createdDirs.push(resourcesDir);
|
||||
|
|
@ -636,6 +646,6 @@ describe('TeamMcpConfigBuilder', () => {
|
|||
const configPath = await builder.writeConfigFile();
|
||||
createdPaths.push(configPath);
|
||||
|
||||
expectTsxEntry(readGeneratedServer(configPath), sourceEntry);
|
||||
expectNodeTsxSourceEntry(readGeneratedServer(configPath), tsxCli, sourceEntry);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue