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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`,
|
||||
};
|
||||
}
|
||||
|
||||
121
src/index.ts
121
src/index.ts
@@ -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)) {
|
||||
|
||||
285
src/manifest.ts
285
src/manifest.ts
@@ -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 ──────────────────────────────────────────────────────
|
||||
{
|
||||
|
||||
76
src/tools.ts
76
src/tools.ts
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user