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:
100
product/animaGen/tiktok_content_prompts.md
Normal file
100
product/animaGen/tiktok_content_prompts.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# SquareMCP TikTok Content Prompts
|
||||
|
||||
Built from the SquareMCP visual prompt foundation — adapted for short-form vertical video (9:16).
|
||||
|
||||
---
|
||||
|
||||
## Video Prompt 1 — The Hook (0–3s)
|
||||
|
||||
**Visual:**
|
||||
Fast zoom into a dark terminal screen. Text types itself: `> connect squaremcp`. Cursor blinks. Cut to a grid of 36 glowing tool icons (email, social, obsidian) snapping into place like a control panel.
|
||||
|
||||
**Prompt:**
|
||||
> Cinematic close-up of a terminal cursor on a deep navy screen, monospace text typing "connect squaremcp", hard cut to a 6x6 grid of minimalist white icons snapping into a glassmorphism dashboard, subtle reflections, dark mode UI, no faces, vertical 9:16, 4K crisp
|
||||
|
||||
**Caption hook:**
|
||||
"One API key. 36 tools. This is how I automated my entire workflow."
|
||||
|
||||
---
|
||||
|
||||
## Video Prompt 2 — The Problem (3–8s)
|
||||
|
||||
**Visual:**
|
||||
Split screen. Left: person switching between 8 different apps (email, LinkedIn, Slack, notes). Right: same actions flowing through a single pipeline.
|
||||
|
||||
**Prompt:**
|
||||
> Split-screen vertical video, left side shows chaotic app-switching blur (email, social, chat, notes), right side shows clean single pipeline with data flowing through one gateway, stark contrast, motion blur on left, crisp motion on right, dark enterprise aesthetic, vertical 9:16
|
||||
|
||||
**Caption hook:**
|
||||
"Before vs After: from 8 tabs to 1 API call."
|
||||
|
||||
---
|
||||
|
||||
## Video Prompt 3 — The Demo (8–20s)
|
||||
|
||||
**Visual:**
|
||||
Screen recording style (but stylized). Show a ChatGPT conversation where the user says "Post my launch video to LinkedIn, Twitter, and Instagram" — then cut to the actual API response with success URLs.
|
||||
|
||||
**Prompt:**
|
||||
> Stylized screen recording aesthetic, chat interface on dark background, text bubbles animating in, cut to JSON response with green checkmarks, URL previews fading in, fast-paced editing with whoosh sound design implied, vertical 9:16, developer tool aesthetic
|
||||
|
||||
**Caption:**
|
||||
"I told my AI to post everywhere. It did. In 12 seconds."
|
||||
|
||||
---
|
||||
|
||||
## Video Prompt 4 — The Proof (20–25s)
|
||||
|
||||
**Visual:**
|
||||
Rapid montage of LinkedIn post, Twitter tweet, Instagram Reel, and TikTok video all going live simultaneously. Timestamps tick up in sync.
|
||||
|
||||
**Prompt:**
|
||||
> Rapid vertical montage, four social platform UIs flickering in sequence (LinkedIn, Twitter/X, Instagram, TikTok), all showing the same video going live, timestamps syncing up, split-second cuts, dark mode interfaces, subtle glow effects on publish buttons, vertical 9:16
|
||||
|
||||
**Caption:**
|
||||
"LinkedIn ✅ Twitter ✅ Instagram ✅ TikTok ✅ All from one prompt."
|
||||
|
||||
---
|
||||
|
||||
## Video Prompt 5 — The CTA (25–30s)
|
||||
|
||||
**Visual:**
|
||||
SquareMCP logo centers on screen. URL types out below. Subtle particle effect of connecting nodes in background.
|
||||
|
||||
**Prompt:**
|
||||
> Minimal dark background, centered wordmark "SquareMCP" in clean sans-serif, URL "squaremcp.com" typing out below, abstract network of thin glowing lines connecting dots in background, slow breathing motion, founder-led product feel, no stock footage, vertical 9:16
|
||||
|
||||
**Caption:**
|
||||
"Link in bio. Early access open."
|
||||
|
||||
---
|
||||
|
||||
## Voiceover Script (30s)
|
||||
|
||||
> "I built an AI that controls my entire digital life.
|
||||
> One API key. Email, social, notes, databases — all reachable from ChatGPT.
|
||||
> I rendered this video, then told my AI to post it everywhere.
|
||||
> It did. In 12 seconds.
|
||||
> SquareMCP. One key. Every tool."
|
||||
|
||||
---
|
||||
|
||||
## Hashtag Strategy
|
||||
|
||||
Primary: `#AITools` `#Automation` `#ChatGPT` `#API` `#DeveloperTools` `#SquareMCP`
|
||||
|
||||
Secondary: `#Productivity` `#SaaS` `#TechTok` `#BuildInPublic` `#Startup`
|
||||
|
||||
---
|
||||
|
||||
## Posting Schedule
|
||||
|
||||
| Day | Content | Platform |
|
||||
|-----|---------|----------|
|
||||
| Mon | Hook + Problem (Prompts 1–2) | TikTok + Reels |
|
||||
| Wed | Demo (Prompt 3) | TikTok + Reels + Shorts |
|
||||
| Fri | Proof + CTA (Prompts 4–5) | All platforms |
|
||||
|
||||
---
|
||||
|
||||
*Do not mention Remotion in any published content.*
|
||||
@@ -5,7 +5,6 @@ COPY product/site/index.html /usr/share/nginx/html/index.html
|
||||
COPY product/site/styles.css /usr/share/nginx/html/styles.css
|
||||
COPY product/site/script.js /usr/share/nginx/html/script.js
|
||||
COPY product/site/squaremcp-logo.svg /usr/share/nginx/html/squaremcp-logo.svg
|
||||
COPY product/site/squaremcp_launch.gif /usr/share/nginx/html/squaremcp_launch.gif
|
||||
COPY product/site/squaremcp_launch_poster.png /usr/share/nginx/html/squaremcp_launch_poster.png
|
||||
COPY product/site/squaremcp-hero-loop.mp4 /usr/share/nginx/html/squaremcp-hero-loop.mp4
|
||||
COPY product/site/privacy.html /usr/share/nginx/html/privacy.html
|
||||
COPY product/site/terms.html /usr/share/nginx/html/terms.html
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 662 KiB After Width: | Height: | Size: 530 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 576 KiB After Width: | Height: | Size: 508 KiB |
@@ -159,28 +159,25 @@ async function run() {
|
||||
await runMobileLayoutChecks(page);
|
||||
}
|
||||
|
||||
const heroImage = page.locator("#heroAnimation");
|
||||
await heroImage.waitFor();
|
||||
const initialSrc = await heroImage.getAttribute("src");
|
||||
const heroVideo = page.locator("#heroAnimation");
|
||||
await heroVideo.waitFor();
|
||||
const src = await heroVideo.getAttribute("src");
|
||||
assert(
|
||||
initialSrc && initialSrc.includes("squaremcp_launch.gif"),
|
||||
"hero image did not start with animated asset"
|
||||
);
|
||||
|
||||
const playMs = Number((await heroImage.getAttribute("data-play-ms")) || "0");
|
||||
assert(playMs > 0, "hero animation play duration missing");
|
||||
await page.waitForTimeout(playMs + 750);
|
||||
const finalSrc = await heroImage.getAttribute("src");
|
||||
assert(
|
||||
finalSrc && finalSrc.includes("squaremcp_launch_poster.png"),
|
||||
"hero image did not swap to poster"
|
||||
src && src.includes("squaremcp-hero-loop.mp4"),
|
||||
"hero video did not load loop asset"
|
||||
);
|
||||
const isAutoplay = await heroVideo.evaluate((el) => el.autoplay);
|
||||
const isLoop = await heroVideo.evaluate((el) => el.loop);
|
||||
const isMuted = await heroVideo.evaluate((el) => el.muted);
|
||||
assert(isAutoplay, "hero video is not autoplay");
|
||||
assert(isLoop, "hero video is not loop");
|
||||
assert(isMuted, "hero video is not muted");
|
||||
|
||||
await page.screenshot({ path: screenshotPath, fullPage: true });
|
||||
if (fs.existsSync(baselinePath)) {
|
||||
const compare = spawnSync(
|
||||
process.execPath,
|
||||
["product/site/compare-screenshot.mjs", screenshotPath, baselinePath, diffPath, "0.02", "0.015"],
|
||||
["product/site/compare-screenshot.mjs", screenshotPath, baselinePath, diffPath, "0.035", "0.025"],
|
||||
{ stdio: "inherit" }
|
||||
);
|
||||
assert(compare.status === 0, `visual diff failed for ${profile}`);
|
||||
|
||||
@@ -51,16 +51,16 @@
|
||||
|
||||
<section class="hero-panel" aria-labelledby="pilot-preview-title">
|
||||
<div class="hero-media">
|
||||
<img
|
||||
<video
|
||||
id="heroAnimation"
|
||||
src="./squaremcp_launch.gif"
|
||||
data-animated-src="./squaremcp_launch.gif"
|
||||
data-poster-src="./squaremcp_launch_poster.png"
|
||||
data-play-ms="9600"
|
||||
alt="SquareMCP launch storyboard preview"
|
||||
src="./squaremcp-hero-loop.mp4"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
width="728"
|
||||
height="410"
|
||||
/>
|
||||
></video>
|
||||
</div>
|
||||
<div class="panel-topline">Typical first deployment</div>
|
||||
<h2 id="pilot-preview-title">Internal support copilot with safe system access</h2>
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
const form = document.getElementById("pilotIntakeForm");
|
||||
const output = document.getElementById("pilotOutput");
|
||||
const copyButton = document.getElementById("copyRequestButton");
|
||||
const heroAnimation = document.getElementById("heroAnimation");
|
||||
const submitEndpoint = "/api/pilot-request";
|
||||
|
||||
if (heroAnimation) {
|
||||
const posterSrc = heroAnimation.dataset.posterSrc;
|
||||
const playMs = Number(heroAnimation.dataset.playMs || "0");
|
||||
|
||||
if (posterSrc && playMs > 0) {
|
||||
window.setTimeout(() => {
|
||||
heroAnimation.src = posterSrc;
|
||||
}, playMs);
|
||||
}
|
||||
}
|
||||
|
||||
function buildMessage(data) {
|
||||
return [
|
||||
"Pilot request for SquareMCP",
|
||||
|
||||
@@ -14,6 +14,7 @@ const contentTypes = {
|
||||
".css": "text/css; charset=utf-8",
|
||||
".js": "text/javascript; charset=utf-8",
|
||||
".json": "application/json; charset=utf-8",
|
||||
".mp4": "video/mp4",
|
||||
};
|
||||
|
||||
function resolvePath(urlPath) {
|
||||
|
||||
@@ -59,11 +59,10 @@ function createEnv({ valid = true, fetchOk = true, clipboardFails = false } = {}
|
||||
const timers = [];
|
||||
const output = { textContent: "", classList: createClassList() };
|
||||
const heroAnimation = {
|
||||
src: "./squaremcp_launch.gif",
|
||||
dataset: {
|
||||
posterSrc: "./squaremcp_launch_poster.png",
|
||||
playMs: "9600",
|
||||
},
|
||||
src: "./squaremcp-hero-loop.mp4",
|
||||
autoplay: true,
|
||||
loop: true,
|
||||
muted: true,
|
||||
};
|
||||
const entries = [
|
||||
["name", "Casey"],
|
||||
@@ -157,10 +156,11 @@ function assert(condition, message) {
|
||||
|
||||
async function run() {
|
||||
const validEnv = createEnv();
|
||||
assert(validEnv.timers.length === 1, "hero animation timer missing");
|
||||
assert(validEnv.timers[0].ms === 9600, "hero animation timer mismatch");
|
||||
validEnv.timers[0].fn();
|
||||
assert(validEnv.heroAnimation.src === "./squaremcp_launch_poster.png", "hero poster swap failed");
|
||||
// Hero is now a looping video — no timer-based poster swap
|
||||
assert(validEnv.timers.length === 0, "hero video should not set a poster swap timer");
|
||||
assert(validEnv.heroAnimation.src === "./squaremcp-hero-loop.mp4", "hero video src mismatch");
|
||||
assert(validEnv.heroAnimation.autoplay === true, "hero video should autoplay");
|
||||
assert(validEnv.heroAnimation.loop === true, "hero video should loop");
|
||||
|
||||
let prevented = false;
|
||||
await validEnv.form.dispatch("submit", {
|
||||
|
||||
BIN
product/site/squaremcp-hero-loop.mp4
Normal file
BIN
product/site/squaremcp-hero-loop.mp4
Normal file
Binary file not shown.
@@ -271,7 +271,8 @@ h3 {
|
||||
background: #08111f;
|
||||
}
|
||||
|
||||
.hero-media img {
|
||||
.hero-media img,
|
||||
.hero-media video {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
|
||||
@@ -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