Ghost

Lightweight, dependency-free, in-memory fake of the Ghost Content + Admin APIs for testing code that uses the real @tryghost/content-api / @tryghost/admin-api SDKs.

Default port: 4845

Quick start

import { GhostServer } from "./services/ghost/src/server.js";

const server = new GhostServer(4845);
await server.start();
// ... run your app/tests ...
await server.stop();

Create a post via the Admin API (Bearer / Ghost JWT), then read it via the Content API (?key=):

await fetch("http://127.0.0.1:4845/ghost/api/admin/posts/", {
  method: "POST",
  headers: { Authorization: "Ghost <jwt>", "Content-Type": "application/json" },
  body: JSON.stringify({ posts: [{ title: "Hello", html: "<p>Body</p>", status: "published" }] }),
});

await fetch("http://127.0.0.1:4845/ghost/api/content/posts/?key=parlel");

Access via MCP / preview URL

When run inside a parlel pool, reachable at its mapped preview URL (e.g. http://127.0.0.1:4845). MCP clients drive content and admin posts using GHOST_CONTENT_API_KEY (Content API ?key=) and GHOST_ADMIN_API_KEY (Admin API Bearer/Ghost JWT).

Implemented operations

State is in-memory and ephemeral.

Content API (?key=<key>)

Admin API (Authorization: Ghost <jwt> or Bearer <jwt>)

Service & inspection (parlel extensions)

Surface coverage

This emulator faithfully replicates the API surface most application code and agents exercise. Anything below the supported lines is either an intentional design choice for a fast, zero-cost local emulator (✓ By design) or a candidate for a future release (⟳ Roadmap) — never a silent inaccuracy.

Legend: ✅ fully supported · ◐ accepted (stored, not strictly enforced) · ✓ by design · ⟳ on the roadmap.

FeatureStatus
Admin posts create / list / get / update / delete✅ Supported
Content posts list (published only) / get by id or slug✅ Supported
{ posts, meta:{ pagination } } wrappers✅ Supported
Content ?key= + Admin Bearer/Ghost auth✓ By design — Any non-empty credential is accepted — no real secrets needed
title validation on create✅ Supported
mobiledoc/lexical → html renderinghtml is stored as-provided; not rendered from mobiledoc
Pages, tags, members, images, themes, webhooks⟳ Roadmap
Real pagination / filtering (filter, include, fields)⟳ Roadmap
Real persistence✓ By design — In-memory by design — fast, isolated, resets cleanly between tests

Error shapes

Ghost uses { errors: [{ message, type, id, ... }] }:

StatusWhen
401Content API without ?key=, or Admin API without Bearer/Ghost auth
422post create without a title (ValidationError)
404unknown post, or draft requested via Content API

Manifest

See services/ghost/manifest.json — name ghost, port 4845, protocol http, healthcheck /health, env GHOST_CONTENT_API_KEY, GHOST_ADMIN_API_KEY, GHOST_URL, GHOST_BASE_URL.

<!-- parlel:testenv:start -->

Configuration — test.env

Copy these into your test.env (used by the bridge sidecar flow). Tokens are Parlel's seeded test credentials — any non-empty value is accepted by the emulator, so you rarely need to change them. Swap in real credentials only when pointing at the live service in prod.env.

GHOST_CONTENT_API_KEY=parlel
GHOST_ADMIN_API_KEY=parlel:parlel
GHOST_URL=http://parlel-bridge:4845
GHOST_BASE_URL=http://parlel-bridge:4845
<!-- parlel:testenv:end -->