MCP Server 2.0 (#452)

* MCP Server 2.0

- Updated application version from 2.23.14 to 2.24.0 in package.json.
- MCP Server improved with new features
- Migrated functionality from Dev Tools to MCP Server
- Improved getMonitoringProvider not to crash application when misconfigured
This commit is contained in:
Giancarlo Buomprisco
2026-02-11 20:42:01 +01:00
committed by GitHub
parent 059408a70a
commit f3ac595d06
123 changed files with 17803 additions and 5265 deletions

View File

@@ -1,471 +1,6 @@
import 'server-only';
import fs from 'fs/promises';
import path from 'path';
import { envVariables } from './env-variables-model';
import {
AppEnvState,
EnvFileInfo,
EnvMode,
EnvVariableState,
ScanOptions,
} from './types';
// Define precedence order for each mode
const ENV_FILE_PRECEDENCE: Record<EnvMode, string[]> = {
development: [
'.env',
'.env.development',
'.env.local',
'.env.development.local',
],
production: [
'.env',
'.env.production',
'.env.local',
'.env.production.local',
],
};
function getSourcePrecedence(source: string, mode: EnvMode): number {
return ENV_FILE_PRECEDENCE[mode].indexOf(source);
}
export async function scanMonorepoEnv(
options: ScanOptions,
): Promise<EnvFileInfo[]> {
const {
rootDir = path.resolve(process.cwd(), '../..'),
apps = ['web'],
mode,
} = options;
const envTypes = ENV_FILE_PRECEDENCE[mode];
const appsDir = path.join(rootDir, 'apps');
const results: EnvFileInfo[] = [];
try {
const appDirs = await fs.readdir(appsDir);
for (const appName of appDirs) {
if (apps.length > 0 && !apps.includes(appName)) {
continue;
}
const appDir = path.join(appsDir, appName);
const stat = await fs.stat(appDir);
if (!stat.isDirectory()) {
continue;
}
const appInfo: EnvFileInfo = {
appName,
filePath: appDir,
variables: [],
};
for (const envType of envTypes) {
const envPath = path.join(appDir, envType);
try {
const content = await fs.readFile(envPath, 'utf-8');
const vars = parseEnvFile(content, envType);
appInfo.variables.push(...vars);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
console.warn(`Error reading ${envPath}:`, error);
}
}
}
if (appInfo.variables.length > 0) {
results.push(appInfo);
}
}
} catch (error) {
console.error('Error scanning monorepo:', error);
throw error;
}
return results;
}
function parseEnvFile(content: string, source: string) {
const variables: Array<{ key: string; value: string; source: string }> = [];
const lines = content.split('\n');
for (const line of lines) {
// Skip comments and empty lines
if (line.trim().startsWith('#') || !line.trim()) {
continue;
}
// Match KEY=VALUE pattern, handling quotes
const match = line.match(/^([^=]+)=(.*)$/);
if (match) {
const [, key = '', rawValue] = match;
let value = rawValue ?? '';
// Remove quotes if present
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
// Handle escaped quotes within the value
value = value
.replace(/\\"/g, '"')
.replace(/\\'/g, "'")
.replace(/\\\\/g, '\\');
variables.push({
key: key.trim(),
value: value.trim(),
source,
});
}
}
return variables;
}
export function processEnvDefinitions(
envInfo: EnvFileInfo,
mode: EnvMode,
): AppEnvState {
const variableMap: Record<string, EnvVariableState> = {};
// First pass: Collect all definitions
for (const variable of envInfo.variables) {
if (!variable) {
continue;
}
const model = envVariables.find((v) => variable.key === v.name);
if (!variableMap[variable.key]) {
variableMap[variable.key] = {
key: variable.key,
isVisible: true,
definitions: [],
effectiveValue: variable.value,
effectiveSource: variable.source,
isOverridden: false,
category: model ? model.category : 'Custom',
validation: {
success: true,
error: {
issues: [],
},
},
};
}
const varState = variableMap[variable.key];
if (!varState) {
continue;
}
varState.definitions.push({
key: variable.key,
value: variable.value,
source: variable.source,
});
}
// Second pass: Determine effective values and override status
for (const key in variableMap) {
const varState = variableMap[key];
if (!varState) {
continue;
}
// Sort definitions by mode-specific precedence
varState.definitions.sort(
(a, b) =>
getSourcePrecedence(a.source, mode) -
getSourcePrecedence(b.source, mode),
);
if (varState.definitions.length > 1) {
const lastDef = varState.definitions[varState.definitions.length - 1];
if (!lastDef) {
continue;
}
const highestPrecedence = getSourcePrecedence(lastDef.source, mode);
varState.isOverridden = true;
varState.effectiveValue = lastDef.value;
varState.effectiveSource = lastDef.source;
// Check for conflicts at highest precedence
const conflictingDefs = varState.definitions.filter(
(def) => getSourcePrecedence(def.source, mode) === highestPrecedence,
);
if (conflictingDefs.length > 1) {
varState.effectiveSource = `${varState.effectiveSource}`;
}
}
}
// after computing the effective values, we can check for errors
for (const key in variableMap) {
const model = envVariables.find((v) => key === v.name);
const varState = variableMap[key];
if (!varState) {
continue;
}
let validation: {
success: boolean;
error: {
issues: string[];
};
} = { success: true, error: { issues: [] } };
if (model) {
const allVariables = Object.values(variableMap).reduce(
(acc, variable) => {
return {
...acc,
[variable.key]: variable.effectiveValue,
};
},
{} as Record<string, string>,
);
// First check if it's required but missing
if (model.required && !varState.effectiveValue) {
validation = {
success: false,
error: {
issues: [
`This variable is required but missing from your environment files`,
],
},
};
} else if (model.contextualValidation) {
// Then check contextual validation
const dependenciesMet = model.contextualValidation.dependencies.some(
(dep) => {
const dependencyValue = allVariables[dep.variable] ?? '';
return dep.condition(dependencyValue, allVariables);
},
);
if (dependenciesMet) {
// Only check for missing value or run validation if dependencies are met
if (!varState.effectiveValue) {
const dependencyErrors = model.contextualValidation.dependencies
.map((dep) => {
const dependencyValue = allVariables[dep.variable] ?? '';
const shouldValidate = dep.condition(
dependencyValue,
allVariables,
);
if (shouldValidate) {
const { success } = model.contextualValidation!.validate({
value: varState.effectiveValue,
variables: allVariables,
mode,
});
if (success) {
return null;
}
return dep.message;
}
return null;
})
.filter((message): message is string => message !== null);
validation = {
success: dependencyErrors.length === 0,
error: {
issues: dependencyErrors
.map((message) => message)
.filter((message) => !!message),
},
};
} else {
// If we have a value and dependencies are met, run contextual validation
const result = model.contextualValidation.validate({
value: varState.effectiveValue,
variables: allVariables,
mode,
});
if (!result.success) {
validation = {
success: false,
error: {
issues: result.error.issues
.map((issue) => issue.message)
.filter((message) => !!message),
},
};
}
}
}
} else if (model.validate && varState.effectiveValue) {
// Only run regular validation if:
// 1. There's no contextual validation
// 2. There's a value to validate
const result = model.validate({
value: varState.effectiveValue,
variables: allVariables,
mode,
});
if (!result.success) {
validation = {
success: false,
error: {
issues: result.error.issues
.map((issue) => issue.message)
.filter((message) => !!message),
},
};
}
}
}
varState.validation = validation;
}
// Final pass: Validate missing variables that are marked as required
// or as having contextual validation
for (const model of envVariables) {
// If the variable exists in appState, use that
const existingVar = variableMap[model.name];
if (existingVar) {
// If the variable is already in the map, skip it
continue;
}
if (model.required || model.contextualValidation) {
if (model.contextualValidation) {
const allVariables = Object.values(variableMap).reduce(
(acc, variable) => {
return {
...acc,
[variable.key]: variable.effectiveValue,
};
},
{} as Record<string, string>,
);
const errors =
model?.contextualValidation?.dependencies
.map((dep) => {
const dependencyValue = allVariables[dep.variable] ?? '';
const shouldValidate = dep.condition(
dependencyValue,
allVariables,
);
if (!shouldValidate) {
return [];
}
const effectiveValue = allVariables[dep.variable] ?? '';
const validation = model.contextualValidation!.validate({
value: effectiveValue,
variables: allVariables,
mode,
});
if (validation) {
return [dep.message];
}
return [];
})
.flat() ?? ([] as string[]);
if (errors.length === 0) {
continue;
} else {
variableMap[model.name] = {
key: model.name,
effectiveValue: '',
effectiveSource: 'MISSING',
isVisible: true,
category: model.category,
isOverridden: false,
definitions: [],
validation: {
success: false,
error: {
issues: errors.map((error) => error),
},
},
};
}
}
// If it doesn't exist but is required or has contextual validation, create an empty state
variableMap[model.name] = {
key: model.name,
effectiveValue: '',
effectiveSource: 'MISSING',
isVisible: true,
category: model.category,
isOverridden: false,
definitions: [],
validation: {
success: false,
error: {
issues: [
`This variable is required but missing from your environment files`,
],
},
},
};
}
}
return {
appName: envInfo.appName,
filePath: envInfo.filePath,
mode,
variables: variableMap,
};
}
export async function getEnvState(
options: ScanOptions,
): Promise<AppEnvState[]> {
const envInfos = await scanMonorepoEnv(options);
return envInfos.map((info) => processEnvDefinitions(info, options.mode));
}
export async function getVariable(key: string, mode: EnvMode) {
// Get the processed environment state for all apps (you can limit to 'web' via options)
const envStates = await getEnvState({ mode, apps: ['web'] });
// Find the state for the "web" app.
const webState = envStates.find((state) => state.appName === 'web');
// Return the effectiveValue based on override status.
return webState?.variables[key]?.effectiveValue ?? '';
}
export {
getEnvState,
getVariable,
processEnvDefinitions,
scanMonorepoEnv,
} from '@kit/mcp-server/env';

File diff suppressed because it is too large Load Diff

View File

@@ -2,83 +2,35 @@
import { revalidatePath } from 'next/cache';
import { envVariables } from '@/app/variables/lib/env-variables-model';
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:url';
import { z } from 'zod';
import {
createKitEnvDeps,
createKitEnvService,
findWorkspaceRoot,
} from '@kit/mcp-server/env';
const Schema = z.object({
name: z.string().min(1),
value: z.string(),
mode: z.enum(['development', 'production']),
});
/**
* Update the environment variable in the specified file.
* @param props
*/
export async function updateEnvironmentVariableAction(
props: z.infer<typeof Schema>,
) {
// Validate the input
const { name, mode, value } = Schema.parse(props);
const root = resolve(process.cwd(), '..');
const model = envVariables.find((item) => item.name === name);
// Determine the source file based on the mode
const source = (() => {
const isSecret = model?.secret ?? true;
const rootPath = findWorkspaceRoot(process.cwd());
const service = createKitEnvService(createKitEnvDeps(rootPath));
switch (mode) {
case 'development':
if (isSecret) {
return '.env.local';
} else {
return '.env.development';
}
const result = await service.update({
key: name,
value,
mode,
});
case 'production':
if (isSecret) {
return '.env.production.local';
} else {
return '.env.production';
}
revalidatePath('/variables');
default:
throw new Error(`Invalid mode: ${mode}`);
}
})();
// check file exists, if not, create it
const filePath = `${root}/apps/web/${source}`;
if (!existsSync(filePath)) {
writeFileSync(filePath, '', 'utf8');
}
const sourceEnvFile = readFileSync(`${root}apps/web/${source}`, 'utf8');
let updatedEnvFile = '';
const isInSourceFile = sourceEnvFile.includes(name);
const isCommentedOut = sourceEnvFile.includes(`#${name}=`);
if (isInSourceFile && !isCommentedOut) {
updatedEnvFile = sourceEnvFile.replace(
new RegExp(`^${name}=.*`, 'm'),
`${name}=${value}`,
);
} else {
// if the key does not exist, append it to the end of the file
updatedEnvFile = `${sourceEnvFile}\n${name}=${value}`;
}
// write the updated content back to the file
writeFileSync(`${root}/apps/web/${source}`, updatedEnvFile, 'utf8');
revalidatePath(`/variables`);
return {
success: true,
message: `Updated ${name} in "${source}"`,
};
return result;
}