Skip to main content
Glama
noelserdna

CV Recruitment Assistant

by noelserdna
mcp-tools.test.js33.3 kB
/** * Batería de tests para verificar el funcionamiento de las herramientas MCP * Sistema CV Recruitment Assistant - Brayan Smith Cordova Tasayco */ const candidateData = require('../src/candidate-data.json'); const { generateKeyPairSync, createHash, sign, verify } = require('node:crypto'); // Generar claves para testing const { publicKey, privateKey } = generateKeyPairSync('rsa', { modulusLength: 2048, publicKeyEncoding: { type: 'spki', format: 'pem' }, privateKeyEncoding: { type: 'pkcs8', format: 'pem' } }); // Funciones de firma digital para testing function createDataHash(data) { const jsonString = JSON.stringify(data, null, 0); return createHash('sha256').update(jsonString).digest('hex'); } function signHash(hash, privateKey) { const signature = sign('sha256', Buffer.from(hash, 'hex'), { key: privateKey, format: 'pem' }); return signature.toString('base64'); } function signResponse(data) { const hash = createDataHash(data); const signature = signHash(hash, privateKey); return { data, hash, signature, timestamp: new Date().toISOString(), publicKey, serverId: 'test-cv-server', version: '1.0' }; } function verifySignedResponse(signedResponse) { try { if (!signedResponse.data || !signedResponse.hash || !signedResponse.signature) { return { isValid: false, error: 'Respuesta firmada incompleta', details: { hashMatch: false, signatureValid: false } }; } const computedHash = createDataHash(signedResponse.data); const hashMatch = computedHash === signedResponse.hash; const signatureValid = verify( 'sha256', Buffer.from(signedResponse.hash, 'hex'), { key: signedResponse.publicKey, format: 'pem' }, Buffer.from(signedResponse.signature, 'base64') ); return { isValid: hashMatch && signatureValid, error: (hashMatch && signatureValid) ? undefined : 'Hash o firma inválidos', details: { hashMatch, signatureValid } }; } catch (error) { return { isValid: false, error: `Error durante verificación: ${error}`, details: { hashMatch: false, signatureValid: false } }; } } // Mock del servidor MCP class TestMCPServer { constructor() { this.tools = new Map(); } tool(name, schema, handler) { this.tools.set(name, { schema, handler }); } async callTool(name, params = {}) { const tool = this.tools.get(name); if (!tool) { throw new Error(`Tool ${name} not found`); } return await tool.handler(params); } } // Clase de test con todas las herramientas MCP class TestMCPAgent { constructor() { this.server = new TestMCPServer(); } async init() { // === CORE CANDIDATE INFORMATION TOOLS === // Get candidate's personal profile and summary this.server.tool("get_candidate_profile", {}, async () => { const data = { name: candidateData.personal.fullName, location: candidateData.personal.location, contact: { phone: candidateData.personal.phone, linkedin: candidateData.personal.linkedin, portfolio: candidateData.personal.portfolio, }, professionalSummary: candidateData.personal.professionalSummary, totalExperience: candidateData.experienceYears.totalExperience, }; const signedResponse = signResponse(data); return { content: [{ type: "text", text: JSON.stringify(signedResponse, null, 2), }], }; }); // Get detailed work experience this.server.tool("get_work_experience", {}, async () => ({ content: [{ type: "text", text: JSON.stringify(candidateData.workExperience, null, 2), }], })); // Get education background this.server.tool("get_education_background", {}, async () => ({ content: [{ type: "text", text: JSON.stringify(candidateData.education, null, 2), }], })); // Get certifications this.server.tool("get_certifications", {}, async () => ({ content: [{ type: "text", text: JSON.stringify(candidateData.certifications, null, 2), }], })); // Get technical skills categorized this.server.tool("get_technical_skills", {}, async () => ({ content: [{ type: "text", text: JSON.stringify({ technical: candidateData.skills.technical, design: candidateData.skills.design, productivity: candidateData.skills.productivity, }, null, 2), }], })); // Get soft skills and competencies this.server.tool("get_soft_skills", {}, async () => ({ content: [{ type: "text", text: JSON.stringify({ softSkills: candidateData.skills.soft, languages: candidateData.languages, }, null, 2), }], })); // Get languages and interests this.server.tool("get_languages_interests", {}, async () => ({ content: [{ type: "text", text: JSON.stringify({ languages: candidateData.languages, interests: candidateData.interests, }, null, 2), }], })); // Search candidate information by keywords this.server.tool("search_candidate_info", {}, async ({ keyword }) => { const searchTerm = keyword.toLowerCase(); const results = []; // Search in all text fields const searchableData = JSON.stringify(candidateData).toLowerCase(); if (searchableData.includes(searchTerm)) { // Search in specific sections if (JSON.stringify(candidateData.skills.technical).toLowerCase().includes(searchTerm)) { results.push({ section: "Technical Skills", match: "Found in technical skills" }); } if (JSON.stringify(candidateData.workExperience).toLowerCase().includes(searchTerm)) { results.push({ section: "Work Experience", match: "Found in work experience" }); } if (JSON.stringify(candidateData.skills.soft).toLowerCase().includes(searchTerm)) { results.push({ section: "Soft Skills", match: "Found in soft skills" }); } if (JSON.stringify(candidateData.interests).toLowerCase().includes(searchTerm)) { results.push({ section: "Interests", match: "Found in interests" }); } } return { content: [{ type: "text", text: results.length > 0 ? JSON.stringify({ keyword: keyword, results: results }, null, 2) : `No matches found for "${keyword}"` }] }; }); // === ONLINE PRESENCE TOOLS === // Get candidate's portfolio/personal website this.server.tool("get_portfolio_website", {}, async () => ({ content: [{ type: "text", text: JSON.stringify({ portfolio: candidateData.personal.portfolio, description: "Portfolio profesional con proyectos y experiencia", status: "available", note: "Sitio web personal donde se muestran proyectos y experiencia profesional" }, null, 2), }], })); // Get candidate's GitHub profile information this.server.tool("get_github_profile", {}, async () => { const githubCertifications = candidateData.certifications.filter(cert => cert.name.toLowerCase().includes('github') || cert.issuer.toLowerCase().includes('github') ); return { content: [{ type: "text", text: JSON.stringify({ github_certifications: githubCertifications, profile_url: "not_specified_in_cv", note: "Candidato certificado en GitHub pero URL específica del perfil no disponible en CV", skills_related: candidateData.skills.technical.tools.filter(tool => tool.toLowerCase().includes('git') ) }, null, 2), }], }; }); // Get candidate's social networks and online presence this.server.tool("get_social_networks", {}, async () => ({ content: [{ type: "text", text: JSON.stringify({ available: { linkedin: candidateData.personal.linkedin, portfolio: candidateData.personal.portfolio }, interests: candidateData.interests.filter(interest => interest.toLowerCase().includes('red') || interest.toLowerCase().includes('social') || interest.toLowerCase().includes('conexión') ), note: "LinkedIn y portfolio son las principales redes profesionales especificadas en CV", contact_preferences: "Disponible para conexión profesional a través de LinkedIn" }, null, 2), }], })); // === ADVANCED ANALYSIS TOOLS === // Evaluate tech stack compatibility this.server.tool("evaluate_tech_stack", {}, async ({ requiredTechnologies }) => { const candidateTechStack = [ ...candidateData.skills.technical.programming, ...candidateData.skills.technical.frameworks, ...candidateData.skills.technical.styling, ...candidateData.skills.technical.databases, ...candidateData.skills.technical.tools, ...candidateData.skills.technical.deployment, ]; const matches = requiredTechnologies.filter((tech) => candidateTechStack.some((candidateTech) => candidateTech.toLowerCase().includes(tech.toLowerCase()) || tech.toLowerCase().includes(candidateTech.toLowerCase()), ), ); const missing = requiredTechnologies.filter((tech) => !candidateTechStack.some((candidateTech) => candidateTech.toLowerCase().includes(tech.toLowerCase()) || tech.toLowerCase().includes(candidateTech.toLowerCase()), ), ); const compatibilityScore = (matches.length / requiredTechnologies.length) * 100; return { content: [{ type: "text", text: JSON.stringify({ compatibilityScore: `${compatibilityScore.toFixed(1)}%`, matchingTechnologies: matches, missingTechnologies: missing, candidateStrengths: candidateTechStack, }, null, 2), }], }; }); // Assess leadership experience this.server.tool("assess_leadership_experience", {}, async () => { const leadershipExperience = candidateData.workExperience.filter((exp) => exp.responsibilities.includes("Team Leadership") || exp.position.toLowerCase().includes("lead"), ); const leadershipSkills = candidateData.skills.soft.filter((skill) => skill.toLowerCase().includes("liderazgo") || skill.toLowerCase().includes("equipo") || skill.toLowerCase().includes("gestión"), ); return { content: [{ type: "text", text: JSON.stringify({ hasLeadershipExperience: leadershipExperience.length > 0, leadershipRoles: leadershipExperience, leadershipDuration: candidateData.experienceYears.leadership, relevantSoftSkills: leadershipSkills, leadershipAchievements: leadershipExperience.flatMap((exp) => exp.achievements), }, null, 2), }], }; }); // Calculate experience years by technology this.server.tool("calculate_experience_years", {}, async ({ technology }) => { const techLower = technology.toLowerCase(); let experienceInfo = "No specific experience found"; // Check predefined experience years const experienceYearsMap = candidateData.experienceYears; if (experienceYearsMap[techLower]) { experienceInfo = experienceYearsMap[techLower]; } else { // Check if technology is in skill set const allTechSkills = [ ...candidateData.skills.technical.programming, ...candidateData.skills.technical.frameworks, ...candidateData.skills.technical.styling, ...candidateData.skills.technical.tools, ]; const hasTech = allTechSkills.some((skill) => skill.toLowerCase().includes(techLower), ); if (hasTech) { experienceInfo = "Present in skill set - likely 2+ years based on academic training since 2022"; } } return { content: [{ type: "text", text: JSON.stringify({ technology: technology, experience: experienceInfo, isCore: candidateData.skills.technical.programming.some((skill) => skill.toLowerCase().includes(techLower), ) || candidateData.skills.technical.frameworks.some((skill) => skill.toLowerCase().includes(techLower), ), }, null, 2), }], }; }); // Match job requirements this.server.tool("match_job_requirements", {}, async ({ jobTitle, requiredSkills, preferredSkills = [] }) => { const allCandidateSkills = [ ...candidateData.skills.technical.programming, ...candidateData.skills.technical.frameworks, ...candidateData.skills.technical.styling, ...candidateData.skills.technical.tools, ...candidateData.skills.soft, ]; const requiredMatches = requiredSkills.filter((skill) => allCandidateSkills.some((candidateSkill) => candidateSkill.toLowerCase().includes(skill.toLowerCase()) || skill.toLowerCase().includes(candidateSkill.toLowerCase()), ), ); const preferredMatches = preferredSkills.filter((skill) => allCandidateSkills.some((candidateSkill) => candidateSkill.toLowerCase().includes(skill.toLowerCase()) || skill.toLowerCase().includes(candidateSkill.toLowerCase()), ), ); const requiredScore = (requiredMatches.length / requiredSkills.length) * 100; const preferredScore = preferredSkills.length > 0 ? (preferredMatches.length / preferredSkills.length) * 100 : 0; return { content: [{ type: "text", text: JSON.stringify({ jobTitle: jobTitle, matchAnalysis: { requiredSkillsMatch: `${requiredScore.toFixed(1)}%`, preferredSkillsMatch: `${preferredScore.toFixed(1)}%`, overallFit: requiredScore >= 70 ? "High" : requiredScore >= 50 ? "Medium" : "Low", }, skillsMatched: { required: requiredMatches, preferred: preferredMatches, }, skillsGap: { required: requiredSkills.filter((skill) => !requiredMatches.includes(skill)), preferred: preferredSkills.filter((skill) => !preferredMatches.includes(skill)), }, candidateStrengths: { leadership: candidateData.experienceYears.leadership, frontend: candidateData.experienceYears.frontend, totalExperience: candidateData.experienceYears.totalExperience, }, }, null, 2), }], }; }); // === DEBUGGING AND VERIFICATION TOOL === this.server.tool("verify_response_signature", { signedResponse: {} }, async ({ signedResponse }) => { try { const parsedResponse = JSON.parse(signedResponse); const verificationResult = verifySignedResponse(parsedResponse); const data = { verificationStatus: verificationResult.isValid ? "VALID" : "INVALID", result: verificationResult, verifiedBy: 'test-cv-server', verificationTimestamp: new Date().toISOString(), }; const signedVerificationResult = signResponse(data); return { content: [{ type: "text", text: JSON.stringify(signedVerificationResult, null, 2), }], }; } catch (error) { const errorData = { verificationStatus: "ERROR", error: `Failed to parse or verify response: ${error}`, verifiedBy: 'test-cv-server', verificationTimestamp: new Date().toISOString(), }; const signedErrorResult = signResponse(errorData); return { content: [{ type: "text", text: JSON.stringify(signedErrorResult, null, 2), }], }; } }); this.server.tool("get_server_public_key", {}, async () => { const data = { publicKey: publicKey, serverId: 'test-cv-server', purpose: "Use this public key to verify signatures from this MCP server", algorithm: "RSA-SHA256", keyFormat: "PEM", }; const signedResponse = signResponse(data); return { content: [{ type: "text", text: JSON.stringify(signedResponse, null, 2), }], }; }); } } // === TESTS === async function runTests() { console.log('🧪 Iniciando batería de tests para CV MCP Tools...\n'); const agent = new TestMCPAgent(); await agent.init(); let passedTests = 0; let totalTests = 0; // Helper para ejecutar tests async function test(testName, testFn) { totalTests++; try { await testFn(); console.log(`✅ ${testName}`); passedTests++; } catch (error) { console.log(`❌ ${testName}: ${error.message}`); } } // === TESTS CORE TOOLS === console.log('📋 Testing Core Information Tools...'); await test('get_candidate_profile should return candidate profile', async () => { const result = await agent.server.callTool('get_candidate_profile'); const signedData = JSON.parse(result.content[0].text); // Verify it's a signed response if (!signedData.data || !signedData.hash || !signedData.signature) { throw new Error('Response should be digitally signed'); } const data = signedData.data; if (!data.name || data.name !== "Brayan Smith Cordova Tasayco") { throw new Error('Profile name incorrect'); } if (!data.contact || !data.contact.linkedin) { throw new Error('Contact information missing'); } }); await test('get_work_experience should return work history', async () => { const result = await agent.server.callTool('get_work_experience'); const data = JSON.parse(result.content[0].text); if (!Array.isArray(data) || data.length === 0) { throw new Error('Work experience should be non-empty array'); } if (!data[0].company || !data[0].position) { throw new Error('Work experience missing required fields'); } }); await test('get_education_background should return education', async () => { const result = await agent.server.callTool('get_education_background'); const data = JSON.parse(result.content[0].text); if (!Array.isArray(data) || data.length === 0) { throw new Error('Education should be non-empty array'); } if (!data[0].institution || !data[0].degree) { throw new Error('Education missing required fields'); } }); await test('get_certifications should return certifications', async () => { const result = await agent.server.callTool('get_certifications'); const data = JSON.parse(result.content[0].text); if (!Array.isArray(data) || data.length === 0) { throw new Error('Certifications should be non-empty array'); } if (!data[0].name || !data[0].issuer) { throw new Error('Certifications missing required fields'); } }); await test('get_technical_skills should return categorized skills', async () => { const result = await agent.server.callTool('get_technical_skills'); const data = JSON.parse(result.content[0].text); if (!data.technical || !data.technical.programming) { throw new Error('Technical skills missing programming category'); } if (!Array.isArray(data.technical.programming) || data.technical.programming.length === 0) { throw new Error('Programming skills should be non-empty array'); } }); await test('get_soft_skills should return soft skills and languages', async () => { const result = await agent.server.callTool('get_soft_skills'); const data = JSON.parse(result.content[0].text); if (!data.softSkills || !Array.isArray(data.softSkills)) { throw new Error('Soft skills should be array'); } if (!data.languages || !Array.isArray(data.languages)) { throw new Error('Languages should be array'); } }); await test('get_languages_interests should return languages and interests', async () => { const result = await agent.server.callTool('get_languages_interests'); const data = JSON.parse(result.content[0].text); if (!data.languages || !data.interests) { throw new Error('Missing languages or interests'); } if (!Array.isArray(data.interests) || data.interests.length === 0) { throw new Error('Interests should be non-empty array'); } }); await test('search_candidate_info should find React', async () => { const result = await agent.server.callTool('search_candidate_info', { keyword: 'React' }); const text = result.content[0].text; if (text.includes('No matches found')) { throw new Error('Should find React in candidate data'); } const data = JSON.parse(text); if (!data.results || data.results.length === 0) { throw new Error('Search results should not be empty'); } }); // === TESTS ONLINE PRESENCE TOOLS === console.log('\n🌐 Testing Online Presence Tools...'); await test('get_portfolio_website should return portfolio URL and info', async () => { const result = await agent.server.callTool('get_portfolio_website'); const data = JSON.parse(result.content[0].text); if (!data.portfolio || !data.portfolio.includes('https://')) { throw new Error('Portfolio should contain valid URL'); } if (!data.description || !data.status) { throw new Error('Portfolio info missing description or status'); } if (data.status !== 'available') { throw new Error('Portfolio status should be available'); } }); await test('get_github_profile should return GitHub certifications and note', async () => { const result = await agent.server.callTool('get_github_profile'); const data = JSON.parse(result.content[0].text); if (!data.github_certifications || !Array.isArray(data.github_certifications)) { throw new Error('GitHub certifications should be array'); } if (data.profile_url !== 'not_specified_in_cv') { throw new Error('Should indicate URL not specified in CV'); } if (!data.note || !data.note.includes('certificado')) { throw new Error('Should include explanatory note about certification'); } // Should find GitHub Foundation certification const hasGithubCert = data.github_certifications.some(cert => cert.name.toLowerCase().includes('github') ); if (!hasGithubCert) { throw new Error('Should find GitHub certification'); } }); await test('get_social_networks should return available networks and interests', async () => { const result = await agent.server.callTool('get_social_networks'); const data = JSON.parse(result.content[0].text); if (!data.available || !data.available.linkedin) { throw new Error('Should include LinkedIn in available networks'); } if (!data.available.portfolio) { throw new Error('Should include portfolio in available networks'); } if (!data.interests || !Array.isArray(data.interests)) { throw new Error('Interests should be array'); } if (!data.note || !data.contact_preferences) { throw new Error('Should include explanatory note and contact preferences'); } // Should find social-related interests const hasSocialInterest = data.interests.some(interest => interest.toLowerCase().includes('social') || interest.toLowerCase().includes('red') || interest.toLowerCase().includes('conexión') ); if (!hasSocialInterest) { throw new Error('Should find social-related interests'); } }); // === TESTS ADVANCED ANALYSIS TOOLS === console.log('\n🔍 Testing Advanced Analysis Tools...'); await test('evaluate_tech_stack should calculate compatibility score', async () => { const result = await agent.server.callTool('evaluate_tech_stack', { requiredTechnologies: ['React', 'JavaScript', 'TypeScript', 'Python'] }); const data = JSON.parse(result.content[0].text); if (!data.compatibilityScore) { throw new Error('Missing compatibility score'); } if (!data.matchingTechnologies || !data.missingTechnologies) { throw new Error('Missing matching/missing technologies'); } // Should match React, JavaScript, TypeScript but not Python if (data.matchingTechnologies.length < 2) { throw new Error('Should find at least 2 matching technologies'); } }); await test('assess_leadership_experience should identify leadership', async () => { const result = await agent.server.callTool('assess_leadership_experience'); const data = JSON.parse(result.content[0].text); if (typeof data.hasLeadershipExperience !== 'boolean') { throw new Error('Missing hasLeadershipExperience boolean'); } if (!data.leadershipDuration) { throw new Error('Missing leadership duration'); } // Should find leadership experience (Lead Frontend developer position) if (!data.hasLeadershipExperience) { throw new Error('Should identify leadership experience'); } }); await test('calculate_experience_years should return React experience', async () => { const result = await agent.server.callTool('calculate_experience_years', { technology: 'React' }); const data = JSON.parse(result.content[0].text); if (data.technology !== 'React') { throw new Error('Technology name should match input'); } if (!data.experience || data.experience === 'No specific experience found') { throw new Error('Should find React experience'); } }); await test('calculate_experience_years should handle unknown technology', async () => { const result = await agent.server.callTool('calculate_experience_years', { technology: 'Rust' }); const data = JSON.parse(result.content[0].text); if (data.experience !== 'No specific experience found') { throw new Error('Should return no experience for unknown tech'); } }); await test('match_job_requirements should calculate job fit', async () => { const result = await agent.server.callTool('match_job_requirements', { jobTitle: 'Frontend Developer', requiredSkills: ['React', 'JavaScript', 'CSS'], preferredSkills: ['TypeScript', 'Next.js'] }); const data = JSON.parse(result.content[0].text); if (!data.matchAnalysis || !data.matchAnalysis.overallFit) { throw new Error('Missing match analysis'); } if (!data.skillsMatched || !data.skillsGap) { throw new Error('Missing skills matched/gap analysis'); } // Should have high compatibility for frontend role if (data.matchAnalysis.overallFit === 'Low') { throw new Error('Should have higher fit for frontend developer role'); } }); // === TESTS EDGE CASES === console.log('\n🚨 Testing Edge Cases...'); await test('search_candidate_info should handle non-existent keyword', async () => { const result = await agent.server.callTool('search_candidate_info', { keyword: 'blockchain' }); const text = result.content[0].text; if (!text.includes('No matches found')) { throw new Error('Should return no matches for non-existent keyword'); } }); await test('evaluate_tech_stack should handle empty requirements', async () => { const result = await agent.server.callTool('evaluate_tech_stack', { requiredTechnologies: [] }); const data = JSON.parse(result.content[0].text); // Should handle edge case gracefully (might cause division by zero) if (!data.compatibilityScore) { throw new Error('Should handle empty requirements gracefully'); } }); // === RESULTS === console.log(`\n📊 Resultados del Testing:`); console.log(`✅ Tests pasados: ${passedTests}/${totalTests}`); console.log(`📈 Porcentaje de éxito: ${((passedTests/totalTests) * 100).toFixed(1)}%`); if (passedTests === totalTests) { console.log('\n🎉 ¡Todos los tests pasaron exitosamente!'); console.log('✨ El sistema MCP está funcionando correctamente.'); } else { console.log(`\n⚠️ ${totalTests - passedTests} tests fallaron.`); console.log('🔧 Revisa los errores anteriores para corregir los problemas.'); } console.log('\n🏁 Testing completo.'); } // Ejecutar tests si se ejecuta directamente if (require.main === module) { runTests().catch(console.error); } module.exports = { TestMCPAgent, runTests };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/noelserdna/cv-dinamic-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server