Files
hermes-mcp/src/tools.ts
Garfield 18b838c268 feat: ChatGPT Custom GPT support in chat bot + sqcp SMTP routing fix
- chat.ts: system prompt now includes step-by-step ChatGPT Custom GPT
  setup (openapi.json import + OAuth), Claude/Cursor/Windsurf config,
  and mortgage broker guidance — bot no longer incorrectly says ChatGPT
  is unsupported
- smtp.ts: all sqcp_* accounts now route to mail.squaremcp.com (SQCP_SMTP_HOST)
  instead of the fetcherpay server
- tools.ts: ACCOUNT_PARAM description now lists all 14 mailboxes including
  the 7 squaremcp.com accounts so Claude picks the right one without guessing
- package.json: postinstall hook runs imapflow patch script after npm install
- hermes-k8s.yaml: updated image digest to current production build

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 18:53:42 -04:00

1283 lines
49 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Tool } from '@modelcontextprotocol/sdk/types.js';
import type { Customer } from './billing/middleware.js';
import { recordUsage, checkLimit } from './billing/usage.js';
import { searchMessages, readMessage, getProfile, listFolders, type Account, type EmailCtx } from './imap.js';
import { sendEmail, createDraft } from './smtp.js';
import type { EmailCredentials } from './multitenancy/credential-store.js';
import { searchNotes, getNote, appendToNote, updateNote, getSyncStatus } from './clients/obsidian.js';
import { sendMessage, sendTemplate, getMessageStatus, listTemplates } from './clients/whatsapp.js';
import { getProfile as getLinkedInProfile, createPost as createLinkedInPost, createVideoPost as createLinkedInVideoPost, searchConnections, sendMessage as sendLinkedInMessage } from './clients/linkedin.js';
import { getMe as getTelegramMe, sendMessage as sendTelegramMessage, sendPhoto as sendTelegramPhoto, getUpdates as getTelegramUpdates, getChat as getTelegramChat } from './clients/telegram.js';
import { getMe as getDiscordMe, getGuilds, getChannels, sendMessage as sendDiscordMessage, getMessages as getDiscordMessages } from './clients/discord.js';
import { getProfile as getInstagramProfile, getMedia as getInstagramMedia, createImagePost as createInstagramPost, createReel as createInstagramReel } from './clients/instagram.js';
import { searchTweets, getUserProfile, getUserTweets, createTweet, uploadVideoAndTweet } from './clients/twitter.js';
import { getUserProfile as getTikTokProfile, getCreatorInfo, createVideo, getVideoStatus } from './clients/tiktok.js';
import { getMe as getSnapchatMe, createSnap, getAdAccounts } from './clients/snapchat.js';
import { getPage, getPosts, createPost as createFacebookPost, createPhotoPost, createVideoPost as createFacebookVideoPost } from './clients/facebook.js';
import { getMe as getSlackMe, getChannels as getSlackChannels, sendMessage as sendSlackMessage, getMessages as getSlackMessages } from './clients/slack.js';
const ACCOUNT_PARAM = {
account: {
type: 'string',
enum: ['yahoo', 'fetcherpay', 'garfield', 'sales', 'leads', 'founder', 'gmail', 'sqcp_garfield', 'sqcp_info', 'sqcp_sales', 'sqcp_support', 'sqcp_founder', 'sqcp_contact', 'sqcp_admin'],
description: 'Which mailbox to use: "yahoo" (gheron01@yahoo.com), "fetcherpay" (garfield.heron@fetcherpay.com), "garfield" (garfield@fetcherpay.com), "sales" (sales@fetcherpay.com), "leads" (leads@fetcherpay.com), "founder" (founder@fetcherpay.com), "gmail" (garfield.heron@gmail.com), "sqcp_garfield" (garfield@squaremcp.com), "sqcp_info" (info@squaremcp.com), "sqcp_sales" (sales@squaremcp.com), "sqcp_support" (support@squaremcp.com), "sqcp_founder" (founder@squaremcp.com), "sqcp_contact" (contact@squaremcp.com), "sqcp_admin" (admin@squaremcp.com). Defaults to "yahoo".',
},
};
export const tools: Tool[] = [
{
name: 'get_profile',
description: 'Get the email account profile (email address and name)',
inputSchema: {
type: 'object',
properties: { ...ACCOUNT_PARAM },
},
},
{
name: 'search_messages',
description: 'Search email messages by keyword, sender, or subject',
inputSchema: {
type: 'object',
properties: {
q: { type: 'string', description: 'Search query (keyword, from:email, subject:text)' },
maxResults: { type: 'number', description: 'Max messages to return (default 20)' },
folder: { type: 'string', description: 'Folder to search (default INBOX). For Gmail, [Gmail]/All Mail searches everywhere.' },
...ACCOUNT_PARAM,
},
required: ['q'],
},
},
{
name: 'read_message',
description: 'Read a full email message by UID',
inputSchema: {
type: 'object',
properties: {
uid: { type: 'number', description: 'Message UID from search results' },
folder: { type: 'string', description: 'Folder the message was found in (default INBOX). Use the folder value from search_results.' },
...ACCOUNT_PARAM,
},
required: ['uid'],
},
},
{
name: 'list_folders',
description: 'List all email folders/mailboxes',
inputSchema: {
type: 'object',
properties: { ...ACCOUNT_PARAM },
},
},
{
name: 'create_draft',
description: 'Create a draft email',
inputSchema: {
type: 'object',
properties: {
to: { type: 'string', description: 'Recipient email address' },
subject: { type: 'string', description: 'Email subject' },
body: { type: 'string', description: 'Email body (plain text)' },
...ACCOUNT_PARAM,
},
required: ['to', 'subject', 'body'],
},
},
{
name: 'send_email',
description: 'Send an email',
inputSchema: {
type: 'object',
properties: {
to: { type: 'string', description: 'Recipient email address' },
subject: { type: 'string', description: 'Email subject' },
body: { type: 'string', description: 'Email body (plain text)' },
...ACCOUNT_PARAM,
},
required: ['to', 'subject', 'body'],
},
},
// ── Obsidian tools ────────────────────────────────────────────────────────
{
name: 'obsidian_search_notes',
description:
'Search across the Obsidian vault by content, tags, or title. Use when the user references "my notes", "in obsidian", "I wrote about", or needs personal knowledge retrieval. Returns note paths relative to the vault root (e.g. "Daily Notes/2026-04-15.md"). Always use these exact relative paths when calling obsidian_read_note or obsidian_append_to_note.',
inputSchema: {
type: 'object',
properties: {
query: { type: 'string', description: 'Text to search for in note content or title' },
tags: {
type: 'array',
items: { type: 'string' },
description: 'Filter by Obsidian tags (all must match)',
},
limit: { type: 'number', description: 'Max results to return (default 10)' },
path_filter: {
type: 'string',
description: 'Only return notes whose relative vault path contains this string (e.g. "Daily Notes")',
},
},
required: ['query'],
},
},
{
name: 'obsidian_read_note',
description:
'Retrieve the full content of a specific Obsidian note by relative vault path. Use when the user asks to read, open, or show a specific note. The path must be relative to the vault root (e.g. "Daily Notes/2026-04-15.md"). Do not use absolute filesystem paths.',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'File path relative to the vault root (e.g. "Daily Notes/2026-04-15.md") or just the note title/filename',
},
},
required: ['path'],
},
},
{
name: 'obsidian_append_to_note',
description:
'Append markdown content to an Obsidian daily note or any specific vault note. Use for logging, journaling, capturing insights from email, or recording action items in your notes. Creates the note if it does not exist. The path must be relative to the vault root (e.g. "Daily Notes/2026-04-15.md"). Do not use absolute filesystem paths.',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'File path relative to the vault root (e.g. "Daily Notes/2026-04-15.md")',
},
content: { type: 'string', description: 'Markdown content to append' },
create_if_missing: {
type: 'boolean',
description: 'Create the note if it does not exist (default true)',
},
header: {
type: 'string',
description: 'Optional H2 section header to add before the content',
},
},
required: ['path', 'content'],
},
},
{
name: 'obsidian_update_note',
description:
'Overwrite the entire content of an Obsidian note with new markdown. Use when you need to replace, rewrite, or structurally edit an existing note rather than append to it. Creates the note if it does not exist.',
inputSchema: {
type: 'object',
properties: {
path: {
type: 'string',
description: 'File path relative to vault root (e.g. "Daily Notes/2026-04-15.md")',
},
content: {
type: 'string',
description: 'Full markdown content to write — replaces existing content entirely',
},
},
required: ['path', 'content'],
},
},
{
name: 'obsidian_sync_status',
description:
'Check the sync status of the Obsidian vault via Syncthing. Use when the user asks if their notes are synced or up to date.',
inputSchema: {
type: 'object',
properties: {},
},
},
// ── WhatsApp Business API tools ────────────────────────────────
{
name: 'whatsapp_send_message',
description:
'Send a WhatsApp message to a phone number. Use when the user asks to send a WhatsApp message, text someone on WhatsApp, or notify via WhatsApp.',
inputSchema: {
type: 'object',
properties: {
to: { type: 'string', description: 'Recipient phone number in international format (e.g. +1234567890)' },
message: { type: 'string', description: 'Message text to send' },
account: { type: 'string', description: 'Which WhatsApp account to use (default: "default")' },
},
required: ['to', 'message'],
},
},
{
name: 'whatsapp_send_template',
description:
'Send a WhatsApp template message (for approved templates). Use when sending structured notifications or alerts via WhatsApp templates.',
inputSchema: {
type: 'object',
properties: {
to: { type: 'string', description: 'Recipient phone number in international format' },
template_name: { type: 'string', description: 'Name of the approved WhatsApp template' },
language: { type: 'string', description: 'Template language code (default: "en")' },
components: { type: 'array', items: { type: 'object' }, description: 'Template components (header, body, buttons) with parameters' },
account: { type: 'string', description: 'Which WhatsApp account to use (default: "default")' },
},
required: ['to', 'template_name'],
},
},
{
name: 'whatsapp_get_message_status',
description:
'[DEPRECATED] Meta Cloud API does not support polling message status. Status updates are only available via webhooks.',
inputSchema: {
type: 'object',
properties: {
message_id: { type: 'string', description: 'WhatsApp message ID (not used - webhook required)' },
account: { type: 'string', description: 'Which WhatsApp account to use (default: "default")' },
},
},
},
{
name: 'whatsapp_list_templates',
description:
'List all approved WhatsApp message templates for the business account. Use when the user asks what templates are available.',
inputSchema: {
type: 'object',
properties: {
account: { type: 'string', description: 'Which WhatsApp account to use (default: "default")' },
},
},
},
// ── LinkedIn tools ─────────────────────────────────────────────
{
name: 'linkedin_get_profile',
description:
'Get the LinkedIn profile of the authenticated user. Use when the user asks about their LinkedIn profile, name, or headline.',
inputSchema: {
type: 'object',
properties: {
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
},
},
},
{
name: 'linkedin_create_post',
description:
'Create a post on LinkedIn. Use when the user wants to publish an update, article, or share content on LinkedIn.',
inputSchema: {
type: 'object',
properties: {
text: { type: 'string', description: 'Post content text' },
visibility: { type: 'string', enum: ['PUBLIC', 'CONNECTIONS'], description: 'Visibility: PUBLIC (anyone) or CONNECTIONS (1st degree only). Default: PUBLIC' },
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
},
required: ['text'],
},
},
{
name: 'linkedin_upload_video',
description:
'Upload a video and create a LinkedIn post. Accepts a publicly accessible video URL, downloads it server-side, uploads it to LinkedIn via the Videos API, and publishes the post. Video must be publicly reachable (not localhost or private network). Large videos may take 3090 seconds.',
inputSchema: {
type: 'object',
properties: {
video_url: { type: 'string', description: 'Publicly accessible URL of the MP4 video to upload' },
text: { type: 'string', description: 'Post caption / commentary text' },
visibility: { type: 'string', enum: ['PUBLIC', 'CONNECTIONS'], description: 'Post visibility. Default: PUBLIC' },
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
},
required: ['video_url', 'text'],
},
},
{
name: 'linkedin_search_connections',
description:
'Search LinkedIn connections. [REQUIRES PARTNERSHIP] LinkedIn removed public API access to connections. This tool will guide you to apply for the Partnership Program.',
inputSchema: {
type: 'object',
properties: {
keywords: { type: 'string', description: 'Search keywords for connections' },
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
},
},
},
{
name: 'linkedin_send_message',
description:
'Send a direct message on LinkedIn. [REQUIRES PARTNERSHIP] LinkedIn messaging is not available through the public API. This tool will guide you to apply for the Partnership Program.',
inputSchema: {
type: 'object',
properties: {
recipient_id: { type: 'string', description: 'LinkedIn person URN or ID of the recipient' },
message: { type: 'string', description: 'Message text to send' },
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
},
required: ['recipient_id', 'message'],
},
},
// ── Telegram tools ─────────────────────────────────────────────
{
name: 'telegram_get_me',
description:
'Get information about the Telegram bot. Use to verify the bot is connected and get its username.',
inputSchema: {
type: 'object',
properties: {
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
},
},
},
{
name: 'telegram_send_message',
description:
'Send a text message via Telegram bot. Use when the user asks to send a Telegram message, DM someone, or notify a group/channel.',
inputSchema: {
type: 'object',
required: ['chat_id', 'text'],
properties: {
chat_id: { type: 'string', description: 'Chat ID, username (@username), or channel ID' },
text: { type: 'string', description: 'Message text to send' },
parse_mode: { type: 'string', enum: ['HTML', 'Markdown', 'MarkdownV2'], description: 'Formatting mode for the message' },
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
},
},
},
{
name: 'telegram_send_photo',
description:
'Send a photo via Telegram bot. Use when the user wants to share an image through Telegram.',
inputSchema: {
type: 'object',
required: ['chat_id', 'photo'],
properties: {
chat_id: { type: 'string', description: 'Chat ID, username (@username), or channel ID' },
photo: { type: 'string', description: 'Photo URL or file_id to send' },
caption: { type: 'string', description: 'Optional caption text' },
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
},
},
},
{
name: 'telegram_get_updates',
description:
'Get recent incoming messages and updates for the Telegram bot. Use when the user asks to check messages or read Telegram DMs.',
inputSchema: {
type: 'object',
properties: {
limit: { type: 'number', description: 'Max number of updates to return (default: 10)' },
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
},
},
},
{
name: 'telegram_get_chat',
description:
'Get information about a Telegram chat or channel. Use to verify a chat exists and get its details.',
inputSchema: {
type: 'object',
required: ['chat_id'],
properties: {
chat_id: { type: 'string', description: 'Chat ID, username (@username), or channel ID' },
account: { type: 'string', description: 'Which Telegram account to use (default: "default")' },
},
},
},
// ── Discord tools ────────────────────────────────────────────
{
name: 'discord_get_me',
description:
'Get information about the Discord bot. Use to verify the bot is connected and get its username.',
inputSchema: {
type: 'object',
properties: {
account: { type: 'string', description: 'Which Discord account to use (default: "default")' },
},
},
},
{
name: 'discord_get_guilds',
description:
'List Discord servers (guilds) the bot is a member of. Use to find server IDs for sending messages.',
inputSchema: {
type: 'object',
properties: {
account: { type: 'string', description: 'Which Discord account to use (default: "default")' },
},
},
},
{
name: 'discord_get_channels',
description:
'List channels in a Discord server. Use to find channel IDs for sending messages.',
inputSchema: {
type: 'object',
required: ['guild_id'],
properties: {
guild_id: { type: 'string', description: 'Discord server (guild) ID' },
account: { type: 'string', description: 'Which Discord account to use (default: "default")' },
},
},
},
{
name: 'discord_send_message',
description:
'Send a message to a Discord channel. Use when the user wants to post in a Discord server.',
inputSchema: {
type: 'object',
required: ['channel_id', 'content'],
properties: {
channel_id: { type: 'string', description: 'Discord channel ID' },
content: { type: 'string', description: 'Message content (up to 2000 chars)' },
account: { type: 'string', description: 'Which Discord account to use (default: "default")' },
},
},
},
{
name: 'discord_get_messages',
description:
'Get recent messages from a Discord channel. Use to read chat history.',
inputSchema: {
type: 'object',
required: ['channel_id'],
properties: {
channel_id: { type: 'string', description: 'Discord channel ID' },
limit: { type: 'number', description: 'Max messages to return (default: 10, max: 100)' },
account: { type: 'string', description: 'Which Discord account to use (default: "default")' },
},
},
},
// ── Slack tools ──────────────────────────────────────────────
{
name: 'slack_get_me',
description:
'Verify the Slack bot is connected and return workspace info. Use to confirm credentials are working.',
inputSchema: {
type: 'object',
properties: {
account: { type: 'string', description: 'Which Slack account to use (default: "default")' },
},
},
},
{
name: 'slack_get_channels',
description:
'List Slack channels the bot has access to. Use to find channel IDs before sending messages.',
inputSchema: {
type: 'object',
properties: {
limit: { type: 'number', description: 'Max channels to return (default: 100)' },
account: { type: 'string', description: 'Which Slack account to use (default: "default")' },
},
},
},
{
name: 'slack_send_message',
description:
'Send a message to a Slack channel. Uses the default channel if channel_id is omitted.',
inputSchema: {
type: 'object',
required: ['text'],
properties: {
channel_id: { type: 'string', description: 'Slack channel ID (e.g. C0123456). Uses default channel if omitted.' },
text: { type: 'string', description: 'Message text. Supports Slack mrkdwn formatting.' },
account: { type: 'string', description: 'Which Slack account to use (default: "default")' },
},
},
},
{
name: 'slack_get_messages',
description:
'Get recent messages from a Slack channel.',
inputSchema: {
type: 'object',
required: ['channel_id'],
properties: {
channel_id: { type: 'string', description: 'Slack channel ID' },
limit: { type: 'number', description: 'Max messages to return (default: 10)' },
account: { type: 'string', description: 'Which Slack account to use (default: "default")' },
},
},
},
// ── Instagram tools ──────────────────────────────────────────
{
name: 'instagram_get_profile',
description:
'Get Instagram Business/Creator account profile info. Use when the user asks about their Instagram stats, followers, or account details.',
inputSchema: {
type: 'object',
properties: {
account: { type: 'string', description: 'Which Instagram account to use (default: "default")' },
},
},
},
{
name: 'instagram_get_media',
description:
'Get recent posts from an Instagram Business/Creator account. Use when the user wants to see their recent content.',
inputSchema: {
type: 'object',
properties: {
limit: { type: 'number', description: 'Max posts to return (default: 10)' },
account: { type: 'string', description: 'Which Instagram account to use (default: "default")' },
},
},
},
{
name: 'instagram_create_post',
description:
'Create an image post on Instagram. [REQUIRES BUSINESS ACCOUNT] Only works with Instagram Business/Creator accounts connected to a Facebook Page.',
inputSchema: {
type: 'object',
required: ['image_url'],
properties: {
image_url: { type: 'string', description: 'Publicly accessible URL of the image to post' },
caption: { type: 'string', description: 'Post caption text' },
account: { type: 'string', description: 'Which Instagram account to use (default: "default")' },
},
},
},
{
name: 'instagram_create_reel',
description:
'Upload a video as an Instagram Reel. [REQUIRES BUSINESS ACCOUNT] Only works with Instagram Business/Creator accounts connected to a Facebook Page.',
inputSchema: {
type: 'object',
required: ['video_url'],
properties: {
video_url: { type: 'string', description: 'Publicly accessible URL of the MP4 video to post as a Reel' },
caption: { type: 'string', description: 'Reel caption text' },
account: { type: 'string', description: 'Which Instagram account to use (default: "default")' },
},
},
},
// ── Twitter/X tools ──────────────────────────────────────────
{
name: 'twitter_search_tweets',
description:
'Search recent tweets on Twitter/X. Use when the user wants to find tweets about a topic, hashtag, or keyword.',
inputSchema: {
type: 'object',
required: ['query'],
properties: {
query: { type: 'string', description: 'Search query (keyword, hashtag, or phrase)' },
max_results: { type: 'number', description: 'Max tweets to return (default: 10, max: 100)' },
account: { type: 'string', description: 'Which Twitter account to use (default: "default")' },
},
},
},
{
name: 'twitter_get_user_profile',
description:
'Get a Twitter/X user profile. Use when the user wants stats, bio, or follower count for a specific account.',
inputSchema: {
type: 'object',
required: ['username'],
properties: {
username: { type: 'string', description: 'Twitter username (without @)' },
account: { type: 'string', description: 'Which Twitter account to use (default: "default")' },
},
},
},
{
name: 'twitter_get_user_tweets',
description:
'Get recent tweets from a specific Twitter/X user. Use to read someones timeline.',
inputSchema: {
type: 'object',
required: ['username'],
properties: {
username: { type: 'string', description: 'Twitter username (without @)' },
max_results: { type: 'number', description: 'Max tweets (default: 10, max: 100)' },
account: { type: 'string', description: 'Which Twitter account to use (default: "default")' },
},
},
},
{
name: 'twitter_create_tweet',
description:
'Post a text tweet on Twitter/X. [REQUIRES PAID TIER] The free API tier is read-only. Upgrade required to post.',
inputSchema: {
type: 'object',
required: ['text'],
properties: {
text: { type: 'string', description: 'Tweet text (max 280 chars)' },
account: { type: 'string', description: 'Which Twitter account to use (default: "default")' },
},
},
},
{
name: 'twitter_upload_video',
description:
'Upload a video and post it as a tweet on Twitter/X. Downloads the video, uploads via Twitter media API, then publishes the tweet. [REQUIRES PAID TIER] The free API tier is read-only. Upgrade required to post.',
inputSchema: {
type: 'object',
required: ['video_url', 'text'],
properties: {
video_url: { type: 'string', description: 'Publicly accessible URL of the MP4 video to post' },
text: { type: 'string', description: 'Tweet text content' },
account: { type: 'string', description: 'Which Twitter account to use (default: "default")' },
},
},
},
// ── TikTok tools ─────────────────────────────────────────────
{
name: 'tiktok_get_profile',
description:
'Get the TikTok user profile including follower count, following count, likes, and video count.',
inputSchema: {
type: 'object',
properties: {
account: { type: 'string', description: 'Which TikTok account to use (default: "default")' },
},
},
},
{
name: 'tiktok_get_creator_info',
description:
'Get TikTok creator publishing info: username, avatar, available privacy levels, and max video duration. Required before publishing.',
inputSchema: {
type: 'object',
properties: {
account: { type: 'string', description: 'Which TikTok account to use (default: "default")' },
},
},
},
{
name: 'tiktok_create_video',
description:
'Post a video to TikTok by providing a publicly accessible video URL. Returns a publish_id to check status.',
inputSchema: {
type: 'object',
required: ['video_url'],
properties: {
video_url: { type: 'string', description: 'Publicly accessible URL of the video to post' },
title: { type: 'string', description: 'Video title (max 150 chars)' },
description: { type: 'string', description: 'Video description / caption' },
account: { type: 'string', description: 'Which TikTok account to use (default: "default")' },
},
},
},
{
name: 'tiktok_get_video_status',
description:
'Check the processing status of a TikTok video upload using the publish_id returned by tiktok_create_video.',
inputSchema: {
type: 'object',
required: ['publish_id'],
properties: {
publish_id: { type: 'string', description: 'Publish ID returned by tiktok_create_video' },
account: { type: 'string', description: 'Which TikTok account to use (default: "default")' },
},
},
},
// ── Snapchat tools ───────────────────────────────────────────
{
name: 'snapchat_get_me',
description:
'Get the authenticated Snapchat user profile (display name, bitmoji avatar). Uses Snapchat Login Kit.',
inputSchema: {
type: 'object',
properties: {
account: { type: 'string', description: 'Which Snapchat account to use (default: "default")' },
},
},
},
{
name: 'snapchat_create_snap',
description:
'[NOT SUPPORTED] Snapchat Creative Kit is a mobile-only SDK — posting Snaps from a server is not possible. Use the iOS/Android Creative Kit SDK instead.',
inputSchema: {
type: 'object',
properties: {
image_url: { type: 'string', description: 'Image URL (not usable server-side)' },
video_url: { type: 'string', description: 'Video URL (not usable server-side)' },
caption: { type: 'string', description: 'Caption text (not usable server-side)' },
account: { type: 'string', description: 'Which Snapchat account to use (default: "default")' },
},
},
},
{
name: 'snapchat_get_ad_accounts',
description:
'List Snapchat Ads Manager ad accounts. Use when the user wants to manage Snapchat advertising campaigns.',
inputSchema: {
type: 'object',
properties: {
account: { type: 'string', description: 'Which Snapchat account to use (default: "default")' },
},
},
},
// ── Facebook tools ───────────────────────────────────────────
{
name: 'facebook_get_page',
description:
'Get a Facebook Page profile including name, category, fan count, and follower count.',
inputSchema: {
type: 'object',
properties: {
account: { type: 'string', description: 'Which Facebook account to use (default: "default")' },
},
},
},
{
name: 'facebook_get_posts',
description:
'Get recent posts from a Facebook Page feed.',
inputSchema: {
type: 'object',
properties: {
limit: { type: 'number', description: 'Max posts to return (default: 10)' },
account: { type: 'string', description: 'Which Facebook account to use (default: "default")' },
},
},
},
{
name: 'facebook_create_post',
description:
'Publish a text post (optionally with a link) to a Facebook Page.',
inputSchema: {
type: 'object',
required: ['message'],
properties: {
message: { type: 'string', description: 'Post text content' },
link: { type: 'string', description: 'Optional URL to attach to the post' },
account: { type: 'string', description: 'Which Facebook account to use (default: "default")' },
},
},
},
{
name: 'facebook_create_photo_post',
description:
'Publish a photo to a Facebook Page using a publicly accessible image URL. Creates a photo post with optional caption.',
inputSchema: {
type: 'object',
required: ['image_url'],
properties: {
image_url: { type: 'string', description: 'Publicly accessible URL of the image to post' },
caption: { type: 'string', description: 'Post caption text' },
account: { type: 'string', description: 'Which Facebook account to use (default: "default")' },
},
},
},
{
name: 'facebook_create_video_post',
description:
'Publish a video to a Facebook Page using a publicly accessible video URL. Creates a video post with optional description.',
inputSchema: {
type: 'object',
required: ['video_url'],
properties: {
video_url: { type: 'string', description: 'Publicly accessible URL of the video to post' },
description: { type: 'string', description: 'Post description text' },
account: { type: 'string', description: 'Which Facebook account to use (default: "default")' },
},
},
},
];
function acct(args: Record<string, unknown>): Account {
return (args.account as Account) ?? 'yahoo';
}
async function resolveEmailCtx(args: Record<string, unknown>, customer?: Customer): Promise<EmailCtx> {
if (customer) {
const creds = await customer.getCredential<EmailCredentials>('email');
if (creds) return creds;
}
return acct(args);
}
const PLATFORM_PREFIXES = [
'linkedin', 'obsidian', 'whatsapp', 'telegram', 'discord',
'instagram', 'twitter', 'tiktok', 'snapchat', 'facebook', 'slack',
];
function toolPlatform(name: string): string {
const prefix = name.split('_')[0];
return PLATFORM_PREFIXES.includes(prefix) ? prefix : 'email';
}
export async function handleToolCall(
name: string,
args: Record<string, unknown>,
customer?: Customer
): Promise<{ content: Array<{ type: string; text: string }>; isError?: boolean }> {
console.log(`[tool] ${name}`, JSON.stringify(args));
const t0 = Date.now();
if (customer) {
const { allowed } = await checkLimit(customer.id, customer.plan);
if (!allowed) {
return {
content: [{ type: 'text', text: 'Monthly tool call limit reached. Please upgrade your plan.' }],
isError: true,
};
}
}
try {
let result: unknown;
switch (name) {
case 'get_profile': {
const emailCtx = await resolveEmailCtx(args, customer);
result = await getProfile(emailCtx);
break;
}
case 'search_messages': {
const emailCtx = await resolveEmailCtx(args, customer);
result = await searchMessages(args.q as string, (args.maxResults as number) ?? 20, emailCtx, args.folder as string | undefined);
break;
}
case 'read_message': {
const emailCtx = await resolveEmailCtx(args, customer);
result = await readMessage(args.uid as number, emailCtx, args.folder as string | undefined);
break;
}
case 'list_folders': {
const emailCtx = await resolveEmailCtx(args, customer);
result = await listFolders(emailCtx);
break;
}
case 'create_draft': {
const emailCtx = await resolveEmailCtx(args, customer);
result = await createDraft(args.to as string, args.subject as string, args.body as string, emailCtx);
break;
}
case 'send_email': {
const emailCtx = await resolveEmailCtx(args, customer);
result = await sendEmail(args.to as string, args.subject as string, args.body as string, emailCtx);
break;
}
// ── Obsidian ──────────────────────────────────────────────────────────
case 'obsidian_search_notes':
result = await searchNotes(
args.query as string,
args.tags as string[] | undefined,
(args.limit as number) ?? 10,
args.path_filter as string | undefined
);
break;
case 'obsidian_read_note':
result = await getNote(args.path as string);
break;
case 'obsidian_append_to_note':
result = await appendToNote(
args.path as string,
args.content as string,
(args.create_if_missing as boolean) ?? true,
args.header as string | undefined
);
break;
case 'obsidian_update_note':
result = await updateNote(args.path as string, args.content as string);
break;
case 'obsidian_sync_status':
result = await getSyncStatus();
break;
// ── WhatsApp Business API ──────────────────────────────────
case 'whatsapp_send_message':
result = await sendMessage({
to: args.to as string,
message: args.message as string,
account: args.account as string | undefined,
}, customer);
break;
case 'whatsapp_send_template':
result = await sendTemplate({
to: args.to as string,
template_name: args.template_name as string,
language: args.language as string | undefined,
components: args.components as unknown[] | undefined,
account: args.account as string | undefined,
}, customer);
break;
case 'whatsapp_get_message_status':
result = await getMessageStatus({
message_id: args.message_id as string,
account: args.account as string | undefined,
}, customer);
break;
case 'whatsapp_list_templates':
result = await listTemplates({
account: args.account as string | undefined,
}, customer);
break;
// ── LinkedIn ───────────────────────────────────────────────
case 'linkedin_get_profile':
result = await getLinkedInProfile({
account: args.account as string | undefined,
}, customer);
break;
case 'linkedin_create_post':
result = await createLinkedInPost({
text: args.text as string,
visibility: (args.visibility as 'PUBLIC' | 'CONNECTIONS') ?? 'PUBLIC',
account: args.account as string | undefined,
}, customer);
break;
case 'linkedin_upload_video':
result = await createLinkedInVideoPost(
{
video_url: args.video_url as string,
text: args.text as string,
visibility: (args.visibility as 'PUBLIC' | 'CONNECTIONS') ?? 'PUBLIC',
account: args.account as string | undefined,
},
customer
);
break;
case 'linkedin_search_connections':
result = await searchConnections({
keywords: args.keywords as string | undefined,
account: args.account as string | undefined,
}, customer);
break;
case 'linkedin_send_message':
result = await sendLinkedInMessage({
recipient_id: args.recipient_id as string,
message: args.message as string,
account: args.account as string | undefined,
}, customer);
break;
// ── Telegram ───────────────────────────────────────────────
case 'telegram_get_me':
result = await getTelegramMe({
account: args.account as string | undefined,
}, customer);
break;
case 'telegram_send_message':
result = await sendTelegramMessage({
chat_id: args.chat_id as string | number,
text: args.text as string,
parse_mode: (args.parse_mode as 'HTML' | 'Markdown' | 'MarkdownV2') ?? undefined,
account: args.account as string | undefined,
}, customer);
break;
case 'telegram_send_photo':
result = await sendTelegramPhoto({
chat_id: args.chat_id as string | number,
photo: args.photo as string,
caption: args.caption as string | undefined,
account: args.account as string | undefined,
}, customer);
break;
case 'telegram_get_updates':
result = await getTelegramUpdates({
limit: (args.limit as number) ?? 10,
account: args.account as string | undefined,
}, customer);
break;
case 'telegram_get_chat':
result = await getTelegramChat({
chat_id: args.chat_id as string | number,
account: args.account as string | undefined,
}, customer);
break;
// ── Discord ─────────────────────────────────────────────────
case 'discord_get_me':
result = await getDiscordMe({
account: args.account as string | undefined,
}, customer);
break;
case 'discord_get_guilds':
result = await getGuilds({
account: args.account as string | undefined,
}, customer);
break;
case 'discord_get_channels':
result = await getChannels({
guild_id: args.guild_id as string,
account: args.account as string | undefined,
}, customer);
break;
case 'discord_send_message':
result = await sendDiscordMessage({
channel_id: args.channel_id as string,
content: args.content as string,
account: args.account as string | undefined,
}, customer);
break;
case 'discord_get_messages':
result = await getDiscordMessages({
channel_id: args.channel_id as string,
limit: (args.limit as number) ?? 10,
account: args.account as string | undefined,
}, customer);
break;
// ── Slack ───────────────────────────────────────────────────
case 'slack_get_me':
result = await getSlackMe({
account: args.account as string | undefined,
}, customer);
break;
case 'slack_get_channels':
result = await getSlackChannels({
limit: args.limit as number | undefined,
account: args.account as string | undefined,
}, customer);
break;
case 'slack_send_message':
result = await sendSlackMessage({
channel_id: args.channel_id as string | undefined,
text: args.text as string,
account: args.account as string | undefined,
}, customer);
break;
case 'slack_get_messages':
result = await getSlackMessages({
channel_id: args.channel_id as string,
limit: args.limit as number | undefined,
account: args.account as string | undefined,
}, customer);
break;
// ── Instagram ───────────────────────────────────────────────
case 'instagram_get_profile':
result = await getInstagramProfile({
account: args.account as string | undefined,
}, customer);
break;
case 'instagram_get_media':
result = await getInstagramMedia({
limit: (args.limit as number) ?? 10,
account: args.account as string | undefined,
}, customer);
break;
case 'instagram_create_post':
result = await createInstagramPost({
image_url: args.image_url as string,
caption: args.caption as string | undefined,
account: args.account as string | undefined,
}, customer);
break;
case 'instagram_create_reel':
result = await createInstagramReel({
video_url: args.video_url as string,
caption: args.caption as string | undefined,
account: args.account as string | undefined,
}, customer);
break;
// ── Twitter/X ───────────────────────────────────────────────
case 'twitter_search_tweets':
result = await searchTweets({
query: args.query as string,
max_results: (args.max_results as number) ?? 10,
account: args.account as string | undefined,
}, customer);
break;
case 'twitter_get_user_profile':
result = await getUserProfile({
username: args.username as string,
account: args.account as string | undefined,
}, customer);
break;
case 'twitter_get_user_tweets':
result = await getUserTweets({
username: args.username as string,
max_results: (args.max_results as number) ?? 10,
account: args.account as string | undefined,
}, customer);
break;
case 'twitter_create_tweet':
result = await createTweet({
text: args.text as string,
account: args.account as string | undefined,
}, customer);
break;
case 'twitter_upload_video':
result = await uploadVideoAndTweet({
videoUrl: args.video_url as string,
text: args.text as string,
account: args.account as string | undefined,
}, customer);
break;
// ── TikTok ─────────────────────────────────────────────────
case 'tiktok_get_profile':
result = await getTikTokProfile({
account: args.account as string | undefined,
}, customer);
break;
case 'tiktok_get_creator_info':
result = await getCreatorInfo({
account: args.account as string | undefined,
}, customer);
break;
case 'tiktok_create_video':
result = await createVideo({
video_url: args.video_url as string,
title: args.title as string | undefined,
description: args.description as string | undefined,
account: args.account as string | undefined,
}, customer);
break;
case 'tiktok_get_video_status':
result = await getVideoStatus({
publish_id: args.publish_id as string,
account: args.account as string | undefined,
}, customer);
break;
// ── Snapchat ────────────────────────────────────────────────
case 'snapchat_get_me':
result = await getSnapchatMe({
account: args.account as string | undefined,
}, customer);
break;
case 'snapchat_create_snap':
result = await createSnap({
image_url: args.image_url as string | undefined,
video_url: args.video_url as string | undefined,
caption: args.caption as string | undefined,
account: args.account as string | undefined,
}, customer);
break;
case 'snapchat_get_ad_accounts':
result = await getAdAccounts({
account: args.account as string | undefined,
}, customer);
break;
// ── Facebook ────────────────────────────────────────────────
case 'facebook_get_page':
result = await getPage({
account: args.account as string | undefined,
}, customer);
break;
case 'facebook_get_posts':
result = await getPosts({
limit: (args.limit as number) ?? 10,
account: args.account as string | undefined,
}, customer);
break;
case 'facebook_create_post':
result = await createFacebookPost({
message: args.message as string,
link: args.link as string | undefined,
account: args.account as string | undefined,
}, customer);
break;
case 'facebook_create_photo_post':
result = await createPhotoPost({
image_url: args.image_url as string,
caption: args.caption as string | undefined,
account: args.account as string | undefined,
}, customer);
break;
case 'facebook_create_video_post':
result = await createFacebookVideoPost({
video_url: args.video_url as string,
description: args.description as string | undefined,
account: args.account as string | undefined,
}, customer);
break;
// Legacy Yahoo-prefixed names — keep working for any cached Claude sessions
case 'yahoo_get_profile':
result = await getProfile('yahoo');
break;
case 'yahoo_search_messages':
result = await searchMessages(args.q as string, (args.maxResults as number) ?? 20, 'yahoo');
break;
case 'yahoo_read_message':
result = await readMessage(args.uid as number, 'yahoo');
break;
case 'yahoo_list_folders':
result = await listFolders('yahoo');
break;
case 'yahoo_create_draft':
result = await createDraft(args.to as string, args.subject as string, args.body as string, 'yahoo');
break;
case 'yahoo_send_email':
result = await sendEmail(args.to as string, args.subject as string, args.body as string, 'yahoo');
break;
default:
throw new Error(`Unknown tool: ${name}`);
}
console.log(`[tool] ${name} OK (${Date.now() - t0}ms)`);
if (customer) {
recordUsage(customer.id, toolPlatform(name), name).catch(() => {});
}
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
};
} catch (error) {
const msg = (error as Error).message;
const stack = (error as Error).stack ?? '';
console.error(`[tool] ${name} ERROR (${Date.now() - t0}ms):`, msg);
console.error(stack);
return {
content: [{ type: 'text', text: `Error: ${msg}` }],
isError: true,
};
}
}
export function stripAccountParam(tool: Tool): Tool {
const props = tool.inputSchema.properties ?? {};
const filtered = Object.fromEntries(Object.entries(props).filter(([k]) => k !== 'account'));
return {
...tool,
inputSchema: {
...tool.inputSchema,
properties: filtered as Tool['inputSchema']['properties'],
},
};
}