feat(profiles): auto-enable deferred MCP loading for Claude Code (#1525)

This commit is contained in:
Ralph Khreish
2025-12-16 23:12:24 +01:00
committed by GitHub
parent e7b9f82f21
commit 1c2228dbb6
7 changed files with 851 additions and 3 deletions

View File

@@ -0,0 +1,9 @@
---
"task-master-ai": minor
---
Add tool search tool for Claude Code MCP server and enable deferred MCP loading
- Added new tool search tool capabilities for the Taskmaster MCP in Claude Code
- Running `task-master rules add claude` now automatically configures your shell (`~/.zshrc`, `~/.bashrc`, or PowerShell profile) with `ENABLE_EXPERIMENTAL_MCP_CLI=true` to enable deferred MCP loading
- **Context savings**: Deferred loading saves ~16% of Claude Code's 200k context window (~33k tokens for Task Master alone). Savings apply to all MCP servers, so total savings may be higher depending on your setup.

View File

@@ -3,7 +3,10 @@
"task-master-ai": { "task-master-ai": {
"type": "stdio", "type": "stdio",
"command": "npx", "command": "npx",
"args": ["-y", "task-master-ai"] "args": ["-y", "task-master-ai"],
"env": {
"TASK_MASTER_TOOLS": "all"
}
} }
} }
} }

View File

@@ -5,3 +5,6 @@
// Re-export everything from slash-commands module // Re-export everything from slash-commands module
export * from './slash-commands/index.js'; export * from './slash-commands/index.js';
// Re-export shell utilities
export * from './shell-utils.js';

View File

@@ -0,0 +1,506 @@
import fs from 'fs';
import os from 'os';
import path from 'path';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import {
addShellExport,
enableDeferredMcpLoading,
getShellConfigPath
} from './shell-utils.js';
// Mock fs and os modules
vi.mock('fs');
vi.mock('os');
describe('shell-utils', () => {
const mockHomeDir = '/home/testuser';
beforeEach(() => {
vi.resetAllMocks();
vi.mocked(os.homedir).mockReturnValue(mockHomeDir);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('getShellConfigPath', () => {
it('should return .zshrc for zsh shell', () => {
// Arrange
const originalShell = process.env.SHELL;
process.env.SHELL = '/bin/zsh';
// Act
const result = getShellConfigPath();
// Assert
expect(result).toBe(path.join(mockHomeDir, '.zshrc'));
// Cleanup
process.env.SHELL = originalShell;
});
it('should return .bashrc for bash shell on Linux', () => {
// Arrange
const originalShell = process.env.SHELL;
const originalPlatform = process.platform;
process.env.SHELL = '/bin/bash';
Object.defineProperty(process, 'platform', { value: 'linux' });
vi.mocked(fs.existsSync).mockReturnValue(false);
// Act
const result = getShellConfigPath();
// Assert
expect(result).toBe(path.join(mockHomeDir, '.bashrc'));
// Cleanup
process.env.SHELL = originalShell;
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
it('should return .bash_profile for bash shell on macOS if it exists', () => {
// Arrange
const originalShell = process.env.SHELL;
const originalPlatform = process.platform;
process.env.SHELL = '/bin/bash';
Object.defineProperty(process, 'platform', { value: 'darwin' });
vi.mocked(fs.existsSync).mockReturnValue(true);
// Act
const result = getShellConfigPath();
// Assert
expect(result).toBe(path.join(mockHomeDir, '.bash_profile'));
// Cleanup
process.env.SHELL = originalShell;
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
it('should return .config/fish/config.fish for fish shell', () => {
// Arrange
const originalShell = process.env.SHELL;
process.env.SHELL = '/usr/bin/fish';
// Act
const result = getShellConfigPath();
// Assert
expect(result).toBe(
path.join(mockHomeDir, '.config', 'fish', 'config.fish')
);
// Cleanup
process.env.SHELL = originalShell;
});
it('should fallback to .zshrc if it exists when shell is unknown', () => {
// Arrange
const originalShell = process.env.SHELL;
process.env.SHELL = '/bin/unknown';
vi.mocked(fs.existsSync).mockImplementation((p) =>
String(p).includes('.zshrc')
);
// Act
const result = getShellConfigPath();
// Assert
expect(result).toBe(path.join(mockHomeDir, '.zshrc'));
// Cleanup
process.env.SHELL = originalShell;
});
it('should return null if no shell config is found', () => {
// Arrange
const originalShell = process.env.SHELL;
process.env.SHELL = '/bin/unknown';
vi.mocked(fs.existsSync).mockReturnValue(false);
// Act
const result = getShellConfigPath();
// Assert
expect(result).toBeNull();
// Cleanup
process.env.SHELL = originalShell;
});
it('should return PowerShell profile on Windows with PSModulePath', () => {
// Arrange
const originalShell = process.env.SHELL;
const originalPSModulePath = process.env.PSModulePath;
const originalPlatform = process.platform;
process.env.SHELL = '';
process.env.PSModulePath = 'C:\\some\\path';
Object.defineProperty(process, 'platform', { value: 'win32' });
vi.mocked(fs.existsSync).mockReturnValue(false);
// Act
const result = getShellConfigPath();
// Assert
expect(result).toBe(
path.join(
mockHomeDir,
'Documents',
'PowerShell',
'Microsoft.PowerShell_profile.ps1'
)
);
// Cleanup
process.env.SHELL = originalShell;
process.env.PSModulePath = originalPSModulePath;
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
it('should return Git Bash .bashrc on Windows without PSModulePath', () => {
// Arrange
const originalShell = process.env.SHELL;
const originalPSModulePath = process.env.PSModulePath;
const originalPlatform = process.platform;
process.env.SHELL = '';
delete process.env.PSModulePath;
Object.defineProperty(process, 'platform', { value: 'win32' });
vi.mocked(fs.existsSync).mockImplementation((p) =>
String(p).includes('.bashrc')
);
// Act
const result = getShellConfigPath();
// Assert
expect(result).toBe(path.join(mockHomeDir, '.bashrc'));
// Cleanup
process.env.SHELL = originalShell;
process.env.PSModulePath = originalPSModulePath;
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
});
describe('addShellExport', () => {
it('should add export to shell config file', () => {
// Arrange
const originalShell = process.env.SHELL;
process.env.SHELL = '/bin/zsh';
const shellConfigPath = path.join(mockHomeDir, '.zshrc');
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue('# existing content\n');
vi.mocked(fs.appendFileSync).mockImplementation(() => {});
// Act
const result = addShellExport('MY_VAR', 'my_value', 'My comment');
// Assert
expect(result.success).toBe(true);
expect(result.shellConfigFile).toBe(shellConfigPath);
expect(result.alreadyExists).toBeUndefined();
expect(fs.appendFileSync).toHaveBeenCalledWith(
shellConfigPath,
'\n# My comment\nexport MY_VAR=my_value\n'
);
// Cleanup
process.env.SHELL = originalShell;
});
it('should skip if export already exists', () => {
// Arrange
const originalShell = process.env.SHELL;
process.env.SHELL = '/bin/zsh';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(
'# existing\nexport MY_VAR=old_value\n'
);
// Act
const result = addShellExport('MY_VAR', 'my_value');
// Assert
expect(result.success).toBe(true);
expect(result.alreadyExists).toBe(true);
expect(fs.appendFileSync).not.toHaveBeenCalled();
// Cleanup
process.env.SHELL = originalShell;
});
it('should NOT skip when variable name only appears in a comment', () => {
// Arrange
const originalShell = process.env.SHELL;
process.env.SHELL = '/bin/zsh';
const shellConfigPath = path.join(mockHomeDir, '.zshrc');
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(
'# MY_VAR is mentioned here but not exported\n# Another comment about MY_VAR\n'
);
vi.mocked(fs.appendFileSync).mockImplementation(() => {});
// Act
const result = addShellExport('MY_VAR', 'my_value');
// Assert
expect(result.success).toBe(true);
expect(result.alreadyExists).toBeUndefined();
expect(fs.appendFileSync).toHaveBeenCalledWith(
shellConfigPath,
'\nexport MY_VAR=my_value\n'
);
// Cleanup
process.env.SHELL = originalShell;
});
it('should NOT skip when variable name is a substring of another variable', () => {
// Arrange
const originalShell = process.env.SHELL;
process.env.SHELL = '/bin/zsh';
const shellConfigPath = path.join(mockHomeDir, '.zshrc');
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(
'export MY_VAR_EXTENDED=some_value\nexport OTHER_MY_VAR=another\n'
);
vi.mocked(fs.appendFileSync).mockImplementation(() => {});
// Act
const result = addShellExport('MY_VAR', 'my_value');
// Assert
expect(result.success).toBe(true);
expect(result.alreadyExists).toBeUndefined();
expect(fs.appendFileSync).toHaveBeenCalledWith(
shellConfigPath,
'\nexport MY_VAR=my_value\n'
);
// Cleanup
process.env.SHELL = originalShell;
});
it('should detect existing export with leading whitespace', () => {
// Arrange
const originalShell = process.env.SHELL;
process.env.SHELL = '/bin/zsh';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(' export MY_VAR=old_value\n');
// Act
const result = addShellExport('MY_VAR', 'my_value');
// Assert
expect(result.success).toBe(true);
expect(result.alreadyExists).toBe(true);
expect(fs.appendFileSync).not.toHaveBeenCalled();
// Cleanup
process.env.SHELL = originalShell;
});
it('should return failure if shell config not found', () => {
// Arrange
const originalShell = process.env.SHELL;
process.env.SHELL = '/bin/zsh';
vi.mocked(fs.existsSync).mockReturnValue(false);
// Act
const result = addShellExport('MY_VAR', 'my_value');
// Assert
expect(result.success).toBe(false);
expect(result.message).toContain('not found');
// Cleanup
process.env.SHELL = originalShell;
});
it('should return failure if shell type cannot be determined', () => {
// Arrange
const originalShell = process.env.SHELL;
process.env.SHELL = '/bin/unknown';
vi.mocked(fs.existsSync).mockReturnValue(false);
// Act
const result = addShellExport('MY_VAR', 'my_value');
// Assert
expect(result.success).toBe(false);
expect(result.message).toContain('Could not determine shell type');
// Cleanup
process.env.SHELL = originalShell;
});
it('should use PowerShell syntax for .ps1 files', () => {
// Arrange
const originalShell = process.env.SHELL;
const originalPSModulePath = process.env.PSModulePath;
const originalPlatform = process.platform;
process.env.SHELL = '';
process.env.PSModulePath = 'C:\\some\\path';
Object.defineProperty(process, 'platform', { value: 'win32' });
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue('# existing content\n');
vi.mocked(fs.appendFileSync).mockImplementation(() => {});
// Act
const result = addShellExport('MY_VAR', 'my_value', 'My comment');
// Assert
expect(result.success).toBe(true);
expect(fs.appendFileSync).toHaveBeenCalledWith(
expect.stringContaining('.ps1'),
'\n# My comment\n$env:MY_VAR = "my_value"\n'
);
// Cleanup
process.env.SHELL = originalShell;
process.env.PSModulePath = originalPSModulePath;
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
it('should NOT skip PowerShell when variable name only appears in a comment', () => {
// Arrange
const originalShell = process.env.SHELL;
const originalPSModulePath = process.env.PSModulePath;
const originalPlatform = process.platform;
process.env.SHELL = '';
process.env.PSModulePath = 'C:\\some\\path';
Object.defineProperty(process, 'platform', { value: 'win32' });
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(
'# MY_VAR is mentioned here but not set\n# $env:MY_VAR in a comment\n'
);
vi.mocked(fs.appendFileSync).mockImplementation(() => {});
// Act
const result = addShellExport('MY_VAR', 'my_value');
// Assert
expect(result.success).toBe(true);
expect(result.alreadyExists).toBeUndefined();
expect(fs.appendFileSync).toHaveBeenCalledWith(
expect.stringContaining('.ps1'),
'\n$env:MY_VAR = "my_value"\n'
);
// Cleanup
process.env.SHELL = originalShell;
process.env.PSModulePath = originalPSModulePath;
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
it('should detect existing PowerShell export', () => {
// Arrange
const originalShell = process.env.SHELL;
const originalPSModulePath = process.env.PSModulePath;
const originalPlatform = process.platform;
process.env.SHELL = '';
process.env.PSModulePath = 'C:\\some\\path';
Object.defineProperty(process, 'platform', { value: 'win32' });
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue('$env:MY_VAR = "old_value"\n');
// Act
const result = addShellExport('MY_VAR', 'my_value');
// Assert
expect(result.success).toBe(true);
expect(result.alreadyExists).toBe(true);
expect(fs.appendFileSync).not.toHaveBeenCalled();
// Cleanup
process.env.SHELL = originalShell;
process.env.PSModulePath = originalPSModulePath;
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
it('should create PowerShell profile and directory if they do not exist', () => {
// Arrange
const originalShell = process.env.SHELL;
const originalPSModulePath = process.env.PSModulePath;
const originalPlatform = process.platform;
process.env.SHELL = '';
process.env.PSModulePath = 'C:\\some\\path';
Object.defineProperty(process, 'platform', { value: 'win32' });
// Nothing exists initially
vi.mocked(fs.existsSync).mockReturnValue(false);
vi.mocked(fs.mkdirSync).mockImplementation(() => undefined);
vi.mocked(fs.writeFileSync).mockImplementation(() => {});
vi.mocked(fs.readFileSync).mockReturnValue('');
vi.mocked(fs.appendFileSync).mockImplementation(() => {});
// Act
const result = addShellExport('MY_VAR', 'my_value');
// Assert
expect(result.success).toBe(true);
// Should create the profile directory
expect(fs.mkdirSync).toHaveBeenCalledWith(expect.any(String), {
recursive: true
});
// Should create the empty profile file
expect(fs.writeFileSync).toHaveBeenCalledWith(
expect.stringContaining('.ps1'),
''
);
// Cleanup
process.env.SHELL = originalShell;
process.env.PSModulePath = originalPSModulePath;
Object.defineProperty(process, 'platform', { value: originalPlatform });
});
});
describe('enableDeferredMcpLoading', () => {
it('should add ENABLE_EXPERIMENTAL_MCP_CLI export', () => {
// Arrange
const originalShell = process.env.SHELL;
process.env.SHELL = '/bin/zsh';
const shellConfigPath = path.join(mockHomeDir, '.zshrc');
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue('# existing content\n');
vi.mocked(fs.appendFileSync).mockImplementation(() => {});
// Act
const result = enableDeferredMcpLoading();
// Assert
expect(result.success).toBe(true);
expect(result.shellConfigFile).toBe(shellConfigPath);
expect(fs.appendFileSync).toHaveBeenCalledWith(
shellConfigPath,
'\n# Claude Code deferred MCP loading (added by Taskmaster)\nexport ENABLE_EXPERIMENTAL_MCP_CLI=true\n'
);
// Cleanup
process.env.SHELL = originalShell;
});
it('should skip if already configured', () => {
// Arrange
const originalShell = process.env.SHELL;
process.env.SHELL = '/bin/zsh';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(
'export ENABLE_EXPERIMENTAL_MCP_CLI=true\n'
);
// Act
const result = enableDeferredMcpLoading();
// Assert
expect(result.success).toBe(true);
expect(result.alreadyExists).toBe(true);
expect(fs.appendFileSync).not.toHaveBeenCalled();
// Cleanup
process.env.SHELL = originalShell;
});
});
});

View File

@@ -0,0 +1,222 @@
/**
* @fileoverview Shell configuration utilities for tm-profiles
* Provides functions to modify shell configuration files (bashrc, zshrc, etc.)
*/
import fs from 'fs';
import os from 'os';
import path from 'path';
export interface ShellConfigResult {
success: boolean;
shellConfigFile?: string;
message: string;
alreadyExists?: boolean;
}
/**
* Detects the user's shell configuration file path
* Supports: zsh, bash, fish, PowerShell (Windows)
* @returns The path to the shell config file, or null if not detected
*/
export function getShellConfigPath(): string | null {
const homeDir = os.homedir();
const shell = process.env.SHELL || '';
// Unix-like shells (Linux, macOS, WSL, Git Bash)
if (shell.includes('zsh')) {
return path.join(homeDir, '.zshrc');
}
if (shell.includes('bash')) {
// macOS uses .bash_profile for login shells - prefer it even if it doesn't exist yet
const bashProfile = path.join(homeDir, '.bash_profile');
if (process.platform === 'darwin') {
return bashProfile;
}
return path.join(homeDir, '.bashrc');
}
if (shell.includes('fish')) {
return path.join(homeDir, '.config', 'fish', 'config.fish');
}
// Windows PowerShell - check $PROFILE env var or use default location
if (process.platform === 'win32') {
// PowerShell sets PSModulePath when running
if (process.env.PSModulePath) {
// Use $PROFILE if set, otherwise use default PowerShell profile path
const psProfile =
process.env.PROFILE ||
path.join(
homeDir,
'Documents',
'WindowsPowerShell',
'Microsoft.PowerShell_profile.ps1'
);
// Also check PowerShell Core location
const pwshProfile = path.join(
homeDir,
'Documents',
'PowerShell',
'Microsoft.PowerShell_profile.ps1'
);
if (fs.existsSync(pwshProfile)) return pwshProfile;
if (fs.existsSync(psProfile)) return psProfile;
// Return PowerShell Core path as default (more modern)
return pwshProfile;
}
// Git Bash on Windows - check for .bashrc
const bashrc = path.join(homeDir, '.bashrc');
if (fs.existsSync(bashrc)) return bashrc;
}
// Fallback - check what exists (covers WSL and other edge cases)
const zshrc = path.join(homeDir, '.zshrc');
if (fs.existsSync(zshrc)) return zshrc;
const bashrc = path.join(homeDir, '.bashrc');
if (fs.existsSync(bashrc)) return bashrc;
return null;
}
/**
* Checks if a shell config file is a PowerShell profile
*/
function isPowerShellProfile(filePath: string): boolean {
return filePath.endsWith('.ps1');
}
/**
* Adds an export statement to the user's shell configuration file
* Handles both Unix-style shells (bash, zsh, fish) and PowerShell
* @param envVar - The environment variable name
* @param value - The value to set
* @param comment - Optional comment to add above the export
* @returns Result object with success status and details
*/
export function addShellExport(
envVar: string,
value: string,
comment?: string
): ShellConfigResult {
// Validate envVar contains only safe characters (prevents ReDoS attacks)
if (!/^[A-Z_][A-Z0-9_]*$/i.test(envVar)) {
return {
success: false,
message: `Invalid environment variable name: ${envVar}. Must start with a letter or underscore and contain only alphanumeric characters and underscores.`
};
}
// Validate value to prevent shell injection
if (
typeof value !== 'string' ||
value.includes('\n') ||
value.includes('\r')
) {
return {
success: false,
message: `Invalid value: must be a single-line string without newlines`
};
}
const shellConfigFile = getShellConfigPath();
if (!shellConfigFile) {
return {
success: false,
message: 'Could not determine shell type (zsh, bash, fish, or PowerShell)'
};
}
try {
// Create the profile directory if it doesn't exist (handles fish's nested ~/.config/fish/ and PowerShell)
const profileDir = path.dirname(shellConfigFile);
if (!fs.existsSync(profileDir)) {
fs.mkdirSync(profileDir, { recursive: true });
}
// Check if file exists, create empty if it doesn't (common for PowerShell and macOS .bash_profile)
if (!fs.existsSync(shellConfigFile)) {
const isBashProfileOnMac =
process.platform === 'darwin' &&
shellConfigFile.endsWith('.bash_profile');
if (isPowerShellProfile(shellConfigFile) || isBashProfileOnMac) {
// Create empty profile file
fs.writeFileSync(shellConfigFile, '');
} else {
return {
success: false,
shellConfigFile,
message: `Shell config file ${shellConfigFile} not found`
};
}
}
const content = fs.readFileSync(shellConfigFile, 'utf8');
// Check if the export already exists using precise regex patterns
// This avoids false positives from comments or partial matches
const alreadyExists = isPowerShellProfile(shellConfigFile)
? new RegExp(`^\\s*\\$env:${envVar}\\s*=`, 'm').test(content)
: new RegExp(`^\\s*export\\s+${envVar}\\s*=`, 'm').test(content);
if (alreadyExists) {
return {
success: true,
shellConfigFile,
message: `${envVar} already configured`,
alreadyExists: true
};
}
// Build the export block based on shell type
let exportLine: string;
let commentPrefix: string;
if (isPowerShellProfile(shellConfigFile)) {
// PowerShell syntax - escape quotes and backticks
const escapedValue = value.replace(/["`$]/g, '`$&');
exportLine = `$env:${envVar} = "${escapedValue}"`;
commentPrefix = '#';
} else {
// Unix shell syntax (bash, zsh, fish) - use single quotes and escape embedded single quotes
const escapedValue = value.replace(/'/g, "'\\''");
exportLine = `export ${envVar}='${escapedValue}'`;
commentPrefix = '#';
}
const commentLine = comment ? `${commentPrefix} ${comment}\n` : '';
const block = `\n${commentLine}${exportLine}\n`;
fs.appendFileSync(shellConfigFile, block);
return {
success: true,
shellConfigFile,
message: `Added ${envVar} to ${shellConfigFile}`
};
} catch (error) {
return {
success: false,
shellConfigFile,
message: `Failed to modify shell config: ${(error as Error).message}`
};
}
}
/**
* Enables Claude Code deferred MCP loading by adding the required env var
* @returns Result object with success status and details
*/
export function enableDeferredMcpLoading(): ShellConfigResult {
return addShellExport(
'ENABLE_EXPERIMENTAL_MCP_CLI',
'true',
'Claude Code deferred MCP loading (added by Taskmaster)'
);
}

View File

@@ -3,6 +3,7 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import { isSilentMode, log } from '../../scripts/modules/utils.js'; import { isSilentMode, log } from '../../scripts/modules/utils.js';
import { createProfile } from './base-profile.js'; import { createProfile } from './base-profile.js';
import { enableDeferredMcpLoading } from '@tm/profiles';
// Helper function to recursively copy directory (adopted from Roo profile) // Helper function to recursively copy directory (adopted from Roo profile)
function copyRecursiveSync(src, dest) { function copyRecursiveSync(src, dest) {
@@ -103,6 +104,25 @@ function onAddRulesProfile(targetDir, assetsDir) {
); );
} }
} }
// Enable deferred MCP loading for reduced context usage
const deferredResult = enableDeferredMcpLoading();
if (deferredResult.success) {
if (deferredResult.alreadyExists) {
log('debug', '[Claude] Deferred MCP loading already configured');
} else {
log(
'info',
`[Claude] Enabled deferred MCP loading in ${deferredResult.shellConfigFile}`
);
log('info', '[Claude] Restart your terminal for changes to take effect');
}
} else {
log(
'debug',
`[Claude] Could not configure deferred loading: ${deferredResult.message}`
);
}
} }
function onRemoveRulesProfile(targetDir) { function onRemoveRulesProfile(targetDir) {
@@ -273,4 +293,9 @@ export const claudeProfile = createProfile({
}); });
// Export lifecycle functions separately to avoid naming conflicts // Export lifecycle functions separately to avoid naming conflicts
export { onAddRulesProfile, onRemoveRulesProfile, onPostConvertRulesProfile }; export {
onAddRulesProfile,
onRemoveRulesProfile,
onPostConvertRulesProfile,
transformToClaudeFormat
};

View File

@@ -1,6 +1,9 @@
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import { claudeProfile } from '../../../src/profiles/claude.js'; import {
claudeProfile,
transformToClaudeFormat
} from '../../../src/profiles/claude.js';
describe('Claude Profile Initialization Functionality', () => { describe('Claude Profile Initialization Functionality', () => {
let claudeProfileContent; let claudeProfileContent;
@@ -60,3 +63,80 @@ describe('Claude Profile Initialization Functionality', () => {
expect(claudeProfileContent).toContain("log('error'"); expect(claudeProfileContent).toContain("log('error'");
}); });
}); });
describe('transformToClaudeFormat', () => {
test('should add type: stdio to MCP server configs', () => {
const input = {
mcpServers: {
'task-master-ai': {
command: 'npx',
args: ['-y', 'task-master-ai'],
env: { ANTHROPIC_API_KEY: 'test-key' }
}
}
};
const result = transformToClaudeFormat(input);
expect(result.mcpServers['task-master-ai']).toEqual({
type: 'stdio',
command: 'npx',
args: ['-y', 'task-master-ai'],
env: { ANTHROPIC_API_KEY: 'test-key' }
});
});
test('should place type as first key in output', () => {
const input = {
mcpServers: {
'my-server': {
command: 'node',
args: ['server.js']
}
}
};
const result = transformToClaudeFormat(input);
const keys = Object.keys(result.mcpServers['my-server']);
// type should be first key
expect(keys[0]).toBe('type');
expect(keys[1]).toBe('command');
expect(keys[2]).toBe('args');
});
test('should handle multiple MCP servers', () => {
const input = {
mcpServers: {
server1: { command: 'cmd1' },
server2: { command: 'cmd2', args: ['arg1'] }
}
};
const result = transformToClaudeFormat(input);
expect(result.mcpServers.server1.type).toBe('stdio');
expect(result.mcpServers.server2.type).toBe('stdio');
});
test('should preserve additional non-standard properties', () => {
const input = {
mcpServers: {
'my-server': {
command: 'node',
customProperty: 'custom-value'
}
}
};
const result = transformToClaudeFormat(input);
expect(result.mcpServers['my-server'].customProperty).toBe('custom-value');
});
test('should return empty object when input has no mcpServers', () => {
const input = {};
const result = transformToClaudeFormat(input);
expect(result).toEqual({});
});
});