feat: WhatsApp + LinkedIn integrations, SquareMCP rebrand, opencode docs

WhatsApp Business API (Meta Cloud API)
- New client: src/clients/whatsapp.ts
- Tools: whatsapp_send_message, whatsapp_send_template, whatsapp_list_templates
- REST endpoints: POST /api/whatsapp/send, POST /api/whatsapp/template, GET /api/whatsapp/templates
- Multi-account env var pattern: WHATSAPP_{ACCOUNT}_*

LinkedIn API (OpenID Connect)
- New client: src/clients/linkedin.ts
- Tools: linkedin_get_profile, linkedin_create_post, linkedin_search_connections, linkedin_send_message
- REST endpoints: GET /api/linkedin/profile, POST /api/linkedin/post, POST /api/linkedin/search-connections, POST /api/linkedin/message
- Multi-account env var pattern: LINKEDIN_{ACCOUNT}_*
- Uses /v2/userinfo (OpenID Connect) for profile reads

Domain migration
- hermes.fetcherpay.com -> hermes.squaremcp.com
- Updated K8s ingress, TLS cert, SERVER_URL env var
- Updated OPENCODE.md and opencode.json references

SquareMCP site
- Added logo assets (SVG, LinkedIn variants)
- Added terms.html
- Updated Dockerfile, nginx config, styles, index, privacy pages

Docs
- Added OPENCODE.md for opencode AI integration setup
- Updated .env.example with WhatsApp and LinkedIn credentials
- Added opencode.json to .gitignore (contains live API key)

Total tools: 19 (email 6, obsidian 5, whatsapp 4, linkedin 4)
This commit is contained in:
Garfield
2026-05-05 01:25:26 -04:00
parent e3a272c332
commit 73f83c0d86
18 changed files with 1207 additions and 45 deletions

142
src/clients/linkedin.ts Normal file
View File

@@ -0,0 +1,142 @@
const LINKEDIN_API_BASE = 'https://api.linkedin.com/v2';
function getEnvVar(account: string, key: string): string {
const envKey = `LINKEDIN_${account.toUpperCase()}_${key}`;
return process.env[envKey] ?? '';
}
function getAccessToken(account: string): string {
return getEnvVar(account, 'ACCESS_TOKEN');
}
async function linkedinRequest(
endpoint: string,
accessToken: string,
method: 'GET' | 'POST' = 'GET',
body?: unknown
) {
const url = `${LINKEDIN_API_BASE}${endpoint}`;
const res = await fetch(url, {
method,
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'X-Restli-Protocol-Version': '2.0.0',
},
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(15000),
});
if (!res.ok) {
const error = await res.text();
throw new Error(`LinkedIn API error (${res.status}): ${error}`);
}
return res.json();
}
export async function getProfile(args: { account?: string }): Promise<{
id: string;
firstName: string;
lastName: string;
email: string;
picture?: string;
}> {
const accessToken = getAccessToken(args.account ?? 'default');
if (!accessToken) {
throw new Error('Missing LinkedIn credentials. Set LINKEDIN_{ACCOUNT}_ACCESS_TOKEN');
}
// OpenID Connect userinfo endpoint (works with profile scope)
const res = await fetch(`${LINKEDIN_API_BASE}/userinfo`, {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
signal: AbortSignal.timeout(15000),
});
if (!res.ok) {
const error = await res.text();
throw new Error(`LinkedIn API error (${res.status}): ${error}`);
}
const data = await res.json();
return {
id: data.sub,
firstName: data.given_name ?? '',
lastName: data.family_name ?? '',
email: data.email ?? '',
picture: data.picture ?? '',
};
}
export async function createPost(args: {
text: string;
visibility?: 'PUBLIC' | 'CONNECTIONS';
account?: string;
}): Promise<{ success: boolean; post_id: string; url: string }> {
const accessToken = getAccessToken(args.account ?? 'default');
if (!accessToken) {
throw new Error('Missing LinkedIn credentials. Set LINKEDIN_{ACCOUNT}_ACCESS_TOKEN');
}
const profile = await getProfile({ account: args.account });
const authorUrn = `urn:li:person:${profile.id}`;
const body = {
author: authorUrn,
lifecycleState: 'PUBLISHED',
specificContent: {
'com.linkedin.ugc.ShareContent': {
shareCommentary: {
text: args.text,
},
shareMediaCategory: 'NONE',
},
},
visibility: {
'com.linkedin.ugc.MemberNetworkVisibility': args.visibility ?? 'PUBLIC',
},
};
const data = await linkedinRequest('/ugcPosts', accessToken, 'POST', body);
const postId = data.id ?? '';
return {
success: true,
post_id: postId,
url: postId ? `https://www.linkedin.com/feed/update/${postId}` : '',
};
}
export async function searchConnections(args: {
keywords?: string;
account?: string;
}): Promise<{ message: string }> {
const accessToken = getAccessToken(args.account ?? 'default');
if (!accessToken) {
throw new Error('Missing LinkedIn credentials. Set LINKEDIN_{ACCOUNT}_ACCESS_TOKEN');
}
throw new Error(
'LinkedIn connections search requires the LinkedIn Partnership Program. ' +
'Public API access to connections was removed. ' +
'Apply at https://developer.linkedin.com/partner-programs'
);
}
export async function sendMessage(args: {
recipient_id: string;
message: string;
account?: string;
}): Promise<{ message: string }> {
const accessToken = getAccessToken(args.account ?? 'default');
if (!accessToken) {
throw new Error('Missing LinkedIn credentials. Set LINKEDIN_{ACCOUNT}_ACCESS_TOKEN');
}
throw new Error(
'LinkedIn messaging requires the LinkedIn Partnership Program. ' +
'Direct messaging is not available through the public API. ' +
'Apply at https://developer.linkedin.com/partner-programs'
);
}

145
src/clients/whatsapp.ts Normal file
View File

@@ -0,0 +1,145 @@
const WHATSAPP_API_VERSION = 'v18.0';
const WHATSAPP_BASE_URL = process.env['WHATSAPP_API_BASE_URL'] ?? 'https://graph.facebook.com';
function getEnvVar(prefix: string, account: string, key: string): string {
const envKey = `WHATSAPP_${account.toUpperCase()}_${key}`;
return process.env[envKey] ?? '';
}
function getPhoneNumberId(account: string): string {
return getEnvVar('WHATSAPP', account, 'PHONE_NUMBER_ID');
}
function getAccessToken(account: string): string {
return getEnvVar('WHATSAPP', account, 'ACCESS_TOKEN');
}
function getBusinessAccountId(account: string): string {
return getEnvVar('WHATSAPP', account, 'BUSINESS_ACCOUNT_ID');
}
interface WhatsAppMessageResponse {
messaging_product: string;
contacts?: Array<{ wa_id: string; input: string }>;
messages?: Array<{ id: string }>;
}
async function whatsappApiRequest(
phoneId: string,
accessToken: string,
endpoint: string,
method: 'GET' | 'POST' = 'POST',
body?: unknown
) {
const url = `${WHATSAPP_BASE_URL}/${WHATSAPP_API_VERSION}/${phoneId}/${endpoint}`;
const res = await fetch(url, {
method,
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(10000),
});
if (!res.ok) {
const error = await res.text();
throw new Error(`WhatsApp API error (${res.status}): ${error}`);
}
return res.json();
}
export async function sendMessage(args: { to: string; message: string; account?: string }): Promise<{ success: boolean; message_id: string }> {
const phoneId = getPhoneNumberId(args.account ?? 'default');
const accessToken = getAccessToken(args.account ?? 'default');
if (!phoneId || !accessToken) {
throw new Error('Missing WhatsApp credentials. Set WHATSAPP_{ACCOUNT}_PHONE_NUMBER_ID and WHATSAPP_{ACCOUNT}_ACCESS_TOKEN');
}
const body = {
messaging_product: 'whatsapp',
to: args.to,
type: 'text',
text: { body: args.message },
};
const data = await whatsappApiRequest(phoneId, accessToken, 'messages', 'POST', body);
const response = data as WhatsAppMessageResponse;
return { success: true, message_id: response.messages?.[0]?.id ?? '' };
}
export async function sendTemplate(args: { to: string; template_name: string; language?: string; components?: unknown[]; account?: string }): Promise<{ success: boolean; message_id: string }> {
const phoneId = getPhoneNumberId(args.account ?? 'default');
const accessToken = getAccessToken(args.account ?? 'default');
if (!phoneId || !accessToken) {
throw new Error('Missing WhatsApp credentials. Set WHATSAPP_{ACCOUNT}_PHONE_NUMBER_ID and WHATSAPP_{ACCOUNT}_ACCESS_TOKEN');
}
const body: Record<string, unknown> = {
messaging_product: 'whatsapp',
to: args.to,
type: 'template',
template: {
name: args.template_name,
language: { code: args.language ?? 'en' },
},
};
if (args.components) {
(body.template as Record<string, unknown>).components = args.components;
}
const data = await whatsappApiRequest(phoneId, accessToken, 'messages', 'POST', body);
const response = data as WhatsAppMessageResponse;
return { success: true, message_id: response.messages?.[0]?.id ?? '' };
}
export async function getMessageStatus(args: { message_id: string; account?: string }): Promise<{ message_id: string; status: string; timestamp?: string }> {
const phoneId = getPhoneNumberId(args.account ?? 'default');
const accessToken = getAccessToken(args.account ?? 'default');
if (!phoneId || !accessToken) {
throw new Error('Missing WhatsApp credentials. Set WHATSAPP_{ACCOUNT}_PHONE_NUMBER_ID and WHATSAPP_{ACCOUNT}_ACCESS_TOKEN');
}
// Note: Meta Cloud API doesn't support polling message status via GET
// Status updates are only available via webhooks (push-based)
throw new Error('whatsapp_get_message_status is not supported. Meta Cloud API only provides delivery status via webhooks. Use POST /api/whatsapp/webhook to receive status updates.');
}
export async function listTemplates(args: { account?: string }): Promise<{ templates: Array<{ name: string; language: string; status: string }> }> {
const account = args.account ?? 'default';
const businessAccountId = getBusinessAccountId(account);
const accessToken = getAccessToken(account);
if (!businessAccountId || !accessToken) {
throw new Error('Missing WhatsApp credentials. Set WHATSAPP_{ACCOUNT}_BUSINESS_ACCOUNT_ID and WHATSAPP_{ACCOUNT}_ACCESS_TOKEN');
}
const url = `${WHATSAPP_BASE_URL}/${WHATSAPP_API_VERSION}/${businessAccountId}/message_templates?fields=name,language,status`;
const res = await fetch(url, {
headers: {
'Authorization': `Bearer ${accessToken}`,
},
signal: AbortSignal.timeout(10000),
});
if (!res.ok) {
const error = await res.text();
throw new Error(`WhatsApp API error (${res.status}): ${error}`);
}
const data = await res.json();
return {
templates: (data.data ?? []).map((t: { name: string; language: string | { code: string }; status: string }) => ({
name: t.name,
language: typeof t.language === 'string' ? t.language : t.language?.code ?? 'en',
status: t.status,
})),
};
}

View File

@@ -547,6 +547,82 @@ app.get('/api/obsidian/sync', requireAuth, async (_req, res) => {
}
});
// ── WhatsApp Business API REST endpoints ──────────────────────────
app.post('/api/whatsapp/send', requireAuth, async (req, res) => {
const { to, message, account } = req.body as Record<string, unknown>;
if (!to || !message) { res.status(400).json({ error: 'to and message are required' }); return; }
try {
const result = await handleToolCall('whatsapp_send_message', { to, message, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
app.post('/api/whatsapp/template', requireAuth, async (req, res) => {
const { to, template_name, language, components, account } = req.body as Record<string, unknown>;
if (!to || !template_name) { res.status(400).json({ error: 'to and template_name are required' }); return; }
try {
const result = await handleToolCall('whatsapp_send_template', { to, template_name, language, components, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
app.get('/api/whatsapp/templates', requireAuth, async (req, res) => {
const account = req.query.account as string | undefined;
try {
const result = await handleToolCall('whatsapp_list_templates', { account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
// ── LinkedIn REST endpoints ─────────────────────────────────────
app.get('/api/linkedin/profile', requireAuth, async (req, res) => {
const account = req.query.account as string | undefined;
try {
const result = await handleToolCall('linkedin_get_profile', { account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
app.post('/api/linkedin/post', requireAuth, async (req, res) => {
const { text, visibility, account } = req.body as Record<string, unknown>;
if (!text) { res.status(400).json({ error: 'text is required' }); return; }
try {
const result = await handleToolCall('linkedin_create_post', { text, visibility, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
app.post('/api/linkedin/search-connections', requireAuth, async (req, res) => {
const { keywords, account } = req.body as Record<string, unknown>;
try {
const result = await handleToolCall('linkedin_search_connections', { keywords, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
app.post('/api/linkedin/message', requireAuth, async (req, res) => {
const { recipient_id, message, account } = req.body as Record<string, unknown>;
if (!recipient_id || !message) { res.status(400).json({ error: 'recipient_id and message are required' }); return; }
try {
const result = await handleToolCall('linkedin_send_message', { recipient_id, message, 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

@@ -390,6 +390,183 @@ export function getManifest(serverUrl: string, authEnabled: boolean) {
],
},
// ── WhatsApp Business API tools ───────────────────────────────────────
{
name: 'whatsapp_send_message',
category: 'whatsapp',
description: 'Send a WhatsApp text message to a phone number',
when_to_use:
'User asks to send a WhatsApp message, text someone on WhatsApp, or notify via WhatsApp. Only works if the recipient has messaged you within the last 24 hours.',
input_schema: {
type: 'object',
required: ['to', 'message'],
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")' },
},
},
returns: {
type: 'object',
properties: {
success: { type: 'boolean' },
message_id: { type: 'string', description: 'WhatsApp message ID' },
},
},
examples: [{ to: '+1234567890', message: 'Hello from Hermes', account: 'default' }],
},
{
name: 'whatsapp_send_template',
category: 'whatsapp',
description: 'Send an approved WhatsApp template message',
when_to_use:
'User wants to send a structured notification or alert via an approved WhatsApp template. Required when outside the 24-hour customer-service window.',
input_schema: {
type: 'object',
required: ['to', 'template_name'],
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', description: 'Template components (header, body, buttons) with parameters' },
account: { type: 'string', description: 'Which WhatsApp account to use (default: "default")' },
},
},
returns: {
type: 'object',
properties: {
success: { type: 'boolean' },
message_id: { type: 'string', description: 'WhatsApp message ID' },
},
},
examples: [{ to: '+1234567890', template_name: 'hello_world', language: 'en', account: 'default' }],
},
{
name: 'whatsapp_list_templates',
category: 'whatsapp',
description: 'List all approved WhatsApp message templates for the business account',
when_to_use:
'User asks what WhatsApp templates are available or wants to know which templates can be sent.',
input_schema: {
type: 'object',
properties: {
account: { type: 'string', description: 'Which WhatsApp account to use (default: "default")' },
},
},
returns: {
type: 'object',
properties: {
templates: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
language: { type: 'string' },
status: { type: 'string' },
},
},
},
},
},
examples: [{ account: 'default' }],
},
// ── LinkedIn tools ──────────────────────────────────────────────────────
{
name: 'linkedin_get_profile',
category: 'linkedin',
description: 'Get the LinkedIn profile of the authenticated user',
when_to_use:
'User asks about their LinkedIn profile, name, headline, or wants to verify which LinkedIn account is connected.',
input_schema: {
type: 'object',
properties: {
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
},
},
returns: {
type: 'object',
properties: {
id: { type: 'string', description: 'LinkedIn person ID (OpenID sub)' },
firstName: { type: 'string' },
lastName: { type: 'string' },
email: { type: 'string' },
picture: { type: 'string', description: 'Profile photo URL' },
},
},
examples: [{ account: 'default' }],
},
{
name: 'linkedin_create_post',
category: 'linkedin',
description: 'Create a post on LinkedIn',
when_to_use:
'User wants to publish an update, share content, or post to their LinkedIn feed.',
input_schema: {
type: 'object',
required: ['text'],
properties: {
text: { type: 'string', description: 'Post content text' },
visibility: { type: 'string', enum: ['PUBLIC', 'CONNECTIONS'], description: 'PUBLIC (anyone) or CONNECTIONS (1st degree only)' },
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
},
},
returns: {
type: 'object',
properties: {
success: { type: 'boolean' },
post_id: { type: 'string' },
url: { type: 'string', description: 'Direct link to the post' },
},
},
examples: [{ text: 'Excited to share our latest product update!', visibility: 'PUBLIC', account: 'default' }],
},
{
name: 'linkedin_search_connections',
category: 'linkedin',
description: 'Search LinkedIn connections [REQUIRES PARTNERSHIP]',
when_to_use:
'User wants to search their LinkedIn network. NOTE: Requires LinkedIn Partnership Program — public API access was removed.',
input_schema: {
type: 'object',
properties: {
keywords: { type: 'string', description: 'Search keywords' },
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
},
},
returns: {
type: 'object',
properties: {
message: { type: 'string', description: 'Error or guidance message' },
},
},
examples: [{ keywords: 'software engineer', account: 'default' }],
},
{
name: 'linkedin_send_message',
category: 'linkedin',
description: 'Send a direct message on LinkedIn [REQUIRES PARTNERSHIP]',
when_to_use:
'User wants to send a LinkedIn DM. NOTE: Requires LinkedIn Partnership Program — messaging is not available through the public API.',
input_schema: {
type: 'object',
required: ['recipient_id', 'message'],
properties: {
recipient_id: { type: 'string', description: 'LinkedIn person URN or ID' },
message: { type: 'string', description: 'Message text' },
account: { type: 'string', description: 'Which LinkedIn account to use (default: "default")' },
},
},
returns: {
type: 'object',
properties: {
message: { type: 'string', description: 'Error or guidance message' },
},
},
examples: [{ recipient_id: 'urn:li:person:abc123', message: 'Hi, thanks for connecting!', account: 'default' }],
},
// ── Obsidian tools ──────────────────────────────────────────────────────
{
name: 'obsidian_search_notes',
@@ -549,6 +726,14 @@ export function getManifest(serverUrl: string, authEnabled: boolean) {
description: 'Email operations for Yahoo, FetcherPay, and Gmail accounts',
icon: '📧',
},
whatsapp: {
description: 'WhatsApp Business API messaging via Meta Cloud API',
icon: '💬',
},
linkedin: {
description: 'LinkedIn profile and posting via LinkedIn API',
icon: '🔗',
},
},
};
}

View File

@@ -2,6 +2,8 @@ import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { searchMessages, readMessage, getProfile, listFolders, type Account } from './imap.js';
import { sendEmail, createDraft } from './smtp.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, searchConnections, sendMessage as sendLinkedInMessage } from './clients/linkedin.js';
const ACCOUNT_PARAM = {
account: {
@@ -174,6 +176,114 @@ export const tools: Tool[] = [
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', 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_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'],
},
},
];
function acct(args: Record<string, unknown>): Account {
@@ -245,6 +355,68 @@ export async function handleToolCall(
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,
});
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,
});
break;
case 'whatsapp_get_message_status':
result = await getMessageStatus({
message_id: args.message_id as string,
account: args.account as string | undefined,
});
break;
case 'whatsapp_list_templates':
result = await listTemplates({
account: args.account as string | undefined,
});
break;
// ── LinkedIn ───────────────────────────────────────────────
case 'linkedin_get_profile':
result = await getLinkedInProfile({
account: args.account as string | undefined,
});
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,
});
break;
case 'linkedin_search_connections':
result = await searchConnections({
keywords: args.keywords as string | undefined,
account: args.account as string | undefined,
});
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,
});
break;
// Legacy Yahoo-prefixed names — keep working for any cached Claude sessions
case 'yahoo_get_profile':
result = await getProfile('yahoo');