Skip to main content
Glama
chat.ts16.6 kB
#!/usr/bin/env node import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import { Command } from "commander"; import "dotenv/config"; import path from "node:path"; import { fileURLToPath } from "node:url"; import OpenAI from "openai"; import * as readline from "readline"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const GOOGLE_SERVER_PATH = path.join(__dirname, "google-mcp-server.js"); const ORDERING_SERVER_PATH = path.join(__dirname, "ordering-mcp-server.js"); // ---- OpenAI tools spec (unchanged) ----------------------------------------- const tools = [ { type: "function", function: { name: "list_calendars", description: "List calendars for the authorized user", parameters: { type: "object", properties: {}, additionalProperties: false }, }, }, { type: "function", function: { name: "list_events", description: "List events from a calendar with optional filtering", parameters: { type: "object", properties: { calendarId: { type: "string" }, maxResults: { type: "number", description: "Maximum number of events to return (default: 10)" }, timeMin: { type: "string", description: "Lower bound for event start times (ISO 8601 datetime)" }, timeMax: { type: "string", description: "Upper bound for event start times (ISO 8601 datetime)" }, singleEvents: { type: "boolean", description: "Whether to expand recurring events into instances (default: true)" }, orderBy: { type: "string", enum: ["startTime", "updated"], description: "Order of the events returned (default: startTime)" }, }, required: ["calendarId"], additionalProperties: false, }, }, }, { type: "function", function: { name: "create_event", description: "Create a Google Calendar event with ISO datetimes and automatic color coding", parameters: { type: "object", properties: { calendarId: { type: "string" }, summary: { type: "string" }, description: { type: "string" }, location: { type: "string" }, attendees: { type: "array", items: { type: "string" } }, start: { type: "string", description: "ISO 8601 datetime" }, end: { type: "string", description: "ISO 8601 datetime" }, timeZone: { type: "string", default: "America/Los_Angeles" }, colorId: { type: "string", description: "Optional color ID (1-11). If not provided, will auto-suggest based on event content" }, }, required: ["calendarId", "summary", "start", "end", "timeZone"], additionalProperties: false, }, }, }, { type: "function", function: { name: "create_recurring_event", description: "Create a recurring Google Calendar event with RRULE pattern and automatic color coding", parameters: { type: "object", properties: { calendarId: { type: "string" }, summary: { type: "string" }, description: { type: "string" }, location: { type: "string" }, attendees: { type: "array", items: { type: "string" } }, start: { type: "string", description: "ISO 8601 datetime" }, end: { type: "string", description: "ISO 8601 datetime" }, timeZone: { type: "string", default: "America/Los_Angeles" }, recurrence: { type: "string", description: "RRULE string without 'RRULE:' prefix (e.g., 'FREQ=WEEKLY;COUNT=10;BYDAY=MO,TU,WE,TH,FR' for weekdays)" }, colorId: { type: "string", description: "Optional color ID (1-11). If not provided, will auto-suggest based on event content" }, }, required: ["calendarId", "summary", "start", "end", "timeZone", "recurrence"], additionalProperties: false, }, }, }, { type: "function", function: { name: "list_emails", description: "List emails from Gmail with optional filtering", parameters: { type: "object", properties: { userId: { type: "string", default: "me", description: "User's email address or 'me' for authenticated user" }, maxResults: { type: "number", description: "Maximum number of emails to return (default: 10)" }, q: { type: "string", description: "Gmail search query (e.g., 'from:example@gmail.com', 'subject:meeting', 'is:unread')" }, labelIds: { type: "array", items: { type: "string" }, description: "Only return emails with these label IDs" }, includeSpamTrash: { type: "boolean", description: "Include emails from spam and trash (default: false)" }, }, required: [], additionalProperties: false, }, }, }, { type: "function", function: { name: "get_email", description: "Retrieve a specific email by ID with full content", parameters: { type: "object", properties: { userId: { type: "string", default: "me", description: "User's email address or 'me' for authenticated user" }, id: { type: "string", description: "Email message ID" }, format: { type: "string", enum: ["full", "metadata", "minimal", "raw"], description: "Format of the email content (default: full)" }, }, required: ["id"], additionalProperties: false, }, }, }, { type: "function", function: { name: "send_email", description: "Compose and send an email", parameters: { type: "object", properties: { userId: { type: "string", default: "me", description: "User's email address or 'me' for authenticated user" }, to: { type: "array", items: { type: "string" }, description: "Recipient email addresses" }, cc: { type: "array", items: { type: "string" }, description: "CC email addresses" }, bcc: { type: "array", items: { type: "string" }, description: "BCC email addresses" }, subject: { type: "string", description: "Email subject" }, body: { type: "string", description: "Email body content (plain text)" }, isHtml: { type: "boolean", description: "Whether the body content is HTML (default: false)" }, }, required: ["to", "subject", "body"], additionalProperties: false, }, }, }, { type: "function", function: { name: "search_emails", description: "Search emails using Gmail search syntax", parameters: { type: "object", properties: { userId: { type: "string", default: "me", description: "User's email address or 'me' for authenticated user" }, query: { type: "string", description: "Gmail search query (e.g., 'from:example@gmail.com', 'subject:meeting', 'is:unread')" }, maxResults: { type: "number", description: "Maximum number of emails to return (default: 10)" }, }, required: ["query"], additionalProperties: false, }, }, }, // ---- Ordering MCP Server Tools -------------------------------------------- { type: "function", function: { name: "order_food_item", description: "Add a food item to your order from the restaurant menu", parameters: { type: "object", properties: { itemName: { type: "string", description: "Name of the food item to order (e.g., 'bacon sandwich', 'coffee')" }, quantity: { type: "number", description: "Quantity to order (default: 1)" }, customizations: { type: "object", description: "Item customizations (e.g., {'size': 'large', 'milk': 'oat'})" } }, required: ["itemName"], additionalProperties: false, }, }, }, { type: "function", function: { name: "browse_menu", description: "Browse the restaurant menu to see available items", parameters: { type: "object", properties: { category: { type: "string", description: "Filter by category (e.g., 'breakfast', 'lunch', 'beverages')" }, search: { type: "string", description: "Search for specific items" } }, additionalProperties: false, }, }, }, { type: "function", function: { name: "get_cart", description: "View current items in your cart", parameters: { type: "object", properties: {}, additionalProperties: false }, }, }, { type: "function", function: { name: "clear_cart", description: "Remove all items from your cart", parameters: { type: "object", properties: {}, additionalProperties: false }, }, }, { type: "function", function: { name: "checkout", description: "Complete your order and proceed to payment", parameters: { type: "object", properties: {}, additionalProperties: false }, }, }, ] as const satisfies OpenAI.Chat.Completions.ChatCompletionTool[]; // ---- OpenAI client/model ---------------------------------------------------- const MODEL = process.env.OPENAI_MODEL ?? "gpt-4o-mini"; const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); // ---- MCP wiring: connect to both servers ------------------------------- async function connectMcpServers(): Promise<{ googleClient: Client; orderingClient: Client }> { console.log("🔗 Connecting to MCP servers..."); // Connect to Google MCP server console.log("📁 Google server path:", GOOGLE_SERVER_PATH); const googleTransport = new StdioClientTransport({ command: process.execPath, args: [GOOGLE_SERVER_PATH], stderr: "inherit", cwd: path.resolve(__dirname, ".."), }); const googleClient = new Client({ name: "ada-chat-google", version: "0.1.0" }); await googleClient.connect(googleTransport); console.log("✅ Google MCP client connected successfully"); // Connect to Ordering MCP server console.log("📁 Ordering server path:", ORDERING_SERVER_PATH); const orderingTransport = new StdioClientTransport({ command: process.execPath, args: [ORDERING_SERVER_PATH], stderr: "inherit", cwd: path.resolve(__dirname, ".."), }); const orderingClient = new Client({ name: "ada-chat-ordering", version: "0.1.0" }); await orderingClient.connect(orderingTransport); console.log("✅ Ordering MCP client connected successfully"); return { googleClient, orderingClient }; } // ---- Chat loop -------------------------------------------------------------- async function chatLoop() { const { googleClient, orderingClient } = await connectMcpServers(); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); // Get current date and time in America/Los_Angeles timezone const now = new Date(); const currentDate = now.toLocaleDateString('en-US', { timeZone: 'America/Los_Angeles', weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }); const currentTime = now.toLocaleTimeString('en-US', { timeZone: 'America/Los_Angeles', hour: '2-digit', minute: '2-digit', second: '2-digit' }); const history: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [ { role: "system", content: `You are a helpful assistant named Ada that can help with both Google Calendar/Gmail tasks and restaurant ordering. The current date and time is: ${currentDate} at ${currentTime} (America/Los_Angeles timezone). Default to America/Los_Angeles unless the user specifies otherwise. On bootup, inform the user of your capabilities and the current date/time. Always use the current date/time provided above when scheduling events or answering date-related questions. Your capabilities include: - Google Calendar: Create events, list events, manage calendars - Gmail: Send emails, search emails, read emails - Restaurant Ordering: Browse menu, order food items, manage cart, checkout Always use the current date/time provided above when scheduling events or answering date-related questions. YOUR CAPABILITIES: - List available calendars - List events from any calendar (with optional filtering by date range, max results, etc.) - Create single events with automatic color coding - Create recurring events with RRULE patterns and automatic color coding - List emails from Gmail with optional filtering - Retrieve specific email content by ID - Send emails with support for CC, BCC, and HTML content - Search emails using Gmail's powerful search syntax IMPORTANT: You have automatic color coding capabilities! When creating events, the system will automatically suggest appropriate colors based on the event content: - Work/Professional events: Blue tones (9=Blueberry, 7=Peacock, 8=Graphite) - Social/Fun events: Pink/Red tones (4=Flamingo, 11=Tomato, 3=Grape) - Health/Wellness: Green tones (10=Basil, 2=Sage) - Learning/Education: Yellow/Orange tones (5=Banana, 6=Tangerine) - Travel: 7=Peacock - Default: 1=Lavender Color ID Reference: 1=Lavender, 2=Sage, 3=Grape, 4=Flamingo, 5=Banana, 6=Tangerine, 7=Peacock, 8=Graphite, 9=Blueberry, 10=Basil, 11=Tomato You can also manually specify a colorId (1-11) if the user requests a specific color, but be careful to use the correct ID for the color you want. GMAIL CAPABILITIES: - Use Gmail search syntax for powerful email filtering (e.g., 'from:example@gmail.com', 'subject:meeting', 'is:unread', 'has:attachment') - When listing emails, you can filter by labels, search queries, and other criteria - For sending emails, you can include CC, BCC recipients and choose between plain text or HTML content - IMPORTANT: When users ask for emails, automatically fetch and summarize the email content using get_email. Don't just show email IDs - provide meaningful summaries of the actual email content including sender, subject, date, and key message points`, }, ]; const ask = (q: string) => new Promise<string>((res) => rl.question(q, res)); try { while (true) { const user = await ask("> "); if (!user.trim() || user.trim().toLowerCase() === "exit") break; history.push({ role: "user", content: user }); const resp = await openai.chat.completions.create({ model: MODEL, messages: history, tools, }); const msg = resp.choices[0]?.message; if (!msg) { console.log("No response from OpenAI"); continue; } if (msg.tool_calls && msg.tool_calls.length > 0) { console.log("🔧 Tool calls detected:", msg.tool_calls.length); const toolResults: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = []; for (const tc of msg.tool_calls) { if (tc.type !== "function") continue; // narrow before accessing .function const name = tc.function.name; const args = tc.function.arguments ? JSON.parse(tc.function.arguments) : {}; console.log(`🔧 Calling tool: ${name} with args:`, args); // Route tool calls to the appropriate MCP server const isOrderingTool = ['order_food_item', 'browse_menu', 'get_cart', 'clear_cart', 'checkout'].includes(name); const client = isOrderingTool ? orderingClient : googleClient; const result = await client.callTool({ name, arguments: args }); console.log(`✅ Tool result:`, result); toolResults.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify(result, null, 2), // tool role expects string content }); } history.push(msg, ...toolResults); const final = await openai.chat.completions.create({ model: MODEL, messages: history, }); const text = final.choices[0]?.message?.content || "(no content)"; console.log(text); history.push({ role: "assistant", content: text }); } else { const text = msg.content || "(no content)"; console.log(text); history.push({ role: "assistant", content: text }); } } } finally { rl.close(); // Transport exposes close() via client.disconnect() in some SDKs; // Here, just let the spawned process die with the parent. } } // ---- CLI entry -------------------------------------------------------------- const program = new Command(); program.command("chat").description("Start the Jarvis chat").action(chatLoop); program.parse(process.argv);

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/davidpak/ada-mcp-servers'

If you have feedback or need assistance with the MCP directory API, please join our Discord server