willow-mcp-server
Version:
MCP server for Willow API integration - list interviews, participants, and get detailed participant information
367 lines • 17.2 kB
JavaScript
#!/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