Skip to main content

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

npm install -g @kekwanulabs/syncline-mcp-server
2

Install MCP SDK

npm install @modelcontextprotocol/sdk
3

Get Platform API Key

Quick Start

Create a simple agent that finds meeting availability:
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);
Run the script:
node agent.js
Output:
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:
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:
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);
Example conversation:
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:
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:
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:
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:
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:
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:
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");
  });
});
Run tests:
npm test