Skip to main content
Glama

Byline

Say what you want to say. Byline writes it in your voice and publishes it where it belongs.

You have the idea. Byline turns it into a finished, credible piece — researched, written as you, illustrated with real photographs — and puts it live. You talk to it in plain English inside the AI tool you already use. There is no dashboard, no editor, no config file to maintain.

Built and open-sourced by IndiaNIC — the team that builds MCP servers and AI agents into other people's workflows for a living. This one we gave away.

Write a post about why our migration took nine months, as me, and publish it
to the company blog as a draft.

That is the whole interface.

Today it publishes to Ghost and WordPress. The architecture is channel-agnostic on purpose: adding a destination is one folder and one line, so a newsletter, a local paper's submission inbox, a social channel, or an email to your team are all the same shape of problem. Every one of them has a byline.

Many voices, many destinations. Write as yourself on one blog and as your company on another — each persona carries its own writing style, tone, sentence rhythm, and the things it would never say. You pick both in the same sentence: "as jordan, to the personal blog". See Author personas.


What you need

  • Node 20 or newer. Check with node --version. Anything older prints one clear line telling you so — not a stack trace.

  • A blog you can publish to — Ghost or WordPress, either self-hosted or managed.

  • An AI tool — Claude Code, Claude Desktop, Cursor, Windsurf, Gemini CLI, or Codex.

You do not need to know what MCP is, edit any JSON, or write any code.


Related MCP server: Ghost CMS MCP Server

60-second quickstart

1. Point npm at the registry — one line, once, in ~/.npmrc:

echo '@indianic:registry=https://npm.indianic.in/' >> ~/.npmrc

That registry is anonymously readable. There is no login, no npm adduser, and no auth token — people assume otherwise and go looking for credentials that do not exist.

2. Install and run the setup wizard:

npm install -g @indianic/byline
byline init

3. Answer the questions. It finds the AI tools already on your machine, then asks you to give this blog a short namemyblog, work, whatever you like — followed by its address and one key. It checks that key against your live blog before accepting it. Every question can be skipped by pressing Enter on an empty line.

4. Restart your AI tool, then type the sentence init printed for you at the end:

Write a blog post about <your topic> and publish it to myblog as a draft.

myblog there is the short name you chose in step 3. Saying draft means nothing goes live and no subscriber is emailed — start there.

That is the whole thing. Everything below is detail for when you want it.


What is an MCP server?

If you have used Claude or Cursor, you have noticed they cannot touch anything outside the chat. They can write you a blog post, but they cannot put it on your blog.

MCP is the standard that fixes that. An MCP server is a small program that runs on your own machine and hands your AI tool a set of things it is allowed to do. byline hands it fourteen: look up your blogs, check they are reachable, build a writing brief, score a draft, generate and upload images, create and update posts, and — only if you configure a key for it — fetch dated, citable research on a topic.

Two consequences worth knowing:

  • It runs locally. Your AI tool starts it on your machine. Nothing about byline is a hosted service, and there is no account to create.

  • Your keys stay on your machine. They are read from a file only you can read, and sent only to the blog and image APIs you configured. See Where your secrets live.

"Registering" byline with an AI tool just means adding a few lines to that tool's config file so it knows the server exists. byline init does that for you, and backs up the file first.


Install

The one-line registry config

@indianic/byline is published to a private-namespace registry, so npm needs to be told where the @indianic scope lives:

echo '@indianic:registry=https://npm.indianic.in/' >> ~/.npmrc

No authentication is required. The registry is readable anonymously. If you find yourself hunting for a token or an npm login, stop — you do not need one.

Install it

npm install -g @indianic/byline

Or skip the install entirely and let npx fetch it on demand:

npx -y @indianic/byline init

Both give you the byline command.

Add Byline to any AI tool

byline init detects and configures Claude Code, Cursor, Windsurf, Gemini CLI and Codex automatically. Any other MCP-capable tool — Antigravity, Claude Desktop, Zed, Continue, JetBrains AI, or something released next month — takes one manual step, because Byline will not write to a config file it has not verified exists.

Open that tool's MCP settings and add one server. Almost every tool uses this shape:

{
  "mcpServers": {
    "byline": {
      "command": "npx",
      "args": ["-y", "@indianic/byline"]
    }
  }
}

A few tools use TOML instead (Codex is one):

[mcp_servers.byline]
command = "npx"
args = ["-y", "@indianic/byline"]

Three things worth knowing:

  • npx keeps you current. It resolves the latest published version each time. If you would rather pin it, install globally and use the absolute path from which byline — but note that path is tied to your Node version and breaks when you switch with nvm.

  • Restart the tool afterwards. MCP configs are read once, at startup.

  • Confirm it worked with byline status, which reports every tool it can see and which scope each registration is in.

To do the same thing from the command line for a tool Byline does know:

byline register --tools claude,cursor        # specific tools
byline register --tools all                  # every tool found on this machine
byline register                              # just print the command, change nothing

Installing from source

You do not need this to use Byline — it is here for contributors and for anyone who wants to pin an exact build.

git clone https://github.com/indianic/byline.git && cd byline
npm install && npm run build
npm pack                                   # produces indianic-byline-1.0.0.tgz
npm install -g ./indianic-byline-1.0.0.tgz

Then register with the absolute path rather than the npx form:

claude mcp add byline -- "$(which byline)"

Note that path is tied to your current Node version — if you switch Node versions with nvm, re-run byline register --tools all.

Staying up to date

Byline checks the registry at most once a day and tells you, after a command finishes, when a newer version exists:

●  A newer version is available: 1.0.0 → 1.1.0. Run `byline update` to upgrade.

byline update re-installs using whichever package manager put it there — npm, pnpm or yarn, detected rather than assumed. If you use the npx form, you are always on the latest and nothing is needed.

Silence it with BYLINE_NO_UPDATE_CHECK=1. It is skipped automatically in CI, and it never runs while Byline is serving your AI tool.


byline init

One command sets everything up. Here is a real run, start to finish.

Home directory paths and the blog hostname below are substituted for examples — everything else is verbatim output.

It finds the AI tools you already have

┌  byline — first-run setup
│
◆  Register byline with which AI tools? (space to toggle, enter to confirm)
│  ◼ Claude Code
│  ◼ Cursor
└

Only tools whose config files actually exist on your machine are offered, and all of them start ticked. It never creates a config for a tool you do not have.

◆  Config scope
│  ● Global — available in every project (recommended)
│  ○ This project only — writes into the current folder

It backs up before it writes

◆  AI tool config
│  Claude Code: updated /Users/you/.claude.json
│    backup: /Users/you/.claude.json.byline-bak
│  Cursor: updated /Users/you/.cursor/mcp.json
│    backup: /Users/you/.cursor/mcp.json.byline-bak

Your existing MCP servers and settings are left alone — the file is merged, not replaced.

It asks for your blog

◆  Which kind of blog?
│  ● Ghost
│  ○ WordPress

◇  Short name for this blog, used when you say "publish to …" (Enter nothing to skip)
│  myblog

◆  Your blog address (Enter nothing to skip)
│  https://blog.example.com

The short name is what you will say to your AI tool — "publish it to myblog". Use lowercase letters, digits, and hyphens.

It tells you exactly which key to get

●  Admin API key — Ghost Admin → Settings → Integrations → Add custom integration.
   Copy the ADMIN API key, not the Content API key — they are not interchangeable.
   (looks like: id:secret)
│
◆  Admin API key (Enter nothing to skip)
│  _

Your typing is masked. Every prompt can be skipped by pressing Enter on an empty line — you can set up the rest now and come back.

It checks the key against your real blog before accepting it

This is the part worth trusting. Enter a wrong key and you find out immediately, from your blog, in its own words:

◇  Admin API key (Enter nothing to skip)
│  ▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪▪
│
◇  Could not connect to https://blog.example.com
■  Ghost rejected these credentials (HTTP 401):
│  Unknown Admin API Key
│
◆  What now?
│  ● Try again (re-enter the credentials)
│  ○ Skip this site

And a key that works says so:

◇  Connected to https://blog.example.com — Example Blog (Ghost 6.44.1)

A key that has not been proven to work is never written to your config. This check is made against an endpoint that genuinely requires the credential — not one that answers anybody — which is a distinction this project learned the hard way.

It tells you where everything went

◆  blog "myblog"
│  config    /Users/you/.byline/config.yaml
│  secret    /Users/you/.byline/.env   (MYBLOG_ADMIN_API_KEY)

◆  author profile
│  /Users/you/.byline/personas/_template.yaml   — copy it to <your-name>.yaml and
│  fill it in to shape how drafts sound

◆  everything written
│  /Users/you/.byline/config.yaml
│  /Users/you/.byline/.env
│  /Users/you/.byline/personas/_template.yaml

◆  what to say in your AI tool
│  Write a blog post about <your topic> and publish it to myblog as a draft.
│
└  Done. Restart your AI tools so they load byline, then paste the line above.

Restart your AI tool. It reads its MCP config at startup, so byline will not appear until you do.


Where to get each key

Ghost — the Admin API key

Ghost Admin → Settings → Integrations → Add custom integration, give it any name, then copy the Admin API Key.

The trap, and it catches almost everyone. That screen shows you two keys. The Content API key is a single string and is read-only — it cannot publish, and byline will reject it. The Admin API key is two parts joined by a colon:

6612abcd1234ef567890abcd:9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c5b4a39281706f5e4d3c2b1a09

If yours has no colon in it, you have copied the wrong one.

The integration is what has permission, not you — so you can revoke it from that same screen at any time without touching your own account.

WordPress — an Application Password

WP Admin → Users → Profile → Application Passwords, enter any name, click Add New Application Password.

  • This is built into WordPress core since 5.6. You do not need a plugin.

  • The password is displayed in space-separated groups, like abcd EFGH ijkl MNOP qrst UVWX. Copy it with the spaces. byline sends it exactly as you paste it; reformatting is a silent way to break a paste that worked.

  • It is shown once. If you lose it, revoke it and make a new one.

  • You will also be asked for your WordPress username — the login name, not your display name and not your email.

Google Gemini — for images (optional)

Google AI Studio → Get API key → Create API key (https://aistudio.google.com/apikey). The free tier is enough to start.

xAI / Grok — the image fallback (optional)

console.x.ai → API Keys → Create API key. Only used when Gemini fails.

Skip both and everything still works — you just get posts without generated hero images.


Research (optional — you probably do not need this)

Byline works without a research key, and most people should leave it that way. If the agent you are talking to already has web access, use it: search however you like, paste what you find as research (at least 200 characters), and news mode is satisfied. Evergreen posts (mode: "blog") need no research at all, and byline init never requires a research key to finish.

A research provider buys two things that path cannot give you:

  • Recency you can check. A finding carries a publishedAt where the provider gives one — it can be absent, which is exactly what RESEARCH_UNDATED catches — and news mode refuses a set where nothing is recent enough, rather than trusting whatever the model recalls.

  • Checkable provenance. Every finding arrives with its URL and, where the provider gives one, its publication date — so score_draft can afterwards tell you which of the URLs your draft links were actually in that research.

What Byline checks, and what it simply trusts

This distinction is the whole point of the feature, so it is worth being exact about.

Provider findings are checked. Byline refuses a result with no findings in it at all, in any mode. In news mode only, it additionally requires at least one finding carrying a readable publication date inside the window the result declares — allowing six hours' grace, because providers round timestamps to the hour and a publisher's date is often the article's rather than its last update. If nothing is dated you are told so; if everything dated is older than the window, you are told the newest date it found. Individual findings that are undated or out of window are accepted, not rejected one by one: an article legitimately cites background alongside its breaking sources. Each is marked as such next to its own entry on the brief, and counted in a warning. Blog mode applies no recency check at all.

A research string you paste is trusted, not verified. In news mode it is checked for one thing — substance, currently 200 characters — and nothing else. Byline did not fetch it, cannot confirm it is recent, and cannot confirm the text matches any source it names. The brief says so on its face — the research block is headed ORIGIN: supplied by the caller — TRUSTED, NOT VERIFIED BY BYLINE — so the writer treats every figure in it as your claim rather than a measured fact. That is not a criticism of the path; it is the honest description of it.

score_draft's citation_provenance check is advisory. Pass the research findings to it and it compares the absolute http(s) URLs in your draft's <a href> links against the findings' URLs, then reports which cited URLs were not in the research and which research sources went uncited. It never blocks — a writer legitimately links a homepage or a definition no search returned. Pass no findings and it reports "not evaluated" rather than passing, because a silent pass would read as "the citations were verified" when nothing was. And note what it does not do: it checks where a URL came from, never whether the page at that URL says what your article claims it says.

One article, one origin

Pass research or findings, never both. Supplying both is refused, naming which to drop. Merging them would make provenance unanswerable — you could not tell which claim came from where, so nothing could be cross-checked and a later correction could not be traced to a source.

Brave or Tavily — pick one, there is no fallback

They do not return the same kind of thing:

Returns

Use when

Tavily

A synthesized answer plus dated sources

You want orientation as well as sources

Brave

Ranked results with snippets, no synthesis

You want the raw result list

Byline never substitutes one for the other. Naming a provider whose key is missing is refused, not redirected, and a failed search is reported rather than retried against the other one. An automatic fallback would silently change what the writer receives — sometimes a summary, sometimes a list.

byline init          # offers both; configure either or both, or neither
BRAVE_API_KEY=BSA…
TAVILY_API_KEY=tvly-…
BYLINE_RESEARCH_PROVIDER=tavily   # optional: the default when both are configured

With both configured and no default pinned, registry order decides — and that order was set by measuring which provider dates a minutes-old event more reliably. On one live query inside one two-minute window, Brave's freshest result was ~55 minutes old and Tavily's freshest on-topic dated result was ~4h39m, so Brave is registered first. That is a measurement, not a preference; Tavily's snippets are richer and its synthesis is a capability Brave lacks. The full table is in docs/RESEARCH-NOTES.md.

Where to get each key

Neither key is a failure to be missing. byline doctor reports an unconfigured research provider as a note, not an error.

What it looks like in practice

Write about last night's match for personal.

The agent decides whether the topic turns on recent events, calls research_topic, passes the whole result to build_writing_brief as findings, and passes the findings again to score_draft so citations can be traced. Both tool descriptions tell it to ask you rather than guess when it is unclear whether you mean the live event or the history — that is an instruction to the agent, which is as far as an MCP server's reach goes.


Your own photos (optional)

Point Byline at a folder of your own images and it will search them, upload the ones you pick, and keep a record of which photograph went into which post so the same one is not published twice. Nothing is generated, nothing is uploaded to a third party, and Byline never writes inside your library folder — the index and the usage record live under ~/.byline/media/.

Adding a library

byline media add ~/Pictures/blog

This adds the folder to config.yaml, derives a library name from the folder's own name (override with --name), and scans it immediately — one command, and the library is searchable right away. byline media also has list, scan, status, release, and remove; every flag is documented in docs/CLI.md.

Restart your AI tool afterward. loadContext() reads config.yaml once, at MCP server startup — a library added from a terminal is invisible to an already-running server until you restart the AI tool talking to it.

Configuring a library by hand

byline media add writes the same media: block shown below, so editing it directly is still a supported path — for a field the command does not expose (index_path, reuse_scope), or if you would rather edit YAML than run a command:

media:
  default_library: shots          # optional; the library used when none is named
  reuse_scope: site               # "site" (default) or "global" — see below
  libraries:
    - name: shots                 # lowercase letters, digits and hyphens
      path: ~/Pictures/blog       # the folder of your own images
      recursive: true             # walk sub-folders too; default true
    - name: archive
      path: /Volumes/Photos/2019
      index_path: ~/byline-index  # optional; where the index and usage record go

Field

What it does

name

How you refer to the library. Same rules as a site slug: lowercase letters, digits, hyphens.

path

The folder to index. ~ is expanded. Must exist and be a directory, or the library is reported as unavailable and the others keep working.

recursive

Walk sub-folders. Defaults to true.

index_path

Where the derived index and the usage record are written. Defaults to ~/.byline/media/. Set it with byline media add --index-path <folder>. It may not be inside the library folder — that is refused both when it is written and when it is read, so Byline never writes into your photos.

default_library

Which library is used when a tool call does not name one. With exactly one library you never need it.

reuse_scope

site (default) — a photo used on one blog is still free for another. global — used once, ever, anywhere.

.jpg, .jpeg, .png, .gif, .webp and .avif are indexed as images; .mp4, .m4v, .mov and .webm as video. Dotfiles are skipped. Anything else is ignored — an allow-list, so sidecars, RAW files and half-finished exports never fill your search results.

The three tools

  • list_media_libraries — what is configured, how many assets each library holds, and when it was last scanned. Pass scan: true to walk the folder and build the index; that is how a new or changed library becomes searchable from inside your AI tool. byline media scan does the same thing from a terminal, and byline media add scans the folder it has just added.

  • find_media — search by keyword and get ranked candidates back, each with a why naming the tokens that matched, so you can judge the match rather than trust a score. Already-used assets are excluded by default.

  • use_media — upload the assets you picked to a site and record them as used. It returns a hosted URL per asset, ready for feature_image or an inline <img>.

In practice you never name them:

Find a photo of the harbour in my library and use it as the hero for that post.

What "already used" means, exactly

A photograph is used from the moment its bytes reach your blog — not from the moment a post goes live. use_media records a reservation; publishing a post that carries the hosted URL turns it into a published record naming the post. Both count as used, because a reservation means the image is already sitting in your media library on the platform.

use_media refuses an asset the record says has been used, names where it went, and carries on with the rest of the batch. Pass allow_reuse: true when publishing the same photograph twice is what you actually want.

Under the default reuse_scope: site, "used" means used on that site — the same photo is still free for a different blog. Set reuse_scope: global if a photo should be published once and never again anywhere.

What this release does not do

Stated plainly, because a promise is worse than a missing feature:

  • No enrichment. Nothing writes captions or keywords, so ranking is based on what your files and folders are named. beach-sunset-goa.jpg inside 2024/holiday/ searches well; IMG_4821.jpg inside Camera Roll/ does not.

  • No video upload. Videos are indexed and searchable so you can see what you have, and use_media refuses to upload one — the upload path handles images only.

  • No way to cancel a reservation from an MCP tool. If use_media succeeds and the post then fails to publish, that asset stays reserved — no tool clears one. From a terminal, byline media release <id> does (see docs/CLI.md); without terminal access, list_media_libraries still reports the count and names the ledger file to edit by hand.

The usage record is not recoverable if you delete it, which is why it is kept in a separate file from the index (<name>.usage.json beside <name>.index.json) — a rescan rewrites the index and never touches it.


Video (optional)

Byline does not upload video — that was dropped as a goal entirely. Instead, embed_video turns a YouTube, Vimeo, or Bunny Stream URL into the <iframe> HTML for an article:

Embed https://youtu.be/dQw4w9WgXcQ in the post, captioned "Watch the full talk".

It normalises whatever form you paste — a watch?v= link does not work inside an <iframe>; /embed/ID does — and refuses anything that is not one of the three supported providers rather than passing an unrecognised URL through as-is:

Provider

Accepted forms

YouTube

youtube.com/watch?v=ID, youtu.be/ID, youtube.com/shorts/ID, youtube.com/embed/ID (with or without www./m.; a t=/start= value is preserved)

Vimeo

vimeo.com/ID, vimeo.com/channels/NAME/ID, player.vimeo.com/video/ID

Bunny Stream

iframe.mediadelivery.net/embed/LIBRARY/GUID or /play/LIBRARY/GUID

Verified by live probe on 2026-08-12: both Ghost and WordPress (for an account holding the unfiltered_html capability) keep the <iframe> and its <figure>/<figcaption> wrapper unchanged on ingest — Ghost additionally wraps it in its own kg-embed-card figure. A plain <video> tag is a separate, worse option: Ghost strips it completely, with nothing surviving; WordPress keeps it, for the same account.

For a WordPress account WITHOUT unfiltered_html, whether the <iframe> survives is UNVERIFIED — WordPress's KSES filter is documented to strip <iframe> for such an account, but no probe has confirmed it, because no such account has ever been available. Pass site naming the WordPress site and embed_video adds that caveat to its warnings; it does not change the HTML produced.


Using it

Talk to your AI tool in plain English. It figures out which tool to call.

Write a blog post about why microservices fail for small teams and publish it to myblog as a draft.

Draft an article on AI agents in fintech for myblog, as jordan.

Take the same topic and write a shorter version for the company blog, in the company voice.

Check my blogs are working.

Name the voice and the destination in the same sentence. "as jordan, to the personal blog" picks a persona and a site together. Different personas produce genuinely different articles from the same topic — different structure, different sentence rhythm, different opinions — because each one carries its own writing style, tone, risk tolerance, and the things that person would never say.

Say "draft" and you get a draft. Nothing goes live, and no subscriber is emailed, unless you ask for a published post. Start there.

Ask it to check first. "Check my blogs are working" runs health_check against every configured site and tells you which ones authenticate.

News articles always need real research. If you ask for a piece about recent events, the brief tool refuses to proceed on the model's training data alone — you have to give it actual research, either as your own notes or from a research provider. Evergreen posts have no such requirement, and neither path needs a key you do not already have. See Research.

Author personas (optional)

A persona is what makes an article sound like a person instead of a content mill. Byline writes in first person as that author, and carries roughly fifteen traits into every draft: writing style, tone, communication style, storytelling approach, sentence structure, focus areas, research methodology, personality traits, bias tendency, how much risk they take in an opinion, cultural context, and their own free-text instructions.

byline init asks for it directly — five questions (name, role, writing style and tone, years of experience, subject expertise), each skippable. Answering them writes a real, working persona; skipping goes straight to a template you fill in by hand. Either way, init prints the exact file path so you always know where to look. Add as many as you like — copy ~/.byline/personas/_template.yaml to your-name.yaml and fill it in; the filename must match the slug field inside:

~/.byline/personas/
  jordan-reyes.yaml     # you, on your own blog
  company-editorial.yaml  # the house voice, on the company blog

Then choose per article: "write this as company-editorial and publish to the company blog." You never edit a config file to switch — you say which one.

The one field worth spending time on is persona_specific_instructions_for_ai. It goes into the brief verbatim, so it is where a real constraint belongs:

persona_specific_instructions_for_ai: |
  Ground every claim in delivery experience. Name the real trade-off, not the
  marketing version. Never write a paragraph that could apply to any company.

The persona shapes the writing — it does not get announced in it

A persona pasted into a prompt has a signature: every article opens by introducing the same person the same way. "As a CEO with 25 years in enterprise delivery…" Once, that reads as authority. Every week, it reads as a template.

So how much of you reaches the page is drawn fresh per article, like the hook and the structure already were. Across articles: 20% state your credential outright — once, early, and never again — 20% bury it in a subordinate clause of a sentence about something else, and 60% never state your role or your years at all. Those carry authority the way a regular columnist does: through a detail only somebody who has done the work would know, through the scale of the decisions described, through simply assuming the reader knows who is talking.

None of that weakens the byline. Generative engines weight unrepeatable first-hand specificity far above a stated job title — every competing article already has the job title. Your profile still governs tone, judgement, and subject matter on every single piece; what varies is how much of it is said out loud.

Alongside it, each article draws a prose texture — uneven rhythm, conceding the strongest counter-argument before answering it, visibly changing your mind mid-piece, refusing abstraction, writing sentences you could say aloud. Every article also carries a fixed standard aimed at what actually gives machine-written prose away: paragraphs of uniform length, relentlessly parallel lists, and an argument that never once commits to anything — plus a long list of words and constructions to avoid outright.

It will not fake being human by breaking things. Introducing typos, padding for rhythm, or inventing a statistic, a client, a date, or a prior article you never wrote are all forbidden explicitly. An invented specific is the one mistake here you cannot take back after publishing.

Byline doesn't have your keys or your voice until you give them. init collects credentials interactively and validates each one against the live platform before accepting it. You can skip every prompt and fill things in later — everything lives in two files it will tell you the path of, and byline status prints them any time.


How it works

When you ask for a post, your AI tool does the thinking and byline does everything that touches the outside world:

  1. build_writing_brief produces a brief tailored to the target platform — because what survives publication differs between platforms, and the brief says so up front rather than letting the writer discover it afterwards.

  2. Your AI tool writes the draft. That part is not byline's job.

  3. score_draft grades it — sentence-length variety, AI tells, whether claims carry evidence, whether the HTML will survive this platform's ingest.

  4. generate_image and upload_image create hero and inline images and put them in your blog's own media library. Gemini first, Grok as fallback; when it falls back it tells you it did and why.

    Images are on by default whenever an image key is configured — you do not have to ask for them. Say nothing about images and you still get a hero and an inline photograph; say "no images" or "just the hero" or hand it your own image instructions and that's what happens instead. Your instruction always wins over the default. If no image key is set up, no image is attempted and none is expected.

    Images are photographs, not AI art, and the rules are fixed rather than improvised per article: every prompt names the article's actual subject, in a real setting, with no text anywhere in the frame and no glowing-circuitry abstraction. The hero image always contains people doing the work the article is about, since it is what appears on the post card and every social share.

    Four independent axes decide how it is shot, so a blog's images do not read as one template:

    axis

    varies across

    Light and camera

    window light, hard midday sun, after dark by screen glow, warm tungsten, cool office fluorescent, blue hour, high-key bright — plus macro close-ups, overhead aerials and low-angle handheld frames

    Scene

    open-plan floors, glass meeting rooms, private cabins, neighbourhood cafes, building forecourts, lunch tables, stairwells, rooftops, canteens, home workspaces

    City

    Bengaluru, Singapore, Berlin, São Paulo, Nairobi, Tokyo, Dubai, Toronto, Amsterdam, Mexico City, Warsaw, Ho Chi Minh City

    Moment

    mid-laugh, mid-argument, deep concentration, relief when something finally works, the tail end of a long day, coffee and thinking

    The axes move independently — 400 sample subjects reach 128 of the 144 possible city-and-scene pairs, and effectively every combination is distinct. Naming a real city is also what carries who is in the frame: asking a model for "diverse people" in the abstract produces a stock-library composite, while naming Nairobi or Ho Chi Minh City produces architecture, clothing, light and faces that genuinely belong together.

    Roughly one image in twelve is an editorial illustration instead — hand-drawn ink line with flat washes and a limited palette, the kind a newspaper opinion page runs. Both images in a single article always share one city and one medium, so they read as a set rather than two unrelated stock pictures.

    Image models sometimes refuse a prompt asking for people. If every provider refuses, byline retries once without people and tells you it did, naming the providers' own reason — you never get a silently peopleless image. A provider that broke rather than refused is reported as the failure it is, not quietly worked around.

  5. create_post publishes — now, as a draft, or at a time you choose. Then it reads the response back and compares it to what was sent, and reports any field the platform quietly dropped.

That last point is the design rule everywhere in this project: nothing fails silently. Every tool returns a result or an error naming the API and its HTTP status.

Scheduling

Write this up and publish it at 10 AM tomorrow.

Say a time and byline publishes then, on either platform.

The time you say is the time on the blog. Not your laptop's timezone, not the server's, not UTC — the blog's own. "10 AM tomorrow" means 10 AM as that blog's readers experience it, and byline looks the blog's timezone up from the platform itself rather than guessing. Send the identical instruction to two blogs in two countries and they publish at two different instants, on purpose:

blog

its timezone

you say

it publishes at

a Ghost blog set to Asia/Kolkata

IST

2026-08-04T10:00

04:30Z

a WordPress blog set to UTC

UTC

2026-08-04T10:00

10:00Z

You can still pin an exact instant by writing the offset yourself — 2026-08-04T10:00:00+05:30 or ...Z — and byline takes that at face value without consulting the blog. Only do that if you actually meant a specific timezone.

Under the hood that is status: "scheduled" plus publish_at, which becomes Ghost's scheduled / published_at or WordPress's future / date_gmt. update_post schedules a draft you already have, and unschedules one. A past time with status: "published" backdates a post instead. The result reports publish_at_local — the time as the blog's clock reads it — alongside the UTC instant the platform actually stored.

Three things byline refuses rather than guessing at:

  • A time under two minutes away. WordPress does not reject a scheduled post whose date is too close. It publishes it immediately, returns 201, and reports no error at all. Measured 2026-08-03: 45 seconds of lead went live, 60 seconds scheduled. After writing, byline re-reads the post and fails loudly if the platform published it anyway — naming the post, its live URL, and the platform's own clock. It does not unpublish it for you; that is your call, not a tool's.

  • A future time with status: "published". The identical request publishes immediately on Ghost and schedules on WordPress. One input cannot be allowed to mean two opposite things, so byline asks you to say "scheduled" if that is what you meant.

  • A local time that does not exist. On a blog whose timezone observes daylight saving, the clocks skip an hour each spring. Asking for a time inside that hour is refused rather than quietly moved.

If the blog does not report a timezone at all, byline says so and asks for an explicit offset — it never falls back to UTC, because that would publish five and a half hours early for an Indian blog while reporting success.

The platforms really do differ

Measured on 2026-07-29 by publishing the same HTML to both:

Ghost

WordPress (account with unfiltered_html)

Styled <table>

survives

survives

Styled <div>

unwrapped — text survives, all styling lost

survives, styles intact

target="_blank"

stripped

survives

Hand-written heading id

overwritten — Ghost generates its own

kept

JSON-LD into <head>

supported

not supported by core — reported as a warning, not silently dropped

This is why the brief is platform-aware. On Ghost, a <div> summary card publishes as unstyled text, so the brief asks for a <table> instead.

A limit worth stating. The WordPress column was measured on a single-site administrator account, which always holds the unfiltered_html capability. Accounts without it (Author, Contributor, or an ordinary Editor on a multisite network) are expected to have some of that markup filtered — but that path has never been measured, only reasoned from WordPress's documented behaviour. Treat it as unverified. See docs/WORDPRESS-NOTES.md.

What each article ships with

Summary block above the first heading; headings phrased as questions a reader would actually type, answered in the first 40–60 words; a closing FAQ; statistics attributed inline with a source and date; Article and FAQPage JSON-LD; and full metadata — excerpt, meta title and description, Open Graph and X card titles, descriptions, and images.


Checking on things

byline status    # what is configured, and where every file lives
byline doctor    # probe every blog, image and research provider; print a fix per failure

doctor on a healthy install:

┌  byline — doctor (v1.1.0)
│
◆  environment
◇  Node v22.23.1
│
◆  secrets
◇  /Users/you/.byline/.env is owner-only (or absent)
│
◆  blogs
◇  myblog (ghost) — Example Blog (Ghost 6.44.1)
│
◆  image generation
◇  gemini — gemini-2.5-flash-image reachable
│
◆  research
◇  brave — Brave Search reachable, key accepted
│
◆  AI tools
◇  Claude Code   /Users/you/.claude.json [global]
◇  Cursor        /Users/you/.cursor/mcp.json [global]
■  Gemini CLI    not registered
■  Windsurf      not registered
■  Codex         not registered
│
└  All checks passed.

Every failure names its own fix. Full command reference: docs/CLI.md.


Troubleshooting

Every row here is a failure that actually happened during this project's development, with the fix that actually resolved it.

Symptom

Cause

Fix

Ghost: 401 Unknown Admin API Key on a key that looks right

You pasted the Content API key. It has no colon in it.

Get the Admin API keyid:secret — from Settings → Integrations.

Ghost: Admin key secret is not hexadecimal

Same cause, caught before any network call: the half after the colon must be hex.

As above.

Ghost: 404 on an otherwise-valid key

The Admin API is served on a different host or path than the public site.

Set api_url for that site in ~/.byline/config.yaml.

Ghost: post publishes but the body is empty

Ghost expects Lexical JSON unless told the body is HTML.

byline always sends ?source=html. If you see this, something else is posting — this is the single most consequential parameter against Ghost, and its absence does not error.

Ghost: a styled <div> card renders as plain text

Ghost unwraps div section aside span small pre mark on ingest. The text survives; the styling does not.

Use a <table> — the only container that survives Ghost's ingest with styling intact.

Ghost: image upload 415 Please select a valid image

The upload carried no MIME type.

Handled automatically. If you hit it, the filename has an extension byline does not map.

Ghost: target="_blank" disappears

Ghost strips it on ingest. rel="noopener noreferrer" does survive.

Nothing to fix — do not ask for target.

WordPress: 400 rest_upload_no_content_type

The media upload sent no Content-Type. WordPress does not guess.

Handled automatically; same cause as the Ghost 415 above.

WordPress: schema_injected: false

WordPress core has no field for injecting into <head>.

Not a failure — an honest warning. The JSON-LD was not silently dropped; you were told. Use an SEO plugin's own fields if you need it.

WordPress: the wrong tag gets applied

WordPress's tag search is a substring match — searching AI also returns AI Ethics.

Handled: byline requires an exact, case-insensitive match before reusing a tag, and creates a new one otherwise.

WordPress: styles stripped even though it worked before

Your account may lack unfiltered_html — role-dependent, and on multisite only Super Admins hold it.

Publish from an account that holds it. Note: this path is unverified — see above.

RESEARCH_PROVIDER_UNCONFIGURED on a provider you named

That provider's key is unset. Byline will not quietly use the other one — they return different shapes.

Set that key, or name the one you did configure, or drop the research provider and paste your own notes as research.

RESEARCH_CONFLICT — both research and findings

An article has exactly one research origin.

Drop whichever you did not mean. The error names both.

RESEARCH_STALE or RESEARCH_UNDATED in news mode

Not one provider finding carries a readable date inside the window asked for — often the event is not indexed yet.

Widen the window, try the other provider, or write it as mode: "blog".

doctor says "not registered" but it clearly works

The registration is project-scoped, not global.

Not a problem. status and doctor now report which scope they found.

npx -y @indianic/bylineE404

npm has not been told where the @indianic scope lives.

Add the one .npmrc line from Install. No login is needed.

The AI tool does not see byline

Its MCP config is read at startup.

Restart the tool. Then byline status to confirm it is registered.

byline in a terminal just prints help

Intended. Over a pipe it starts the MCP server; in a terminal it shows help rather than hanging.

Nothing to fix.

byline requires Node >= 20

Your shell's node is older.

nvm use 20 (or newer) and re-run.

An unexpected error with no detail

The stack trace is suppressed by default — it is noise for someone who just wants their setup fixed.

Re-run with BYLINE_DEBUG=1 for the full trace.


Where your secrets live

Two files, in ~/.byline/:

.env — every secret, and nothing else. Created mode 600: readable and writable by your user only.

MYBLOG_ADMIN_API_KEY=<your key>

config.yaml — everything else. Secrets appear only as ${VARIABLE} references, never as values:

sites:
  myblog:
    platform: ghost
    url: https://blog.example.com
    admin_api_key: ${MYBLOG_ADMIN_API_KEY}
default_site: myblog

That split is deliberate: config.yaml can be shared, committed, or pasted into a bug report without leaking anything. doctor checks that .env is still owner-only and tells you if it is not.

Nothing leaves your machine except the requests byline makes to the APIs you configured — your blog, and your image provider if you set one up. There is no telemetry, no hosted component, and no account. Your keys are read from .env at startup and sent only in the Authorization header of requests to your own blog.

To see exactly where everything resolved from, run byline status. To remove it all, byline reset --yes.


Documentation

docs/CLI.md

Every command, flag, and environment variable

CONTEXT.md

Architecture, for contributors and agents

CONTRIBUTING.md

How to work on this

docs/GHOST-NOTES.md

Verified Ghost behaviour, and why each one matters

docs/WORDPRESS-NOTES.md

The same for WordPress, with the unverified parts marked

docs/RESEARCH-NOTES.md

Measured Brave and Tavily behaviour, and the recency table that set the registry order

docs/ADDING-A-PLATFORM.md

Adding a third platform, written from actually doing it

CLAUDE.md

The rules for changing this repository, and what each one cost

CHANGELOG.md

Release history


Built by IndiaNIC

Byline is developed and maintained by IndiaNIC and given away under MIT. Everything in it — the platform probes, the measured behaviour in docs/*-NOTES.md, the refusals that stop a post publishing at the wrong hour — came out of work we do for clients, and it is here in full rather than as a demo.

That is also what we do commercially: MCP servers and AI agents that fit the way a team already works, and the integration work that decides whether they survive contact with production rather than stopping at a convincing pilot.

If you have a workflow worth automating, we would like to hear about it.

Talk to us → · www.indianic.com


License

MIT — see LICENSE.

Available Tools

14 tools
add_siteAdd siteA

Add a publishing target to config/sites.yaml. Secret credential fields are recorded as a reference to a .env variable name; the value itself goes in .env, never in config.yaml.

ParametersJSON Schema
NameRequiredDescriptionDefault
urlYes
slugYesShort name used when publishing, e.g. "indianic". Lowercase letters, digits, and hyphens only.
platformYesOne of: ghost, wordpress
credentialsYesKeyed by this platform's credential field names. For a secret field pass the NAME of the .env variable holding the value; for a non-secret field pass the value itself. Call list_sites or read the error from a wrong platform to see the field names.
default_authorNo

TDQS

A3.6/5.0
Behavior3/5

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

With no annotations, the description takes on the burden of disclosure. It reveals that secret credential fields are stored as .env variable references rather than literal values, which is important behavioral context. However, it does not mention other traits such as overwrite behavior, validation, or required permissions.

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 compact, with the main purpose stated in the first sentence and a security-relevant caveat in the second. While the caveat repeats schema content, it's relevant and doesn't bloat the description.

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

Completeness3/5

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

For a simple add operation, the description covers the core purpose and secret handling, but lacks specifics on outcomes, error scenarios, or integration with other site-management tools. The presence of nested objects and no output schema suggests more detail could be helpful.

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

Parameters2/5

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

The description does not add meaning beyond the schema; the secret-field behavior it mentions is already stated in the credentials parameter description. It also does not clarify the url or default_author parameters, leaving 40% of the schema without additional explanation.

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

Purpose5/5

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

The description clearly states the tool's function with a specific verb and resource ('Add a publishing target to config/sites.yaml'). This distinguishes it from sibling tools like remove_site and list_sites.

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

Usage Guidelines3/5

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

The description implies usage (adding a publishing target) but does not explicitly provide when-to-use guidance or contrast with alternatives. The secret-handling caveat offers a directive but not usage context.

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

build_writing_briefBuild writing briefA

Build a randomized, persona-specific writing brief covering voice, structure, visual blocks, AEO and GEO. RESEARCH IS MANDATORY IN NEWS MODE, from exactly ONE origin: either your own findings as research (your web access, /last30days, or notes a human pasted — TRUSTED but not verified by Byline), or a research_topic result as findings (checked to exist, and that AT LEAST ONE finding is dated and inside the window; any that are not are marked on the brief). Passing both is refused. Do not summarise a recent topic from your own knowledge — the model cutoff cannot know the last 30 days, and an article built on recalled facts will carry stale or invented figures. If it is unclear whether a topic depends on recent events, ASK THE USER rather than guessing; an evergreen topic should use mode: "blog", which needs no research at all. Returns the seed so a brief can be reproduced, plus researchOrigin and any warnings.

ParametersJSON Schema
NameRequiredDescriptionDefault
modeNoblog
seedNo
siteNoWhich site this is written for — its platform decides the HTML rules. Defaults to the default site.
topicYes
personaYes
findingsNoA whole research_topic result. Mutually exclusive with `research` — passing both is refused. This is the checkable origin: the findings are checked to exist, and, in news mode, at least one of them to be dated and inside the window it declares. Findings that are undated or outside that window are accepted (an article may cite background too) but marked as such on the brief.
languageNo
researchNoResearch findings to ground the article in
word_countNo

TDQS

A4.7/5.0
Behavior5/5

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

With no annotations, the description carries the full burden and it delivers: it discloses refusal behavior, verification criteria, warning marking for out-of-window findings, trust levels for the research origin, and return fields (seed, researchOrigin, warnings). This goes well beyond what the schema provides.

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

Conciseness4/5

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

The description is a dense single paragraph, but most sentences carry operational rules (refusal, verification, asking the user). It is front-loaded with purpose and avoids filler, though the all-caps emphasis and some repetition of schema text slightly reduce elegance.

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

Completeness4/5

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

For a 9-parameter tool with a nested object and no output schema, the description addresses the main ambiguity (research origins) and tells the agent what to return (seed, researchOrigin, warnings). It doesn't describe the brief's output format or side effects, but these are less critical than research handling. It is largely complete for safe invocation.

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

Parameters4/5

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

Schema coverage is only 33%, so the description must add meaning. It does for the critical parameters research/findings/mode, explaining mutual exclusivity, mandatory news-mode research, and how to choose them, but persona, word_count, language, and seed are left self-evident. Overall it compensates for the low coverage on the high-stakes parameters.

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

Purpose5/5

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

The opening sentence uses the specific verb 'Build' with a concrete deliverable ('randomized, persona-specific writing brief') and scope ('voice, structure, visual blocks, AEO and GEO'). This clearly differentiates it from siblings like research_topic (research only) and create_post (publishing).

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

Usage Guidelines5/5

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

It explicitly states RESEARCH IS MANDATORY IN NEWS MODE, gives exactly one origin, and says passing both is refused. It also directs the agent to ASK THE USER when unclear and to use blog mode for evergreen topics, providing clear when-to-use and when-not-to-use guidance.

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

create_postCreate postA

Publish an article. Defaults to status "published" — pass "draft" only when the user asked for a draft, or "scheduled" with publish_at to go live at a set time. The author accepts a persona slug and resolves to that site's author id. HTML must not still contain [[content_image]]. Every article gets a hero (feature_image) and an inline by default when an image provider is configured — refused otherwise; pass images: "hero" | "inline" | "none" to opt out.

ParametersJSON Schema
NameRequiredDescriptionDefault
faqNoBuilds FAQPage JSON-LD. Must match the visible FAQ section exactly.
htmlYes
siteYes
tagsNo
titleYes
authorNoByline. Either a persona slug (resolved to that site's author id) or a raw platform-native author id, to attribute the post to someone with no persona file — the id's format is specific to the target site's platform and is not the same across every site. Omit to use the site default_author. Run list_authors against the target site to find its ids.
imagesNoWhich of the hero image (feature_image) and the in-body <img> are required before publishing. Only enforced when an image provider is configured; pass "none" if this article genuinely has no image.both
schemaNoInject Article (+FAQPage) JSON-LD into the page head for AEO/GEO
statusNo"published" goes live now, "draft" is not visible, "scheduled" goes live at publish_at (which is then required).published
keywordsNoFeeds Article JSON-LD
og_imageNoDefaults to feature_image
og_titleNoFacebook/LinkedIn card title
meta_titleNoSEO title; defaults to title
publish_atNoWhen the post should be published, as a date and a time of day — "2026-08-04T10:00". **It is read in the TARGET BLOG'S OWN timezone, never yours and never the user's.** So "publish at 10am tomorrow" is simply "2026-08-04T10:00": pass the wall-clock time the user said, verbatim. Do NOT convert it to UTC, do NOT convert it to your own timezone, and do NOT ask the user which timezone they mean — the blog decides, and Byline looks its timezone up from the platform. The same string sent to two blogs in two countries is two different instants, on purpose. An explicit offset ("2026-08-04T10:00:00+05:30" or "...Z") is also accepted and is then taken at face value, but only use one if the user actually named a timezone. Required with status "scheduled", where it must be at least 2 minutes in the future. With status "published" it must be in the PAST — that backdates the post; a future time with status "published" is refused, because Ghost would publish it immediately while WordPress would schedule it. The result reports publish_at_local, the time as the blog's own clock reads it — tell the user that one, not the UTC value.
canonical_urlNo
feature_imageNoURL from upload_image
twitter_imageNoDefaults to feature_image
twitter_titleNoX card title
custom_excerptNoShown in listings and feeds
og_descriptionNo
feature_image_idNoThe native id upload_image returned alongside the url (its `id` field), needed by platforms that reference media by id rather than URL — e.g. WordPress's featured_media. Ghost has no such field and ignores this.
meta_descriptionNo
feature_image_altNo
twitter_descriptionNo
feature_image_captionNo

TDQS

A4.4/5.0
Behavior4/5

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

With no annotations, the description carries full behavioral burden. It discloses default status, author resolution, the [[content_image]] placeholder constraint, and image requirement/refusal behavior. While it doesn't cover every edge case, it addresses the most consequential behaviors.

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?

Four sentences, each providing necessary operational detail. It front-loads the core action and then layers critical caveats without redundancy or fluff.

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

Completeness4/5

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

For a 25-param create tool with no output schema, the description covers the main behavioral complexity (status, images, author) while the schema's rich parameter descriptions handle the rest. It lacks explicit return-value info, but the schema and sibling context partially compensate.

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

Parameters4/5

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

Schema coverage is 60%, and the description adds practical semantics for key parameters: status defaults, author resolution, and image options with default behavior. It compensates for gaps but doesn't fully explain all 25 parameters.

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

Purpose5/5

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

States 'Publish an article' with a specific verb and resource, and clarifies default status (published) and creation semantics. Clearly distinguishes from siblings like update_post and upload_image by framing it as the create/publish action.

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

Usage Guidelines4/5

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

Provides clear context on when to use 'draft', 'scheduled', or 'published' statuses, and explains author field resolution. Does not explicitly name alternatives like update_post for existing posts, so it lacks explicit when-not-to-use guidance.

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

generate_imageGenerate imageA

Generate a photograph with Gemini, falling back to Grok. Pass prompt as the SUBJECT ONLY — what is happening and where; the photographic style is applied for you. Writes a PNG under the runs directory and returns its path plus which provider produced it.

ParametersJSON Schema
NameRequiredDescriptionDefault
lookNoThe camera register from build_writing_brief's IMAGES block, passed verbatim so every image in one article matches. Omitted, one is derived from the prompt.
slotNoFilename stem, e.g. "hero" or "inline"image
styleNophotoreal_people (default) — a photograph with people doing the work, for the hero/feature image. photoreal_scene — a photograph with no people required. diagram — no photographic styling at all; an escape hatch, and rarely right, since image models render text as gibberish.photoreal_people
aspectNo16:9
promptYesThe SUBJECT only — what is happening and where, specific to this article. Do not include camera, lighting, or style words; those are added for you.
providerNoPin a provider; disables fallback

TDQS

A4.2/5.0
Behavior4/5

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

With no annotations, the description carries the full burden. It discloses key behaviors: provider fallback (Gemini then Grok), file output (writes a PNG under the runs directory), and return value (path plus provider). It doesn't cover failure modes or auth/rate limits, but for this tool the disclosed behavior is substantial and directly actionable.

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 compact—three sentences—and front-loads the primary action and fallback. Every sentence provides essential information (what, how to phrase prompt, output behavior). No redundant wording or fluff.

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

Completeness4/5

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

For a tool with 6 parameters, 1 required, and no output schema, the description covers the essential context: purpose, fallback, output location, and return content. It doesn't describe the full parameter matrix, but the schema handles that. The description sufficiently orients the agent without needing to repeat schema details.

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?

Schema description coverage is high (83%), so the schema already explains most parameters. The description adds minimal parameter context—chiefly emphasizing that the prompt should be subject-only and that style is applied automatically. This aligns with existing schema descriptions, so the description adds limited extra meaning beyond the schema.

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

Purpose5/5

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

The description clearly states a specific action: 'Generate a photograph with Gemini, falling back to Grok.' It identifies the resource (a photograph/PNG) and outcome (writes a PNG, returns path and provider). This distinguishes it from siblings like upload_image, which handle existing images rather than generating new ones.

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

Usage Guidelines4/5

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

The description provides explicit guidance on how to use the tool: 'Pass `prompt` as the SUBJECT ONLY' and explains that style is applied automatically. It doesn't explicitly contrast with alternatives (e.g., when to use upload_image instead), but the generation context is clear, making the usage circumstances easy to infer.

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

get_personaGet personaC

Full author profile — voice, style, bias, and instructions.

ParametersJSON Schema
NameRequiredDescriptionDefault
slugYes

TDQS

C2.6/5.0
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 implies a read-only operation but does not state this explicitly, nor does it mention error handling, permission requirements, or what happens when the slug is invalid. Minimal behavioral context is added.

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

Conciseness4/5

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

The description is extremely short and front-loaded, using a dash to present key attributes. It is efficient and avoids waste, though it is a fragment rather than a complete sentence. Every word contributes meaning.

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?

For a tool with one parameter and no output schema or annotations, the description is insufficiently complete. It specifies the general content of the profile but omits the meaning of the input, return format, and any edge-case behavior. An agent would need to guess at slug semantics and result structure.

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

Parameters1/5

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

The schema has 0% description coverage, and the tool description does not mention the 'slug' parameter at all. The agent is left without any explanation of what a slug is or how to supply it, so the description fails to compensate for the missing schema information.

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 'Full author profile — voice, style, bias, and instructions' clearly identifies the tool's output as a comprehensive persona profile, differentiating it from the sibling list_personas tool. Though no explicit verb is present, the resource and scope are specific enough for an agent to infer the retrieval intent.

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?

No guidance is provided on when to use this tool versus alternatives such as list_personas or other author-related tools. The description gives no context about prerequisites, use cases, or situations where a different tool would be more appropriate.

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

health_checkHealth checkA

Probe every configured blog plus every image and research provider. Returns per-API ok/fail with the real error. Run this first when anything fails.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.7/5.0
Behavior4/5

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

No annotations are provided, so the description carries the full burden. It discloses the scope (all blogs, image/research providers), granularity (per-API), and result semantics (ok/fail with real error). It does not mention potential side effects or performance implications, but as a health check the behavioral profile is well covered.

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?

Two sentences deliver the purpose, scope, output, and usage directive with no redundancy. The description is front-loaded with the action and immediately provides actionable guidance.

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

Completeness5/5

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

For a parameterless diagnostic tool with no output schema, the description fully covers what it does, what it affects, what it returns, and when to use it. It is complete for its simplicity.

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

Parameters4/5

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

The tool has zero parameters and schema coverage is trivially 100%, so the description need not add parameter details. The baseline score for 0-parameter tools is 4, and the description appropriately focuses on behavior rather than parameters.

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

Purpose5/5

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

Clear verb 'probe' plus specific resources: every configured blog, image provider, and research provider. The output format (per-API ok/fail with error) is also stated, fully distinguishing this from all sibling tools.

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

Usage Guidelines5/5

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

Explicitly instructs 'Run this first when anything fails', giving a clear when-to-use directive. While no alternatives are mentioned, none are needed since this is the designated diagnostic tool.

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

list_authorsList authorsA

List the author accounts on a site, with their platform ids. Pass an id as create_post's author to byline someone who has no persona file, or copy it into a persona's platform_authors.

ParametersJSON Schema
NameRequiredDescriptionDefault
siteYes

TDQS

A4.1/5.0
Behavior4/5

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

No annotations are provided, so the description carries the full burden. It discloses the output format (platform ids) and adds behavioral context by explaining how the ids can be used in create_post and persona configuration, which goes beyond a simple listing statement.

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 concise and well-structured: the first sentence states the purpose, and the second provides usage guidance. No unnecessary words or fluff.

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

Completeness4/5

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

Given the tool's simplicity (one parameter, no output schema, no annotations), the description covers the core behavior, output, and intended usage. It lacks parameter details but is otherwise complete for a listing tool.

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

Parameters2/5

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

The schema provides 0% description coverage for the sole 'site' parameter, and the description only mentions 'on a site' without explaining what a valid site is (e.g., site id or name) or how to obtain it. The description does not compensate for the lack of schema information.

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

Purpose5/5

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

The description clearly states 'List the author accounts on a site, with their platform ids,' specifying the verb, resource, scope, and output. It also distinguishes authors from personas, which is helpful given the sibling tool list_personas.

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

Usage Guidelines4/5

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

The description provides concrete usage guidance: 'Pass an id as create_post's author to byline someone who has no persona file, or copy it into a persona's platform_authors.' This explains when to use the tool but does not explicitly mention when not to use it or name alternatives like list_personas.

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

list_personasList personasA

List available author personas.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A3.8/5.0
Behavior3/5

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

No annotations are provided, so the description carries the full burden. It clearly indicates a read-only listing operation, which is inherently transparent. However, it does not disclose any additional behavioral traits such as ordering, filtering, or whether the list includes inactive or draft personas, which could be relevant for an agent.

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 a single, concise sentence that is front-loaded with the action and object. Every word earns its place; there is no fluff or redundancy. It is optimally sized for the simplicity of the tool.

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

Completeness4/5

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

Given the tool's low complexity (no parameters, no output schema, simple list operation), the description is nearly complete. It could be slightly more informative by noting that the list is of pre-defined/available author personas, but the current level is sufficient for an agent to understand the tool's basic purpose.

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

Parameters4/5

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

The input schema has zero parameters, so the baseline is 4. The description adds no parameter information because there are none to describe. This is appropriate and requires no further elaboration.

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

Purpose5/5

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

The description uses a specific verb 'List' and clearly identifies the resource as 'available author personas.' This distinguishes it from sibling tools like list_authors (which likely lists authors) and get_persona (which fetches a specific persona), making the purpose unambiguous.

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?

There is no explicit guidance on when to use this tool versus alternatives. While it is implied that this is for seeing available author personas, the description does not mention when not to use it or how it differs from list_authors or get_persona. The agent receives no directional context beyond the name.

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

list_sitesList sitesA

List configured publishing targets. Never returns key material.

ParametersJSON Schema
NameRequiredDescriptionDefault

No parameters

TDQS

A4.5/5.0
Behavior4/5

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

With no annotations provided, the description carries full responsibility for behavioral disclosure. It explicitly states a key safety guarantee ('Never returns key material') and implies a read-only nature via 'List'. This goes beyond the minimum, though it does not cover authentication or error scenarios.

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 two short sentences, front-loaded with the action and resource. Every word adds value: 'configured' clarifies scope and 'Never returns key material' is a critical guarantee. No redundancy.

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

Completeness5/5

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

For a simple zero-parameter list tool, this description is fully complete. It states what it returns (configured publishing targets) and an important exclusion (key material). No output schema exists, but the description covers the essential return value sufficiently.

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

Parameters4/5

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

The tool has zero parameters, so the description does not need to explain any. The baseline score of 4 applies because there is nothing missing regarding parameters.

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

Purpose5/5

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

The description uses a specific verb ('List') and resource ('configured publishing targets'), clearly distinguishing it from sibling tools like add_site and remove_site. The scope is well-defined and immediately understandable.

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

Usage Guidelines4/5

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

The description clearly communicates that this tool is for listing configured publishing targets, which is contextually distinct from the add/remove siblings. It does not explicitly exclude any use cases or name alternatives, but the purpose itself is sufficient for an agent to decide when to invoke it.

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

remove_siteRemove siteA

Remove a publishing target from config/sites.yaml. Leaves .env untouched.

ParametersJSON Schema
NameRequiredDescriptionDefault
slugYes

TDQS

A3.8/5.0
Behavior3/5

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

No annotations are provided, so the description carries the full burden. The statement 'Leaves .env untouched' provides useful scope, but it does not disclose other behaviors such as reversibility, error handling, or whether the operation is permanent. This is a moderate level of transparency.

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 two concise sentences, front-loaded with the core action and followed by a useful caveat. Every word earns its place, with no redundancy or unnecessary detail.

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

Completeness3/5

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

The tool is simple with one parameter and no output schema, so the description is mostly adequate. It states the action and scope, but it fails to mention return values, error cases, or what happens if the slug doesn't exist. For a clean removal tool, it is acceptable but lacks some contextual completeness.

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

Parameters2/5

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

The schema has one required parameter 'slug' with no description, and the tool description does not explain what slug refers to. Given 0% schema description coverage, the description should have clarified that slug is the identifier of the site to remove, but it does not, leaving the agent to infer meaning.

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

Purpose5/5

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

The description uses a specific verb 'Remove' and a clear resource 'publishing target from config/sites.yaml', which precisely defines the action. It also distinguishes itself from sibling tools like add_site and list_sites by focusing on the removal operation.

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

Usage Guidelines4/5

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

The description implies when to use it by specifying the action and the target file (config/sites.yaml), giving clear context. However, it does not explicitly state when not to use it or list alternatives, so it doesn't fully meet the highest standard.

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

research_topicResearch a topicA

OPTIONAL: fetch attributable findings on a topic from a single configured research provider (Brave or Tavily). If you already have live web access, use it directly and pass what you find as research to build_writing_brief instead — this tool exists mainly for callers without one. What it adds is provenance: every finding carries its source URL, and its publication date wherever the provider supplies one, so claims drawn from it can be attributed. That provenance is what makes two downstream checks possible: build_writing_brief's news-mode recency guard, and score_draft's citation_provenance check, which cross-checks the URLs a draft links against these findings (advisory, not blocking). A hand-supplied research string gets neither: it is trusted, not verified. Before calling, decide whether the topic actually turns on recent events: "the Ashes result" does, "the history of the Ashes" does not. If that is genuinely unclear, ASK THE USER in chat rather than guessing. Byline never substitutes one provider for another — pick one explicitly with provider, or rely on BYLINE_RESEARCH_PROVIDER or whichever single provider is configured; an unconfigured named provider is refused, never swapped for the other. Findings come back in the provider's own order, with no re-sorting or relevance filtering by Byline — pass the WHOLE result as build_writing_brief's findings argument. Do NOT summarize it into research: that throws away the URLs and dates that make each finding checkable, and turns a verifiable origin into an unverifiable one.

ParametersJSON Schema
NameRequiredDescriptionDefault
topicYesWhat to search for, in the words a person would use
windowNoHow far back to reach. Use "day" for something that happened in the last few hours.week
providerNoPin a provider. Omit to use BYLINE_RESEARCH_PROVIDER, or the only configured one. Never falls back to the other. Brave returns ranked snippets with no synthesis; Tavily also returns a synthesis (`answer`) plus its own sources.
max_resultsNo

TDQS

A4.9/5.0
Behavior5/5

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

With no annotations, the description carries the full transparency burden and does so comprehensively. It discloses provider refusal (never swaps), absence of re-sorting or relevance filtering, provenance formatting, the advisory (non-blocking) nature of downstream checks, and the distinction between verified findings and untrusted hand-supplied strings. This goes far beyond the schema.

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?

Though long, every sentence earns its place. The description is logically structured: purpose, alternatives, provenance value, downstream use, recency decision, provider policy, output handling, and explicit do-not-summarize warning. It front-loads the core action and builds context without fluff.

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

Completeness5/5

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

This is a complex tool with no output schema and no annotations. The description fills every gap: what it returns (findings with URLs and dates), order, provider behavior, configuration, downstream integration, and failure modes (refused unconfigured provider). An agent has enough context to invoke it correctly and interpret results.

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

Parameters4/5

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

Schema coverage is 75%, so the baseline is 3. The description adds significant value by detailing provider-specific behavior ('Brave returns ranked snippets with no synthesis; Tavily also returns a synthesis (`answer`) plus its own sources') and reinforcing window semantics ('Use "day" for something that happened in the last few hours'). max_results lacks schema description but its schema bounds (1-20, default 10) provide adequate meaning; the description does not add further detail there, so not a 5.

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

Purpose5/5

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

The description opens with a specific verb and resource: 'fetch attributable findings on a topic from a single configured research provider.' It clearly distinguishes itself from sibling tools like build_writing_brief and score_draft by emphasizing provenance and downstream checks, making its unique role unmistakable.

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

Usage Guidelines5/5

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

Provides explicit guidance on when to use this tool versus alternatives: 'If you already have live web access, use it directly and pass what you find as `research` to build_writing_brief instead.' It also instructs when to ask the user if recency is unclear, and explicitly warns against summarizing findings into `research`, naming the alternative approach clearly.

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

score_draftScore draftA

Mechanically score a draft for human-voice quality: burstiness, AI-tell phrasing, paragraph uniformity, evidence density, first-hand experience, target-platform HTML validity, and — when findings is passed — whether every cited URL actually came from the research. Verdict blocked means fix before publishing. No external API is called.

ParametersJSON Schema
NameRequiredDescriptionDefault
htmlYes
siteNoWhich site this is written for — its platform decides the HTML rules. Defaults to the default site.
findingsNoThe `findings` array from the research_topic result this draft was written from. Supplying it enables the citation_provenance check, which verifies every cited URL actually came from the research. Omit it and that check reports "not evaluated" rather than passing.
feature_imageNoThe feature image, which lives outside the HTML and is otherwise invisible to this tool. Omit it before the image exists; the check reports "not evaluated" rather than failing.

TDQS

A4.4/5.0
Behavior4/5

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

With no annotations provided, the description carries the burden of behavioral disclosure. It clearly states the tool is mechanical, makes no external API calls, and explains the conditional nature of the citation provenance and feature-image checks. It does not describe the full output structure, but 'Verdict blocked' hints at the verdict format.

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 three sentences with no filler. It front-loads the core purpose and enumerates checks efficiently, then adds conditional behavior in a compact em-dash clause. Every sentence earns its place.

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

Completeness4/5

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

The tool has no output schema and multiple nested optional parameters, but the description covers the key behavioral aspects: what is scored, when verdicts matter, and what happens when optional inputs are omitted. It lacks explicit thresholds and return-format details, but the guidance is sufficient for an agent to select and invoke the tool correctly.

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

Parameters4/5

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

Schema description coverage is 75%, so the schema already documents most parameters. The description adds meaningful context for the two optional parameters: findings enables the citation_provenance check, and feature_image is invisible to the tool unless supplied. This goes beyond the schema's field-level descriptions and helps the agent decide whether to pass them.

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

Purpose5/5

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

The description opens with a specific verb+resource ('Mechanically score a draft') and enumerates the exact scoring dimensions (burstiness, AI-tell phrasing, paragraph uniformity, etc.), making the tool's purpose unmistakable and clearly distinguishing it from sibling tools like create_post or health_check.

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

Usage Guidelines4/5

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

The description implies usage context: it is meant for evaluating a draft before publishing, as shown by 'Verdict blocked means fix before publishing.' It also clarifies optional behavior with findings and feature_image. However, it does not explicitly state when not to use this tool or name alternative tools for related tasks.

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

update_postUpdate postA

Edit an existing post in place. Only the fields you pass are changed. Also the way to schedule an existing draft (status "scheduled" plus publish_at), to unschedule one (status "draft"), or to correct a post's date (publish_at in the past).

ParametersJSON Schema
NameRequiredDescriptionDefault
htmlNo
siteYes
tagsNo
titleNo
statusNo
post_idYes
og_imageNo
og_titleNo
meta_titleNo
publish_atNoa date and a time of day — "2026-08-04T10:00". **It is read in the TARGET BLOG'S OWN timezone, never yours and never the user's.** So "publish at 10am tomorrow" is simply "2026-08-04T10:00": pass the wall-clock time the user said, verbatim. Do NOT convert it to UTC, do NOT convert it to your own timezone, and do NOT ask the user which timezone they mean — the blog decides, and Byline looks its timezone up from the platform. The same string sent to two blogs in two countries is two different instants, on purpose. An explicit offset ("2026-08-04T10:00:00+05:30" or "...Z") is also accepted and is then taken at face value, but only use one if the user actually named a timezone. Required with status "scheduled", where it must be at least 2 minutes in the future. With status "published" it must be in the PAST — that backdates the post; a future time with status "published" is refused, because Ghost would publish it immediately while WordPress would schedule it. The result reports publish_at_local, the time as the blog's own clock reads it — tell the user that one, not the UTC value. Note that Ghost refuses to move an already-published post back to "scheduled" (it answers "Your post is already published"); set it to "draft" first.
canonical_urlNo
feature_imageNo
twitter_imageNo
twitter_titleNo
custom_excerptNo
og_descriptionNo
feature_image_idNoThe native id upload_image returned alongside the url, needed by platforms that reference media by id rather than URL — e.g. WordPress's featured_media. Ghost has no such field and ignores this.
meta_descriptionNo
feature_image_altNo
codeinjection_headNo
twitter_descriptionNo
feature_image_captionNo

TDQS

A3.9/5.0
Behavior3/5

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

The description discloses the key behavioral trait that only passed fields are changed, which is valuable for a partial-update tool. It also mentions the scheduling/unscheduling/backdating behaviors. However, with no annotations, it omits details about permissions, error conditions, or whether changes are irreversible, leaving the agent to infer mutation safety.

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 three short, purposeful sentences. The first states the core action, the second explains the partial-update behavior, and the third lists additional use cases. Every sentence earns its place with no redundancy or filler.

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

Completeness3/5

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

The description gives enough context to understand primary use and the scheduling nuances. But with 22 parameters, no output schema, and no annotations, it leaves out return values, error handling, and permission requirements. It is adequate for basic usage but not fully complete for an agent operating autonomously.

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 description adds semantics for status and publish_at by explaining their combined usage for scheduling/unscheduling/backdating, and it clarifies that any subset of fields can be updated. Schema coverage is only 9%, so the description should compensate for the other 20 parameters; it does not, though most property names (e.g., title, tags, html) are self-explanatory.

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

Purpose5/5

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

The description opens with 'Edit an existing post in place,' which clearly states the verb (edit), resource (existing post), and scope (in place). It further distinguishes itself from the sibling create_post by emphasizing edits to existing posts and by enumerating specific use cases like scheduling/unscheduling drafts and backdating.

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

Usage Guidelines4/5

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

The description explicitly lists when to use the tool: to edit a post, schedule an existing draft, unschedule one, or correct a date. It implicitly signals this is not for creating new posts, but it does not explicitly name create_post as an alternative or state what not to do. Clear context exists, but no formal exclusions are given.

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

upload_imageUpload imageA

Upload a local image to a site's media store and return the hosted URL.

ParametersJSON Schema
NameRequiredDescriptionDefault
altNo
pathYesLocal path returned by generate_image
siteYes

TDQS

A3.7/5.0
Behavior3/5

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

No annotations are provided, so the description carries the burden. It names the action and return value (hosted URL) but does not disclose side effects, error behavior, or prerequisites. The transparency is adequate but minimal.

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 a single, clear sentence with no redundant or extraneous information. It is appropriately sized for the tool's simplicity.

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

Completeness4/5

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

For a simple tool with three parameters and no output schema, the description conveys the core functionality and return value. It could mention prerequisites like 'site must be existing' but is otherwise sufficiently complete.

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

Parameters2/5

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

Schema description coverage is only 33% (only 'path' is described). The description does not add meaning for 'site' or 'alt', leaving their roles ambiguous.

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

Purpose5/5

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

The description clearly states the action (upload), the object (local image), the destination (site's media store), and the result (hosted URL). This distinguishes it from sibling tools like generate_image which creates an image.

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

Usage Guidelines3/5

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

The description implies usage: when you have a local image to host, upload it to a site. However, it does not explicitly mention alternatives, exclusions, or prerequisites such as the site needing to exist.

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

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

  1. 14 tool updatesv1.6.1
    • First observedadd_site
    • First observedbuild_writing_brief
    • First observedcreate_post
    • First observedgenerate_image
    • First observedget_persona
    • First observedhealth_check
    • First observedlist_authors
    • First observedlist_personas
    • First observedlist_sites
    • First observedremove_site
    • First observedresearch_topic
    • First observedscore_draft
    • First observedupdate_post
    • First observedupload_image

TDQS

A3.7/5.0
Disambiguation5/5

Each tool targets a distinct resource and action. Even the list_* tools (sites, authors, personas) are clearly separated by what they enumerate, and the descriptions clarify the differences. No two tools could be easily confused.

Naming Consistency4/5

The vast majority of tools follow a consistent verb_noun pattern (list_sites, add_site, create_post, update_post). The only deviation is health_check, which is a common compound noun and not confusing, but it breaks the otherwise uniform verb-first style.

Tool Count5/5

14 tools is within the ideal 3-15 range. Each tool serves a clear purpose in the blogging workflow, from configuration and research to image generation and publishing. No tool feels redundant or unnecessary.

Completeness3/5

The set covers site management, persona lookup, research, brief building, scoring, image handling, and post creation/updating, but lacks any way to read or delete posts. This is a notable gap for a publishing server, as an agent cannot list existing posts or retrieve a post to update it intelligently.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

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/indianic/byline'

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