From e1e7d88c8aaa9a75d96c1945f7de73208c83f6bf Mon Sep 17 00:00:00 2001 From: Garfield Date: Tue, 5 May 2026 22:01:21 -0400 Subject: [PATCH] feat: Discord + Instagram integrations Discord Bot API - New client: src/clients/discord.ts - Tools: discord_get_me, discord_get_guilds, discord_get_channels, discord_send_message, discord_get_messages - REST endpoints: GET /api/discord/me, /api/discord/guilds, /api/discord/channels, /api/discord/messages, POST /api/discord/message - Multi-account env var: DISCORD_{ACCOUNT}_BOT_TOKEN Instagram Graph API - New client: src/clients/instagram.ts - Tools: instagram_get_profile, instagram_get_media, instagram_create_post - REST endpoints: GET /api/instagram/profile, /api/instagram/media, POST /api/instagram/post - Multi-account env vars: INSTAGRAM_{ACCOUNT}_ACCESS_TOKEN, INSTAGRAM_{ACCOUNT}_BUSINESS_ACCOUNT_ID Total tools: 32 --- .env.example | 14 +++ src/clients/discord.ts | 101 ++++++++++++++++++ src/clients/instagram.ts | 139 +++++++++++++++++++++++++ src/index.ts | 90 ++++++++++++++++ src/manifest.ts | 216 +++++++++++++++++++++++++++++++++++++++ src/tools.ts | 164 +++++++++++++++++++++++++++++ 6 files changed, 724 insertions(+) create mode 100644 src/clients/discord.ts create mode 100644 src/clients/instagram.ts diff --git a/.env.example b/.env.example index c4cb5a7..b3c4891 100644 --- a/.env.example +++ b/.env.example @@ -60,3 +60,17 @@ LINKEDIN_DEFAULT_CLIENT_SECRET=your-linkedin-client-secret # For default account: TELEGRAM_DEFAULT_BOT_TOKEN=your-telegram-bot-token # For additional accounts, duplicate with TELEGRAM_{ACCOUNT}_* + +# ── Discord Bot API ────────────────────────────────────────────────────────── +# Create a bot at https://discord.com/developers/applications → New Application → Bot → Copy Token +# For default account: +DISCORD_DEFAULT_BOT_TOKEN=your-discord-bot-token +# For additional accounts, duplicate with DISCORD_{ACCOUNT}_* + +# ── Instagram Graph API ────────────────────────────────────────────────────── +# Requires Instagram Business/Creator account connected to Facebook Page +# Get token from Facebook Developer Console with instagram_basic + instagram_content_publish scopes +# For default account: +INSTAGRAM_DEFAULT_ACCESS_TOKEN=your-instagram-access-token +INSTAGRAM_DEFAULT_BUSINESS_ACCOUNT_ID=your-instagram-business-account-id +# For additional accounts, duplicate with INSTAGRAM_{ACCOUNT}_* diff --git a/src/clients/discord.ts b/src/clients/discord.ts new file mode 100644 index 0000000..03a9ec5 --- /dev/null +++ b/src/clients/discord.ts @@ -0,0 +1,101 @@ +const DISCORD_API_BASE = 'https://discord.com/api/v10'; + +function getToken(account: string): string { + const envKey = `DISCORD_${account.toUpperCase()}_BOT_TOKEN`; + return process.env[envKey] ?? ''; +} + +async function discordRequest( + token: string, + endpoint: string, + method: 'GET' | 'POST' = 'GET', + body?: unknown +) { + const url = `${DISCORD_API_BASE}${endpoint}`; + const res = await fetch(url, { + method, + headers: { + 'Authorization': `Bot ${token}`, + 'Content-Type': 'application/json', + }, + body: body ? JSON.stringify(body) : undefined, + signal: AbortSignal.timeout(15000), + }); + + if (!res.ok) { + const error = await res.text(); + throw new Error(`Discord API error (${res.status}): ${error}`); + } + + return res.json(); +} + +export async function getMe(args: { account?: string }): Promise<{ + id: string; + username: string; + bot: boolean; +}> { + const token = getToken(args.account ?? 'default'); + if (!token) { + throw new Error('Missing Discord credentials. Set DISCORD_{ACCOUNT}_BOT_TOKEN'); + } + return discordRequest(token, '/users/@me'); +} + +export async function getGuilds(args: { account?: string }): Promise> { + const token = getToken(args.account ?? 'default'); + if (!token) { + throw new Error('Missing Discord credentials. Set DISCORD_{ACCOUNT}_BOT_TOKEN'); + } + return discordRequest(token, '/users/@me/guilds'); +} + +export async function getChannels(args: { guild_id: string; account?: string }): Promise> { + const token = getToken(args.account ?? 'default'); + if (!token) { + throw new Error('Missing Discord credentials. Set DISCORD_{ACCOUNT}_BOT_TOKEN'); + } + return discordRequest(token, `/guilds/${args.guild_id}/channels`); +} + +export async function sendMessage(args: { + channel_id: string; + content: string; + account?: string; +}): Promise<{ id: string; channel_id: string }> { + const token = getToken(args.account ?? 'default'); + if (!token) { + throw new Error('Missing Discord credentials. Set DISCORD_{ACCOUNT}_BOT_TOKEN'); + } + + return discordRequest(token, `/channels/${args.channel_id}/messages`, 'POST', { + content: args.content, + }); +} + +export async function getMessages(args: { + channel_id: string; + limit?: number; + account?: string; +}): Promise> { + const token = getToken(args.account ?? 'default'); + if (!token) { + throw new Error('Missing Discord credentials. Set DISCORD_{ACCOUNT}_BOT_TOKEN'); + } + + const limit = args.limit ?? 10; + return discordRequest(token, `/channels/${args.channel_id}/messages?limit=${limit}`); +} diff --git a/src/clients/instagram.ts b/src/clients/instagram.ts new file mode 100644 index 0000000..8a8461b --- /dev/null +++ b/src/clients/instagram.ts @@ -0,0 +1,139 @@ +const INSTAGRAM_API_BASE = 'https://graph.facebook.com/v18.0'; + +function getAccessToken(account: string): string { + const envKey = `INSTAGRAM_${account.toUpperCase()}_ACCESS_TOKEN`; + return process.env[envKey] ?? ''; +} + +function getBusinessAccountId(account: string): string { + const envKey = `INSTAGRAM_${account.toUpperCase()}_BUSINESS_ACCOUNT_ID`; + return process.env[envKey] ?? ''; +} + +async function instagramRequest( + endpoint: string, + accessToken: string, + method: 'GET' | 'POST' = 'GET', + params?: Record +) { + const url = new URL(`${INSTAGRAM_API_BASE}${endpoint}`); + url.searchParams.set('access_token', accessToken); + + const res = await fetch(url.toString(), { + method, + headers: params ? { 'Content-Type': 'application/json' } : undefined, + body: params ? JSON.stringify(params) : undefined, + signal: AbortSignal.timeout(15000), + }); + + if (!res.ok) { + const error = await res.text(); + throw new Error(`Instagram API error (${res.status}): ${error}`); + } + + return res.json(); +} + +export async function getProfile(args: { account?: string }): Promise<{ + id: string; + username: string; + name: string; + followers_count: number; + follows_count: number; + media_count: number; +}> { + const accessToken = getAccessToken(args.account ?? 'default'); + const businessAccountId = getBusinessAccountId(args.account ?? 'default'); + + if (!accessToken || !businessAccountId) { + throw new Error('Missing Instagram credentials. Set INSTAGRAM_{ACCOUNT}_ACCESS_TOKEN and INSTAGRAM_{ACCOUNT}_BUSINESS_ACCOUNT_ID'); + } + + const data = await instagramRequest( + `/${businessAccountId}?fields=username,name,followers_count,follows_count,media_count`, + accessToken + ); + + return { + id: data.id, + username: data.username ?? '', + name: data.name ?? '', + followers_count: data.followers_count ?? 0, + follows_count: data.follows_count ?? 0, + media_count: data.media_count ?? 0, + }; +} + +export async function getMedia(args: { limit?: number; account?: string }): Promise> { + const accessToken = getAccessToken(args.account ?? 'default'); + const businessAccountId = getBusinessAccountId(args.account ?? 'default'); + + if (!accessToken || !businessAccountId) { + throw new Error('Missing Instagram credentials. Set INSTAGRAM_{ACCOUNT}_ACCESS_TOKEN and INSTAGRAM_{ACCOUNT}_BUSINESS_ACCOUNT_ID'); + } + + const limit = args.limit ?? 10; + const data = await instagramRequest( + `/${businessAccountId}/media?fields=id,caption,media_type,media_url,permalink,timestamp&limit=${limit}`, + accessToken + ); + + return (data.data ?? []).map((item: { id: string; caption?: string; media_type: string; media_url?: string; permalink?: string; timestamp?: string }) => ({ + id: item.id, + caption: item.caption, + media_type: item.media_type, + media_url: item.media_url, + permalink: item.permalink, + timestamp: item.timestamp, + })); +} + +export async function createPost(args: { + image_url: string; + caption?: string; + account?: string; +}): Promise<{ success: boolean; media_id: string }> { + const accessToken = getAccessToken(args.account ?? 'default'); + const businessAccountId = getBusinessAccountId(args.account ?? 'default'); + + if (!accessToken || !businessAccountId) { + throw new Error('Missing Instagram credentials. Set INSTAGRAM_{ACCOUNT}_ACCESS_TOKEN and INSTAGRAM_{ACCOUNT}_BUSINESS_ACCOUNT_ID'); + } + + // Step 1: Create media container + const container = await instagramRequest( + `/${businessAccountId}/media`, + accessToken, + 'POST', + { + image_url: args.image_url, + caption: args.caption, + media_type: 'REELS', + } + ); + + const creationId = container.id; + if (!creationId) { + throw new Error('Failed to create Instagram media container'); + } + + // Step 2: Publish the container + const publish = await instagramRequest( + `/${businessAccountId}/media_publish`, + accessToken, + 'POST', + { creation_id: creationId } + ); + + return { + success: true, + media_id: publish.id, + }; +} diff --git a/src/index.ts b/src/index.ts index 809dcb0..499b221 100644 --- a/src/index.ts +++ b/src/index.ts @@ -679,6 +679,96 @@ app.get('/api/telegram/chat', requireAuth, async (req, res) => { } }); +// ── Discord REST endpoints ────────────────────────────────────── +app.get('/api/discord/me', requireAuth, async (req, res) => { + const account = req.query.account as string | undefined; + try { + const result = await handleToolCall('discord_get_me', { account }); + res.json(parseToolResult(result)); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } +}); + +app.get('/api/discord/guilds', requireAuth, async (req, res) => { + const account = req.query.account as string | undefined; + try { + const result = await handleToolCall('discord_get_guilds', { account }); + res.json(parseToolResult(result)); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } +}); + +app.get('/api/discord/channels', requireAuth, async (req, res) => { + const guild_id = req.query.guild_id as string | undefined; + const account = req.query.account as string | undefined; + if (!guild_id) { res.status(400).json({ error: 'guild_id is required' }); return; } + try { + const result = await handleToolCall('discord_get_channels', { guild_id, account }); + res.json(parseToolResult(result)); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } +}); + +app.post('/api/discord/message', requireAuth, async (req, res) => { + const { channel_id, content, account } = req.body as Record; + if (!channel_id || !content) { res.status(400).json({ error: 'channel_id and content are required' }); return; } + try { + const result = await handleToolCall('discord_send_message', { channel_id, content, account }); + res.json(parseToolResult(result)); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } +}); + +app.get('/api/discord/messages', requireAuth, async (req, res) => { + const channel_id = req.query.channel_id as string | undefined; + const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : undefined; + const account = req.query.account as string | undefined; + if (!channel_id) { res.status(400).json({ error: 'channel_id is required' }); return; } + try { + const result = await handleToolCall('discord_get_messages', { channel_id, limit, account }); + res.json(parseToolResult(result)); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } +}); + +// ── Instagram REST endpoints ──────────────────────────────────── +app.get('/api/instagram/profile', requireAuth, async (req, res) => { + const account = req.query.account as string | undefined; + try { + const result = await handleToolCall('instagram_get_profile', { account }); + res.json(parseToolResult(result)); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } +}); + +app.get('/api/instagram/media', requireAuth, async (req, res) => { + const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : undefined; + const account = req.query.account as string | undefined; + try { + const result = await handleToolCall('instagram_get_media', { limit, account }); + res.json(parseToolResult(result)); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } +}); + +app.post('/api/instagram/post', requireAuth, async (req, res) => { + const { image_url, caption, account } = req.body as Record; + if (!image_url) { res.status(400).json({ error: 'image_url is required' }); return; } + try { + const result = await handleToolCall('instagram_create_post', { image_url, caption, account }); + res.json(parseToolResult(result)); + } catch (err) { + res.status(500).json({ error: (err as Error).message }); + } +}); + app.post('/api/pilot-request', async (req, res) => { const origin = req.get('origin'); if (origin && !SQUAREMCP_ALLOWED_ORIGINS.has(origin)) { diff --git a/src/manifest.ts b/src/manifest.ts index baac04a..c53e8d6 100644 --- a/src/manifest.ts +++ b/src/manifest.ts @@ -703,6 +703,214 @@ export function getManifest(serverUrl: string, authEnabled: boolean) { examples: [{ chat_id: '@mychannel', account: 'default' }], }, + // ── Discord tools ────────────────────────────────────────────────────── + { + name: 'discord_get_me', + category: 'discord', + description: 'Get information about the Discord bot', + when_to_use: + 'User asks about the Discord bot, wants to verify it is connected, or needs the bot username.', + input_schema: { + type: 'object', + properties: { + account: { type: 'string', description: 'Which Discord account to use (default: "default")' }, + }, + }, + returns: { + type: 'object', + properties: { + id: { type: 'string', description: 'Bot user ID' }, + username: { type: 'string' }, + bot: { type: 'boolean' }, + }, + }, + examples: [{ account: 'default' }], + }, + { + name: 'discord_get_guilds', + category: 'discord', + description: 'List Discord servers the bot is in', + when_to_use: + 'User wants to know which Discord servers are available or needs a server ID.', + input_schema: { + type: 'object', + properties: { + account: { type: 'string', description: 'Which Discord account to use (default: "default")' }, + }, + }, + returns: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Guild ID' }, + name: { type: 'string', description: 'Server name' }, + }, + }, + }, + examples: [{ account: 'default' }], + }, + { + name: 'discord_get_channels', + category: 'discord', + description: 'List channels in a Discord server', + when_to_use: + 'User needs to find a channel ID to send a message to.', + input_schema: { + 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")' }, + }, + }, + returns: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Channel ID' }, + name: { type: 'string' }, + type: { type: 'number', description: 'Channel type code' }, + }, + }, + }, + examples: [{ guild_id: '1234567890123456789', account: 'default' }], + }, + { + name: 'discord_send_message', + category: 'discord', + description: 'Send a message to a Discord channel', + when_to_use: + 'User wants to post a message in a Discord server channel.', + input_schema: { + type: 'object', + required: ['channel_id', 'content'], + properties: { + channel_id: { type: 'string', description: 'Discord channel ID' }, + content: { type: 'string', description: 'Message content (max 2000 chars)' }, + account: { type: 'string', description: 'Which Discord account to use (default: "default")' }, + }, + }, + returns: { + type: 'object', + properties: { + id: { type: 'string', description: 'Message ID' }, + channel_id: { type: 'string' }, + }, + }, + examples: [{ channel_id: '1234567890123456789', content: 'Hello Discord!', account: 'default' }], + }, + { + name: 'discord_get_messages', + category: 'discord', + description: 'Get recent messages from a Discord channel', + when_to_use: + 'User wants to read recent chat history from a Discord channel.', + input_schema: { + type: 'object', + required: ['channel_id'], + properties: { + channel_id: { type: 'string', description: 'Discord channel ID' }, + limit: { type: 'number', description: 'Max messages (default: 10, max: 100)' }, + account: { type: 'string', description: 'Which Discord account to use (default: "default")' }, + }, + }, + returns: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'string', description: 'Message ID' }, + content: { type: 'string' }, + author: { type: 'object', properties: { username: { type: 'string' }, id: { type: 'string' } } }, + timestamp: { type: 'string', format: 'date-time' }, + }, + }, + }, + examples: [{ channel_id: '1234567890123456789', limit: 10, account: 'default' }], + }, + + // ── Instagram tools ──────────────────────────────────────────────────── + { + name: 'instagram_get_profile', + category: 'instagram', + description: 'Get Instagram Business/Creator account profile', + when_to_use: + 'User asks about their Instagram stats, followers, or account details.', + input_schema: { + type: 'object', + properties: { + account: { type: 'string', description: 'Which Instagram account to use (default: "default")' }, + }, + }, + returns: { + type: 'object', + properties: { + id: { type: 'string' }, + username: { type: 'string' }, + name: { type: 'string' }, + followers_count: { type: 'number' }, + follows_count: { type: 'number' }, + media_count: { type: 'number' }, + }, + }, + examples: [{ account: 'default' }], + }, + { + name: 'instagram_get_media', + category: 'instagram', + description: 'Get recent posts from Instagram', + when_to_use: + 'User wants to see their recent Instagram content.', + input_schema: { + type: 'object', + properties: { + limit: { type: 'number', description: 'Max posts (default: 10)' }, + account: { type: 'string', description: 'Which Instagram account to use (default: "default")' }, + }, + }, + returns: { + type: 'array', + items: { + type: 'object', + properties: { + id: { type: 'string' }, + caption: { type: 'string' }, + media_type: { type: 'string' }, + media_url: { type: 'string' }, + permalink: { type: 'string' }, + timestamp: { type: 'string' }, + }, + }, + }, + examples: [{ limit: 5, account: 'default' }], + }, + { + name: 'instagram_create_post', + category: 'instagram', + description: 'Create a post on Instagram', + when_to_use: + 'User wants to publish an image to their Instagram Business/Creator account.', + input_schema: { + type: 'object', + required: ['image_url'], + properties: { + image_url: { type: 'string', description: 'Publicly accessible image URL' }, + caption: { type: 'string', description: 'Post caption' }, + account: { type: 'string', description: 'Which Instagram account to use (default: "default")' }, + }, + }, + returns: { + type: 'object', + properties: { + success: { type: 'boolean' }, + media_id: { type: 'string' }, + }, + }, + examples: [{ image_url: 'https://example.com/photo.jpg', caption: 'New post!', account: 'default' }], + }, + // ── Obsidian tools ────────────────────────────────────────────────────── { name: 'obsidian_search_notes', @@ -874,6 +1082,14 @@ export function getManifest(serverUrl: string, authEnabled: boolean) { description: 'Telegram messaging via Bot API', icon: '✈️', }, + discord: { + description: 'Discord server messaging via Bot API', + icon: '🎮', + }, + instagram: { + description: 'Instagram Business/Creator account via Graph API', + icon: '📸', + }, }, }; } diff --git a/src/tools.ts b/src/tools.ts index 7a4fa78..ca5ec5c 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -5,6 +5,8 @@ import { searchNotes, getNote, appendToNote, updateNote, getSyncStatus } from '. import { sendMessage, sendTemplate, getMessageStatus, listTemplates } from './clients/whatsapp.js'; import { getProfile as getLinkedInProfile, createPost as createLinkedInPost, 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, createPost as createInstagramPost } from './clients/instagram.js'; const ACCOUNT_PARAM = { account: { @@ -353,6 +355,110 @@ export const tools: Tool[] = [ }, }, }, + + // ── 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")' }, + }, + }, + }, + + // ── 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 a 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")' }, + }, + }, + }, ]; function acct(args: Record): Account { @@ -525,6 +631,64 @@ export async function handleToolCall( }); break; + // ── Discord ───────────────────────────────────────────────── + case 'discord_get_me': + result = await getDiscordMe({ + account: args.account as string | undefined, + }); + break; + + case 'discord_get_guilds': + result = await getGuilds({ + account: args.account as string | undefined, + }); + break; + + case 'discord_get_channels': + result = await getChannels({ + guild_id: args.guild_id as string, + account: args.account as string | undefined, + }); + 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, + }); + 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, + }); + break; + + // ── Instagram ─────────────────────────────────────────────── + case 'instagram_get_profile': + result = await getInstagramProfile({ + account: args.account as string | undefined, + }); + break; + + case 'instagram_get_media': + result = await getInstagramMedia({ + limit: (args.limit as number) ?? 10, + account: args.account as string | undefined, + }); + 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, + }); + break; + // Legacy Yahoo-prefixed names — keep working for any cached Claude sessions case 'yahoo_get_profile': result = await getProfile('yahoo');