Skip to main content
Glama
cjkcr

X(Twitter) MCP Server

by cjkcr

publish_draft

Publishes a saved draft tweet or thread to X (Twitter) using its unique draft ID.

Instructions

Publish a draft tweet or thread

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
draft_idYesID of the draft to publish

Implementation Reference

  • Registration of the 'publish_draft' tool in the MCP server's list_tools() method. Defines the tool name, description, and input schema which requires a 'draft_id' string.
        name="publish_draft",
        description="Publish a draft tweet or thread",
        inputSchema={
            "type": "object",
            "properties": {
                "draft_id": {
                    "type": "string",
                    "description": "ID of the draft to publish",
                },
            },
            "required": ["draft_id"],
        },
    ),
  • Input schema for the publish_draft tool: an object requiring 'draft_id' as a string.
    inputSchema={
        "type": "object",
        "properties": {
            "draft_id": {
                "type": "string",
                "description": "ID of the draft to publish",
            },
        },
        "required": ["draft_id"],
    },
  • Main handler implementation for publish_draft tool. Loads draft JSON file, handles various draft types (single tweet, reply, quote tweet, media tweet, thread), publishes via Tweepy Twitter API v2 client, uploads media if needed using v1.1 API, deletes draft on success or configurable failure, returns published tweet/thread IDs.
    async def handle_publish_draft(arguments: Any) -> Sequence[TextContent]:
        if not isinstance(arguments, dict) or "draft_id" not in arguments:
            raise ValueError("Invalid arguments for publish_draft")
        draft_id = arguments["draft_id"]
        filepath = os.path.join("drafts", draft_id)
        if not os.path.exists(filepath):
            raise ValueError(f"Draft {draft_id} does not exist")
        
        # Read the draft first
        try:
            with open(filepath, "r") as f:
                draft = json.load(f)
        except Exception as e:
            logger.error(f"Error reading draft {draft_id}: {str(e)}")
            raise RuntimeError(f"Error reading draft {draft_id}: {str(e)}")
        
        # Try to publish the draft
        try:
            if "content" in draft:
                content = draft["content"]
                
                # Check if this is a reply draft
                if draft.get("type") == "reply" and "reply_to_tweet_id" in draft:
                    # Reply to existing tweet
                    reply_to_tweet_id = draft["reply_to_tweet_id"]
                    response = get_write_client().create_tweet(text=content, in_reply_to_tweet_id=reply_to_tweet_id)
                    tweet_id = response.data['id']
                    logger.info(f"Published reply tweet ID {tweet_id} to tweet {reply_to_tweet_id}")
                    
                    # Only delete the draft after successful publishing
                    os.remove(filepath)
                    return [
                        TextContent(
                            type="text",
                            text=f"Draft {draft_id} published as reply tweet ID {tweet_id} to tweet {reply_to_tweet_id}",
                        )
                    ]
                else:
                    # Single tweet
                    response = get_write_client().create_tweet(text=content)
                    tweet_id = response.data['id']
                    logger.info(f"Published tweet ID {tweet_id}")
                    
                    # Only delete the draft after successful publishing
                    os.remove(filepath)
                    return [
                        TextContent(
                            type="text",
                            text=f"Draft {draft_id} published as tweet ID {tweet_id}",
                        )
                    ]
            elif "comment" in draft and draft.get("type") == "quote_tweet":
                # Quote tweet draft
                comment = draft["comment"]
                quote_tweet_id = draft["quote_tweet_id"]
                
                response = get_write_client().create_tweet(text=comment, quote_tweet_id=quote_tweet_id)
                tweet_id = response.data['id']
                logger.info(f"Published quote tweet ID {tweet_id} quoting tweet {quote_tweet_id}")
                
                # Only delete the draft after successful publishing
                os.remove(filepath)
                return [
                    TextContent(
                        type="text",
                        text=f"Draft {draft_id} published as quote tweet ID {tweet_id} quoting tweet {quote_tweet_id}",
                    )
                ]
            elif "media_files" in draft and draft.get("type") == "tweet_with_media":
                # Tweet with media draft
                content = draft["content"]
                media_files = draft["media_files"]
                
                # Upload media files and collect media IDs
                media_ids = []
                for media_file in media_files:
                    file_path = media_file["file_path"]
                    media_type = media_file["media_type"]
                    alt_text = media_file.get("alt_text")
                    
                    # Check if file exists
                    if not os.path.exists(file_path):
                        raise ValueError(f"Media file not found: {file_path}")
                    
                    # Upload media
                    media_upload = api.media_upload(filename=file_path)
                    media_id = media_upload.media_id_string
                    media_ids.append(media_id)
                    
                    # Add alt text if provided and media is an image
                    if alt_text and media_type in ["image", "gif"]:
                        api.create_media_metadata(media_id=media_id, alt_text=alt_text)
                    
                    logger.info(f"Uploaded {media_type} for draft: {media_id}")
                
                # Create tweet with media
                response = get_write_client().create_tweet(text=content, media_ids=media_ids)
                tweet_id = response.data['id']
                logger.info(f"Published tweet with media ID {tweet_id}, media IDs: {media_ids}")
                
                # Only delete the draft after successful publishing
                os.remove(filepath)
                return [
                    TextContent(
                        type="text",
                        text=f"Draft {draft_id} published as tweet with media ID {tweet_id} ({len(media_ids)} media files)",
                    )
                ]
            elif "contents" in draft:
                # Thread
                contents = draft["contents"]
                # Publish the thread
                published_tweet_ids = []
                last_tweet_id = None
                
                try:
                    for i, content in enumerate(contents):
                        if last_tweet_id is None:
                            response = get_write_client().create_tweet(text=content)
                        else:
                            response = get_write_client().create_tweet(text=content, in_reply_to_tweet_id=last_tweet_id)
                        last_tweet_id = response.data['id']
                        published_tweet_ids.append(last_tweet_id)
                        await asyncio.sleep(1)  # Avoid hitting rate limits
                    
                    logger.info(f"Published thread with {len(published_tweet_ids)} tweets, starting with ID {published_tweet_ids[0]}")
                    
                    # Only delete the draft after successful publishing of entire thread
                    os.remove(filepath)
                    return [
                        TextContent(
                            type="text",
                            text=f"Draft {draft_id} published as thread with {len(published_tweet_ids)} tweets, starting with tweet ID {published_tweet_ids[0]}",
                        )
                    ]
                except Exception as thread_error:
                    # If thread publishing fails partway through, log which tweets were published
                    if published_tweet_ids:
                        logger.error(f"Thread publishing failed after {len(published_tweet_ids)} tweets. Published tweet IDs: {published_tweet_ids}")
                        # Delete the draft even if thread partially published
                        delete_draft_on_failure(draft_id, filepath)
                        status_msg = "Draft has been deleted." if AUTO_DELETE_FAILED_DRAFTS else "Draft preserved for retry."
                        raise RuntimeError(f"Thread publishing failed after {len(published_tweet_ids)} tweets. Published tweets: {published_tweet_ids}. {status_msg} Error: {thread_error}")
                    else:
                        # No tweets were published, the error will be handled by the outer exception handler
                        raise thread_error
            else:
                raise ValueError(f"Invalid draft format for {draft_id}")
                
        except tweepy.TweepError as e:
            logger.error(f"Twitter API error publishing draft {draft_id}: {e}")
            delete_draft_on_failure(draft_id, filepath)
            status_msg = "Draft has been deleted." if AUTO_DELETE_FAILED_DRAFTS else "Draft preserved for retry."
            raise RuntimeError(f"Twitter API error publishing draft {draft_id}: {e}. {status_msg}")
        except Exception as e:
            logger.error(f"Error publishing draft {draft_id}: {str(e)}")
            delete_draft_on_failure(draft_id, filepath)
            status_msg = "Draft has been deleted." if AUTO_DELETE_FAILED_DRAFTS else "Draft preserved for retry."
            raise RuntimeError(f"Error publishing draft {draft_id}: {str(e)}. {status_msg}")
  • Helper utility called by publish_draft handler on errors to conditionally delete the draft file based on AUTO_DELETE_FAILED_DRAFTS configuration.
    def delete_draft_on_failure(draft_id: str, filepath: str) -> None:
        """Delete draft file if auto-delete is enabled"""
        if AUTO_DELETE_FAILED_DRAFTS:
            try:
                os.remove(filepath)
                logger.info(f"Deleted draft {draft_id} due to publishing failure (auto-delete enabled)")
            except Exception as delete_error:
                logger.error(f"Failed to delete draft {draft_id} after publishing error: {delete_error}")
        else:
            logger.info(f"Draft {draft_id} preserved for retry (auto-delete disabled)")
  • Dispatch registration in the @server.call_tool() handler that routes 'publish_draft' calls to the handle_publish_draft function.
    elif name == "publish_draft":
        return await handle_publish_draft(arguments)
Behavior2/5

Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?

With no annotations provided, the description carries the full burden of behavioral disclosure. It states the action ('Publish') which implies a write/mutation operation, but doesn't disclose any behavioral traits such as whether this is destructive (e.g., does publishing remove the draft?), what permissions are required, error conditions, or what happens after publishing. This leaves significant gaps for an agent to understand the tool's behavior.

Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.

Conciseness5/5

Is the description appropriately sized, front-loaded, and free of redundancy?

The description is extremely concise - a single sentence with zero wasted words. It's front-loaded with the core action and resource, making it immediately clear what the tool does without any unnecessary elaboration.

Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.

Completeness2/5

Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?

Given this is a mutation tool (publishing implies writing/changing state) with no annotations and no output schema, the description is incomplete. It doesn't explain what happens after publishing, what gets returned, error scenarios, or how this differs from other publishing tools in the sibling list. For a tool that changes system state, more context is needed.

Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.

Parameters3/5

Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?

The schema description coverage is 100%, with the single parameter 'draft_id' clearly documented in the schema. The description doesn't add any additional semantic context about the parameter beyond what's in the schema (e.g., format examples, where to find draft IDs, or validation rules). This meets the baseline of 3 when the schema does the heavy lifting.

Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.

Purpose4/5

Does the description clearly state what the tool does and how it differs from similar tools?

The description clearly states the action ('Publish') and the resource ('a draft tweet or thread'), making the purpose immediately understandable. However, it doesn't differentiate this tool from its siblings like 'quote_tweet' or 'reply_to_tweet' which also involve publishing content, leaving some ambiguity about when this specific tool should be used versus those alternatives.

Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.

Usage Guidelines2/5

Does the description explain when to use this tool, when not to, or what alternatives exist?

The description provides no guidance on when to use this tool versus alternatives like 'quote_tweet' or 'reply_to_tweet'. It doesn't mention prerequisites (e.g., needing an existing draft), exclusions, or contextual factors that would help an agent choose correctly among the publishing-related tools in the sibling list.

Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.

Install Server

Other Tools

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/cjkcr/x-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server