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

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');