️ Improve update_post method

This commit is contained in:
Fanyang Meng
2025-02-11 20:39:27 -05:00
parent ffd9ef8cb5
commit db421b6f03
3 changed files with 80 additions and 47 deletions

View File

@@ -8,7 +8,8 @@ authors = [
dependencies = [
"httpx",
"pyjwt",
"mcp[cli]>=1.2.1"
"mcp[cli]>=1.2.1",
"pytz"
]
requires-python = ">=3.12"
[build-system]

View File

@@ -7,6 +7,8 @@ from mcp.server.fastmcp import FastMCP, Context
from .api import make_ghost_request, get_auth_headers
from .config import STAFF_API_KEY
from .exceptions import GhostError
from datetime import datetime, timedelta
import pytz
async def read_user(user_id: str, ctx: Context = None) -> str:
"""Get the details of a specific user.
@@ -914,12 +916,12 @@ ID: {post.get('id', 'Unknown')}
return str(e)
async def update_post(post_id: str, update_data: dict, ctx: Context = None) -> str:
"""Update a blog post using lexical content.
"""Update a blog post with new data.
Args:
post_id: The ID of the post to update
update_data: Dictionary containing the lexical content and updated_at timestamp.
Expected to have at least 'lexical' and 'updated_at' keys.
update_data: Dictionary containing the updated data and updated_at timestamp.
Note: 'updated_at' is required. If 'lexical' is provided, it must be a valid JSON string.
The lexical content must be a properly escaped JSON string in this format:
{
"root": {
@@ -955,10 +957,50 @@ async def update_post(post_id: str, update_data: dict, ctx: Context = None) -> s
update_data = {
"post_id": "67abcffb7f82ac000179d76f",
"update_data": {
"lexical": "{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello World\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}",
"updated_at": "2025-02-11T22:54:40.000Z"
"updated_at": "2025-02-11T22:54:40.000Z",
"lexical": "{\"root\":{\"children\":[{\"children\":[{\"detail\":0,\"format\":0,\"mode\":\"normal\",\"style\":\"\",\"text\":\"Hello World\",\"type\":\"text\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"paragraph\",\"version\":1}],\"direction\":\"ltr\",\"format\":\"\",\"indent\":0,\"type\":\"root\",\"version\":1}}"
}
}
Updatable fields for a blog post:
- slug: Unique URL slug for the post.
- id: Identifier of the post.
- uuid: Universally unique identifier for the post.
- title: The title of the post.
- lexical: JSON string representing the post content in lexical format.
- html: HTML version of the post content.
- comment_id: Identifier for the comment thread.
- feature_image: URL to the posts feature image.
- feature_image_alt: Alternate text for the feature image.
- feature_image_caption: Caption for the feature image.
- featured: Boolean flag indicating if the post is featured.
- status: The publication status (e.g., published, draft).
- visibility: Visibility setting (e.g., public, private).
- created_at: Timestamp when the post was created.
- updated_at: Timestamp when the post was last updated.
- published_at: Timestamp when the post was published.
- custom_excerpt: Custom excerpt text for the post.
- codeinjection_head: Code to be injected into the head section.
- codeinjection_foot: Code to be injected into the footer section.
- custom_template: Custom template assigned to the post.
- canonical_url: The canonical URL for SEO purposes.
- tags: List of tag objects associated with the post.
- authors: List of author objects for the post.
- primary_author: The primary author object.
- primary_tag: The primary tag object.
- url: Direct URL link to the post.
- excerpt: Short excerpt or summary of the post.
- og_image: Open Graph image URL for social sharing.
- og_title: Open Graph title for social sharing.
- og_description: Open Graph description for social sharing.
- twitter_image: Twitter-specific image URL.
- twitter_title: Twitter-specific title.
- twitter_description: Twitter-specific description.
- meta_title: Meta title for SEO.
- meta_description: Meta description for SEO.
- email_only: Boolean flag indicating if the post is for email distribution only.
- newsletter: Dictionary containing newsletter configuration details.
- email: Dictionary containing email details related to the post.
ctx: Optional context for logging
Returns:
@@ -970,71 +1012,52 @@ async def update_post(post_id: str, update_data: dict, ctx: Context = None) -> s
if ctx:
ctx.info(f"Updating post with ID: {post_id}")
if 'updated_at' not in update_data:
error_msg = "updated_at field is required for post updates"
if ctx:
ctx.error(error_msg)
return error_msg
if 'lexical' not in update_data:
error_msg = "lexical field is required for post updates"
if ctx:
ctx.error(error_msg)
return error_msg
try:
# First, get the current post data to obtain the correct updated_at
if ctx:
ctx.debug("Getting auth headers")
ctx.debug("Getting current post data")
headers = await get_auth_headers(STAFF_API_KEY)
current_post = await make_ghost_request(f"posts/{post_id}/", headers, ctx)
current_updated_at = current_post["posts"][0]["updated_at"]
# Prepare update payload with lexical content
# Prepare update payload
post_update = {
"posts": [{
"id": post_id,
"updated_at": update_data["updated_at"],
"lexical": update_data["lexical"]
"updated_at": current_updated_at # Use the current updated_at timestamp
}]
}
# Optionally handle tag updates if provided
import copy
update_fields = copy.deepcopy(update_data)
if "tags" in update_fields:
tags = update_fields.pop("tags")
post_update["posts"][0]["tags"] = [{"name": tag} if isinstance(tag, str) else tag for tag in tags]
# Copy any additional fields
for key, value in update_fields.items():
if key not in ["updated_at", "lexical"]:
post_update["posts"][0][key] = value
# Copy all update fields
for key, value in update_data.items():
if key != "updated_at": # Skip updated_at from input
if key == "tags" and isinstance(value, list):
post_update["posts"][0]["tags"] = [
{"name": tag} if isinstance(tag, str) else tag
for tag in value
]
else:
post_update["posts"][0][key] = value
if ctx:
ctx.debug(f"Update payload: {json.dumps(post_update, indent=2)}")
if ctx:
ctx.debug(f"Making PUT request to /posts/{post_id}/")
# Make the update request
data = await make_ghost_request(
f"posts/{post_id}/?formats=lexical&include=tags,authors",
f"posts/{post_id}/",
headers,
ctx,
http_method="PUT",
json_data=post_update
)
if ctx:
ctx.debug(f"API Response: {json.dumps(data, indent=2)}")
# Process response...
post = data["posts"][0]
# Format tags and authors for display
# Format response...
tags = [tag.get('name', 'Unknown') for tag in post.get('tags', [])]
authors = [author.get('name', 'Unknown') for author in post.get('authors', [])]
# Get content preview (lexical content)
lexical_content = post.get('lexical', '')
content_preview = lexical_content[:200] + "..." if lexical_content else 'No content available'
excerpt = post.get('excerpt', 'No excerpt available')
return f"""
Post Updated Successfully:
Title: {post.get('title', 'Untitled')}
@@ -1047,8 +1070,6 @@ Tags: {', '.join(tags) if tags else 'None'}
Authors: {', '.join(authors) if authors else 'None'}
Published At: {post.get('published_at', 'Not published')}
Updated At: {post.get('updated_at', 'Unknown')}
Content Preview: {content_preview}
Excerpt: {excerpt}
"""
except GhostError as e:
if ctx:

11
uv.lock generated
View File

@@ -62,6 +62,7 @@ dependencies = [
{ name = "httpx" },
{ name = "mcp", extra = ["cli"] },
{ name = "pyjwt" },
{ name = "pytz" },
]
[package.metadata]
@@ -69,6 +70,7 @@ requires-dist = [
{ name = "httpx" },
{ name = "mcp", extras = ["cli"], specifier = ">=1.2.1" },
{ name = "pyjwt" },
{ name = "pytz" },
]
[[package]]
@@ -265,6 +267,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 },
]
[[package]]
name = "pytz"
version = "2025.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5f/57/df1c9157c8d5a05117e455d66fd7cf6dbc46974f832b1058ed4856785d8a/pytz-2025.1.tar.gz", hash = "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e", size = 319617 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/38/ac33370d784287baa1c3d538978b5e2ea064d4c1b93ffbd12826c190dd10/pytz-2025.1-py2.py3-none-any.whl", hash = "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57", size = 507930 },
]
[[package]]
name = "rich"
version = "13.9.4"