
Security News
Happy Birthday, Shai-Hulud
It has been one year since Shai-Hulud made its first appearance on npm.
@motiblog/mcp
Advanced tools
Agent-first SEO research and publishing: competitors, SERPs, internal links, and Search Console
Agent-first control surface over the MotiBlog API (docs/product/agent-first-direction.md, Direction B). Any MCP-capable agent — Claude Code, Cursor, Codex — can run a whole blog: propose topics, generate, review fact-check reports, approve through the governed gate, publish to targets, or export content to deploy into its own codebase.
The transport is mounted inside the API process, so the deployed API is the MCP endpoint. No install, no repository checkout, no separate service:
# 1. Get a per-project API key (dashboard: Project → Blog API → copy/rotate)
# 2. Point any MCP-capable agent at the hosted endpoint:
claude mcp add --transport http motiblog https://api.motiblog.ai/mcp \
--header "x-api-key: <your-key>"
Verify by hand:
curl -s https://api.motiblog.ai/mcp \
-H 'content-type: application/json' \
-H 'accept: application/json, text/event-stream' \
-H 'x-api-key: <your-key>' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
Running from the repository, for work on the tool layer itself:
claude mcp add motiblog \
--env MOTIBLOG_API_KEY=<your-key> \
--env MOTIBLOG_PROJECT_ID=<optional-default-project> \
-- pnpm --filter @motiblog/mcp start
The package is also published to npm for local stdio use; the hosted endpoint is the recommended customer path because OAuth needs no copied project key.
Or use the committed .mcp.json: set MOTIBLOG_API_KEY in your environment and start Claude Code in the repo root.
| Env var | Required | Meaning |
|---|---|---|
MOTIBLOG_API_KEY | yes | Per-project key (Project.blogApiKey). Rotatable from dashboard. |
MOTIBLOG_API_URL | no | API base URL. Default http://localhost:3001. |
MOTIBLOG_PROJECT_ID | no | Default project; tools still accept explicit project_id. |
The handler (src/http-handler.ts) is mounted inside the API process as
Express middleware — apps/api/src/mcp/mcp.middleware.ts, registered from
apps/api/src/main.ts. Consequences worth knowing:
POST api.motiblog.ai/mcp works with the ordinary API deploy. There is no
second container and no extra reverse-proxy rule — which is exactly why the
endpoint was unreachable before: the code existed, the route did not.app.use() before app.listen(), so it sits ahead
of Nest's router. The global ValidationPipe and TransformInterceptor
never see it; if they did, they would rewrap the JSON-RPC envelope in the
API's { success, data } shape and no client could parse it.MOTIBLOG_MCP_API_URL.Covered by apps/api/src/mcp/mcp.middleware.spec.ts, which boots a real Nest
app with those globals installed and asserts a full handshake survives.
For local work, or to serve MCP from somewhere other than the API:
pnpm --filter @motiblog/mcp start:http
# → http://127.0.0.1:3021/mcp (POST, JSON-RPC; stateless)
| Env var | Default | Meaning |
|---|---|---|
MOTIBLOG_HTTP_PORT | 3021 | Listen port |
MOTIBLOG_HTTP_HOST | 127.0.0.1 | Bind host (use 0.0.0.0 behind a proxy) |
Every request must carry the key (x-api-key or Authorization: Bearer); requests are authenticated and served independently (stateless — no session affinity).
The hosted endpoint supports OAuth 2.1 discovery, dynamic client registration, PKCE, refresh rotation, and human-selected project access. A person can allow all projects, an explicit subset, or only projects the agent creates. Per-project keys remain available for automations and local stdio use.
Published as @motiblog/mcp
under the motiblog org. Consumers run it with:
claude mcp add motiblog --env MOTIBLOG_API_KEY=<key> -- npx -y @motiblog/mcp
Prerequisites for whoever publishes: membership of the motiblog org,
npm login, and 2FA enabled — npm rejects publishes without it
(403 … Two-factor authentication or granular access token with bypass 2fa enabled is required). Either pass --otp=<code> or use a granular access
token with Bypass 2FA in ~/.npmrc.
Then, from apps/mcp:
pnpm publish --access public # runs tsup via prepublishOnly
Verify before you push the button:
pnpm pack # writes motiblog-mcp-<version>.tgz
tar -tzf motiblog-mcp-*.tgz # dist/ + README.md, nothing else
tar -xzOf motiblog-mcp-*.tgz package/package.json | grep -E '"main"|"bin"'
Three things that must stay true, and all are easy to break:
The mcp bin alias. npx -y @motiblog/mcp resolves the bin whose key
matches the package's unscoped name. Published 0.1.0 had only
motiblog-mcp and motiblog-mcp-http, so the obvious command failed with
"could not determine executable to run" — fixed in 0.1.1 by aliasing mcp
to the stdio entry. Test the bare npx -y @motiblog/mcp after any change to
publishConfig.bin, not just the long form.
@motiblog/shared is bundled, not depended on. It is a workspace
package that will never exist on npm, so tsup.config.ts lists it under
noExternal and the manifest keeps it in devDependencies. Move it back to
dependencies and every install resolves fine right up until the first
export_blog call. grep -c '@motiblog/shared' dist/*.js must print 0.
main differs between the workspace and the tarball. The repo consumes
raw TypeScript (src/lib.ts) because apps/api imports this package in
process; npm consumers get dist/lib.js. publishConfig performs that swap
at publish time, so neither side needs the other's layout.
Smoke-test the tarball the way a customer meets it:
mkdir /tmp/t && cd /tmp/t && npm init -y
npm install /path/to/motiblog-mcp-<version>.tgz
node -e "console.log(Object.keys(require('@motiblog/mcp')))"
MOTIBLOG_HTTP_PORT=3098 node_modules/.bin/motiblog-mcp-http
server.json in this directory is the listing, validated against schema
2025-12-11. The registry is the canonical directory since 2026 and downstream
directories and newsletters ingest it, so one publish propagates — do this
before filling in any individual directory form.
server.json claims ai.motiblog/mcp — the reverse-DNS namespace for a domain
we own, which is the identity a product should have rather than a personal
GitHub handle. Claiming it means proving control of motiblog.ai.
The keypair is already generated and the proof is already deployed. The
public half is served from the web app at
/.well-known/mcp-registry-auth;
the private half is at ~/.motiblog/mcp-registry-key (mode 600, never in the
repo, same convention as the agent API key).
HTTP verification was chosen over the DNS TXT record deliberately: it lives in this repository, so the proof is reviewable, versioned and deploys with the app instead of depending on a registrar dashboard nobody remembers the login for. The registry accepts either, with the same key — if you ever prefer DNS, the record is:
motiblog.ai. IN TXT "v=MCPv1; k=ed25519; p=<the same public key>"
mcp-publisher is a Go binary from
github.com/modelcontextprotocol/registry releases
(v1.8.1 at time of writing) — it is not on npm.
cd apps/mcp
mcp-publisher login http \
--domain motiblog.ai \
--private-key "$(cat ~/.motiblog/mcp-registry-key)"
mcp-publisher publish
The web app must be deployed with the well-known route before the login will
verify — the registry fetches https://motiblog.ai/.well-known/mcp-registry-auth
and checks the signature against it. Confirm it is live first:
curl https://motiblog.ai/.well-known/mcp-registry-auth
# v=MCPv1; k=ed25519; p=...
If you rotate the key, the constant in the route and the DNS record (if one exists) must change together. A mismatch fails with a signature error that does not explain itself.
mcpName — the pairing the registry enforcespackage.json carries "mcpName": "ai.motiblog/mcp", matching name in
server.json. This is not optional decoration: the registry refuses to list a
server that names an npm package unless that package points back at the
listing, because the pairing is how it proves one owner controls both.
Omit it and publishing fails with:
NPM package '@motiblog/mcp' is missing required 'mcpName' field
Since the check reads the published package, a fix means republishing to npm first, then to the registry. Keep the two names in step.
Live at ai.motiblog/mcp. Confirm with:
curl -s 'https://registry.modelcontextprotocol.io/v0.1/servers?search=motiblog'
Validate before publishing — the registry rejects on schema, and the limits
are tighter than they look (description caps at 100 characters, which
the first draft of ours blew past):
python3 - <<'PY'
import json, urllib.request, jsonschema
s = json.load(urllib.request.urlopen(json.load(open('server.json'))['$schema']))
jsonschema.validate(json.load(open('server.json')), s)
print('valid')
PY
Updating a listing needs a new version. Bump version in both
package.json and server.json whenever the copy or the package changes, or
the publish is a no-op.
| File | Shape | Use |
|---|---|---|
src/lib.ts | pure exports, no side effects | what main points at; what the API imports |
src/index.ts | executes on import | stdio server (pnpm start) |
src/http.ts | executable + import-safe server factory | standalone HTTP server (pnpm start:http) |
Import the library surface in production code. http.ts is import-safe for
transport tests; index.ts remains an executable-only entry point.
list_projects / create_project → suggest_topics → approve_content_plan → generate_article
→ list_review_queue → get_article (factCheckReport, seoScore)
→ update_article (fix flagged claims / supply_product_fact)
→ approve_publication → publish_to_integration …or… export_blog
get_pipeline_logs diagnoses failures; list_refresh_suggestions finds decaying published posts worth refreshing; start_pipeline runs the fully autonomous crawl→plan→generate loop when you want hands-off operation.
| Tool | Notes |
|---|---|
list_projects | Projects permitted by the credential + approval/quota settings |
create_project | Create in an available account slot; restricted OAuth grants automatically receive access |
get_project | Full pipeline config, positioning inputs |
update_project | Change approval/autonomy, language/model, positioning, links and banner settings |
start_pipeline | Autonomous crawl→plan→generate run |
get_pipeline_status | Run state + steps |
retry_pipeline | Retry a failed run |
| Tool | Notes |
|---|---|
suggest_topics | Add topic(s) to content plan (DRAFT entries) |
list_content_plans | Plan queue with statuses |
approve_content_plan | Clear a plan entry for generation |
regenerate_content_plan | AI-rewrite of a plan entry |
reschedule_content_plan | Move a plan on the calendar |
create_content_plan | Add a fully agent-authored plan entry (no AI rewrite) |
update_content_plan | Edit a plan's title/keyword/summary/type/date, pause via DRAFT |
bulk_reschedule_content_plans | Reorder up to 100 plans in one transactional call |
delete_content_plan | Soft-delete ungenerated backlog (restorable) |
restore_content_plan | Undo a soft delete, status and date intact |
list_deleted_content_plans | Audit what was deleted, find ids to restore |
generate_article | Full pipeline from APPROVED plan |
generate_programmatic_articles | Up to 500 rows through a title template |
get_calendar | Scheduled entries between two ISO dates |
| Tool | Notes |
|---|---|
list_review_queue | Articles by lifecycle status (default REVIEW) |
get_article | Full markdown + factCheckReport, topicGate, seoScore, optional per-phase logs |
get_article_feedback | Aggregate reader ratings for one article |
update_article | Edit content/title/meta/status — how agents answer fact-check flags |
approve_publication | Governed approval gate; publish_now=true publishes immediately |
schedule_publication | Set/clear scheduled publish time |
| Tool | Notes |
|---|---|
regenerate_article | Full rerun (destructive) |
regenerate_chapter | One section only (safe) |
get_pipeline_logs | Phase telemetry: status, tokens, USD cost |
list_refresh_suggestions | GSC-driven decay candidates across permitted projects |
dismiss_refresh_suggestion | Remove an addressed suggestion from the queue |
rewrite_text | Inline editor rewrite actions without mutating the article |
| Tool | Notes |
|---|---|
list_integrations | WEBHOOK/WORDPRESS/GHOST/WEBFLOW/SHOPIFY/DEVTO/SANITY/CUSTOM_API |
get_integration | Target state with credentials redacted |
create_integration | Create any supported provider target |
update_integration / delete_integration | Edit, enable/disable, or remove targets |
create_webhook_integration | Push target for your own infra; deliveries signed X-Signature: sha256=HMAC(secret) |
test_integration | ping or full connection test |
publish_to_integration | APPROVED article → PUBLISHING → PUBLISHED |
retry_publish | Retry failed attempt |
mark_integration_synced | Record a verified manual sync in publish history |
list_publish_logs | Success/failure history with provider errors |
| Tool | Notes |
|---|---|
add_keyword / list_keywords / update_keyword / delete_keyword | Full keyword lifecycle |
list_competitors / add_competitor / delete_competitor | Competitive set management |
list_pillar_clusters | Pillar/member coverage and publication progress |
supply_product_fact / list_product_facts / get_product_fact / delete_product_fact | Ground truth enforced by strict fact-checking — supply BEFORE generation |
| Tool group | Notes |
|---|---|
get_brand_voice / get_brand_voice_preview / refresh_brand_voice | Inspect and re-extract writing voice |
list_authors / get_author / create_author / update_author / delete_author | Full author profiles |
get_content_source / update_content_source / detect_content_links | Sitemap/blog-root/manual internal-link source |
list_content_pages / delete_content_page / get_content_stats | Content-intelligence inventory |
start_site_crawl / get_crawl_status / list_crawled_pages / get_crawled_page / get_site_analysis | Website crawling and analysis |
| Tool group | Notes |
|---|---|
upload_article_banner / delete_article_banner | Replace or remove an article banner; pipeline banner generation stays internal |
upload_image | Import a public HTTPS image into MotiBlog storage |
get_banner_preview / suggest_banner_prompt | Inspect and configure the banner style without exposing raw AI image generation |
| Tool group | Notes |
|---|---|
get_search_console_status / connect_search_console / list_search_console_properties / select_search_console_property / disconnect_search_console | Connection lifecycle; Google consent remains a human browser step |
sync_search_console / get_search_console_date_range / get_search_console_performance | Sync and overview analytics |
list_search_console_queries / list_search_console_pages / get_article_position_history | Detailed search performance |
list_search_console_insights / dismiss_search_console_insight / get_search_console_action_queue | Agent action loop |
| Tool group | Notes |
|---|---|
get_account_profile / update_account_profile | Safe identity fields only; no billing, password, email, key, or OAuth-grant changes |
get_account_entitlement | Read-only plan/allowance state (no payment identifiers; cannot start a purchase) |
list_notifications / get_unread_notification_count / mark_notification_read / mark_all_notifications_read | Notification inbox |
| Tool | Notes |
|---|---|
export_blog | Writes {out_dir}/{slug}/index.md (+ YAML frontmatter incl. motiblogArticleId) and manifest.json. Portable GFM: single H1, <img>→markdown images, iframes→watch links. Drop into Astro/Next/Jekyll/Hugo/plain git and deploy anywhere. |
AdminGuard rejects machine callers), even if the owner is an admin.pnpm --filter @motiblog/mcp test # jest unit tests (client envelope handling, export format)
pnpm --filter @motiblog/mcp lint # tsc --noEmit
pnpm --filter @motiblog/mcp dev # tsx watch (restart your MCP client after edits)
FAQs
Agent-first SEO research and publishing: competitors, SERPs, internal links, and Search Console
The npm package @motiblog/mcp receives a total of 81 weekly downloads. As such, @motiblog/mcp popularity was classified as not popular.
We found that @motiblog/mcp demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
It has been one year since Shai-Hulud made its first appearance on npm.

Research
/Security News
Operators behind PolinRider used a compromised GitHub account to plant malware in four development versions of a Packagist package with 700,000+ downloads.

Security News
GitHub Actions now supports cache-mode, a least-privilege control on the Actions cache aimed at the cache poisoning technique behind recent compromises.