x_post
Post to X as a single tweet or a reply thread. Each message supports up to 280 graphemes. Uses OAuth 1.0a HMAC-SHA1 signing. X's free tier allows 17 tweets per 24 hours.
Instructions
Post directly to X (Twitter) as a single tweet (max 280 graphemes) or a reply-chained thread via X v2 /tweets. OAuth 1.0a HMAC-SHA1 signing built in. FREE in this server, but X's free API tier caps writes at 17 tweets per 24h. Requires social.x.api_key, api_secret, access_token, access_token_secret via setup. Returns: { id, url, tweets?: [{ id, url }] }. Common errors: missing credentials (VALIDATION_ERROR), 280-grapheme overflow (VALIDATION_ERROR), 429 rate-limit (PLATFORM_ERROR).
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| text | No | Single post text (<= 280 chars). Mutually exclusive with `thread`. | |
| thread | No | Array of posts to chain as a reply thread. Each <= 280 chars. |
Implementation Reference
- src/tools/broadcast-tools.ts:168-172 (schema)Zod schema for x_post tool input: text (string, <= 280 chars) or thread (array of strings, each <= 280 chars), mutually exclusive.
/** Zod schema for the `x_post` tool input. */ export const xPostSchema = z.object({ text: z.string().optional().describe("Single post text (<= 280 chars). Mutually exclusive with `thread`."), thread: z.array(z.string()).optional().describe("Array of posts to chain as a reply thread. Each <= 280 chars."), }); - src/tools/broadcast-tools.ts:174-222 (handler)handleXPost function: validates credentials, dispatches single post (postToX) or threaded series (postThreadToX), returns url/id/posts.
/** * Post directly to X (Twitter). * * Free tool — no credit cost, but X's free API tier caps writes at 17 * tweets per 24h per user. Requires `social.x` with OAuth 1.0a credentials * (consumer_key, consumer_secret, access_token, access_token_secret). * * Supports a single post (`text`) or a threaded series (`thread`). Threads * are chained via the v2 `reply.in_reply_to_tweet_id` field. */ export async function handleXPost(input: z.infer<typeof xPostSchema>) { if (!input.text && (!input.thread || input.thread.length === 0)) { return makeError("VALIDATION_ERROR", "Provide either `text` or `thread`"); } if (input.text && input.thread && input.thread.length > 0) { return makeError("VALIDATION_ERROR", "Provide either `text` or `thread`, not both"); } const config = readConfig(); const creds = config.social?.x; if ( !creds?.consumer_key || !creds?.consumer_secret || !creds?.access_token || !creds?.access_token_secret ) { return makeError( "AUTH_FAILED", 'X not configured. Run the "setup" tool with platform: "x" and OAuth 1.0a credentials: { consumer_key, consumer_secret, access_token, access_token_secret }.' ); } if (input.thread && input.thread.length > 0) { const result = await postThreadToX(input.thread, creds); if (!result.success) return result; return makeSuccess({ posts: result.data.posts, thread_url: result.data.posts[0]?.url, count: result.data.posts.length, }); } const result = await postToX(input.text!, creds); if (!result.success) return result; return makeSuccess({ url: result.data.url, id: result.data.id, }); } - src/index.ts:259-263 (registration)Server registration of the x_post tool with description, schema shape, and async handler that parses input and delegates to handleXPost.
server.tool("x_post", "Post directly to X (Twitter) as a single tweet (max 280 graphemes) or a reply-chained thread via X v2 /tweets. OAuth 1.0a HMAC-SHA1 signing built in. FREE in this server, but X's free API tier caps writes at 17 tweets per 24h. Requires social.x.api_key, api_secret, access_token, access_token_secret via setup. Returns: { id, url, tweets?: [{ id, url }] }. Common errors: missing credentials (VALIDATION_ERROR), 280-grapheme overflow (VALIDATION_ERROR), 429 rate-limit (PLATFORM_ERROR).", xPostSchema.shape, async (input) => { const parsed = xPostSchema.parse(input); const result = await handleXPost(parsed); return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] }; }); - src/broadcast/x.ts:105-152 (helper)postToX: core function that posts a single tweet to X v2 /tweets endpoint with OAuth 1.0a HMAC-SHA1 signed header.
export async function postToX( text: string, credentials: XCredentials, options?: { replyToId?: string } ): Promise<ToolResult<XPost>> { if (!text.trim()) { return makeError("VALIDATION_ERROR", "Post text cannot be empty"); } if ([...text].length > MAX_LENGTH) { return makeError( "VALIDATION_ERROR", `X posts must be <= ${MAX_LENGTH} characters (got ${[...text].length})` ); } const url = "https://api.twitter.com/2/tweets"; const body: Record<string, unknown> = { text }; if (options?.replyToId) { body.reply = { in_reply_to_tweet_id: options.replyToId }; } const authHeader = buildOAuthHeader("POST", url, credentials); const result = await httpRequest(url, { method: "POST", headers: { Authorization: authHeader }, body, }); if (!result.success) { return makeError(result.error.code, result.error.message, { platform: "x", retryable: result.error.retryable, }); } const data = result.data as { data?: { id?: string } }; if (!data.data?.id) { return makeError("PLATFORM_ERROR", "X response missing tweet id", { platform: "x", }); } return makeSuccess({ id: data.data.id, url: `https://x.com/i/status/${data.data.id}`, }); } - src/broadcast/x.ts:162-189 (helper)postThreadToX: posts a reply-chained thread by calling postToX sequentially, linking each via in_reply_to_tweet_id.
export async function postThreadToX( posts: string[], credentials: XCredentials ): Promise<ToolResult<{ posts: XPost[] }>> { if (posts.length === 0) { return makeError("VALIDATION_ERROR", "Thread must contain at least one post"); } for (const [i, p] of posts.entries()) { if ([...p].length > MAX_LENGTH) { return makeError( "VALIDATION_ERROR", `Post ${i + 1} exceeds ${MAX_LENGTH} chars (got ${[...p].length})` ); } } const results: XPost[] = []; let replyTo: string | undefined; for (const text of posts) { const result = await postToX(text, credentials, { replyToId: replyTo }); if (!result.success) return result; results.push(result.data); replyTo = result.data.id; } return makeSuccess({ posts: results }); }