Files
hermes-mcp/src/clients/whatsapp.ts
Garfield 73f83c0d86 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)
2026-05-05 01:25:26 -04:00

146 lines
5.1 KiB
TypeScript

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,
})),
};
}