fix(config): Improve config-manager.js for MCP server integration

- Fixed MCP server initialization warnings by refactoring config-manager.js to handle missing project roots silently during startup

- Added project root tracking (loadedConfigRoot) to improve config caching and prevent unnecessary reloads

- Modified _loadAndValidateConfig to return defaults without warnings when no explicitRoot provided

- Improved getConfig to only update cache when loading config with a specific project root

- Ensured warning messages still appear when explicitly specified roots have missing/invalid configs

- Prevented console output during MCP startup that was causing JSON parsing errors

- Verified parse_prd and other MCP tools still work correctly with the new config loading approach.

- Replaces test perplexity api key in mcp.json and rolls it. It's invalid now.
This commit is contained in:
Eyal Toledano
2025-04-24 13:34:51 -04:00
parent be3f68e777
commit 205a11e82c
6 changed files with 86 additions and 125 deletions

View File

@@ -5,7 +5,7 @@
"args": ["./mcp-server/server.js"], "args": ["./mcp-server/server.js"],
"env": { "env": {
"ANTHROPIC_API_KEY": "sk-ant-apikeyhere", "ANTHROPIC_API_KEY": "sk-ant-apikeyhere",
"PERPLEXITY_API_KEY": "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", "PERPLEXITY_API_KEY": "pplx-apikeyhere",
"OPENAI_API_KEY": "sk-proj-1234567890", "OPENAI_API_KEY": "sk-proj-1234567890",
"GOOGLE_API_KEY": "AIzaSyB1234567890", "GOOGLE_API_KEY": "AIzaSyB1234567890",
"GROK_API_KEY": "gsk_1234567890", "GROK_API_KEY": "gsk_1234567890",

View File

@@ -12,41 +12,21 @@ import {
enableSilentMode, enableSilentMode,
disableSilentMode disableSilentMode
} from '../../../../scripts/modules/utils.js'; } from '../../../../scripts/modules/utils.js';
import {
getAnthropicClientForMCP,
getModelConfig
} from '../utils/ai-client-utils.js';
/** /**
* Direct function wrapper for parsing PRD documents and generating tasks. * Direct function wrapper for parsing PRD documents and generating tasks.
* *
* @param {Object} args - Command arguments containing input, numTasks or tasks, and output options. * @param {Object} args - Command arguments containing projectRoot, input, output, numTasks options.
* @param {Object} log - Logger object. * @param {Object} log - Logger object.
* @param {Object} context - Context object containing session data. * @param {Object} context - Context object containing session data.
* @returns {Promise<Object>} - Result object with success status and data/error information. * @returns {Promise<Object>} - Result object with success status and data/error information.
*/ */
export async function parsePRDDirect(args, log, context = {}) { export async function parsePRDDirect(args, log, context = {}) {
const { session } = context; // Only extract session, not reportProgress const { session } = context; // Only extract session
try { try {
log.info(`Parsing PRD document with args: ${JSON.stringify(args)}`); log.info(`Parsing PRD document with args: ${JSON.stringify(args)}`);
// Initialize AI client for PRD parsing
let aiClient;
try {
aiClient = getAnthropicClientForMCP(session, log);
} catch (error) {
log.error(`Failed to initialize AI client: ${error.message}`);
return {
success: false,
error: {
code: 'AI_CLIENT_ERROR',
message: `Cannot initialize AI client: ${error.message}`
},
fromCache: false
};
}
// Validate required parameters // Validate required parameters
if (!args.projectRoot) { if (!args.projectRoot) {
const errorMessage = 'Project root is required for parsePRDDirect'; const errorMessage = 'Project root is required for parsePRDDirect';
@@ -57,7 +37,6 @@ export async function parsePRDDirect(args, log, context = {}) {
fromCache: false fromCache: false
}; };
} }
if (!args.input) { if (!args.input) {
const errorMessage = 'Input file path is required for parsePRDDirect'; const errorMessage = 'Input file path is required for parsePRDDirect';
log.error(errorMessage); log.error(errorMessage);
@@ -67,7 +46,6 @@ export async function parsePRDDirect(args, log, context = {}) {
fromCache: false fromCache: false
}; };
} }
if (!args.output) { if (!args.output) {
const errorMessage = 'Output file path is required for parsePRDDirect'; const errorMessage = 'Output file path is required for parsePRDDirect';
log.error(errorMessage); log.error(errorMessage);
@@ -137,58 +115,35 @@ export async function parsePRDDirect(args, log, context = {}) {
success: (message, ...args) => log.info(message, ...args) // Map success to info success: (message, ...args) => log.info(message, ...args) // Map success to info
}; };
// Get model config from session
const modelConfig = getModelConfig(session);
// Enable silent mode to prevent console logs from interfering with JSON response // Enable silent mode to prevent console logs from interfering with JSON response
enableSilentMode(); enableSilentMode();
try { try {
// Make sure the output directory exists // Execute core parsePRD function - It now handles AI internally
const outputDir = path.dirname(outputPath); const tasksDataResult = await parsePRD(inputPath, outputPath, numTasks, {
if (!fs.existsSync(outputDir)) { mcpLog: logWrapper,
log.info(`Creating output directory: ${outputDir}`); session
fs.mkdirSync(outputDir, { recursive: true }); });
// Check the result from the core function (assuming it might return data or null/undefined)
if (!tasksDataResult || !tasksDataResult.tasks) {
throw new Error(
'Core parsePRD function did not return valid task data.'
);
} }
// Execute core parsePRD function with AI client log.info(
await parsePRD( `Successfully parsed PRD and generated ${tasksDataResult.tasks?.length || 0} tasks`
inputPath,
outputPath,
numTasks,
{
mcpLog: logWrapper,
session
},
aiClient,
modelConfig
); );
// Since parsePRD doesn't return a value but writes to a file, we'll read the result return {
// to return it to the caller success: true,
if (fs.existsSync(outputPath)) { data: {
const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8')); message: `Successfully generated ${tasksDataResult.tasks?.length || 0} tasks from PRD`,
log.info( taskCount: tasksDataResult.tasks?.length || 0,
`Successfully parsed PRD and generated ${tasksData.tasks?.length || 0} tasks` outputPath
); },
fromCache: false // This operation always modifies state
return { };
success: true,
data: {
message: `Successfully generated ${tasksData.tasks?.length || 0} tasks from PRD`,
taskCount: tasksData.tasks?.length || 0,
outputPath
},
fromCache: false // This operation always modifies state and should never be cached
};
} else {
const errorMessage = `Tasks file was not created at ${outputPath}`;
log.error(errorMessage);
return {
success: false,
error: { code: 'OUTPUT_FILE_NOT_CREATED', message: errorMessage },
fromCache: false
};
}
} finally { } finally {
// Always restore normal logging // Always restore normal logging
disableSilentMode(); disableSilentMode();
@@ -201,7 +156,7 @@ export async function parsePRDDirect(args, log, context = {}) {
return { return {
success: false, success: false,
error: { error: {
code: 'PARSE_PRD_ERROR', code: error.code || 'PARSE_PRD_ERROR', // Use error code if available
message: error.message || 'Unknown error parsing PRD' message: error.message || 'Unknown error parsing PRD'
}, },
fromCache: false fromCache: false

View File

@@ -209,7 +209,7 @@ async function _unifiedServiceRunner(serviceType, params) {
let providerName, modelId, apiKey, roleParams, providerFnSet, providerApiFn; let providerName, modelId, apiKey, roleParams, providerFnSet, providerApiFn;
try { try {
log('info', `Attempting service call with role: ${currentRole}`); log('info', `New AI service call with role: ${currentRole}`);
// --- Corrected Config Fetching --- // --- Corrected Config Fetching ---
// 1. Get Config: Provider, Model, Parameters for the current role // 1. Get Config: Provider, Model, Parameters for the current role

View File

@@ -73,7 +73,8 @@ const DEFAULTS = {
}; };
// --- Internal Config Loading --- // --- Internal Config Loading ---
let loadedConfig = null; // Cache for loaded config let loadedConfig = null;
let loadedConfigRoot = null; // Track which root loaded the config
// Custom Error for configuration issues // Custom Error for configuration issues
class ConfigurationError extends Error { class ConfigurationError extends Error {
@@ -84,25 +85,20 @@ class ConfigurationError extends Error {
} }
function _loadAndValidateConfig(explicitRoot = null) { function _loadAndValidateConfig(explicitRoot = null) {
// Determine the root path to use
const rootToUse = explicitRoot || findProjectRoot();
const defaults = DEFAULTS; // Use the defined defaults const defaults = DEFAULTS; // Use the defined defaults
// If no explicit root is provided (e.g., during initial server load),
// return defaults immediately and silently.
if (!explicitRoot) {
return defaults;
}
// --- Proceed with loading from the provided explicitRoot ---
const configPath = path.join(explicitRoot, CONFIG_FILE_NAME);
let config = { ...defaults }; // Start with a deep copy of defaults
let configExists = false; let configExists = false;
if (!rootToUse) { if (fs.existsSync(configPath)) {
console.warn(
chalk.yellow(
'Warning: Could not determine project root. Using default configuration.'
)
);
// Allow proceeding with defaults if root finding fails, but validation later might trigger error
// Or perhaps throw here? Let's throw later based on file existence check.
}
const configPath = rootToUse ? path.join(rootToUse, CONFIG_FILE_NAME) : null;
let config = defaults; // Start with defaults
if (configPath && fs.existsSync(configPath)) {
configExists = true; configExists = true;
try { try {
const rawData = fs.readFileSync(configPath, 'utf-8'); const rawData = fs.readFileSync(configPath, 'utf-8');
@@ -125,59 +121,55 @@ function _loadAndValidateConfig(explicitRoot = null) {
global: { ...defaults.global, ...parsedConfig?.global } global: { ...defaults.global, ...parsedConfig?.global }
}; };
// --- Validation (Only warn if file exists but content is invalid) --- // --- Validation (Warn if file content is invalid) ---
// Validate main provider/model // Only use console.warn here, as this part runs only when an explicitRoot *is* provided
if (!validateProvider(config.models.main.provider)) { if (!validateProvider(config.models.main.provider)) {
console.warn( console.warn(
chalk.yellow( chalk.yellow(
`Warning: Invalid main provider "${config.models.main.provider}" in ${CONFIG_FILE_NAME}. Falling back to default.` `Warning: Invalid main provider "${config.models.main.provider}" in ${configPath}. Falling back to default.`
) )
); );
config.models.main = { ...defaults.models.main }; config.models.main = { ...defaults.models.main };
} }
// Validate research provider/model
if (!validateProvider(config.models.research.provider)) { if (!validateProvider(config.models.research.provider)) {
console.warn( console.warn(
chalk.yellow( chalk.yellow(
`Warning: Invalid research provider "${config.models.research.provider}" in ${CONFIG_FILE_NAME}. Falling back to default.` `Warning: Invalid research provider "${config.models.research.provider}" in ${configPath}. Falling back to default.`
) )
); );
config.models.research = { ...defaults.models.research }; config.models.research = { ...defaults.models.research };
} }
// Validate fallback provider if it exists
if ( if (
config.models.fallback?.provider && config.models.fallback?.provider &&
!validateProvider(config.models.fallback.provider) !validateProvider(config.models.fallback.provider)
) { ) {
console.warn( console.warn(
chalk.yellow( chalk.yellow(
`Warning: Invalid fallback provider "${config.models.fallback.provider}" in ${CONFIG_FILE_NAME}. Fallback model configuration will be ignored.` `Warning: Invalid fallback provider "${config.models.fallback.provider}" in ${configPath}. Fallback model configuration will be ignored.`
) )
); );
config.models.fallback.provider = undefined; config.models.fallback.provider = undefined;
config.models.fallback.modelId = undefined; config.models.fallback.modelId = undefined;
} }
} catch (error) { } catch (error) {
// Use console.error for actual errors during parsing
console.error( console.error(
chalk.red( chalk.red(
`Error reading or parsing ${configPath}: ${error.message}. Using default configuration.` `Error reading or parsing ${configPath}: ${error.message}. Using default configuration.`
) )
); );
config = defaults; // Reset to defaults on parse error config = { ...defaults }; // Reset to defaults on parse error
// Do not throw ConfigurationError here, allow fallback to defaults if file is corrupt
} }
} else { } else {
// Config file doesn't exist // Config file doesn't exist at the provided explicitRoot.
// **Changed: Log warning instead of throwing error** // Use console.warn because an explicit root *was* given.
console.warn( console.warn(
chalk.yellow( chalk.yellow(
`Warning: ${CONFIG_FILE_NAME} not found at project root (${rootToUse || 'unknown'}). Using default configuration. Run 'task-master models --setup' to configure.` `Warning: ${CONFIG_FILE_NAME} not found at provided project root (${explicitRoot}). Using default configuration. Run 'task-master models --setup' to configure.`
) )
); );
// Return defaults instead of throwing error // Keep config as defaults
return defaults; config = { ...defaults };
} }
return config; return config;
@@ -185,18 +177,32 @@ function _loadAndValidateConfig(explicitRoot = null) {
/** /**
* Gets the current configuration, loading it if necessary. * Gets the current configuration, loading it if necessary.
* Handles MCP initialization context gracefully.
* @param {string|null} explicitRoot - Optional explicit path to the project root. * @param {string|null} explicitRoot - Optional explicit path to the project root.
* @param {boolean} forceReload - Force reloading the config file. * @param {boolean} forceReload - Force reloading the config file.
* @returns {object} The loaded configuration object. * @returns {object} The loaded configuration object.
*/ */
function getConfig(explicitRoot = null, forceReload = false) { function getConfig(explicitRoot = null, forceReload = false) {
if (!loadedConfig || forceReload) { // Determine if a reload is necessary
loadedConfig = _loadAndValidateConfig(explicitRoot); const needsLoad =
} !loadedConfig ||
// If an explicitRoot was provided for a one-off check, don't cache it permanently forceReload ||
if (explicitRoot && !forceReload) { (explicitRoot && explicitRoot !== loadedConfigRoot);
return _loadAndValidateConfig(explicitRoot);
if (needsLoad) {
const newConfig = _loadAndValidateConfig(explicitRoot); // _load handles null explicitRoot
// Only update the global cache if loading was forced or if an explicit root
// was provided (meaning we attempted to load a specific project's config).
// We avoid caching the initial default load triggered without an explicitRoot.
if (forceReload || explicitRoot) {
loadedConfig = newConfig;
loadedConfigRoot = explicitRoot; // Store the root used for this loaded config
}
return newConfig; // Return the newly loaded/default config
} }
// If no load was needed, return the cached config
return loadedConfig; return loadedConfig;
} }

View File

@@ -79,14 +79,14 @@ async function getModelConfiguration(options = {}) {
// Check API keys // Check API keys
const mainCliKeyOk = isApiKeySet(mainProvider); const mainCliKeyOk = isApiKeySet(mainProvider);
const mainMcpKeyOk = getMcpApiKeyStatus(mainProvider); const mainMcpKeyOk = getMcpApiKeyStatus(mainProvider, projectRoot);
const researchCliKeyOk = isApiKeySet(researchProvider); const researchCliKeyOk = isApiKeySet(researchProvider);
const researchMcpKeyOk = getMcpApiKeyStatus(researchProvider); const researchMcpKeyOk = getMcpApiKeyStatus(researchProvider, projectRoot);
const fallbackCliKeyOk = fallbackProvider const fallbackCliKeyOk = fallbackProvider
? isApiKeySet(fallbackProvider) ? isApiKeySet(fallbackProvider)
: true; : true;
const fallbackMcpKeyOk = fallbackProvider const fallbackMcpKeyOk = fallbackProvider
? getMcpApiKeyStatus(fallbackProvider) ? getMcpApiKeyStatus(fallbackProvider, projectRoot)
: true; : true;
// Get available models to find detailed info // Get available models to find detailed info

View File

@@ -162,7 +162,7 @@ describe('Unified AI Services', () => {
expect(mockLog).toHaveBeenNthCalledWith( expect(mockLog).toHaveBeenNthCalledWith(
2, 2,
'info', 'info',
'Attempting service call with role: main' 'New AI service call with role: main'
); );
expect(mockLog).toHaveBeenNthCalledWith( expect(mockLog).toHaveBeenNthCalledWith(
3, 3,
@@ -229,7 +229,7 @@ describe('Unified AI Services', () => {
); );
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
'info', 'info',
'Attempting service call with role: main' 'New AI service call with role: main'
); );
expect(mockLog).toHaveBeenCalledWith('info', 'Retrieved AI client', { expect(mockLog).toHaveBeenCalledWith('info', 'Retrieved AI client', {
provider: mockClient.provider, provider: mockClient.provider,
@@ -277,7 +277,7 @@ describe('Unified AI Services', () => {
// Check subsequent fallback attempts (which also fail) // Check subsequent fallback attempts (which also fail)
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
'info', 'info',
'Attempting service call with role: fallback' 'New AI service call with role: fallback'
); );
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
'error', 'error',
@@ -285,7 +285,7 @@ describe('Unified AI Services', () => {
); );
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
'info', 'info',
'Attempting service call with role: research' 'New AI service call with role: research'
); );
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
'error', 'error',
@@ -349,7 +349,7 @@ describe('Unified AI Services', () => {
); );
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
'info', 'info',
'Attempting service call with role: fallback' 'New AI service call with role: fallback'
); );
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
'info', 'info',
@@ -431,7 +431,7 @@ describe('Unified AI Services', () => {
); );
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
'info', 'info',
'Attempting service call with role: research' 'New AI service call with role: research'
); );
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
'info', 'info',
@@ -509,7 +509,7 @@ describe('Unified AI Services', () => {
); );
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
'info', 'info',
'Attempting service call with role: research' 'New AI service call with role: research'
); );
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
'info', 'info',
@@ -554,7 +554,7 @@ describe('Unified AI Services', () => {
// Check logs for sequence // Check logs for sequence
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
'info', 'info',
'Attempting service call with role: fallback' 'New AI service call with role: fallback'
); );
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
'error', 'error',
@@ -568,7 +568,7 @@ describe('Unified AI Services', () => {
); );
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
'info', 'info',
'Attempting service call with role: research' 'New AI service call with role: research'
); );
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
'info', 'info',
@@ -613,7 +613,7 @@ describe('Unified AI Services', () => {
// Check logs for sequence // Check logs for sequence
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
'info', 'info',
'Attempting service call with role: research' 'New AI service call with role: research'
); );
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
'error', 'error',
@@ -627,7 +627,7 @@ describe('Unified AI Services', () => {
); );
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
'info', 'info',
'Attempting service call with role: fallback' 'New AI service call with role: fallback'
); );
expect(mockLog).toHaveBeenCalledWith( expect(mockLog).toHaveBeenCalledWith(
'info', 'info',