feat: social video uploads + hero page video + TikTok content

Hero page:
- Replace GIF with squaremcp-hero-loop.mp4 (autoplay, muted, loop)
- Update styles, scripts, tests, Dockerfile, baselines
- Deployed and verified

Social video uploads:
- Twitter/X: uploadVideoAndTweet via v1.1 media/upload + v2 tweets
- Facebook: createVideoPost via Graph API /{pageId}/videos
- Instagram: createReel via Graph API (container → poll → publish)
- TikTok: REST endpoints + OpenAPI schema for video upload

Marketing:
- TikTok content prompts, scripts, and posting schedule

Note: Remotion not mentioned in any user-facing content
This commit is contained in:
Garfield
2026-05-11 13:55:58 -04:00
parent de9d74bb2b
commit ecdf332b78
17 changed files with 854 additions and 54 deletions

View File

@@ -158,3 +158,31 @@ export async function createPhotoPost(
throw err;
}
}
export async function createVideoPost(
args: { video_url: string; description?: string; account?: string },
customer?: Customer
): Promise<{ success: boolean; video_id: string; post_id: string }> {
const audit = customer ? createToolAudit(customer.id, 'facebook:createVideoPost') : null;
const auditArgs = { video_url: args.video_url };
const { accessToken, pageId } = await resolveCreds(args, customer);
try {
const body: Record<string, unknown> = {
file_url: args.video_url,
};
if (args.description) body.description = args.description;
const data = await fbRequest(`/${pageId}/videos`, accessToken, 'POST', body);
const result = {
success: true,
video_id: String(data.id ?? ''),
post_id: String(data.post_id ?? data.id ?? ''),
};
if (audit) await audit.success(auditArgs);
return result;
} catch (err) {
if (audit) await audit.error(auditArgs, String(err));
throw err;
}
}

View File

@@ -116,11 +116,11 @@ export async function getMedia(
}));
}
export async function createPost(
export async function createImagePost(
args: { image_url: string; caption?: string; account?: string },
customer?: Customer
): Promise<{ success: boolean; media_id: string }> {
const audit = customer ? createToolAudit(customer.id, 'instagram:createPost') : null;
const audit = customer ? createToolAudit(customer.id, 'instagram:createImagePost') : null;
const auditArgs = { image_url: args.image_url };
const { accessToken, businessAccountId } = await resolveCreds(args, customer);
@@ -129,7 +129,7 @@ export async function createPost(
`/${businessAccountId}/media`,
accessToken,
'POST',
{ image_url: args.image_url, caption: args.caption, media_type: 'REELS' }
{ image_url: args.image_url, caption: args.caption }
);
const creationId = container.id;
@@ -150,3 +150,65 @@ export async function createPost(
throw err;
}
}
export async function createReel(
args: { video_url: string; caption?: string; account?: string },
customer?: Customer
): Promise<{ success: boolean; media_id: string }> {
const audit = customer ? createToolAudit(customer.id, 'instagram:createReel') : null;
const auditArgs = { video_url: args.video_url };
const { accessToken, businessAccountId } = await resolveCreds(args, customer);
try {
const container = await instagramRequest(
`/${businessAccountId}/media`,
accessToken,
'POST',
{ media_type: 'REELS', video_url: args.video_url, caption: args.caption, share_to_feed: true }
);
const creationId = container.id;
if (!creationId) throw new Error('Failed to create Instagram media container');
// Poll for container status
let retries = 0;
const maxRetries = 10;
const delayMs = 5000;
while (retries < maxRetries) {
const statusData = await instagramRequest(
`/${creationId}?fields=status_code`,
accessToken
);
if (statusData.status_code === 'FINISHED') {
break;
}
if (statusData.status_code === 'ERROR') {
throw new Error('Instagram media container processing failed');
}
retries++;
if (retries >= maxRetries) {
throw new Error('Instagram media container polling timed out');
}
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
const publish = await instagramRequest(
`/${businessAccountId}/media_publish`,
accessToken,
'POST',
{ creation_id: creationId }
);
const result = { success: true, media_id: publish.id };
if (audit) await audit.success(auditArgs);
return result;
} catch (err) {
if (audit) await audit.error(auditArgs, String(err));
throw err;
}
}

View File

@@ -2,6 +2,7 @@ import type { Customer } from '../billing/middleware.js';
import type { OAuthCredentials } from '../multitenancy/credential-store.js';
const TWITTER_API_BASE = 'https://api.twitter.com/2';
const TWITTER_UPLOAD_BASE = 'https://upload.twitter.com/1.1';
function getEnvToken(account: string): string {
const envKey = `TWITTER_${account.toUpperCase()}_BEARER_TOKEN`;
@@ -118,3 +119,154 @@ export async function createTweet(
'Upgrade to Basic ($100/month) or Pro ($5,000/month) at https://developer.twitter.com/en/portal/products'
);
}
export async function uploadVideoAndTweet(
args: { videoUrl: string; text: string; account?: string },
customer?: Customer
): Promise<{ success: boolean; tweet_id: string; url: string }> {
const bearerToken = await resolveToken(args, customer);
// 1. Download video
const videoRes = await fetch(args.videoUrl, { signal: AbortSignal.timeout(60000) });
if (!videoRes.ok) {
throw new Error(`Failed to download video: ${videoRes.status} ${videoRes.statusText}`);
}
const videoBuffer = Buffer.from(await videoRes.arrayBuffer());
const totalBytes = videoBuffer.length;
// 2. INIT
const initUrl = new URL(`${TWITTER_UPLOAD_BASE}/media/upload.json`);
initUrl.searchParams.set('command', 'INIT');
initUrl.searchParams.set('total_bytes', String(totalBytes));
initUrl.searchParams.set('media_type', 'video/mp4');
const initRes = await fetch(initUrl.toString(), {
method: 'POST',
headers: { 'Authorization': `Bearer ${bearerToken}` },
signal: AbortSignal.timeout(15000),
});
if (!initRes.ok) {
const err = await initRes.text();
throw new Error(`Twitter media INIT error (${initRes.status}): ${err}`);
}
const initData = (await initRes.json()) as { media_id_string?: string };
const mediaId = initData.media_id_string;
if (!mediaId) throw new Error('Twitter media INIT did not return media_id_string');
// 3. APPEND chunks
const CHUNK_SIZE = 2 * 1024 * 1024;
let segmentIndex = 0;
for (let offset = 0; offset < totalBytes; offset += CHUNK_SIZE) {
const chunk = videoBuffer.subarray(offset, offset + CHUNK_SIZE);
const formData = new FormData();
formData.append('media', new Blob([chunk]));
const appendUrl = new URL(`${TWITTER_UPLOAD_BASE}/media/upload.json`);
appendUrl.searchParams.set('command', 'APPEND');
appendUrl.searchParams.set('media_id', mediaId);
appendUrl.searchParams.set('segment_index', String(segmentIndex));
const appendRes = await fetch(appendUrl.toString(), {
method: 'POST',
headers: { 'Authorization': `Bearer ${bearerToken}` },
body: formData,
signal: AbortSignal.timeout(30000),
});
if (!appendRes.ok) {
const err = await appendRes.text();
throw new Error(`Twitter media APPEND error (${appendRes.status}): ${err}`);
}
segmentIndex++;
}
// 4. FINALIZE
const finalizeUrl = new URL(`${TWITTER_UPLOAD_BASE}/media/upload.json`);
finalizeUrl.searchParams.set('command', 'FINALIZE');
finalizeUrl.searchParams.set('media_id', mediaId);
const finalizeRes = await fetch(finalizeUrl.toString(), {
method: 'POST',
headers: { 'Authorization': `Bearer ${bearerToken}` },
signal: AbortSignal.timeout(15000),
});
if (!finalizeRes.ok) {
const err = await finalizeRes.text();
throw new Error(`Twitter media FINALIZE error (${finalizeRes.status}): ${err}`);
}
const finalizeData = (await finalizeRes.json()) as {
processing_info?: { state?: string; check_after_secs?: number; error?: { message?: string } };
};
// 5. STATUS polling
let processingInfo = finalizeData.processing_info;
while (processingInfo && processingInfo.state !== 'succeeded') {
if (processingInfo.state === 'failed') {
const errMsg = processingInfo.error?.message ?? 'Unknown processing error';
throw new Error(`Twitter media processing failed: ${errMsg}`);
}
const waitSeconds = processingInfo.check_after_secs ?? 5;
await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000));
const statusUrl = new URL(`${TWITTER_UPLOAD_BASE}/media/upload.json`);
statusUrl.searchParams.set('command', 'STATUS');
statusUrl.searchParams.set('media_id', mediaId);
const statusRes = await fetch(statusUrl.toString(), {
method: 'GET',
headers: { 'Authorization': `Bearer ${bearerToken}` },
signal: AbortSignal.timeout(15000),
});
if (!statusRes.ok) {
const err = await statusRes.text();
throw new Error(`Twitter media STATUS error (${statusRes.status}): ${err}`);
}
const statusData = (await statusRes.json()) as {
processing_info?: { state?: string; check_after_secs?: number; error?: { message?: string } };
};
processingInfo = statusData.processing_info;
if (processingInfo?.state === 'failed') {
const errMsg = processingInfo.error?.message ?? 'Unknown processing error';
throw new Error(`Twitter media processing failed: ${errMsg}`);
}
}
// 6. Post tweet with video
const tweetRes = await fetch(`${TWITTER_API_BASE}/tweets`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${bearerToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
text: args.text,
media: { media_keys: [mediaId] },
}),
signal: AbortSignal.timeout(15000),
});
if (!tweetRes.ok) {
const err = await tweetRes.text();
throw new Error(`Twitter tweet error (${tweetRes.status}): ${err}`);
}
const tweetData = (await tweetRes.json()) as { data?: { id?: string } };
const tweetId = tweetData.data?.id;
if (!tweetId) throw new Error('Twitter tweet creation did not return an ID');
return {
success: true,
tweet_id: tweetId,
url: `https://twitter.com/i/web/status/${tweetId}`,
};
}

View File

@@ -935,6 +935,17 @@ app.post('/api/instagram/post', requireAuth, async (req, res) => {
}
});
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 });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
// ── Twitter/X REST endpoints ────────────────────────────────────
app.get('/api/twitter/search', requireAuth, async (req, res) => {
const query = req.query.query as string | undefined;
@@ -985,6 +996,116 @@ app.post('/api/twitter/tweet', requireAuth, async (req, res) => {
}
});
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 });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
// ── Facebook REST endpoints ─────────────────────────────────────
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 });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
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 });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
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 });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
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 });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
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 });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
// ── TikTok REST endpoints ───────────────────────────────────────
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 });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
app.get('/api/tiktok/videos', requireAuth, async (req, res) => {
const max_count = req.query.max_count ? parseInt(req.query.max_count as string, 10) : undefined;
const account = req.query.account as string | undefined;
try {
const result = await handleToolCall('tiktok_get_videos', { max_count, account });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
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 });
res.json(parseToolResult(result));
} catch (err) {
res.status(500).json({ error: (err as Error).message });
}
});
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 });
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

@@ -565,6 +565,29 @@ export function getOpenApiSpec(serverUrl: string) {
responses: { '200': { description: 'Photo posted' } },
},
},
'/api/facebook/video': {
post: {
operationId: 'facebook_create_video_post',
summary: 'Publish video post to Facebook Page',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['video_url'],
properties: {
video_url: { type: 'string', description: 'Publicly accessible URL of the video' },
description: { type: 'string', description: 'Video description text' },
account: { type: 'string' },
},
},
},
},
},
responses: { '200': { description: 'Video posted' } },
},
},
// ── Twitter/X ───────────────────────────────────────────────
'/api/twitter/search': {
@@ -602,6 +625,97 @@ export function getOpenApiSpec(serverUrl: string) {
responses: { '200': { description: 'Tweet list' } },
},
},
'/api/twitter/video': {
post: {
operationId: 'twitter_upload_video',
summary: 'Upload video and post tweet',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['video_url', 'text'],
properties: {
video_url: { type: 'string', description: 'Publicly accessible URL of the MP4 video' },
text: { type: 'string', description: 'Tweet text content' },
account: { type: 'string' },
},
},
},
},
},
responses: { '200': { description: 'Video tweet posted' } },
},
},
'/api/instagram/reel': {
post: {
operationId: 'instagram_create_reel',
summary: 'Create Instagram Reel',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['video_url'],
properties: {
video_url: { type: 'string', description: 'Publicly accessible URL of the MP4 video' },
caption: { type: 'string', description: 'Reel caption text' },
account: { type: 'string' },
},
},
},
},
},
responses: { '200': { description: 'Reel created' } },
},
},
'/api/tiktok/profile': {
get: {
operationId: 'tiktok_get_profile',
summary: 'Get TikTok profile',
parameters: [
{ name: 'account', in: 'query', schema: { type: 'string' } },
],
responses: { '200': { description: 'Profile info' } },
},
},
'/api/tiktok/videos': {
get: {
operationId: 'tiktok_get_videos',
summary: 'Get TikTok videos',
parameters: [
{ name: 'max_count', in: 'query', schema: { type: 'integer' } },
{ name: 'account', in: 'query', schema: { type: 'string' } },
],
responses: { '200': { description: 'Video list' } },
},
},
'/api/tiktok/video': {
post: {
operationId: 'tiktok_create_video',
summary: 'Upload video to TikTok',
requestBody: {
required: true,
content: {
'application/json': {
schema: {
type: 'object',
required: ['video_url'],
properties: {
video_url: { type: 'string', description: 'Publicly accessible URL of the video' },
title: { type: 'string', description: 'Video title (max 150 chars)' },
description: { type: 'string', description: 'Video description / caption' },
account: { type: 'string' },
},
},
},
},
},
responses: { '200': { description: 'Video upload initiated' } },
},
},
},
};
}
@@ -1326,6 +1440,30 @@ export function getManifest(serverUrl: string, authEnabled: boolean) {
},
examples: [{ image_url: 'https://example.com/photo.jpg', caption: 'New post!', account: 'default' }],
},
{
name: 'instagram_create_reel',
category: 'instagram',
description: 'Upload a video as an Instagram Reel',
when_to_use:
'User wants to publish a video to their Instagram Business/Creator account as a Reel.',
input_schema: {
type: 'object',
required: ['video_url'],
properties: {
video_url: { type: 'string', description: 'Publicly accessible URL of the MP4 video' },
caption: { type: 'string', description: 'Reel caption' },
account: { type: 'string', description: 'Which Instagram account to use (default: "default")' },
},
},
returns: {
type: 'object',
properties: {
success: { type: 'boolean' },
media_id: { type: 'string' },
},
},
examples: [{ video_url: 'https://example.com/video.mp4', caption: 'New reel!', account: 'default' }],
},
// ── Facebook tools ─────────────────────────────────────────────────────
{
@@ -1427,6 +1565,30 @@ export function getManifest(serverUrl: string, authEnabled: boolean) {
},
examples: [{ image_url: 'https://example.com/image.jpg', caption: 'New post!', account: 'default' }],
},
{
name: 'facebook_create_video_post',
category: 'facebook',
description: 'Publish a video post to a Facebook Page using a public video URL',
when_to_use: 'User wants to post a video to their Facebook Page.',
input_schema: {
type: 'object',
required: ['video_url'],
properties: {
video_url: { type: 'string', description: 'Publicly accessible URL of the video' },
description: { type: 'string', description: 'Video description text' },
account: { type: 'string', description: 'Which Facebook account to use (default: "default")' },
},
},
returns: {
type: 'object',
properties: {
success: { type: 'boolean' },
video_id: { type: 'string' },
post_id: { type: 'string' },
},
},
examples: [{ video_url: 'https://example.com/video.mp4', description: 'New video!', account: 'default' }],
},
// ── Twitter/X tools ────────────────────────────────────────────────────
{
@@ -1536,6 +1698,129 @@ export function getManifest(serverUrl: string, authEnabled: boolean) {
},
examples: [{ text: 'Hello from Hermes MCP!', account: 'default' }],
},
{
name: 'twitter_upload_video',
category: 'twitter',
description: 'Upload a video and post it as a tweet on Twitter/X',
when_to_use:
'User wants to post a video to Twitter/X. NOTE: Free tier is read-only. Paid upgrade required.',
input_schema: {
type: 'object',
required: ['video_url', 'text'],
properties: {
video_url: { type: 'string', description: 'Publicly accessible URL of the MP4 video' },
text: { type: 'string', description: 'Tweet text content' },
account: { type: 'string', description: 'Which Twitter account to use (default: "default")' },
},
},
returns: {
type: 'object',
properties: {
success: { type: 'boolean' },
tweet_id: { type: 'string' },
url: { type: 'string', description: 'Direct link to the tweet' },
},
},
examples: [{ video_url: 'https://example.com/video.mp4', text: 'Check this out!', account: 'default' }],
},
{
name: 'tiktok_get_profile',
category: 'tiktok',
description: 'Get the TikTok user profile including follower count, following count, likes, and video count',
when_to_use: 'User asks about their TikTok profile stats or account details.',
input_schema: {
type: 'object',
properties: {
account: { type: 'string', description: 'Which TikTok account to use (default: "default")' },
},
},
returns: {
type: 'object',
properties: {
open_id: { type: 'string' },
display_name: { type: 'string' },
follower_count: { type: 'number' },
following_count: { type: 'number' },
likes_count: { type: 'number' },
video_count: { type: 'number' },
},
},
examples: [{ account: 'default' }],
},
{
name: 'tiktok_get_videos',
category: 'tiktok',
description: 'List recent videos from the authenticated TikTok account',
when_to_use: 'User wants to see their recent TikTok videos and performance stats.',
input_schema: {
type: 'object',
properties: {
max_count: { type: 'number', description: 'Max videos to return (default: 10, max: 20)' },
account: { type: 'string', description: 'Which TikTok account to use (default: "default")' },
},
},
returns: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string' },
title: { type: 'string' },
view_count: { type: 'number' },
like_count: { type: 'number' },
share_url: { type: 'string' },
},
},
},
examples: [{ max_count: 10, account: 'default' }],
},
{
name: 'tiktok_create_video',
category: 'tiktok',
description: 'Post a video to TikTok by providing a publicly accessible video URL',
when_to_use: 'User wants to upload a video to TikTok.',
input_schema: {
type: 'object',
required: ['video_url'],
properties: {
video_url: { type: 'string', description: 'Publicly accessible URL of the video to post' },
title: { type: 'string', description: 'Video title (max 150 chars)' },
description: { type: 'string', description: 'Video description / caption' },
account: { type: 'string', description: 'Which TikTok account to use (default: "default")' },
},
},
returns: {
type: 'object',
properties: {
publish_id: { type: 'string' },
status: { type: 'string' },
},
},
examples: [{ video_url: 'https://example.com/video.mp4', title: 'My video', account: 'default' }],
},
{
name: 'tiktok_get_video_status',
category: 'tiktok',
description: 'Check the processing status of a TikTok video upload',
when_to_use: 'User wants to check if their TikTok video upload is complete.',
input_schema: {
type: 'object',
required: ['publish_id'],
properties: {
publish_id: { type: 'string', description: 'Publish ID returned by tiktok_create_video' },
account: { type: 'string', description: 'Which TikTok account to use (default: "default")' },
},
},
returns: {
type: 'object',
properties: {
publish_id: { type: 'string' },
status: { type: 'string' },
fail_reason: { type: 'string' },
},
},
examples: [{ publish_id: 'v123456', account: 'default' }],
},
// ── Obsidian tools ──────────────────────────────────────────────────────
{

View File

@@ -7,11 +7,11 @@ import { sendMessage, sendTemplate, getMessageStatus, listTemplates } from './cl
import { getProfile as getLinkedInProfile, createPost as createLinkedInPost, createVideoPost as createLinkedInVideoPost, searchConnections, sendMessage as sendLinkedInMessage } from './clients/linkedin.js';
import { getMe as getTelegramMe, sendMessage as sendTelegramMessage, sendPhoto as sendTelegramPhoto, getUpdates as getTelegramUpdates, getChat as getTelegramChat } from './clients/telegram.js';
import { getMe as getDiscordMe, getGuilds, getChannels, sendMessage as sendDiscordMessage, getMessages as getDiscordMessages } from './clients/discord.js';
import { getProfile as getInstagramProfile, getMedia as getInstagramMedia, createPost as createInstagramPost } from './clients/instagram.js';
import { searchTweets, getUserProfile, getUserTweets, createTweet } from './clients/twitter.js';
import { getProfile as getInstagramProfile, getMedia as getInstagramMedia, createImagePost as createInstagramPost, createReel as createInstagramReel } from './clients/instagram.js';
import { searchTweets, getUserProfile, getUserTweets, createTweet, uploadVideoAndTweet } from './clients/twitter.js';
import { getUserProfile as getTikTokProfile, getUserVideos, createVideo, getVideoStatus } from './clients/tiktok.js';
import { getMe as getSnapchatMe, createSnap, getAdAccounts } from './clients/snapchat.js';
import { getPage, getPosts, createPost as createFacebookPost, createPhotoPost } from './clients/facebook.js';
import { getPage, getPosts, createPost as createFacebookPost, createPhotoPost, createVideoPost as createFacebookVideoPost } from './clients/facebook.js';
const ACCOUNT_PARAM = {
account: {
@@ -468,7 +468,7 @@ export const tools: Tool[] = [
{
name: 'instagram_create_post',
description:
'Create a post on Instagram. [REQUIRES BUSINESS ACCOUNT] Only works with Instagram Business/Creator accounts connected to a Facebook Page.',
'Create an image post on Instagram. [REQUIRES BUSINESS ACCOUNT] Only works with Instagram Business/Creator accounts connected to a Facebook Page.',
inputSchema: {
type: 'object',
required: ['image_url'],
@@ -479,6 +479,20 @@ export const tools: Tool[] = [
},
},
},
{
name: 'instagram_create_reel',
description:
'Upload a video as an Instagram Reel. [REQUIRES BUSINESS ACCOUNT] Only works with Instagram Business/Creator accounts connected to a Facebook Page.',
inputSchema: {
type: 'object',
required: ['video_url'],
properties: {
video_url: { type: 'string', description: 'Publicly accessible URL of the MP4 video to post as a Reel' },
caption: { type: 'string', description: 'Reel caption text' },
account: { type: 'string', description: 'Which Instagram account to use (default: "default")' },
},
},
},
// ── Twitter/X tools ──────────────────────────────────────────
{
@@ -525,7 +539,7 @@ export const tools: Tool[] = [
{
name: 'twitter_create_tweet',
description:
'Post a tweet on Twitter/X. [REQUIRES PAID TIER] The free API tier is read-only. Upgrade required to post.',
'Post a text tweet on Twitter/X. [REQUIRES PAID TIER] The free API tier is read-only. Upgrade required to post.',
inputSchema: {
type: 'object',
required: ['text'],
@@ -535,6 +549,20 @@ export const tools: Tool[] = [
},
},
},
{
name: 'twitter_upload_video',
description:
'Upload a video and post it as a tweet on Twitter/X. Downloads the video, uploads via Twitter media API, then publishes the tweet. [REQUIRES PAID TIER] The free API tier is read-only. Upgrade required to post.',
inputSchema: {
type: 'object',
required: ['video_url', 'text'],
properties: {
video_url: { type: 'string', description: 'Publicly accessible URL of the MP4 video to post' },
text: { type: 'string', description: 'Tweet text content' },
account: { type: 'string', description: 'Which Twitter account to use (default: "default")' },
},
},
},
// ── TikTok tools ─────────────────────────────────────────────
{
@@ -679,6 +707,20 @@ export const tools: Tool[] = [
},
},
},
{
name: 'facebook_create_video_post',
description:
'Publish a video to a Facebook Page using a publicly accessible video URL. Creates a video post with optional description.',
inputSchema: {
type: 'object',
required: ['video_url'],
properties: {
video_url: { type: 'string', description: 'Publicly accessible URL of the video to post' },
description: { type: 'string', description: 'Post description text' },
account: { type: 'string', description: 'Which Facebook account to use (default: "default")' },
},
},
},
];
function acct(args: Record<string, unknown>): Account {
@@ -922,6 +964,14 @@ export async function handleToolCall(
}, customer);
break;
case 'instagram_create_reel':
result = await createInstagramReel({
video_url: args.video_url as string,
caption: args.caption as string | undefined,
account: args.account as string | undefined,
}, customer);
break;
// ── Twitter/X ───────────────────────────────────────────────
case 'twitter_search_tweets':
result = await searchTweets({
@@ -953,6 +1003,14 @@ export async function handleToolCall(
}, customer);
break;
case 'twitter_upload_video':
result = await uploadVideoAndTweet({
videoUrl: args.video_url as string,
text: args.text as string,
account: args.account as string | undefined,
}, customer);
break;
// ── TikTok ─────────────────────────────────────────────────
case 'tiktok_get_profile':
result = await getTikTokProfile({
@@ -1035,6 +1093,14 @@ export async function handleToolCall(
}, customer);
break;
case 'facebook_create_video_post':
result = await createFacebookVideoPost({
video_url: args.video_url as string,
description: args.description as string | undefined,
account: args.account as string | undefined,
}, customer);
break;
// Legacy Yahoo-prefixed names — keep working for any cached Claude sessions
case 'yahoo_get_profile':
result = await getProfile('yahoo');