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
This commit is contained in:
Garfield
2026-05-05 22:01:21 -04:00
parent 385f91de4d
commit e1e7d88c8a
6 changed files with 724 additions and 0 deletions

View File

@@ -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}_*

101
src/clients/discord.ts Normal file
View File

@@ -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<Array<{
id: string;
name: string;
icon?: string;
}>> {
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<Array<{
id: string;
name: string;
type: number;
}>> {
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<Array<{
id: string;
content: string;
author: { username: string; id: string };
timestamp: string;
}>> {
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}`);
}

139
src/clients/instagram.ts Normal file
View File

@@ -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<string, unknown>
) {
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<Array<{
id: string;
caption?: string;
media_type: string;
media_url?: string;
permalink?: string;
timestamp?: 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');
}
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,
};
}

View File

@@ -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<string, unknown>;
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<string, unknown>;
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)) {

View File

@@ -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: '📸',
},
},
};
}

View File

@@ -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<string, unknown>): 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');