Tool reference

Blog tools

All 20 blog MCP tools with input shape, response shape, and example calls.

Twenty tools cover every blog operation an agent might want — discovery, taxonomy (categories/tags), content writes, lifecycle management, and analytics (stubbed for now).

All endpoints follow the pattern POST https://mcp.vlozi.app/tools/blog.<name> with Authorization: Bearer ls_xxx. All responses use this envelope:

{ "data": <payload>, "error": null }   // success
{ "data": null,      "error": "..." }  // failure (with HTTP 4xx/5xx)

Discovery

blog.list_posts

List posts in your workspace.

Scope: blog:posts.read

Input:

Field Type Notes
status "draft" | "published" | "scheduled" Filter by state
page number 1-based, default 1
limit number Default 20, max 100
sort "createdAt" | "publishedAt" | "title" Default createdAt
order "asc" | "desc" Default desc

Response: { posts: Post[], pagination: { page, limit, total, totalPages, hasMore } }

Each Post includes id, title, slug, status, excerpt, categoryId, featuredImageUrl, seoTitle, seoDescription, scheduledFor, publishedAt, createdAt, updatedAt, tags[].

curl -X POST https://mcp.vlozi.app/tools/blog.list_posts \
  -H "Authorization: Bearer $VLOZI_API_KEY" \
  -H "content-type: application/json" \
  -d '{"status": "published", "limit": 10, "sort": "publishedAt"}'

blog.get_post

Get one post by id OR slug — pass exactly one. Returns the full TipTap content body plus hydrated tags and category.

Scope: blog:posts.read

Input: { id?: string, slug?: string }

Response: { post: FullPost }FullPost includes content (TipTap doc), tags[], and category.

# By id
curl -X POST https://mcp.vlozi.app/tools/blog.get_post \
  -H "Authorization: Bearer $VLOZI_API_KEY" \
  -d '{"id": "post_xxx"}'
 
# By slug
curl -X POST https://mcp.vlozi.app/tools/blog.get_post \
  -H "Authorization: Bearer $VLOZI_API_KEY" \
  -d '{"slug": "hello-world"}'

blog.search_posts

Substring search across title, slug, and excerpt (case-insensitive).

Scope: blog:posts.read

Input: { query: string, limit?: number } (limit default 20, max 50)

Response: { posts: PostSummary[], query: string }

TIP

For semantic search across post bodies, use brain.query against ingested content instead.

curl -X POST https://mcp.vlozi.app/tools/blog.search_posts \
  -H "Authorization: Bearer $VLOZI_API_KEY" \
  -d '{"query": "ai", "limit": 5}'

blog.list_categories

List all categories with how many posts use each.

Scope: blog:posts.read

Input: { limit?: number } (default 100, max 500)

Response: { categories: { id, name, slug, postCount }[] }

Call this before passing categoryId to create_draft or update_post so your agent can pick a real one.


blog.list_tags

List all tags with how many posts use each.

Scope: blog:posts.read

Input: { limit?: number } (default 100, max 500)

Response: { tags: { id, name, slug, postCount }[] }

NOTE

When creating or updating posts, pass tag names (not IDs) — the service upserts new tags automatically. Use list_tags to see what already exists if you want to reuse them.

Taxonomy

CRUD for categories and tags, at parity with the admin dashboard. Prefer passing tag names directly to create_draft/update_post (which upsert automatically) — use create_tag only to pre-create a tag without attaching it to a post yet.

blog.create_category

Create a new category. Call list_categories first — only create one when no suitable category exists.

Scope: blog:posts.create

Input: { name: string } (1–100 chars)

Response: { category: { id, name, slug } }

NOTE

Returns 409 if a category with the same name (or derived slug) already exists in the workspace.

curl -X POST https://mcp.vlozi.app/tools/blog.create_category \
  -H "Authorization: Bearer $VLOZI_API_KEY" \
  -d '{"name": "Product Updates"}'

blog.update_category

Rename a category. The slug is re-derived from the new name.

Scope: blog:posts.update

Input: { id: string, name: string } (1–100 chars)

Response: { category: { id, name, slug } }

Returns 404 if the category isn't in this workspace, or 409 if another category already uses the resulting name/slug.


blog.delete_category

Delete a category. Posts in that category are not deleted — their categoryId is cleared, not the post itself.

Scope: blog:posts.delete

Input: { id: string }

Response: { id: string, deleted: true }

CAUTION

Irreversible. Agents should confirm with the user before calling. Returns 404 if the category isn't in this workspace.


blog.create_tag

Create a new tag. The name must be unique in the workspace.

Scope: blog:posts.create

Input: { name: string } (1–50 chars)

Response: { tag: { id, name, slug } }

NOTE

Returns 409 on a duplicate name/slug.


blog.update_tag

Rename a tag. The slug is re-derived, and every post using the tag reflects the new name.

Scope: blog:posts.update

Input: { id: string, name: string } (1–50 chars)

Response: { tag: { id, name, slug } }

Returns 404 if the tag isn't in this workspace, or 409 if another tag already uses the new name/slug.


blog.delete_tag

Delete a tag. It's removed from every post that used it — the posts themselves are kept.

Scope: blog:posts.delete

Input: { id: string }

Response: { id: string, deleted: true }

CAUTION

Irreversible. Agents should confirm with the user before calling. Returns 404 if the tag isn't in this workspace.

Content writes

blog.create_draft

Create a new draft.

Scope: blog:posts.create

Input:

Field Type Notes
title string (required) Max 255 chars
content string | object Plain text (auto-wrapped as paragraph) or full TipTap doc
slug string Auto-generated from title if omitted; collision-safe
excerpt string Max 1000 chars
categoryId string Must exist in your workspace
tags string[] Tag names (not IDs); new tags auto-created
seoTitle string Defaults to title
seoDescription string Max 500 chars
featuredImageUrl string

Response: { post: FullPost } with status: "draft".

# Plain text
curl -X POST https://mcp.vlozi.app/tools/blog.create_draft \
  -H "Authorization: Bearer $VLOZI_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "title": "My first agent-written post",
    "content": "Hello, world. I am an AI.",
    "tags": ["ai-generated", "intro"],
    "excerpt": "Drafted by an agent."
  }'
 
# Rich content (TipTap doc)
curl -X POST https://mcp.vlozi.app/tools/blog.create_draft \
  -H "Authorization: Bearer $VLOZI_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "title": "Rich content",
    "content": {
      "type": "doc",
      "content": [
        { "type": "heading", "attrs": { "level": 2 }, "content": [{ "type": "text", "text": "Intro" }] },
        { "type": "paragraph", "content": [{ "type": "text", "text": "Body paragraph." }] }
      ]
    }
  }'

blog.generate_draft

AI-write a full draft from a title + optional brief, then save it as a draft — the model writes the body, you review/edit and publish separately. A good flow action: turn an idea (or a form submission) into a ready-to-edit draft.

Scope: blog:posts.create

Input:

Field Type Notes
title string (required) Max 255 chars — also steers the content
brief string Optional key points / angle / audience for the model to cover
tone string Optional, e.g. "friendly", "authoritative", "playful"
length "short" | "medium" | "long" ~300w / ~600w / ~1000w. Default medium
categoryId string Must exist in your workspace — validated before charging
tags string[] Tag names (not IDs); new tags auto-created

Response: { post: FullPost, creditsCharged: 30, model: string }

IMPORTANT

Charges a flat 30-credit AI fee up front, refunded automatically if generation fails. Returns 503 if no AI model key is configured, 402 if you're out of credits, 403 if the plan is inactive, or 502 (with the charge refunded) if generation itself fails.

curl -X POST https://mcp.vlozi.app/tools/blog.generate_draft \
  -H "Authorization: Bearer $VLOZI_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "title": "Why we rebuilt our onboarding flow",
    "brief": "Cover the 3 biggest drop-off points we fixed and the metrics that moved.",
    "tone": "friendly",
    "length": "medium"
  }'

blog.update_post

Update any subset of fields. Omit a field to leave it unchanged.

Scope: blog:posts.update

Input: Same shape as create_draft plus required id. Special semantics:

Behavior How
Replace tag set Pass tags: ["a", "b"] — REPLACES existing tags
Clear all tags Pass tags: []
Clear category Pass categoryId: null
Keep existing tags Omit tags from request

WARNING

tags is a replace, not a merge. To preserve existing tags while adding new ones, call get_post first, then send the merged list.

Response: { post: FullPost }

curl -X POST https://mcp.vlozi.app/tools/blog.update_post \
  -H "Authorization: Bearer $VLOZI_API_KEY" \
  -H "content-type: application/json" \
  -d '{
    "id": "post_xxx",
    "title": "Updated title",
    "excerpt": "Refreshed by an agent."
  }'

blog.delete_post

Soft-delete a post — sets a tombstone and cancels any pending publish schedule. Recoverable via restore_post. The category is untouched; tags stay attached (they're only cascade-cleared if the post is later hard-deleted at the database level, which no MCP tool does).

Scope: blog:posts.delete

Input: { id: string }

Response: { id: string, deleted: true }

NOTE

Not permanent — see restore_post below. Agents should still confirm with the user before calling, since the post disappears from every listing immediately.


blog.restore_post

Restore a soft-deleted post — reverses delete_post.

Scope: blog:posts.delete

Input: { id: string }

Response: { id: string, restored: true }

NOTE

Returns 404 if the post doesn't exist or isn't currently deleted. A post deleted while "scheduled" restores as "draft", not "scheduled" — its publish schedule was already cancelled at delete time. If a live post has taken the same slug in the meantime, the restored post's slug gets a unique suffix.

Lifecycle

blog.publish_post

Publish a draft immediately, or schedule it for a future ISO 8601 timestamp.

Scope: blog:posts.publish

Input: { id: string, scheduledFor?: string }

scheduledFor value Result
Omitted Publishes immediately, sets publishedAt: now, status → "published"
Future ISO 8601 Schedules, sets scheduledFor, status → "scheduled"
Past or invalid 400 invalid_input — rejected

Response: { post: { id, status, publishedAt, scheduledFor } }

# Publish now
curl -X POST https://mcp.vlozi.app/tools/blog.publish_post \
  -H "Authorization: Bearer $VLOZI_API_KEY" \
  -d '{"id": "post_xxx"}'
 
# Schedule for tomorrow 9am UTC
curl -X POST https://mcp.vlozi.app/tools/blog.publish_post \
  -H "Authorization: Bearer $VLOZI_API_KEY" \
  -d '{"id": "post_xxx", "scheduledFor": "2026-05-15T09:00:00Z"}'

blog.unpublish_post

Move a published or scheduled post back to draft. Public URLs stop serving it.

Scope: blog:posts.publish

Input: { id: string }

Response: { post: { id, status } }


blog.unschedule_post

Clear a post's scheduled publish time, returning it to draft. Does not affect already-published posts — calling this on a live post returns 409 to prevent accidental takedowns.

Scope: blog:posts.publish

Input: { id: string }

Response on scheduled post: { post: { id, status: "draft" } } Response on published post: HTTP 409, { error: "post is not scheduled (current status: published)" }

Analytics

blog.get_analytics

Get analytics for a post.

Scope: blog:posts.read

Input: { id: string, from?: string, to?: string }

Response:

{
  "post": { "id": "post_xxx", "title": "...", "status": "published", "publishedAt": "..." },
  "analytics": { "views": 0, "reads": 0, "avgScrollDepth": null },
  "collected": false,
  "note": "Analytics ingestion is not yet enabled — values are placeholders."
}

NOTE

The real analytics pipeline isn't yet wired — collected: false until ingestion ships.

For a complete end-to-end agent flow, see the content pipeline recipe.

MCP · Tool referenceEdit on GitHub