UNPKG

willow-mcp-server

Version:

MCP server for Willow API integration - list interviews, participants, and get detailed participant information

367 lines 17.2 kB
#!/usr/bin/env node import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; class WillowMCPServer { server; apiKey; baseUrl; constructor() { this.server = new Server({ name: "willow-mcp-server", version: "1.0.0", }, { capabilities: { tools: {}, }, }); // Get API key from environment variable this.apiKey = process.env.WILLOW_API_KEY || ""; this.baseUrl = process.env.WILLOW_BASE_URL || "https://api.willotalent.com"; this.setupToolHandlers(); } setupToolHandlers() { this.server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools: [ { name: "list_interviews", description: "List interviews from Willow API with optional filters", inputSchema: { type: "object", properties: { department: { type: "string", description: "Filter by department", }, owner: { type: "string", description: "Filter by owner", }, search: { type: "string", description: "Search term", }, ordering: { type: "string", description: "Sort order (e.g., 'scheduled_at', '-scheduled_at')", }, page_size: { type: "number", description: "Number of results per page (1-30)", minimum: 1, maximum: 30, }, api_key: { type: "string", description: "Willow API key (if not set in environment)", }, }, }, }, { name: "list_participants", description: "List participants with optional filters", inputSchema: { type: "object", properties: { interview: { type: "string", description: "Key of the interview to filter by", }, department: { type: "string", description: "Key of the department to filter by", }, owner: { type: "string", description: "Key of the owner to filter by", }, search: { type: "string", description: "Search by name, email, interview title", }, page_size: { type: "number", description: "Number of results per page (1-30)", minimum: 1, maximum: 30, }, api_key: { type: "string", description: "Willow API key (if not set in environment)", }, }, }, }, { name: "get_participant", description: "Get details for a specific participant", inputSchema: { type: "object", properties: { participant_id: { type: "string", description: "Participant ID to get details for", }, api_key: { type: "string", description: "Willow API key (if not set in environment)", }, }, required: ["participant_id"], }, }, ], }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { if (request.params.name === "list_interviews") { return await this.listInterviews(request.params.arguments); } else if (request.params.name === "list_participants") { return await this.listParticipants(request.params.arguments); } else if (request.params.name === "get_participant") { return await this.getParticipant(request.params.arguments); } throw new Error(`Unknown tool: ${request.params.name}`); }); } async listInterviews(args = {}) { const apiKey = args.api_key || this.apiKey; if (!apiKey) { throw new Error("API key is required. Set WILLOW_API_KEY environment variable or provide api_key parameter."); } // Build query parameters const params = new URLSearchParams(); if (args.department) params.append("department", args.department); if (args.owner) params.append("owner", args.owner); if (args.search) params.append("search", args.search); if (args.ordering) params.append("ordering", args.ordering); if (args.page_size) { const pageSize = Math.max(1, Math.min(30, args.page_size)); params.append("page_size", pageSize.toString()); } const url = `${this.baseUrl}/api/integrations/v2/interviews/?${params.toString()}`; try { const response = await fetch(url, { method: "GET", headers: { "Authorization": apiKey, "Content-Type": "application/json", }, }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Willow API error (${response.status}): ${errorText}`); } const data = await response.json(); return { content: [ { type: "text", text: JSON.stringify({ count: data.count, next: data.next, previous: data.previous, results: data.results.map(interview => ({ key: interview.key, title: interview.title, owner: { key: interview.owner.key, email: interview.owner.email, full_name: interview.owner.full_name, }, organisation: { key: interview.organisation.key, name: interview.organisation.name, }, department: { key: interview.department.key, name: interview.department.name, }, invite_link: interview.invite_link, })), }, null, 2), }, ], }; } catch (error) { throw new Error(`Failed to fetch interviews: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async listParticipants(args = {}) { const apiKey = args.api_key || this.apiKey; if (!apiKey) { throw new Error("API key is required. Set WILLOW_API_KEY environment variable or provide api_key parameter."); } // Build query parameters const params = new URLSearchParams(); if (args.interview) params.append("interview", args.interview); if (args.department) params.append("department", args.department); if (args.owner) params.append("owner", args.owner); if (args.search) params.append("search", args.search); if (args.page_size) { const pageSize = Math.max(1, Math.min(30, args.page_size)); params.append("page_size", pageSize.toString()); } const url = `${this.baseUrl}/api/integrations/v2/participants/?${params.toString()}`; try { const response = await fetch(url, { method: "GET", headers: { "Authorization": apiKey, "Content-Type": "application/json", }, }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Willow API error (${response.status}): ${errorText}`); } const data = await response.json(); return { content: [ { type: "text", text: JSON.stringify({ count: data.count, next: data.next, previous: data.previous, results: data.results.map(participant => ({ key: participant.key, name: participant.name, email: participant.email, phone: participant.phone, interview: { key: participant.interview.key, title: participant.interview.title, }, department: { key: participant.department.key, name: participant.department.name, }, created_at: participant.created_at, location: participant.location, showcase_link: participant.showcase_link, overview_link: participant.overview_link, avatar_remote_link: participant.avatar_remote_link, custom_id: participant.custom_id, availability: participant.availability, })), }, null, 2), }, ], }; } catch (error) { throw new Error(`Failed to fetch participants: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async getParticipant(args = {}) { const apiKey = args.api_key || this.apiKey; if (!apiKey) { throw new Error("API key is required. Set WILLOW_API_KEY environment variable or provide api_key parameter."); } if (!args.participant_id) { throw new Error("participant_id parameter is required"); } const url = `${this.baseUrl}/api/integrations/v2/participants/${args.participant_id}/`; try { const response = await fetch(url, { method: "GET", headers: { "Authorization": apiKey, "Content-Type": "application/json", }, }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Willow API error (${response.status}): ${errorText}`); } const participant = await response.json(); return { content: [ { type: "text", text: JSON.stringify({ key: participant.key, email: participant.email, name: participant.name, phone: participant.phone, status: participant.status, status_changed_by: participant.status_changed_by, status_changed_at: participant.status_changed_at, send_notifications: participant.send_notifications, custom_id: participant.custom_id, summary: participant.summary, interview: { key: participant.interview.key, title: participant.interview.title, }, department: { key: participant.department.key, name: participant.department.name, }, created_at: participant.created_at, updated_at: participant.updated_at, location: participant.location, showcase_link: participant.showcase_link, candidate_overview_link: participant.candidate_overview_link, avatar_remote_link: participant.avatar_remote_link, availability: participant.availability, answers: participant.answers?.map(answer => ({ key: answer.key, created_at: answer.created_at, updated_at: answer.updated_at, is_finished: answer.is_finished, media_extension: answer.media_extension, remote_link: answer.remote_link, is_media_deleted: answer.is_media_deleted, text: answer.text, spent_time: answer.spent_time, additional_info: answer.additional_info, type_specific_data: answer.type_specific_data, transcript: answer.transcript, status: answer.status, reason: answer.reason, question: { key: answer.question.key, text: answer.question.text, order: answer.question.order, max_duration: answer.question.max_duration, max_retakes: answer.question.max_retakes, answer_type: answer.question.answer_type, thinking_time: answer.question.thinking_time, max_characters: answer.question.max_characters, max_words: answer.question.max_words, type_specific_data: answer.question.type_specific_data, }, })), }, null, 2), }, ], }; } catch (error) { throw new Error(`Failed to fetch participant: ${error instanceof Error ? error.message : 'Unknown error'}`); } } async run() { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error("Willow MCP server running on stdio"); } } const server = new WillowMCPServer(); server.run().catch(console.error); //# sourceMappingURL=index.js.map