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 });
},
};