@moikas/moidvk
Version:
The Ultimate DevKit - MCP server for development best practices
294 lines (259 loc) • 8.54 kB
JavaScript
import { exec } from 'child_process';
import { promisify } from 'util';
import { validatePythonCode, detectPythonVersion } from '../utils/python-validation.js';
import fs from 'fs/promises';
import path from 'path';
import os from 'os';
const execAsync = promisify(exec);
/**
* Python Type Checker using mypy
*/
export const pythonTypeCheckerTool = {
name: 'python_type_checker',
description: 'Type checks Python code using mypy. Detects type errors, missing type hints, and type safety issues.',
inputSchema: {
type: 'object',
properties: {
code: {
type: 'string',
description: 'The Python code to type check (max 100KB)'
},
filename: {
type: 'string',
description: 'Optional filename for context (e.g., "main.py")'
},
pythonVersion: {
type: 'string',
enum: ['3.7', '3.8', '3.9', '3.10', '3.11', '3.12'],
description: 'Python version for type checking (default: auto-detect)'
},
strict: {
type: 'boolean',
description: 'Enable strict mode (default: false)'
},
ignoreErrors: {
type: 'array',
items: { type: 'string' },
description: 'Error codes to ignore (e.g., ["import", "name-defined"])'
},
followImports: {
type: 'string',
enum: ['normal', 'silent', 'skip', 'error'],
description: 'How to handle imports (default: "skip")'
},
disallowUntyped: {
type: 'boolean',
description: 'Disallow untyped definitions (default: false)'
},
checkUntyped: {
type: 'boolean',
description: 'Check untyped function bodies (default: true)'
},
limit: {
type: 'number',
description: 'Maximum number of errors to return (default: 50)',
minimum: 1,
maximum: 500,
default: 50
},
offset: {
type: 'number',
description: 'Starting index for pagination (default: 0)',
minimum: 0,
default: 0
},
severity: {
type: 'string',
enum: ['error', 'warning', 'note', 'all'],
description: 'Filter by severity level',
default: 'all'
}
},
required: ['code']
}
};
export async function handlePythonTypeChecker(args) {
const startTime = Date.now();
try {
// Validate input
const validation = validatePythonCode(args.code);
if (!validation.isValid) {
return {
content: [{
type: 'text',
text: JSON.stringify({
error: `Invalid Python code: ${validation.error}`,
executionTime: Date.now() - startTime
}, null, 2)
}]
};
}
// Detect Python version if not specified
const pythonVersion = args.pythonVersion || detectPythonVersion(args.code);
// Check if mypy is available
try {
await execAsync('mypy --version', { timeout: 5000 });
} catch (error) {
return {
content: [{
type: 'text',
text: JSON.stringify({
error: 'mypy is not installed. Please install it with: pip install mypy',
executionTime: Date.now() - startTime
}, null, 2)
}]
};
}
// Create temporary file
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'mypy-'));
const tempFile = path.join(tempDir, args.filename || 'temp.py');
try {
await fs.writeFile(tempFile, args.code, 'utf8');
// Build mypy command
const mypyArgs = [
`--python-version=${pythonVersion}`,
'--show-error-codes',
'--show-column-numbers',
'--show-error-context',
'--no-error-summary',
'--no-color-output'
];
if (args.strict) {
mypyArgs.push('--strict');
} else {
if (args.disallowUntyped) mypyArgs.push('--disallow-untyped-defs');
if (args.checkUntyped !== false) mypyArgs.push('--check-untyped-defs');
}
if (args.followImports) {
mypyArgs.push(`--follow-imports=${args.followImports}`);
} else {
mypyArgs.push('--follow-imports=skip');
}
if (args.ignoreErrors && args.ignoreErrors.length > 0) {
args.ignoreErrors.forEach(code => {
mypyArgs.push(`--disable-error-code=${code}`);
});
}
const command = `mypy ${mypyArgs.join(' ')} "${tempFile}"`;
// Run mypy
let output = '';
let errors = [];
try {
const result = await execAsync(command, {
timeout: 30000,
maxBuffer: 1024 * 1024 * 10 // 10MB
});
output = result.stdout;
} catch (error) {
// mypy returns non-zero exit code when it finds errors
output = error.stdout || '';
}
// Parse mypy output
const lines = output.split('\n').filter(line => line.trim());
for (const line of lines) {
// Parse mypy output format: file.py:line:col: severity: message [error-code]
const match = line.match(/^([^:]+):(\d+):(\d+):\s*(\w+):\s*(.+?)(?:\s*\[([^\]]+)\])?$/);
if (match) {
const [, file, lineNum, column, severity, message, errorCode] = match;
// Apply severity filter
if (args.severity !== 'all' && severity.toLowerCase() !== args.severity) {
continue;
}
errors.push({
line: parseInt(lineNum),
column: parseInt(column),
severity: severity.toLowerCase(),
message: message.trim(),
errorCode: errorCode || 'unknown',
context: getLineContext(args.code, parseInt(lineNum))
});
}
}
// Sort errors by line number
errors.sort((a, b) => a.line - b.line || a.column - b.column);
// Apply pagination
const totalErrors = errors.length;
const paginatedErrors = errors.slice(args.offset, args.offset + args.limit);
// Calculate type coverage
const typeCoverage = calculateTypeCoverage(args.code);
return {
content: [{
type: 'text',
text: JSON.stringify({
success: true,
errors: paginatedErrors,
totalErrors,
pagination: {
offset: args.offset,
limit: args.limit,
hasMore: args.offset + args.limit < totalErrors
},
metrics: {
typeCoverage,
pythonVersion,
strict: args.strict || false,
executionTime: Date.now() - startTime
},
summary: {
errors: errors.filter(e => e.severity === 'error').length,
warnings: errors.filter(e => e.severity === 'warning').length,
notes: errors.filter(e => e.severity === 'note').length
}
}, null, 2)
}]
};
} finally {
// Cleanup
await fs.rm(tempDir, { recursive: true, force: true });
}
} catch (error) {
return {
content: [{
type: 'text',
text: JSON.stringify({
error: `Type checking failed: ${error.message}`,
executionTime: Date.now() - startTime
}, null, 2)
}]
};
}
}
function getLineContext(code, lineNumber, contextLines = 2) {
const lines = code.split('\n');
const startLine = Math.max(1, lineNumber - contextLines);
const endLine = Math.min(lines.length, lineNumber + contextLines);
const context = [];
for (let i = startLine - 1; i < endLine; i++) {
context.push({
line: i + 1,
text: lines[i] || '',
isError: i + 1 === lineNumber
});
}
return context;
}
function calculateTypeCoverage(code) {
const lines = code.split('\n');
let typedLines = 0;
let totalLines = 0;
const typePatterns = [
/:\s*\w+/, // Type hints
/->\s*\w+/, // Return type hints
/:\s*[A-Z]\w*\[/, // Generic types
/:\s*Union\[/, // Union types
/:\s*Optional\[/, // Optional types
/:\s*List\[/, // List types
/:\s*Dict\[/, // Dict types
/:\s*Tuple\[/, // Tuple types
/:\s*Any\b/ // Any type
];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
totalLines++;
if (typePatterns.some(pattern => pattern.test(line))) {
typedLines++;
}
}
return totalLines > 0 ? Math.round((typedLines / totalLines) * 100) : 0;
}