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:
142
src/clients/linkedin.ts
Normal file
142
src/clients/linkedin.ts
Normal 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
145
src/clients/whatsapp.ts
Normal 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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
76
src/index.ts
76
src/index.ts
@@ -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)) {
|
||||
|
||||
185
src/manifest.ts
185
src/manifest.ts
@@ -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: '🔗',
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
172
src/tools.ts
172
src/tools.ts
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user