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
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