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>)
GET /ghost/api/content/posts/— list published posts ({ posts:[], meta:{ pagination } }).GET /ghost/api/content/posts/:id— fetch a published post (by id or slug).GET /ghost/api/content/settings/— public site settings.
Admin API (Authorization: Ghost <jwt> or Bearer <jwt>)
GET /ghost/api/admin/posts/— list all posts (drafts included).POST /ghost/api/admin/posts/— create a post (201 { posts:[{ id, uuid, title, slug, html, status, ... }] }).titlerequired.GET /ghost/api/admin/posts/:id— fetch any post.PUT /ghost/api/admin/posts/:id— update title/html/slug/status/feature_image.DELETE /ghost/api/admin/posts/:id— delete a post (204).GET /ghost/api/admin/site/— admin site info.
Service & inspection (parlel extensions)
GET /— service metadata.GET /health—{ status: "ok" }.POST /__parlel/reset— reset all in-memory state.OPTIONS *— CORS preflight (204).
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.
| Feature | Status |
|---|---|
| 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 rendering | ◐ html 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, ... }] }:
| Status | When |
|---|---|
401 | Content API without ?key=, or Admin API without Bearer/Ghost auth |
422 | post create without a title (ValidationError) |
404 | unknown 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.
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 -->