@motiblog/mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,236 @@
1
+ # MotiBlog MCP server
2
+
3
+ Agent-first control surface over the MotiBlog API ([`docs/product/agent-first-direction.md`](../../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**.
4
+
5
+ ## Quick start — hosted (what a customer uses)
6
+
7
+ The transport is mounted inside the API process, so the deployed API *is* the
8
+ MCP endpoint. No install, no repository checkout, no separate service:
9
+
10
+ ```bash
11
+ # 1. Get a per-project API key (dashboard: Project → Blog API → copy/rotate)
12
+ # 2. Point any MCP-capable agent at the hosted endpoint:
13
+ claude mcp add --transport http motiblog https://api.motiblog.ai/mcp \
14
+ --header "x-api-key: <your-key>"
15
+ ```
16
+
17
+ Verify by hand:
18
+
19
+ ```bash
20
+ curl -s https://api.motiblog.ai/mcp \
21
+ -H 'content-type: application/json' \
22
+ -H 'accept: application/json, text/event-stream' \
23
+ -H 'x-api-key: <your-key>' \
24
+ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
25
+ ```
26
+
27
+ ## Quick start — local stdio (development)
28
+
29
+ Running from the repository, for work on the tool layer itself:
30
+
31
+ ```bash
32
+ claude mcp add motiblog \
33
+ --env MOTIBLOG_API_KEY=<your-key> \
34
+ --env MOTIBLOG_PROJECT_ID=<optional-default-project> \
35
+ -- pnpm --filter @motiblog/mcp start
36
+ ```
37
+
38
+ The package is not published to npm and does not need to be: the hosted
39
+ endpoint covers every customer path, and stdio is for people who have the
40
+ repository anyway.
41
+
42
+ Or use the committed [`.mcp.json`](../../.mcp.json): set `MOTIBLOG_API_KEY` in your environment and start Claude Code in the repo root.
43
+
44
+ | Env var | Required | Meaning |
45
+ |---|---|---|
46
+ | `MOTIBLOG_API_KEY` | yes | Per-project key (`Project.blogApiKey`). Rotatable from dashboard. |
47
+ | `MOTIBLOG_API_URL` | no | API base URL. Default `http://localhost:3001`. |
48
+ | `MOTIBLOG_PROJECT_ID` | no | Default project; tools still accept explicit `project_id`. |
49
+
50
+ ## Remote agents: streamable HTTP
51
+
52
+ ### How it is served in production
53
+
54
+ The handler (`src/http-handler.ts`) is mounted **inside the API process** as
55
+ Express middleware — `apps/api/src/mcp/mcp.middleware.ts`, registered from
56
+ `apps/api/src/main.ts`. Consequences worth knowing:
57
+
58
+ - `POST api.motiblog.ai/mcp` works with the ordinary API deploy. There is no
59
+ second container and no extra reverse-proxy rule — which is exactly why the
60
+ endpoint was unreachable before: the code existed, the route did not.
61
+ - It is registered with `app.use()` *before* `app.listen()`, so it sits ahead
62
+ of Nest's router. The global `ValidationPipe` and `TransformInterceptor`
63
+ never see it; if they did, they would rewrap the JSON-RPC envelope in the
64
+ API's `{ success, data }` shape and no client could parse it.
65
+ - Nest's body-parser has already drained the request stream by then, so the
66
+ parsed body is handed to the transport explicitly. Skip that and the
67
+ transport waits on a stream that will never emit again.
68
+ - Tool calls loop back to the same API over loopback, so an agent's key passes
69
+ the same guards an external caller meets. Being mounted in-process buys no
70
+ privileged shortcut. Override the loopback base with `MOTIBLOG_MCP_API_URL`.
71
+
72
+ Covered by `apps/api/src/mcp/mcp.middleware.spec.ts`, which boots a real Nest
73
+ app with those globals installed and asserts a full handshake survives.
74
+
75
+ ### Standalone server
76
+
77
+ For local work, or to serve MCP from somewhere other than the API:
78
+
79
+ ```bash
80
+ pnpm --filter @motiblog/mcp start:http
81
+ # → http://127.0.0.1:3021/mcp (POST, JSON-RPC; stateless)
82
+ ```
83
+
84
+ | Env var | Default | Meaning |
85
+ |---|---|---|
86
+ | `MOTIBLOG_HTTP_PORT` | `3021` | Listen port |
87
+ | `MOTIBLOG_HTTP_HOST` | `127.0.0.1` | Bind host (use `0.0.0.0` behind a proxy) |
88
+
89
+ Every request must carry the key (`x-api-key` or `Authorization: Bearer`); requests are authenticated and served independently (stateless — no session affinity).
90
+
91
+ OAuth lands later — per-project keys are the MVP contract per `docs/product/agent-first-direction.md`.
92
+
93
+ ## Publishing to npm
94
+
95
+ The package is publish-ready but **not yet published**. Until it is, leave the
96
+ "no published npm package" line on `/mcp` and in `llms.txt` alone — an agent
97
+ that reads an install instruction and hits a 404 is worse off than one told
98
+ the truth.
99
+
100
+ Prerequisites, all of which are a human's job:
101
+
102
+ 1. An npm account, and membership of an org named `motiblog` (the `@motiblog`
103
+ scope). A free org is enough — scoped packages publish publicly with
104
+ `access: public`, already set in `publishConfig`.
105
+ 2. `npm login` on the machine doing the publish.
106
+
107
+ Then, from `apps/mcp`:
108
+
109
+ ```bash
110
+ pnpm publish --access public # runs tsup via prepublishOnly
111
+ ```
112
+
113
+ Verify before you push the button:
114
+
115
+ ```bash
116
+ pnpm pack # writes motiblog-mcp-<version>.tgz
117
+ tar -tzf motiblog-mcp-*.tgz # dist/ + README.md, nothing else
118
+ tar -xzOf motiblog-mcp-*.tgz package/package.json | grep -E '"main"|"bin"'
119
+ ```
120
+
121
+ Two things that must stay true, and both are easy to break:
122
+
123
+ - **`@motiblog/shared` is bundled, not depended on.** It is a workspace
124
+ package that will never exist on npm, so `tsup.config.ts` lists it under
125
+ `noExternal` and the manifest keeps it in `devDependencies`. Move it back to
126
+ `dependencies` and every install resolves fine right up until the first
127
+ `export_blog` call. `grep -c '@motiblog/shared' dist/*.js` must print 0.
128
+ - **`main` differs between the workspace and the tarball.** The repo consumes
129
+ raw TypeScript (`src/lib.ts`) because `apps/api` imports this package in
130
+ process; npm consumers get `dist/lib.js`. `publishConfig` performs that swap
131
+ at publish time, so neither side needs the other's layout.
132
+
133
+ Smoke-test the tarball the way a customer meets it:
134
+
135
+ ```bash
136
+ mkdir /tmp/t && cd /tmp/t && npm init -y
137
+ npm install /path/to/motiblog-mcp-<version>.tgz
138
+ node -e "console.log(Object.keys(require('@motiblog/mcp')))"
139
+ MOTIBLOG_HTTP_PORT=3098 node_modules/.bin/motiblog-mcp-http
140
+ ```
141
+
142
+ ## Entry points
143
+
144
+ | File | Shape | Use |
145
+ |---|---|---|
146
+ | `src/lib.ts` | pure exports, no side effects | what `main` points at; what the API imports |
147
+ | `src/index.ts` | executes on import | stdio server (`pnpm start`) |
148
+ | `src/http.ts` | executes on import | standalone HTTP server (`pnpm start:http`) |
149
+
150
+ Import the library surface, never an entry point — importing `index.ts` or
151
+ `http.ts` starts a server as a side effect of the import.
152
+
153
+ ## The agent loop
154
+
155
+ ```
156
+ list_projects → suggest_topics → approve_content_plan → generate_article
157
+ → list_review_queue → get_article (factCheckReport, seoScore)
158
+ → update_article (fix flagged claims / supply_product_fact)
159
+ → approve_publication → publish_to_integration …or… export_blog
160
+ ```
161
+
162
+ `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.
163
+
164
+ ## Tools
165
+
166
+ ### Discovery & system
167
+ | Tool | Notes |
168
+ |---|---|
169
+ | `list_projects` | All accessible projects + approval/quota settings |
170
+ | `get_project` | Full pipeline config, positioning inputs |
171
+ | `start_pipeline` | Autonomous crawl→plan→generate run |
172
+ | `get_pipeline_status` | Run state + steps |
173
+
174
+ ### Topics & planning
175
+ | Tool | Notes |
176
+ |---|---|
177
+ | `suggest_topics` | Add topic(s) to content plan (DRAFT entries) |
178
+ | `list_content_plans` | Plan queue with statuses |
179
+ | `approve_content_plan` | Clear a plan entry for generation |
180
+ | `regenerate_content_plan` | AI-rewrite of a plan entry |
181
+ | `generate_article` | Full pipeline from APPROVED plan |
182
+ | `get_calendar` | Scheduled entries between two ISO dates |
183
+
184
+ ### Review loop (the governance core)
185
+ | Tool | Notes |
186
+ |---|---|
187
+ | `list_review_queue` | Articles by lifecycle status (default REVIEW) |
188
+ | `get_article` | Full markdown + `factCheckReport`, `topicGate`, seoScore, optional per-phase logs |
189
+ | `update_article` | Edit content/title/meta/status — how agents answer fact-check flags |
190
+ | `approve_publication` | Governed approval gate; `publish_now=true` publishes immediately |
191
+ | `schedule_publication` | Set/clear scheduled publish time |
192
+
193
+ ### Regeneration ops
194
+ | Tool | Notes |
195
+ |---|---|
196
+ | `regenerate_article` | Full rerun (destructive) |
197
+ | `regenerate_chapter` | One section only (safe) |
198
+ | `get_pipeline_logs` | Phase telemetry: status, tokens, USD cost |
199
+ | `list_refresh_suggestions` | GSC-driven decay candidates across projects |
200
+
201
+ ### Publishing targets
202
+ | Tool | Notes |
203
+ |---|---|
204
+ | `list_integrations` | WEBHOOK/WORDPRESS/GHOST/WEBFLOW/SHOPIFY/DEVTO/SANITY/CUSTOM_API |
205
+ | `create_webhook_integration` | Push target for your own infra; deliveries signed `X-Signature: sha256=HMAC(secret)` |
206
+ | `test_integration` | `ping` or `full` connection test |
207
+ | `publish_to_integration` | APPROVED article → PUBLISHING → PUBLISHED |
208
+ | `retry_publish` | Retry failed attempt |
209
+ | `list_publish_logs` | Success/failure history with provider errors |
210
+
211
+ ### Agent-supplied knowledge
212
+ | Tool | Notes |
213
+ |---|---|
214
+ | `add_keyword` / `list_keywords` | SEO keywords driving topic relevance |
215
+ | `supply_product_fact` / `list_product_facts` | Ground truth enforced by strict fact-checking — supply BEFORE generation |
216
+
217
+ ### Export (deploy it yourself)
218
+ | Tool | Notes |
219
+ |---|---|
220
+ | `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. |
221
+
222
+ ## Security model
223
+
224
+ - The API key authenticates as the **project owner** on every existing route — ownership checks are unchanged; there is no privilege escalation.
225
+ - **Admin routes refuse API keys** (`AdminGuard` rejects machine callers), even if the owner is an admin.
226
+ - Publication always passes the typed approval gate with provenance — MCP adds no bypass.
227
+ - Keys are per-project and rotatable; rotate immediately if exposed.
228
+ - Rate limiting: global ThrottlerGuard applies unchanged.
229
+
230
+ ## Development
231
+
232
+ ```bash
233
+ pnpm --filter @motiblog/mcp test # jest unit tests (client envelope handling, export format)
234
+ pnpm --filter @motiblog/mcp lint # tsc --noEmit
235
+ pnpm --filter @motiblog/mcp dev # tsx watch (restart your MCP client after edits)
236
+ ```
package/dist/http.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { createServer } from 'http';
3
+
4
+ /**
5
+ * MotiBlog MCP server — standalone streamable HTTP entrypoint.
6
+ *
7
+ * This is the local/self-hosted way to serve remote agents. In production the
8
+ * same handler is mounted inside the API process instead, so the endpoint is
9
+ * `POST api.motiblog.ai/mcp` with no extra service and no extra proxy rule —
10
+ * see apps/api/src/mcp/mcp.middleware.ts.
11
+ *
12
+ * Stateless: every request authenticates with its own per-project API key and
13
+ * gets a fresh server + transport (no session affinity needed).
14
+ *
15
+ * Env:
16
+ * MOTIBLOG_API_URL — API base for tool calls (default http://localhost:3001)
17
+ * MOTIBLOG_HTTP_PORT — listen port (default 3021)
18
+ * MOTIBLOG_HTTP_HOST — bind host (default 127.0.0.1; use 0.0.0.0 behind a proxy)
19
+ * MOTIBLOG_PROJECT_ID — optional default project
20
+ *
21
+ * Auth: `x-api-key: <key>` or `Authorization: Bearer <key>` on every request.
22
+ */
23
+ interface HttpServerOptions {
24
+ port: number;
25
+ host: string;
26
+ apiBaseUrl: string;
27
+ defaultProjectId?: string;
28
+ }
29
+ declare function startHttpServer(opts: HttpServerOptions): ReturnType<typeof createServer>;
30
+
31
+ export { type HttpServerOptions, startHttpServer };