Skip to main content
Glama
noelserdna

CV Recruitment Assistant

by noelserdna
index.ts17 kB
import { McpAgent } from "agents/mcp"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { z } from "zod"; import candidateData from "./candidate-data.json"; import { MCPCrypto, SignedMCPResponse } from "./crypto.js"; // Define our MCP agent with tools export class MyMCP extends McpAgent { server = new McpServer({ name: "CV Recruitment Assistant", version: "1.0.0", }); private crypto: MCPCrypto = new MCPCrypto(); async init(env?: Env) { this.crypto = new MCPCrypto(env); // === 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 = this.crypto.signResponse(data); return { content: [ { type: "text", text: JSON.stringify(signedResponse, null, 2), }, ], }; }); // Get detailed work experience this.server.tool("get_work_experience", {}, async () => { const signedResponse = this.crypto.signResponse(candidateData.workExperience); return { content: [ { type: "text", text: JSON.stringify(signedResponse, null, 2), }, ], }; }); // Get education background this.server.tool("get_education_background", {}, async () => { const signedResponse = this.crypto.signResponse(candidateData.education); return { content: [ { type: "text", text: JSON.stringify(signedResponse, null, 2), }, ], }; }); // Get certifications this.server.tool("get_certifications", {}, async () => { const signedResponse = this.crypto.signResponse(candidateData.certifications); return { content: [ { type: "text", text: JSON.stringify(signedResponse, null, 2), }, ], }; }); // Get technical skills categorized this.server.tool("get_technical_skills", {}, async () => { const data = { technical: candidateData.skills.technical, design: candidateData.skills.design, productivity: candidateData.skills.productivity, }; const signedResponse = this.crypto.signResponse(data); return { content: [ { type: "text", text: JSON.stringify(signedResponse, null, 2), }, ], }; }); // Get soft skills and competencies this.server.tool("get_soft_skills", {}, async () => { const data = { softSkills: candidateData.skills.soft, languages: candidateData.languages, }; const signedResponse = this.crypto.signResponse(data); return { content: [ { type: "text", text: JSON.stringify(signedResponse, null, 2), }, ], }; }); // Get languages and interests this.server.tool("get_languages_interests", {}, async () => { const data = { languages: candidateData.languages, interests: candidateData.interests, }; const signedResponse = this.crypto.signResponse(data); return { content: [ { type: "text", text: JSON.stringify(signedResponse, null, 2), }, ], }; }); // Search candidate information by keywords this.server.tool( "search_candidate_info", { keyword: z .string() .describe("Keyword to search across all candidate information"), }, 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" }); } } const data = results.length > 0 ? { keyword: keyword, results: results } : { keyword: keyword, message: `No matches found for "${keyword}"` }; const signedResponse = this.crypto.signResponse(data); return { content: [ { type: "text", text: JSON.stringify(signedResponse, null, 2), }, ], }; }, ); // === ONLINE PRESENCE TOOLS === // Get candidate's portfolio/personal website this.server.tool("get_portfolio_website", {}, async () => { const data = { portfolio: candidateData.personal.portfolio, description: "Portfolio profesional con proyectos y experiencia", status: "available", note: "Sitio web personal donde se muestran proyectos y experiencia profesional" }; const signedResponse = this.crypto.signResponse(data); return { content: [ { type: "text", text: JSON.stringify(signedResponse, 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') ); const data = { 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') ) }; const signedResponse = this.crypto.signResponse(data); return { content: [ { type: "text", text: JSON.stringify(signedResponse, null, 2), }, ], }; }); // Get candidate's social networks and online presence this.server.tool("get_social_networks", {}, async () => { const data = { 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" }; const signedResponse = this.crypto.signResponse(data); return { content: [ { type: "text", text: JSON.stringify(signedResponse, null, 2), }, ], }; }); // === ADVANCED ANALYSIS TOOLS === // Evaluate tech stack compatibility this.server.tool( "evaluate_tech_stack", { requiredTechnologies: z .array(z.string()) .describe("Array of required technologies for the position"), }, 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; const data = { compatibilityScore: `${compatibilityScore.toFixed(1)}%`, matchingTechnologies: matches, missingTechnologies: missing, candidateStrengths: candidateTechStack, }; const signedResponse = this.crypto.signResponse(data); return { content: [ { type: "text", text: JSON.stringify(signedResponse, 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"), ); const data = { hasLeadershipExperience: leadershipExperience.length > 0, leadershipRoles: leadershipExperience, leadershipDuration: candidateData.experienceYears.leadership, relevantSoftSkills: leadershipSkills, leadershipAchievements: leadershipExperience.flatMap( (exp) => exp.achievements, ), }; const signedResponse = this.crypto.signResponse(data); return { content: [ { type: "text", text: JSON.stringify(signedResponse, null, 2), }, ], }; }); // Calculate experience years by technology this.server.tool( "calculate_experience_years", { technology: z .string() .describe("Technology name to calculate experience for"), }, async ({ technology }) => { const techLower = technology.toLowerCase(); let experienceInfo = "No specific experience found"; // Check predefined experience years const experienceYearsMap = candidateData.experienceYears as Record< string, string >; 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"; } } const data = { 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), ), }; const signedResponse = this.crypto.signResponse(data); return { content: [ { type: "text", text: JSON.stringify(signedResponse, null, 2), }, ], }; }, ); // Match job requirements this.server.tool( "match_job_requirements", { jobTitle: z.string().describe("Job title"), requiredSkills: z .array(z.string()) .describe("Required skills for the position"), preferredSkills: z .array(z.string()) .optional() .describe("Preferred skills for the position"), minimumExperience: z .string() .optional() .describe("Minimum experience required"), }, 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; const data = { 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, }, }; const signedResponse = this.crypto.signResponse(data); return { content: [ { type: "text", text: JSON.stringify(signedResponse, null, 2), }, ], }; }, ); // === DEBUGGING AND VERIFICATION TOOL === // Verify signed response integrity and authenticity this.server.tool( "verify_response_signature", { signedResponse: z .string() .describe("JSON string of a signed MCP response to verify"), }, async ({ signedResponse }) => { try { const parsedResponse = JSON.parse(signedResponse); const verificationResult = this.crypto.verifySignedResponse(parsedResponse); const data = { verificationStatus: verificationResult.isValid ? "VALID" : "INVALID", result: verificationResult, verifiedBy: this.crypto.getServerId(), verificationTimestamp: new Date().toISOString(), }; // Esta respuesta también debe ser firmada para garantizar su autenticidad const signedVerificationResult = this.crypto.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: this.crypto.getServerId(), verificationTimestamp: new Date().toISOString(), }; const signedErrorResult = this.crypto.signResponse(errorData); return { content: [ { type: "text", text: JSON.stringify(signedErrorResult, null, 2), }, ], }; } }, ); // Get server public key for external verification this.server.tool("get_server_public_key", {}, async () => { const data = { publicKey: this.crypto.getPublicKey(), serverId: this.crypto.getServerId(), purpose: "Use this public key to verify signatures from this MCP server", algorithm: "RSA-SHA256", keyFormat: "PEM", }; const signedResponse = this.crypto.signResponse(data); return { content: [ { type: "text", text: JSON.stringify(signedResponse, null, 2), }, ], }; }); } } export default { async fetch(request: Request, env: Env, ctx: ExecutionContext) { const url = new URL(request.url); if (url.pathname === "/sse" || url.pathname === "/sse/message") { return MyMCP.serveSSE("/sse").fetch(request, env, ctx); } if (url.pathname === "/mcp") { return MyMCP.serve("/mcp").fetch(request, env, ctx); } return new Response("Not found", { status: 404 }); }, };

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