mirror of
https://github.com/jlengrand/ghost-mcp.git
synced 2026-03-10 08:21:19 +00:00
⚡️ Improve update_post method
This commit is contained in:
@@ -8,7 +8,8 @@ authors = [
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"httpx",
|
"httpx",
|
||||||
"pyjwt",
|
"pyjwt",
|
||||||
"mcp[cli]>=1.2.1"
|
"mcp[cli]>=1.2.1",
|
||||||
|
"pytz"
|
||||||
]
|
]
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
[build-system]
|
[build-system]
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ from mcp.server.fastmcp import FastMCP, Context
|
|||||||
from .api import make_ghost_request, get_auth_headers
|
from .api import make_ghost_request, get_auth_headers
|
||||||
from .config import STAFF_API_KEY
|
from .config import STAFF_API_KEY
|
||||||
from .exceptions import GhostError
|
from .exceptions import GhostError
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import pytz
|
||||||
|
|
||||||
async def read_user(user_id: str, ctx: Context = None) -> str:
|
async def read_user(user_id: str, ctx: Context = None) -> str:
|
||||||
"""Get the details of a specific user.
|
"""Get the details of a specific user.
|
||||||
@@ -914,12 +916,12 @@ ID: {post.get('id', 'Unknown')}
|
|||||||
return str(e)
|
return str(e)
|
||||||
|
|
||||||
async def update_post(post_id: str, update_data: dict, ctx: Context = None) -> str:
|
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:
|
Args:
|
||||||
post_id: The ID of the post to update
|
post_id: The ID of the post to update
|
||||||
update_data: Dictionary containing the lexical content and updated_at timestamp.
|
update_data: Dictionary containing the updated data and updated_at timestamp.
|
||||||
Expected to have at least 'lexical' and 'updated_at' keys.
|
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:
|
The lexical content must be a properly escaped JSON string in this format:
|
||||||
{
|
{
|
||||||
"root": {
|
"root": {
|
||||||
@@ -955,10 +957,50 @@ async def update_post(post_id: str, update_data: dict, ctx: Context = None) -> s
|
|||||||
update_data = {
|
update_data = {
|
||||||
"post_id": "67abcffb7f82ac000179d76f",
|
"post_id": "67abcffb7f82ac000179d76f",
|
||||||
"update_data": {
|
"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 post’s 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
|
ctx: Optional context for logging
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -970,71 +1012,52 @@ async def update_post(post_id: str, update_data: dict, ctx: Context = None) -> s
|
|||||||
if ctx:
|
if ctx:
|
||||||
ctx.info(f"Updating post with ID: {post_id}")
|
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:
|
try:
|
||||||
|
# First, get the current post data to obtain the correct updated_at
|
||||||
if ctx:
|
if ctx:
|
||||||
ctx.debug("Getting auth headers")
|
ctx.debug("Getting current post data")
|
||||||
headers = await get_auth_headers(STAFF_API_KEY)
|
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 = {
|
post_update = {
|
||||||
"posts": [{
|
"posts": [{
|
||||||
"id": post_id,
|
"id": post_id,
|
||||||
"updated_at": update_data["updated_at"],
|
"updated_at": current_updated_at # Use the current updated_at timestamp
|
||||||
"lexical": update_data["lexical"]
|
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
||||||
# Optionally handle tag updates if provided
|
# Copy all update fields
|
||||||
import copy
|
for key, value in update_data.items():
|
||||||
update_fields = copy.deepcopy(update_data)
|
if key != "updated_at": # Skip updated_at from input
|
||||||
if "tags" in update_fields:
|
if key == "tags" and isinstance(value, list):
|
||||||
tags = update_fields.pop("tags")
|
post_update["posts"][0]["tags"] = [
|
||||||
post_update["posts"][0]["tags"] = [{"name": tag} if isinstance(tag, str) else tag for tag in tags]
|
{"name": tag} if isinstance(tag, str) else tag
|
||||||
|
for tag in value
|
||||||
# Copy any additional fields
|
]
|
||||||
for key, value in update_fields.items():
|
else:
|
||||||
if key not in ["updated_at", "lexical"]:
|
|
||||||
post_update["posts"][0][key] = value
|
post_update["posts"][0][key] = value
|
||||||
|
|
||||||
if ctx:
|
if ctx:
|
||||||
ctx.debug(f"Update payload: {json.dumps(post_update, indent=2)}")
|
ctx.debug(f"Update payload: {json.dumps(post_update, indent=2)}")
|
||||||
|
|
||||||
if ctx:
|
# Make the update request
|
||||||
ctx.debug(f"Making PUT request to /posts/{post_id}/")
|
|
||||||
data = await make_ghost_request(
|
data = await make_ghost_request(
|
||||||
f"posts/{post_id}/?formats=lexical&include=tags,authors",
|
f"posts/{post_id}/",
|
||||||
headers,
|
headers,
|
||||||
ctx,
|
ctx,
|
||||||
http_method="PUT",
|
http_method="PUT",
|
||||||
json_data=post_update
|
json_data=post_update
|
||||||
)
|
)
|
||||||
|
|
||||||
if ctx:
|
# Process response...
|
||||||
ctx.debug(f"API Response: {json.dumps(data, indent=2)}")
|
|
||||||
|
|
||||||
post = data["posts"][0]
|
post = data["posts"][0]
|
||||||
|
|
||||||
# Format tags and authors for display
|
# Format response...
|
||||||
tags = [tag.get('name', 'Unknown') for tag in post.get('tags', [])]
|
tags = [tag.get('name', 'Unknown') for tag in post.get('tags', [])]
|
||||||
authors = [author.get('name', 'Unknown') for author in post.get('authors', [])]
|
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"""
|
return f"""
|
||||||
Post Updated Successfully:
|
Post Updated Successfully:
|
||||||
Title: {post.get('title', 'Untitled')}
|
Title: {post.get('title', 'Untitled')}
|
||||||
@@ -1047,8 +1070,6 @@ Tags: {', '.join(tags) if tags else 'None'}
|
|||||||
Authors: {', '.join(authors) if authors else 'None'}
|
Authors: {', '.join(authors) if authors else 'None'}
|
||||||
Published At: {post.get('published_at', 'Not published')}
|
Published At: {post.get('published_at', 'Not published')}
|
||||||
Updated At: {post.get('updated_at', 'Unknown')}
|
Updated At: {post.get('updated_at', 'Unknown')}
|
||||||
Content Preview: {content_preview}
|
|
||||||
Excerpt: {excerpt}
|
|
||||||
"""
|
"""
|
||||||
except GhostError as e:
|
except GhostError as e:
|
||||||
if ctx:
|
if ctx:
|
||||||
|
|||||||
11
uv.lock
generated
11
uv.lock
generated
@@ -62,6 +62,7 @@ dependencies = [
|
|||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "mcp", extra = ["cli"] },
|
{ name = "mcp", extra = ["cli"] },
|
||||||
{ name = "pyjwt" },
|
{ name = "pyjwt" },
|
||||||
|
{ name = "pytz" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
@@ -69,6 +70,7 @@ requires-dist = [
|
|||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "mcp", extras = ["cli"], specifier = ">=1.2.1" },
|
{ name = "mcp", extras = ["cli"], specifier = ">=1.2.1" },
|
||||||
{ name = "pyjwt" },
|
{ name = "pyjwt" },
|
||||||
|
{ name = "pytz" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[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 },
|
{ 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]]
|
[[package]]
|
||||||
name = "rich"
|
name = "rich"
|
||||||
version = "13.9.4"
|
version = "13.9.4"
|
||||||
|
|||||||
Reference in New Issue
Block a user