feat(saas): full SquareMCP SaaS platform v1

- JWT auth with bcrypt password hashing, cookie sessions, forgot/reset password
- Per-user encrypted credential storage (Redis + AES-256-GCM) for all 9 platforms
- Usage tracking with monthly limits per plan (free/starter/growth/enterprise)
- Invoice generation and retrieval (admin + user views)
- Admin panel with customer listing (role-based access)
- Web app UI at app.squaremcp.com — login, dashboard, connections, usage, invoices
- Unified auth middleware: API key, OAuth Bearer, and JWT cookie support
- Facebook Graph API fixes: published_posts endpoint, photo/video post support
- TikTok sandbox compliance: SELF_ONLY privacy for unaudited apps
- URL verification files for TikTok app review
This commit is contained in:
Garfield
2026-05-13 08:42:33 -04:00
parent 7796de12bf
commit a5e4c55885
46 changed files with 4054 additions and 171 deletions

View File

@@ -1,6 +1,7 @@
import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { SSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
@@ -14,7 +15,7 @@ import { tools, handleToolCall } from './tools.js';
import { getManifest, getOpenApiSpec, getOpenApiSpecMail, getOpenApiSpecSocial } from './manifest.js';
import { routeWhatsAppWebhook, registerWhatsAppNumber, type RoutedWebhookEvent } from './multitenancy/webhook-router.js';
import { storeCredential, type Platform } from './multitenancy/credential-store.js';
import { meterMiddleware, type Customer } from './billing/middleware.js';
import { meterMiddleware, resolveCustomerByApiKey, resolveCustomerById, type Customer } from './billing/middleware.js';
import {
registerClient,
getClient,
@@ -23,9 +24,13 @@ import {
validateAccessToken,
getAuthorizeHtml,
} from './oauth.js';
import { initDatabase } from './db.js';
import { initDatabase, getPool } from './db.js';
import { hashPassword, verifyPassword, signJWT, verifyJWT, findCustomerByEmail, createCustomer, setResetToken, findCustomerByResetToken, clearResetToken, updatePassword } from './auth.js';
import { recordUsage, getMonthlyUsage, getUsageBreakdown, checkLimit } from './billing/usage.js';
import { getCustomerInvoices, getInvoiceByNumber, markInvoiceSent, markInvoicePaid, generateMonthlyInvoice } from './billing/invoices.js';
const app = express();
app.use(cookieParser());
app.use(cors({
origin: '*',
methods: ['GET', 'POST', 'DELETE', 'OPTIONS'],
@@ -152,14 +157,14 @@ async function appendPilotRequestToVault(requestId: string, body: PilotRequestBo
const content = formatPilotRequestMarkdown(requestId, body, req);
const dailyNotePath = `Daily Notes/${getEasternDateString()}.md`;
await handleToolCall('obsidian_append_to_note', {
await callTool(req, 'obsidian_append_to_note', {
path: 'SquareMCP/Pilot Requests.md',
header: 'Pilot Requests',
content,
create_if_missing: true,
});
await handleToolCall('obsidian_append_to_note', {
await callTool(req, 'obsidian_append_to_note', {
path: dailyNotePath,
header: 'SquareMCP Pilot Requests',
content,
@@ -302,19 +307,48 @@ async function requireAuth(req: express.Request, res: express.Response, next: ex
// No API key configured = open access
if (!API_KEY) return next();
// 1. Check x-api-key header or query param (backward compatibility)
// 1. Check x-api-key header or query param (backward compatibility — global key)
const apiKeyProvided = (req.headers['x-api-key'] as string | undefined) || (req.query.key as string | undefined);
if (apiKeyProvided === API_KEY) return next();
// 2. Check OAuth Bearer token
// 2. Check customer API key (per-user SaaS auth)
if (apiKeyProvided) {
const customer = await resolveCustomerByApiKey(apiKeyProvided);
if (customer && customer.active) {
(req as express.Request & { customer?: Customer }).customer = customer;
return next();
}
}
// 3. Check OAuth Bearer token
const bearerToken = extractBearerToken(req);
if (bearerToken && await validateAccessToken(bearerToken)) return next();
// 4. Check JWT session cookie (web app auth)
const jwtCookie = req.cookies?.session;
if (jwtCookie) {
try {
const payload = verifyJWT(jwtCookie);
const customer = await resolveCustomerById(payload.sub);
if (customer && customer.active) {
(req as express.Request & { customer?: Customer; jwtUser?: { id: string; email: string; plan: string } }).customer = customer;
(req as express.Request & { jwtUser?: { id: string; email: string; plan: string } }).jwtUser = {
id: payload.sub,
email: payload.email,
plan: payload.plan,
};
return next();
}
} catch {
// invalid JWT, fall through to 401
}
}
res.setHeader(
'WWW-Authenticate',
`Bearer realm="hermes", resource_metadata="${PROTECTED_RESOURCE_METADATA_URL}"`
);
res.status(401).json({ error: 'Unauthorized — provide x-api-key header, ?key= query param, or Bearer token' });
res.status(401).json({ error: 'Unauthorized — provide x-api-key header, ?key= query param, Bearer token, or session cookie' });
} catch (err) {
next(err);
}
@@ -717,7 +751,7 @@ app.post('/hermes-mcp/:linkSegment/:toolName', requireAuth, async (req, res) =>
const args = (req.body ?? {}) as Record<string, unknown>;
console.log(`[chatgpt-mcp] ${toolName}`, JSON.stringify(args).substring(0, 200));
try {
const result = await handleToolCall(toolName, args);
const result = await callTool(req, toolName, args);
const text = result.content[0].text;
if (text.startsWith('Error:')) {
res.status(400).json({ error: text.slice(7).trim() });
@@ -734,7 +768,7 @@ app.get('/hermes-mcp/:linkSegment/:toolName', requireAuth, async (req, res) => {
const args = req.query as Record<string, unknown>;
console.log(`[chatgpt-mcp] GET ${toolName}`, JSON.stringify(args).substring(0, 200));
try {
const result = await handleToolCall(toolName, args);
const result = await callTool(req, toolName, args);
const text = result.content[0].text;
if (text.startsWith('Error:')) {
res.status(400).json({ error: text.slice(7).trim() });
@@ -799,7 +833,7 @@ app.get('/api/obsidian/search', requireAuth, async (req, res) => {
const tagsRaw = req.query.tags as string | undefined;
const tags = tagsRaw ? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean) : undefined;
try {
const result = await handleToolCall('obsidian_search_notes', { query, limit, path_filter, tags });
const result = await callTool(req, 'obsidian_search_notes', { query, limit, path_filter, tags });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -810,7 +844,7 @@ app.get('/api/obsidian/note', requireAuth, async (req, res) => {
const path = req.query.path as string | undefined;
if (!path) { res.status(400).json({ error: 'path is required' }); return; }
try {
const result = await handleToolCall('obsidian_read_note', { path });
const result = await callTool(req, 'obsidian_read_note', { path });
res.json(parseToolResult(result));
} catch (err) {
const msg = (err as Error).message;
@@ -822,7 +856,7 @@ app.post('/api/obsidian/note/append', requireAuth, async (req, res) => {
const { path, content, header, create_if_missing } = req.body as Record<string, unknown>;
if (!path || !content) { res.status(400).json({ error: 'path and content are required' }); return; }
try {
const result = await handleToolCall('obsidian_append_to_note', { path, content, header, create_if_missing });
const result = await callTool(req, 'obsidian_append_to_note', { path, content, header, create_if_missing });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -833,16 +867,16 @@ app.put('/api/obsidian/note', requireAuth, async (req, res) => {
const { path, content } = req.body as Record<string, unknown>;
if (!path || !content) { res.status(400).json({ error: 'path and content are required' }); return; }
try {
const result = await handleToolCall('obsidian_update_note', { path, content });
const result = await callTool(req, 'obsidian_update_note', { path, content });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
app.get('/api/obsidian/sync', requireAuth, async (_req, res) => {
app.get('/api/obsidian/sync', requireAuth, async (req, res) => {
try {
const result = await handleToolCall('obsidian_sync_status', {});
const result = await callTool(req, 'obsidian_sync_status', {});
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -854,7 +888,7 @@ 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 });
const result = await callTool(req, 'whatsapp_send_message', { to, message, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -865,7 +899,7 @@ 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 });
const result = await callTool(req, 'whatsapp_send_template', { to, template_name, language, components, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -875,7 +909,7 @@ app.post('/api/whatsapp/template', requireAuth, async (req, res) => {
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 });
const result = await callTool(req, 'whatsapp_list_templates', { account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -916,6 +950,93 @@ app.post('/webhook/whatsapp', express.json(), async (req, res) => {
}
});
// ── Auth endpoints ──────────────────────────────────────────────
app.post('/api/auth/signup', express.json(), async (req, res) => {
const { email, password } = req.body as Record<string, string>;
if (!email || !password) {
res.status(400).json({ error: 'Email and password required' });
return;
}
if (password.length < 8) {
res.status(400).json({ error: 'Password must be at least 8 characters' });
return;
}
const existing = await findCustomerByEmail(email);
if (existing) {
res.status(409).json({ error: 'Email already registered' });
return;
}
const id = crypto.randomUUID();
const apiKey = crypto.randomUUID().replace(/-/g, '');
const passwordHash = await hashPassword(password);
try {
// Check if this is the first user — make them admin
const [countRows] = await getPool().query<any[]>('SELECT COUNT(*) as c FROM customers');
const isFirstUser = countRows[0]?.c === 0;
await createCustomer(id, email, passwordHash, apiKey);
if (isFirstUser) {
await getPool().query("UPDATE customers SET role = 'admin', plan = 'enterprise' WHERE id = ?", [id]);
}
const token = signJWT({ sub: id, email, plan: isFirstUser ? 'enterprise' : 'free' });
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});
res.status(201).json({ id, email, plan: 'free', api_key: apiKey });
} catch (err) {
res.status(500).json({ error: 'Failed to create account' });
}
});
app.post('/api/auth/login', express.json(), async (req, res) => {
const { email, password } = req.body as Record<string, string>;
if (!email || !password) {
res.status(400).json({ error: 'Email and password required' });
return;
}
const customer = await findCustomerByEmail(email);
if (!customer || !customer.password_hash) {
res.status(401).json({ error: 'Invalid credentials' });
return;
}
const valid = await verifyPassword(password, customer.password_hash);
if (!valid) {
res.status(401).json({ error: 'Invalid credentials' });
return;
}
const token = signJWT({ sub: customer.id, email: customer.email, plan: customer.plan });
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000,
});
res.json({ id: customer.id, email: customer.email, plan: customer.plan, api_key: customer.api_key });
});
app.post('/api/auth/logout', (_req, res) => {
res.clearCookie('session');
res.json({ success: true });
});
app.get('/api/auth/me', requireAuth, async (req, res) => {
const jwtUser = (req as express.Request & { jwtUser?: { id: string; email: string; plan: string } }).jwtUser;
if (jwtUser) {
res.json(jwtUser);
return;
}
res.status(401).json({ error: 'Not authenticated' });
});
// ── Customer onboarding endpoints ───────────────────────────────
// Connect WhatsApp — called after customer enters their Meta credentials
@@ -1013,11 +1134,174 @@ app.get('/api/connections', meterMiddleware, async (req, res) => {
res.json({ customerId: customer.id, connections: status });
});
// ── Usage & Limits ──────────────────────────────────────────────
app.get('/api/usage', meterMiddleware, async (req, res) => {
const customer = (req as unknown as { customer: Customer }).customer;
const used = await getMonthlyUsage(customer.id);
const breakdown = await getUsageBreakdown(customer.id);
const limitCheck = await checkLimit(customer.id, customer.plan);
res.json({
plan: customer.plan,
monthlyLimit: limitCheck.limit,
used,
remaining: limitCheck.limit === -1 ? -1 : Math.max(0, limitCheck.limit - used),
breakdown,
});
});
// ── Invoices ────────────────────────────────────────────────────
app.get('/api/invoices', meterMiddleware, async (req, res) => {
const customer = (req as unknown as { customer: Customer }).customer;
const invoices = await getCustomerInvoices(customer.id);
res.json({ invoices });
});
app.get('/api/invoices/:number', meterMiddleware, async (req, res) => {
const invoice = await getInvoiceByNumber(req.params.number);
if (!invoice) {
res.status(404).json({ error: 'Invoice not found' });
return;
}
res.json(invoice);
});
// ── Password Reset ──────────────────────────────────────────────
app.post('/api/auth/forgot-password', express.json(), async (req, res) => {
const { email } = req.body as Record<string, string>;
if (!email) {
res.status(400).json({ error: 'Email required' });
return;
}
const token = crypto.randomUUID().replace(/-/g, '');
const success = await setResetToken(email, token);
if (!success) {
// Don't reveal if email exists
res.json({ message: 'If an account exists, a reset link has been sent.' });
return;
}
// In production, send email here. For now, return the token in dev mode.
const resetUrl = `https://app.squaremcp.com/reset-password?token=${token}`;
res.json({
message: 'Password reset link generated.',
resetUrl,
token,
});
});
app.post('/api/auth/reset-password', express.json(), async (req, res) => {
const { token, password } = req.body as Record<string, string>;
if (!token || !password) {
res.status(400).json({ error: 'Token and password required' });
return;
}
if (password.length < 8) {
res.status(400).json({ error: 'Password must be at least 8 characters' });
return;
}
const customer = await findCustomerByResetToken(token);
if (!customer) {
res.status(400).json({ error: 'Invalid or expired reset token' });
return;
}
const passwordHash = await hashPassword(password);
await updatePassword(customer.id, passwordHash);
await clearResetToken(customer.id);
res.json({ message: 'Password updated successfully.' });
});
// ── Admin Endpoints ─────────────────────────────────────────────
function callTool(req: express.Request, name: string, args: Record<string, unknown>) {
const customer = (req as express.Request & { customer?: Customer }).customer;
return handleToolCall(name, args, customer);
}
async function requireAdmin(req: express.Request, res: express.Response, next: express.NextFunction) {
// Global API key = superadmin access
const apiKeyProvided = (req.headers['x-api-key'] as string | undefined) || (req.query.key as string | undefined);
if (apiKeyProvided === API_KEY) return next();
// Check JWT first
const jwtCookie = req.cookies?.session;
let customerId: string | null = null;
if (jwtCookie) {
try {
const payload = verifyJWT(jwtCookie);
customerId = payload.sub;
} catch {
// fall through
}
}
// Check customer API key
if (!customerId && apiKeyProvided) {
const customer = await resolveCustomerByApiKey(apiKeyProvided);
if (customer) customerId = customer.id;
}
if (!customerId) {
res.status(401).json({ error: 'Unauthorized' });
return;
}
const [rows] = await getPool().query<any[]>('SELECT role FROM customers WHERE id = ?', [customerId]);
if (!rows.length || rows[0].role !== 'admin') {
res.status(403).json({ error: 'Admin access required' });
return;
}
next();
}
app.get('/api/admin/customers', requireAdmin, async (req, res) => {
const [rows] = await getPool().query<any[]>(
'SELECT id, email, plan, active, role, created_at FROM customers ORDER BY created_at DESC'
);
res.json({ customers: rows });
});
app.get('/api/admin/customers/:id/usage', requireAdmin, async (req, res) => {
const customerId = req.params.id;
const used = await getMonthlyUsage(customerId);
const breakdown = await getUsageBreakdown(customerId);
res.json({ customerId, used, breakdown });
});
app.post('/api/admin/customers/:id/invoice', requireAdmin, async (req, res) => {
const customerId = req.params.id;
const invoice = await generateMonthlyInvoice(customerId);
if (!invoice) {
res.status(400).json({ error: 'No usage to invoice' });
return;
}
res.json(invoice);
});
app.post('/api/admin/invoices/:number/send', requireAdmin, async (req, res) => {
await markInvoiceSent(req.params.number);
res.json({ sent: true });
});
app.post('/api/admin/invoices/:number/pay', requireAdmin, async (req, res) => {
await markInvoicePaid(req.params.number);
res.json({ paid: true });
});
// ── 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 });
const result = await callTool(req, 'linkedin_get_profile', { account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1028,7 +1312,7 @@ 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 });
const result = await callTool(req, 'linkedin_create_post', { text, visibility, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1038,7 +1322,7 @@ app.post('/api/linkedin/post', requireAuth, async (req, res) => {
app.post('/api/linkedin/video', requireAuth, async (req, res) => {
try {
const { video_url, text, visibility, account } = req.body as Record<string, unknown>;
const result = await handleToolCall('linkedin_upload_video', { video_url, text, visibility, account });
const result = await callTool(req, 'linkedin_upload_video', { video_url, text, visibility, account });
res.json(result);
} catch (err) {
res.status(500).json({ error: String(err) });
@@ -1048,7 +1332,7 @@ app.post('/api/linkedin/video', requireAuth, async (req, res) => {
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 });
const result = await callTool(req, 'linkedin_search_connections', { keywords, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1059,7 +1343,7 @@ 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 });
const result = await callTool(req, 'linkedin_send_message', { recipient_id, message, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1070,7 +1354,7 @@ app.post('/api/linkedin/message', requireAuth, async (req, res) => {
app.get('/api/telegram/me', requireAuth, async (req, res) => {
const account = req.query.account as string | undefined;
try {
const result = await handleToolCall('telegram_get_me', { account });
const result = await callTool(req, 'telegram_get_me', { account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1081,7 +1365,7 @@ app.post('/api/telegram/message', requireAuth, async (req, res) => {
const { chat_id, text, parse_mode, account } = req.body as Record<string, unknown>;
if (!chat_id || !text) { res.status(400).json({ error: 'chat_id and text are required' }); return; }
try {
const result = await handleToolCall('telegram_send_message', { chat_id, text, parse_mode, account });
const result = await callTool(req, 'telegram_send_message', { chat_id, text, parse_mode, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1092,7 +1376,7 @@ app.post('/api/telegram/photo', requireAuth, async (req, res) => {
const { chat_id, photo, caption, account } = req.body as Record<string, unknown>;
if (!chat_id || !photo) { res.status(400).json({ error: 'chat_id and photo are required' }); return; }
try {
const result = await handleToolCall('telegram_send_photo', { chat_id, photo, caption, account });
const result = await callTool(req, 'telegram_send_photo', { chat_id, photo, caption, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1103,7 +1387,7 @@ app.get('/api/telegram/updates', 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('telegram_get_updates', { limit, account });
const result = await callTool(req, 'telegram_get_updates', { limit, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1115,7 +1399,7 @@ app.get('/api/telegram/chat', requireAuth, async (req, res) => {
const account = req.query.account as string | undefined;
if (!chat_id) { res.status(400).json({ error: 'chat_id is required' }); return; }
try {
const result = await handleToolCall('telegram_get_chat', { chat_id, account });
const result = await callTool(req, 'telegram_get_chat', { chat_id, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1126,7 +1410,7 @@ app.get('/api/telegram/chat', requireAuth, async (req, res) => {
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 });
const result = await callTool(req, 'discord_get_me', { account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1136,7 +1420,7 @@ app.get('/api/discord/me', requireAuth, async (req, res) => {
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 });
const result = await callTool(req, 'discord_get_guilds', { account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1148,7 +1432,7 @@ app.get('/api/discord/channels', requireAuth, async (req, res) => {
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 });
const result = await callTool(req, 'discord_get_channels', { guild_id, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1159,7 +1443,7 @@ 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 });
const result = await callTool(req, 'discord_send_message', { channel_id, content, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1172,7 +1456,7 @@ app.get('/api/discord/messages', requireAuth, async (req, res) => {
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 });
const result = await callTool(req, 'discord_get_messages', { channel_id, limit, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1183,7 +1467,7 @@ app.get('/api/discord/messages', requireAuth, async (req, res) => {
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 });
const result = await callTool(req, 'instagram_get_profile', { account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1194,7 +1478,7 @@ 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 });
const result = await callTool(req, 'instagram_get_media', { limit, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1205,7 +1489,7 @@ 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 });
const result = await callTool(req, 'instagram_create_post', { image_url, caption, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1216,7 +1500,7 @@ app.post('/api/instagram/reel', requireAuth, async (req, res) => {
const { video_url, caption, account } = req.body as Record<string, unknown>;
if (!video_url) { res.status(400).json({ error: 'video_url is required' }); return; }
try {
const result = await handleToolCall('instagram_create_reel', { video_url, caption, account });
const result = await callTool(req, 'instagram_create_reel', { video_url, caption, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1230,7 +1514,7 @@ app.get('/api/twitter/search', requireAuth, async (req, res) => {
const account = req.query.account as string | undefined;
if (!query) { res.status(400).json({ error: 'query is required' }); return; }
try {
const result = await handleToolCall('twitter_search_tweets', { query, max_results, account });
const result = await callTool(req, 'twitter_search_tweets', { query, max_results, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1242,7 +1526,7 @@ app.get('/api/twitter/user', requireAuth, async (req, res) => {
const account = req.query.account as string | undefined;
if (!username) { res.status(400).json({ error: 'username is required' }); return; }
try {
const result = await handleToolCall('twitter_get_user_profile', { username, account });
const result = await callTool(req, 'twitter_get_user_profile', { username, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1255,7 +1539,7 @@ app.get('/api/twitter/tweets', requireAuth, async (req, res) => {
const account = req.query.account as string | undefined;
if (!username) { res.status(400).json({ error: 'username is required' }); return; }
try {
const result = await handleToolCall('twitter_get_user_tweets', { username, max_results, account });
const result = await callTool(req, 'twitter_get_user_tweets', { username, max_results, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1266,7 +1550,7 @@ app.post('/api/twitter/tweet', requireAuth, async (req, res) => {
const { text, account } = req.body as Record<string, unknown>;
if (!text) { res.status(400).json({ error: 'text is required' }); return; }
try {
const result = await handleToolCall('twitter_create_tweet', { text, account });
const result = await callTool(req, 'twitter_create_tweet', { text, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1277,7 +1561,7 @@ app.post('/api/twitter/video', requireAuth, async (req, res) => {
const { video_url, text, account } = req.body as Record<string, unknown>;
if (!video_url || !text) { res.status(400).json({ error: 'video_url and text are required' }); return; }
try {
const result = await handleToolCall('twitter_upload_video', { video_url, text, account });
const result = await callTool(req, 'twitter_upload_video', { video_url, text, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1288,7 +1572,7 @@ app.post('/api/twitter/video', requireAuth, async (req, res) => {
app.get('/api/facebook/page', requireAuth, async (req, res) => {
const account = req.query.account as string | undefined;
try {
const result = await handleToolCall('facebook_get_page', { account });
const result = await callTool(req, 'facebook_get_page', { account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1299,7 +1583,7 @@ app.get('/api/facebook/posts', 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('facebook_get_posts', { limit, account });
const result = await callTool(req, 'facebook_get_posts', { limit, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1310,7 +1594,7 @@ app.post('/api/facebook/post', requireAuth, async (req, res) => {
const { message, link, account } = req.body as Record<string, unknown>;
if (!message) { res.status(400).json({ error: 'message is required' }); return; }
try {
const result = await handleToolCall('facebook_create_post', { message, link, account });
const result = await callTool(req, 'facebook_create_post', { message, link, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1321,7 +1605,7 @@ app.post('/api/facebook/photo', 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('facebook_create_photo_post', { image_url, caption, account });
const result = await callTool(req, 'facebook_create_photo_post', { image_url, caption, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1332,7 +1616,7 @@ app.post('/api/facebook/video', requireAuth, async (req, res) => {
const { video_url, description, account } = req.body as Record<string, unknown>;
if (!video_url) { res.status(400).json({ error: 'video_url is required' }); return; }
try {
const result = await handleToolCall('facebook_create_video_post', { video_url, description, account });
const result = await callTool(req, 'facebook_create_video_post', { video_url, description, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1343,7 +1627,7 @@ app.post('/api/facebook/video', requireAuth, async (req, res) => {
app.get('/api/tiktok/profile', requireAuth, async (req, res) => {
const account = req.query.account as string | undefined;
try {
const result = await handleToolCall('tiktok_get_profile', { account });
const result = await callTool(req, 'tiktok_get_profile', { account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1353,7 +1637,7 @@ app.get('/api/tiktok/profile', requireAuth, async (req, res) => {
app.get('/api/tiktok/creator-info', requireAuth, async (req, res) => {
const account = req.query.account as string | undefined;
try {
const result = await handleToolCall('tiktok_get_creator_info', { account });
const result = await callTool(req, 'tiktok_get_creator_info', { account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1364,7 +1648,7 @@ app.post('/api/tiktok/video', requireAuth, async (req, res) => {
const { video_url, title, description, account } = req.body as Record<string, unknown>;
if (!video_url) { res.status(400).json({ error: 'video_url is required' }); return; }
try {
const result = await handleToolCall('tiktok_create_video', { video_url, title, description, account });
const result = await callTool(req, 'tiktok_create_video', { video_url, title, description, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
@@ -1375,7 +1659,7 @@ app.post('/api/tiktok/video/status', requireAuth, async (req, res) => {
const { publish_id, account } = req.body as Record<string, unknown>;
if (!publish_id) { res.status(400).json({ error: 'publish_id is required' }); return; }
try {
const result = await handleToolCall('tiktok_get_video_status', { publish_id, account });
const result = await callTool(req, 'tiktok_get_video_status', { publish_id, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });