feat(profiles): Add MCP configuration to Claude Code rules (#980)

* add .mcp.json with claude profile

* add changeset

* update changeset

* update test
This commit is contained in:
Joe Danziger
2025-07-16 03:07:33 -04:00
committed by Ralph Khreish
parent 901eec1058
commit 0a70ab6179
7 changed files with 149 additions and 66 deletions

View File

@@ -2,6 +2,7 @@ import { jest } from '@jest/globals';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { claudeProfile } from '../../../src/profiles/claude.js';
// Mock external modules
jest.mock('child_process', () => ({
@@ -77,11 +78,22 @@ describe('Claude Profile Integration', () => {
expect(mkdirCalls).toHaveLength(0);
});
test('does not create MCP configuration files', () => {
test('supports MCP configuration when using rule transformer', () => {
// This test verifies that the Claude profile is configured to support MCP
// The actual MCP file creation is handled by the rule transformer
// Assert - Claude profile should now support MCP configuration
expect(claudeProfile.mcpConfig).toBe(true);
expect(claudeProfile.mcpConfigName).toBe('.mcp.json');
expect(claudeProfile.mcpConfigPath).toBe('.mcp.json');
});
test('mock function does not create MCP configuration files', () => {
// Act
mockCreateClaudeStructure();
// Assert - Claude profile should not create any MCP config files
// Assert - The mock function should not create MCP config files
// (This is expected since the mock doesn't use the rule transformer)
const writeFileCalls = fs.writeFileSync.mock.calls;
const mcpConfigCalls = writeFileCalls.filter(
(call) =>

View File

@@ -92,7 +92,12 @@ describe('MCP Configuration Validation', () => {
RULE_PROFILES.forEach((profileName) => {
const profile = getRulesProfile(profileName);
if (profile.mcpConfig !== false) {
expect(profile.mcpConfigPath).toMatch(/^\.[\w-]+\/[\w_.]+$/);
// Claude profile uses root directory (.), so its path is just '.mcp.json'
if (profileName === 'claude') {
expect(profile.mcpConfigPath).toBe('.mcp.json');
} else {
expect(profile.mcpConfigPath).toMatch(/^\.[\w-]+\/[\w_.]+$/);
}
}
});
});
@@ -123,17 +128,13 @@ describe('MCP Configuration Validation', () => {
});
test('should have null config name for non-MCP profiles', () => {
const clineProfile = getRulesProfile('cline');
expect(clineProfile.mcpConfigName).toBe(null);
// Only codex, cline, and trae profiles should have null config names
const nonMcpProfiles = ['codex', 'cline', 'trae'];
const traeProfile = getRulesProfile('trae');
expect(traeProfile.mcpConfigName).toBe(null);
const claudeProfile = getRulesProfile('claude');
expect(claudeProfile.mcpConfigName).toBe(null);
const codexProfile = getRulesProfile('codex');
expect(codexProfile.mcpConfigName).toBe(null);
for (const profileName of nonMcpProfiles) {
const profile = getRulesProfile(profileName);
expect(profile.mcpConfigName).toBe(null);
}
});
});
@@ -185,17 +186,19 @@ describe('MCP Configuration Validation', () => {
describe('MCP Configuration Creation Logic', () => {
test('should indicate which profiles require MCP configuration creation', () => {
// Get all profiles that have MCP configuration enabled
const mcpEnabledProfiles = RULE_PROFILES.filter((profileName) => {
const profile = getRulesProfile(profileName);
return profile.mcpConfig !== false;
});
// Verify expected MCP-enabled profiles
expect(mcpEnabledProfiles).toContain('claude');
expect(mcpEnabledProfiles).toContain('cursor');
expect(mcpEnabledProfiles).toContain('gemini');
expect(mcpEnabledProfiles).toContain('roo');
expect(mcpEnabledProfiles).toContain('vscode');
expect(mcpEnabledProfiles).toContain('windsurf');
expect(mcpEnabledProfiles).not.toContain('claude');
expect(mcpEnabledProfiles).not.toContain('cline');
expect(mcpEnabledProfiles).not.toContain('codex');
expect(mcpEnabledProfiles).not.toContain('trae');
@@ -215,18 +218,25 @@ describe('MCP Configuration Validation', () => {
describe('MCP Configuration Path Usage Verification', () => {
test('should verify that rule transformer functions use mcpConfigPath correctly', () => {
// This test verifies that the mcpConfigPath property exists and is properly formatted
// for use with the setupMCPConfiguration function
RULE_PROFILES.forEach((profileName) => {
const profile = getRulesProfile(profileName);
if (profile.mcpConfig !== false) {
// Verify the path is properly formatted for path.join usage
expect(profile.mcpConfigPath.startsWith('/')).toBe(false);
expect(profile.mcpConfigPath).toContain('/');
// Claude profile uses root directory (.), so its path is just '.mcp.json'
if (profileName === 'claude') {
expect(profile.mcpConfigPath).toBe('.mcp.json');
} else {
expect(profile.mcpConfigPath).toContain('/');
}
// Verify it matches the expected pattern: profileDir/configName
const expectedPath = `${profile.profileDir}/${profile.mcpConfigName}`;
expect(profile.mcpConfigPath).toBe(expectedPath);
// For Claude, path.join('.', '.mcp.json') returns '.mcp.json'
const normalizedExpected =
profileName === 'claude' ? '.mcp.json' : expectedPath;
expect(profile.mcpConfigPath).toBe(normalizedExpected);
}
});
});
@@ -250,20 +260,19 @@ describe('MCP Configuration Validation', () => {
describe('MCP Configuration Function Integration', () => {
test('should verify that setupMCPConfiguration receives the correct mcpConfigPath parameter', () => {
// This test verifies the integration between rule transformer and mcp-utils
RULE_PROFILES.forEach((profileName) => {
const profile = getRulesProfile(profileName);
if (profile.mcpConfig !== false) {
// Verify that the mcpConfigPath can be used directly with setupMCPConfiguration
// The function signature is: setupMCPConfiguration(projectDir, mcpConfigPath)
expect(profile.mcpConfigPath).toBeDefined();
expect(typeof profile.mcpConfigPath).toBe('string');
// Verify the path structure is correct for the new function signature
const parts = profile.mcpConfigPath.split('/');
expect(parts).toHaveLength(2); // Should be profileDir/configName
expect(parts[0]).toBe(profile.profileDir);
expect(parts[1]).toBe(profile.mcpConfigName);
if (profileName === 'claude') {
// Claude profile uses root directory, so path is just '.mcp.json'
expect(profile.mcpConfigPath).toBe('.mcp.json');
} else {
const parts = profile.mcpConfigPath.split('/');
expect(parts).toHaveLength(2); // Should be profileDir/configName
expect(parts[0]).toBe(profile.profileDir);
expect(parts[1]).toBe(profile.mcpConfigName);
}
}
});
});
@@ -271,7 +280,9 @@ describe('MCP Configuration Validation', () => {
describe('MCP configuration validation', () => {
const mcpProfiles = ['cursor', 'gemini', 'roo', 'windsurf', 'vscode'];
const nonMcpProfiles = ['claude', 'codex', 'cline', 'trae'];
const nonMcpProfiles = ['codex', 'cline', 'trae'];
const profilesWithLifecycle = ['claude'];
const profilesWithoutLifecycle = ['codex'];
test.each(mcpProfiles)(
'should have valid MCP config for %s profile',

View File

@@ -3,6 +3,7 @@ import {
getRulesProfile
} from '../../../src/utils/rule-transformer.js';
import { RULE_PROFILES } from '../../../src/constants/profiles.js';
import path from 'path';
describe('Rule Transformer - General', () => {
describe('Profile Configuration Validation', () => {
@@ -166,19 +167,13 @@ describe('Rule Transformer - General', () => {
// Check types based on MCP configuration
expect(typeof profileConfig.mcpConfig).toBe('boolean');
if (profileConfig.mcpConfig === false) {
// Profiles without MCP configuration
expect(profileConfig.mcpConfigName).toBe(null);
expect(profileConfig.mcpConfigPath).toBe(null);
} else {
// Profiles with MCP configuration
expect(typeof profileConfig.mcpConfigName).toBe('string');
expect(typeof profileConfig.mcpConfigPath).toBe('string');
if (profileConfig.mcpConfig !== false) {
// Check that mcpConfigPath is properly constructed
expect(profileConfig.mcpConfigPath).toBe(
`${profileConfig.profileDir}/${profileConfig.mcpConfigName}`
const expectedPath = path.join(
profileConfig.profileDir,
profileConfig.mcpConfigName
);
expect(profileConfig.mcpConfigPath).toBe(expectedPath);
}
});
});
@@ -186,9 +181,9 @@ describe('Rule Transformer - General', () => {
it('should have correct MCP configuration for each profile', () => {
const expectedConfigs = {
claude: {
mcpConfig: false,
mcpConfigName: null,
expectedPath: null
mcpConfig: true,
mcpConfigName: '.mcp.json',
expectedPath: '.mcp.json'
},
cline: {
mcpConfig: false,
@@ -245,25 +240,19 @@ describe('Rule Transformer - General', () => {
it('should have consistent profileDir and mcpConfigPath relationship', () => {
RULE_PROFILES.forEach((profile) => {
const profileConfig = getRulesProfile(profile);
if (profileConfig.mcpConfig === false) {
// Profiles without MCP configuration have null mcpConfigPath
expect(profileConfig.mcpConfigPath).toBe(null);
} else {
if (profileConfig.mcpConfig !== false) {
// Profiles with MCP configuration should have valid paths
// The mcpConfigPath should start with the profileDir
expect(profileConfig.mcpConfigPath).toMatch(
new RegExp(
`^${profileConfig.profileDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/`
)
);
// The mcpConfigPath should end with the mcpConfigName
expect(profileConfig.mcpConfigPath).toMatch(
new RegExp(
`${profileConfig.mcpConfigName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`
)
);
if (profile === 'claude') {
// Claude uses root directory (.), so path.join('.', '.mcp.json') = '.mcp.json'
expect(profileConfig.mcpConfigPath).toBe('.mcp.json');
} else {
expect(profileConfig.mcpConfigPath).toMatch(
new RegExp(
`^${profileConfig.profileDir.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/`
)
);
}
}
});
});