Overview
Use the Node.js MCP SDK to build custom AI agents that can schedule meetings, find availability, and manage calendars through the Syncline MCP server.Prerequisites
1
Install Syncline MCP Server
Copy
npm install -g @kekwanulabs/syncline-mcp-server
2
Install MCP SDK
Copy
npm install @modelcontextprotocol/sdk
3
Get Platform API Key
Sign up at syncline.run/developer/login
Quick Start
Create a simple agent that finds meeting availability:Copy
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
async function main() {
// Configure transport to Syncline MCP server
const transport = new StdioClientTransport({
command: "npx",
args: ["-y", "@kekwanulabs/syncline-mcp-server"],
env: {
...process.env,
SYNCLINE_API_KEY: "sk_live_your_api_key_here"
}
});
// Create client
const client = new Client({
name: "my-scheduling-agent",
version: "1.0.0"
}, {
capabilities: {}
});
// Connect to server
await client.connect(transport);
// List available tools
const { tools } = await client.listTools();
console.log("Available tools:", tools.map(t => t.name));
// Find mutual availability
const result = await client.callTool({
name: "find_mutual_availability",
arguments: {
attendees: ["alice@example.com", "bob@example.com"],
duration_minutes: 30
}
});
console.log("Available time slots:");
console.log(result.content[0].text);
// Close connection
await client.close();
}
main().catch(console.error);
Copy
node agent.js
Copy
Available tools: [ 'find_mutual_availability', 'schedule_meeting', 'check_availability', 'update_preferences' ]
Available time slots:
{
"slots": [
{
"start_time": "2025-01-22T14:00:00-08:00",
"end_time": "2025-01-22T14:30:00-08:00",
"score": 0.95,
"quality": "excellent"
},
...
]
}
Building a Meeting Scheduler Agent
Create an AI agent that can schedule meetings via natural language:Copy
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
class MeetingSchedulerAgent {
constructor(apiKey) {
this.apiKey = apiKey;
this.client = null;
}
async connect() {
const transport = new StdioClientTransport({
command: "npx",
args: ["-y", "@kekwanulabs/syncline-mcp-server"],
env: {
...process.env,
SYNCLINE_API_KEY: this.apiKey
}
});
this.client = new Client({
name: "meeting-scheduler",
version: "1.0.0"
}, {
capabilities: {}
});
await this.client.connect(transport);
}
async disconnect() {
if (this.client) {
await this.client.close();
}
}
async findAvailability(attendees, duration = 30) {
const result = await this.client.callTool({
name: "find_mutual_availability",
arguments: {
attendees,
duration_minutes: duration
}
});
return JSON.parse(result.content[0].text);
}
async scheduleMeeting(attendees, startTime, title, duration = 30) {
const result = await this.client.callTool({
name: "schedule_meeting",
arguments: {
attendees,
start_time: startTime,
title,
duration_minutes: duration
}
});
return JSON.parse(result.content[0].text);
}
async checkCalendar(email, daysAhead = 7) {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(endDate.getDate() + daysAhead);
const result = await this.client.callTool({
name: "check_availability",
arguments: {
email,
start_date: startDate.toISOString(),
end_date: endDate.toISOString()
}
});
return JSON.parse(result.content[0].text);
}
async updatePreferences(email, preferences) {
const result = await this.client.callTool({
name: "update_preferences",
arguments: {
email,
preferences
}
});
return JSON.parse(result.content[0].text);
}
}
async function main() {
const agent = new MeetingSchedulerAgent("sk_live_your_api_key_here");
try {
await agent.connect();
// Find when Alice and Bob are both free
console.log("Finding availability...");
const availability = await agent.findAvailability(
["alice@example.com", "bob@example.com"],
30
);
// Get the best time slot
const bestSlot = availability.slots[0];
console.log(`\nBest time slot: ${bestSlot.start_time}`);
console.log(`Quality score: ${bestSlot.score}`);
// Schedule the meeting
console.log("\nScheduling meeting...");
const meeting = await agent.scheduleMeeting(
["alice@example.com", "bob@example.com"],
bestSlot.start_time,
"Product Demo",
30
);
console.log("\n✓ Meeting scheduled!");
console.log(` Meeting ID: ${meeting.meeting_id}`);
console.log(` Google Meet: ${meeting.google_meet_link}`);
console.log(` Calendar: ${meeting.calendar_event_link}`);
} finally {
await agent.disconnect();
}
}
main().catch(console.error);
Advanced Examples
Conversational Scheduling Bot
Build a bot that schedules meetings through natural language:Copy
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
import readline from "readline";
class SchedulingBot {
constructor(apiKey) {
this.apiKey = apiKey;
this.client = null;
}
async connect() {
const transport = new StdioClientTransport({
command: "npx",
args: ["-y", "@kekwanulabs/syncline-mcp-server"],
env: {
...process.env,
SYNCLINE_API_KEY: this.apiKey
}
});
this.client = new Client({
name: "scheduling-bot",
version: "1.0.0"
}, {
capabilities: {}
});
await this.client.connect(transport);
}
async disconnect() {
if (this.client) {
await this.client.close();
}
}
parseIntent(message) {
const lowerMessage = message.toLowerCase();
// Extract emails
const emailRegex = /[\w\.-]+@[\w\.-]+\.\w+/g;
const emails = message.match(emailRegex) || [];
// Detect intent
if (lowerMessage.includes("schedule") || lowerMessage.includes("book")) {
return { intent: "schedule", attendees: emails };
} else if (lowerMessage.includes("available") || lowerMessage.includes("free")) {
return { intent: "find_availability", attendees: emails };
} else if (lowerMessage.includes("check") && lowerMessage.includes("calendar")) {
return { intent: "check_calendar", email: emails[0] };
} else {
return { intent: "unknown" };
}
}
async handleMessage(message) {
const intentData = this.parseIntent(message);
try {
if (intentData.intent === "find_availability") {
const result = await this.client.callTool({
name: "find_mutual_availability",
arguments: {
attendees: intentData.attendees,
duration_minutes: 30
}
});
return this.formatAvailabilityResponse(JSON.parse(result.content[0].text));
} else if (intentData.intent === "schedule") {
// First find availability
const availResult = await this.client.callTool({
name: "find_mutual_availability",
arguments: {
attendees: intentData.attendees,
duration_minutes: 30
}
});
const availability = JSON.parse(availResult.content[0].text);
const bestSlot = availability.slots[0];
// Schedule the meeting
const meetingResult = await this.client.callTool({
name: "schedule_meeting",
arguments: {
attendees: intentData.attendees,
start_time: bestSlot.start_time,
title: "Meeting",
duration_minutes: 30
}
});
return this.formatMeetingResponse(JSON.parse(meetingResult.content[0].text));
} else if (intentData.intent === "check_calendar") {
const startDate = new Date();
const endDate = new Date();
endDate.setDate(endDate.getDate() + 7);
const result = await this.client.callTool({
name: "check_availability",
arguments: {
email: intentData.email,
start_date: startDate.toISOString(),
end_date: endDate.toISOString()
}
});
return this.formatCalendarResponse(JSON.parse(result.content[0].text));
} else {
return "I can help you schedule meetings, find availability, or check calendars. Try asking: 'When are alice@example.com and bob@example.com free?'";
}
} catch (error) {
return `Error: ${error.message}`;
}
}
formatAvailabilityResponse(availability) {
let response = "Here are the available time slots:\n\n";
availability.slots.slice(0, 5).forEach((slot, i) => {
response += `${i + 1}. ${slot.start_time} (quality: ${slot.quality})\n`;
});
return response;
}
formatMeetingResponse(meeting) {
return `✓ Meeting scheduled!\nGoogle Meet: ${meeting.google_meet_link}\nCalendar: ${meeting.calendar_event_link}`;
}
formatCalendarResponse(calendar) {
const freeSlots = calendar.free_slots?.length || 0;
const busySlots = calendar.busy_slots?.length || 0;
return `Calendar checked. Free slots: ${freeSlots}, Busy slots: ${busySlots}`;
}
}
async function chatLoop() {
const bot = new SchedulingBot("sk_live_your_api_key_here");
await bot.connect();
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
console.log("Scheduling Bot Ready! Ask me to schedule meetings or find availability.");
console.log("Type 'quit' to exit.\n");
const askQuestion = () => {
rl.question("You: ", async (input) => {
if (input.toLowerCase() === "quit" || input.toLowerCase() === "exit") {
console.log("Goodbye!");
await bot.disconnect();
rl.close();
return;
}
const response = await bot.handleMessage(input);
console.log(`Bot: ${response}\n`);
askQuestion();
});
};
askQuestion();
}
chatLoop().catch(console.error);
Copy
Scheduling Bot Ready! Ask me to schedule meetings or find availability.
Type 'quit' to exit.
You: When are alice@example.com and bob@example.com free?
Bot: Here are the available time slots:
1. 2025-01-22T14:00:00-08:00 (quality: excellent)
2. 2025-01-22T15:00:00-08:00 (quality: good)
3. 2025-01-23T10:00:00-08:00 (quality: good)
4. 2025-01-23T14:00:00-08:00 (quality: excellent)
5. 2025-01-24T09:00:00-08:00 (quality: fair)
You: Schedule a meeting with alice@example.com and bob@example.com
Bot: ✓ Meeting scheduled!
Google Meet: https://meet.google.com/abc-defg-hij
Calendar: https://calendar.google.com/calendar/event?eid=...
Error Handling
Handle errors gracefully:Copy
async function safeScheduleMeeting(agent, attendees, startTime, title) {
try {
const meeting = await agent.scheduleMeeting(attendees, startTime, title);
return { success: true, meeting };
} catch (error) {
console.error(`Error scheduling meeting: ${error.message}`);
return { success: false, error: error.message };
}
}
// Usage
const agent = new MeetingSchedulerAgent("sk_live_your_key");
await agent.connect();
const result = await safeScheduleMeeting(
agent,
["alice@example.com", "bob@example.com"],
"2025-01-22T14:00:00-08:00",
"Product Demo"
);
if (result.success) {
console.log(`✓ Meeting scheduled: ${result.meeting.google_meet_link}`);
} else {
console.log(`✗ Failed to schedule: ${result.error}`);
}
await agent.disconnect();
Best Practices
Use Environment Variables
Store API keys securely:Copy
import dotenv from "dotenv";
dotenv.config();
const apiKey = process.env.SYNCLINE_API_KEY;
if (!apiKey) {
throw new Error("SYNCLINE_API_KEY environment variable not set");
}
const agent = new MeetingSchedulerAgent(apiKey);
Implement Retry Logic
Handle transient failures:Copy
async function scheduleWithRetry(agent, attendees, startTime, title, maxRetries = 3) {
let lastError;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await agent.scheduleMeeting(attendees, startTime, title);
} catch (error) {
lastError = error;
console.log(`Attempt ${attempt} failed: ${error.message}`);
if (attempt < maxRetries) {
// Exponential backoff
const delay = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
throw new Error(`Failed after ${maxRetries} attempts: ${lastError.message}`);
}
// Usage
try {
const meeting = await scheduleWithRetry(
agent,
["alice@example.com", "bob@example.com"],
"2025-01-22T14:00:00-08:00",
"Product Demo"
);
console.log(`✓ Scheduled: ${meeting.meeting_id}`);
} catch (error) {
console.error(`✗ Failed: ${error.message}`);
}
Validate Input
Validate user input before calling tools:Copy
function validateEmail(email) {
const regex = /^[\w\.-]+@[\w\.-]+\.\w+$/;
return regex.test(email);
}
function validateAttendees(emails) {
const validated = emails.filter(email => {
if (!validateEmail(email)) {
console.warn(`Invalid email: ${email}`);
return false;
}
return true;
});
if (validated.length === 0) {
throw new Error("No valid email addresses provided");
}
return validated;
}
// Usage
const userEmails = ["alice@example.com", "not-an-email", "bob@example.com"];
const validEmails = validateAttendees(userEmails);
// Returns: ["alice@example.com", "bob@example.com"]
Graceful Shutdown
Ensure proper cleanup:Copy
class MeetingSchedulerAgent {
// ... existing code ...
async scheduleWithCleanup(attendees, startTime, title) {
try {
await this.connect();
const meeting = await this.scheduleMeeting(attendees, startTime, title);
return meeting;
} finally {
await this.disconnect();
}
}
}
// Usage with proper error handling
async function main() {
const agent = new MeetingSchedulerAgent(process.env.SYNCLINE_API_KEY);
try {
const meeting = await agent.scheduleWithCleanup(
["alice@example.com", "bob@example.com"],
"2025-01-22T14:00:00-08:00",
"Product Demo"
);
console.log(`✓ Meeting scheduled: ${meeting.meeting_id}`);
} catch (error) {
console.error(`✗ Error: ${error.message}`);
process.exit(1);
}
}
main();
Testing
Write tests for your agent:Copy
import { jest } from "@jest/globals";
describe("MeetingSchedulerAgent", () => {
let agent;
let mockClient;
beforeEach(() => {
// Mock the MCP client
mockClient = {
connect: jest.fn(),
close: jest.fn(),
callTool: jest.fn()
};
agent = new MeetingSchedulerAgent("test_key");
agent.client = mockClient;
});
test("findAvailability returns time slots", async () => {
// Mock response
mockClient.callTool.mockResolvedValue({
content: [{
text: JSON.stringify({
slots: [
{ start_time: "2025-01-22T14:00:00Z", score: 0.95 }
]
})
}]
});
// Test
const result = await agent.findAvailability(
["alice@example.com", "bob@example.com"],
30
);
// Assert
expect(result.slots).toHaveLength(1);
expect(result.slots[0].score).toBe(0.95);
});
test("scheduleMeeting creates meeting", async () => {
// Mock response
mockClient.callTool.mockResolvedValue({
content: [{
text: JSON.stringify({
meeting_id: "mtg_123",
google_meet_link: "https://meet.google.com/abc-defg-hij"
})
}]
});
// Test
const meeting = await agent.scheduleMeeting(
["alice@example.com", "bob@example.com"],
"2025-01-22T14:00:00Z",
"Product Demo",
30
);
// Assert
expect(meeting.meeting_id).toBe("mtg_123");
expect(meeting.google_meet_link).toContain("meet.google.com");
});
});
Copy
npm test