@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/dist/index.js ADDED
@@ -0,0 +1,1034 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/index.ts
5
+ var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
6
+
7
+ // src/config.ts
8
+ function loadConfig(env = process.env) {
9
+ const apiBaseUrl = (env.MOTIBLOG_API_URL || "http://localhost:3001").replace(/\/+$/, "");
10
+ const apiKey = env.MOTIBLOG_API_KEY?.trim();
11
+ if (!apiKey) {
12
+ throw new Error(
13
+ "MOTIBLOG_API_KEY is required. Get a per-project key from the MotiBlog dashboard (Project \u2192 Blog API) or via POST /projects/:id/blog-api/rotate."
14
+ );
15
+ }
16
+ return {
17
+ apiBaseUrl,
18
+ apiKey,
19
+ defaultProjectId: env.MOTIBLOG_PROJECT_ID?.trim() || void 0
20
+ };
21
+ }
22
+
23
+ // src/server.ts
24
+ var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
25
+
26
+ // src/api-client.ts
27
+ var MotiblogApiError = class extends Error {
28
+ constructor(status, url, body, message) {
29
+ super(message);
30
+ this.status = status;
31
+ this.url = url;
32
+ this.body = body;
33
+ this.name = "MotiblogApiError";
34
+ }
35
+ status;
36
+ url;
37
+ body;
38
+ };
39
+ var MotiblogApiClient = class {
40
+ constructor(apiBaseUrl, apiKey, fetchImpl = fetch) {
41
+ this.apiBaseUrl = apiBaseUrl;
42
+ this.apiKey = apiKey;
43
+ this.fetchImpl = fetchImpl;
44
+ }
45
+ apiBaseUrl;
46
+ apiKey;
47
+ fetchImpl;
48
+ async request(method, path, opts = {}) {
49
+ const attempts = method === "GET" ? 2 : 1;
50
+ let lastErr;
51
+ for (let attempt = 1; attempt <= attempts; attempt++) {
52
+ try {
53
+ return await this.requestOnce(method, path, opts);
54
+ } catch (err) {
55
+ lastErr = err;
56
+ if (err instanceof MotiblogApiError) throw err;
57
+ if (attempt === attempts) break;
58
+ await new Promise((r) => setTimeout(r, 400 * attempt));
59
+ }
60
+ }
61
+ throw lastErr;
62
+ }
63
+ async requestOnce(method, path, opts = {}) {
64
+ let url = `${this.apiBaseUrl}${path}`;
65
+ const search = new URLSearchParams();
66
+ for (const [k, v] of Object.entries(opts.query ?? {})) {
67
+ if (v !== void 0 && v !== "") search.set(k, String(v));
68
+ }
69
+ const qs = search.toString();
70
+ if (qs) url += `?${qs}`;
71
+ const res = await this.fetchImpl(url, {
72
+ method,
73
+ headers: {
74
+ "x-api-key": this.apiKey,
75
+ ...opts.body !== void 0 ? { "content-type": "application/json" } : {}
76
+ },
77
+ body: opts.body !== void 0 ? JSON.stringify(opts.body) : void 0
78
+ });
79
+ const text = await res.text();
80
+ let parsed;
81
+ try {
82
+ parsed = text ? JSON.parse(text) : void 0;
83
+ } catch {
84
+ parsed = text;
85
+ }
86
+ if (!res.ok) {
87
+ const bodyObj = parsed && typeof parsed === "object" ? parsed : {};
88
+ const validation = bodyObj.errors;
89
+ const detail = Array.isArray(bodyObj.message) ? bodyObj.message.join("; ") : typeof bodyObj.message === "string" ? bodyObj.message : typeof parsed === "string" && parsed ? parsed : `HTTP ${res.status}`;
90
+ const suffix = validation ? ` \u2014 ${JSON.stringify(validation)}` : "";
91
+ throw new MotiblogApiError(res.status, url, parsed, `${detail}${suffix}`);
92
+ }
93
+ if (parsed && typeof parsed === "object" && "data" in parsed) {
94
+ return parsed.data;
95
+ }
96
+ return parsed;
97
+ }
98
+ // ── Projects ───────────────────────────────────────────────────────────────
99
+ listProjects() {
100
+ return this.request("GET", "/projects");
101
+ }
102
+ getProject(projectId) {
103
+ return this.request("GET", `/projects/${projectId}`);
104
+ }
105
+ pipelineStart(projectId) {
106
+ return this.request("POST", `/projects/${projectId}/pipeline/start`);
107
+ }
108
+ pipelineStatus(projectId) {
109
+ return this.request("GET", `/projects/${projectId}/pipeline/status`);
110
+ }
111
+ // ── Content plans / topics ────────────────────────────────────────────────
112
+ createContentPlanFromGap(projectId, topic) {
113
+ return this.request("POST", `/projects/${projectId}/content-plans/from-gap`, {
114
+ body: { topic }
115
+ });
116
+ }
117
+ listContentPlans(projectId) {
118
+ return this.request("GET", `/projects/${projectId}/content-plans`);
119
+ }
120
+ approveContentPlan(projectId, planId) {
121
+ return this.request(
122
+ "PATCH",
123
+ `/projects/${projectId}/content-plans/${planId}/approve`
124
+ );
125
+ }
126
+ regenerateContentPlan(projectId, planId) {
127
+ return this.request(
128
+ "POST",
129
+ `/projects/${projectId}/content-plans/${planId}/regenerate`
130
+ );
131
+ }
132
+ generateArticleFromPlan(projectId, planId) {
133
+ return this.request(
134
+ "POST",
135
+ `/projects/${projectId}/content-plans/${planId}/generate`
136
+ );
137
+ }
138
+ getCalendar(projectId, start, end) {
139
+ return this.request("GET", `/projects/${projectId}/calendar`, {
140
+ query: { start, end }
141
+ });
142
+ }
143
+ // ── Articles ──────────────────────────────────────────────────────────────
144
+ listArticles(projectId, status) {
145
+ return this.request("GET", `/projects/${projectId}/articles`, {
146
+ query: { status }
147
+ });
148
+ }
149
+ getArticle(projectId, articleId) {
150
+ return this.request("GET", `/projects/${projectId}/articles/${articleId}`);
151
+ }
152
+ updateArticle(projectId, articleId, patch) {
153
+ return this.request("PATCH", `/projects/${projectId}/articles/${articleId}`, {
154
+ body: patch
155
+ });
156
+ }
157
+ scheduleArticle(projectId, articleId, scheduledFor) {
158
+ return this.request(
159
+ "PATCH",
160
+ `/projects/${projectId}/articles/${articleId}/schedule`,
161
+ { body: { scheduledFor } }
162
+ );
163
+ }
164
+ approveAndPublish(projectId, articleId) {
165
+ return this.request(
166
+ "POST",
167
+ `/projects/${projectId}/articles/${articleId}/approve-and-publish`
168
+ );
169
+ }
170
+ regenerateArticle(projectId, articleId) {
171
+ return this.request(
172
+ "POST",
173
+ `/projects/${projectId}/articles/${articleId}/regenerate`
174
+ );
175
+ }
176
+ regenerateChapter(projectId, articleId, chapterIndex) {
177
+ return this.request(
178
+ "POST",
179
+ `/projects/${projectId}/articles/${articleId}/regenerate-chapter`,
180
+ { body: { chapterIndex } }
181
+ );
182
+ }
183
+ getPipelineLogs(projectId, articleId) {
184
+ return this.request(
185
+ "GET",
186
+ `/projects/${projectId}/articles/${articleId}/pipeline-logs`
187
+ );
188
+ }
189
+ listRefreshSuggestions() {
190
+ return this.request("GET", "/articles/refresh-suggestions");
191
+ }
192
+ // ── Integrations / publishing ─────────────────────────────────────────────
193
+ listIntegrations(projectId) {
194
+ return this.request("GET", `/projects/${projectId}/integrations`);
195
+ }
196
+ createIntegration(projectId, dto) {
197
+ return this.request("POST", `/projects/${projectId}/integrations`, { body: dto });
198
+ }
199
+ testIntegration(projectId, integrationId, mode) {
200
+ return this.request(
201
+ "POST",
202
+ `/projects/${projectId}/integrations/${integrationId}/test`,
203
+ { query: { mode } }
204
+ );
205
+ }
206
+ publishToIntegration(projectId, articleId, integrationId) {
207
+ return this.request(
208
+ "POST",
209
+ `/projects/${projectId}/articles/${articleId}/publish/${integrationId}`
210
+ );
211
+ }
212
+ retryPublish(projectId, articleId, integrationId) {
213
+ return this.request(
214
+ "POST",
215
+ `/projects/${projectId}/articles/${articleId}/publish/${integrationId}/retry`
216
+ );
217
+ }
218
+ listPublishLogs(projectId, articleId, limit) {
219
+ const base = articleId ? `/projects/${projectId}/articles/${articleId}/publish-logs` : `/projects/${projectId}/publish-logs`;
220
+ return this.request("GET", base, { query: { limit } });
221
+ }
222
+ // ── Keywords & proprietary facts (agent-supplied knowledge) ───────────────
223
+ addKeyword(projectId, dto) {
224
+ return this.request("POST", `/projects/${projectId}/keywords`, { body: dto });
225
+ }
226
+ listKeywords(projectId) {
227
+ return this.request("GET", `/projects/${projectId}/keywords`);
228
+ }
229
+ listProductFacts(projectId, activeOnly) {
230
+ return this.request("GET", `/projects/${projectId}/proprietary-facts`, {
231
+ query: { active: activeOnly ? "true" : void 0 }
232
+ });
233
+ }
234
+ createProductFact(projectId, dto) {
235
+ return this.request("POST", `/projects/${projectId}/proprietary-facts`, {
236
+ body: dto
237
+ });
238
+ }
239
+ updateProductFact(projectId, factId, dto) {
240
+ return this.request(
241
+ "PATCH",
242
+ `/projects/${projectId}/proprietary-facts/${factId}`,
243
+ { body: dto }
244
+ );
245
+ }
246
+ };
247
+
248
+ // src/tools.ts
249
+ var import_zod = require("zod");
250
+
251
+ // src/export.ts
252
+ var import_fs = require("fs");
253
+ var import_path = require("path");
254
+
255
+ // ../../packages/shared/src/constants/index.ts
256
+ var PLAN = {
257
+ name: "MotiBlog Pro",
258
+ articlesPerMonth: 30,
259
+ originalPrice: 99,
260
+ monthly: { price: 39, interval: "month" },
261
+ yearly: { price: 390, interval: "year", monthlyEquivalent: 32.5, savings: 78 },
262
+ features: [
263
+ "30 articles a month, 2,500-4,000 words each",
264
+ "Keyword research from competitor gap analysis",
265
+ "Internal links woven in before every publish",
266
+ "Publishes to WordPress, Webflow, Ghost, Shopify and more",
267
+ "Search Console positions and impressions",
268
+ "Competitor tracking",
269
+ "Up to 10 projects",
270
+ "Support by email"
271
+ ]
272
+ };
273
+ var PLANS = {
274
+ PRO: { name: PLAN.name, articlesPerMonth: PLAN.articlesPerMonth, projects: 10, price: PLAN.monthly.price }
275
+ };
276
+
277
+ // ../../packages/shared/src/export/article-markdown.ts
278
+ function attr(tag, name) {
279
+ const m = tag.match(new RegExp(`${name}\\s*=\\s*["']([^"']*)["']`, "i"));
280
+ return m?.[1] ?? "";
281
+ }
282
+ function embedSrcToWatchUrl(src) {
283
+ const yt = src.match(/youtube(?:-nocookie)?\.com\/embed\/([A-Za-z0-9_-]{5,})/i);
284
+ if (yt) return `https://www.youtube.com/watch?v=${yt[1]}`;
285
+ const vimeo = src.match(/player\.vimeo\.com\/video\/(\d+)/i);
286
+ if (vimeo) return `https://vimeo.com/${vimeo[1]}`;
287
+ return src;
288
+ }
289
+ function outsideCodeFences(content, transform) {
290
+ const parts = content.split(/(```[\s\S]*?```|~~~[\s\S]*?~~~)/);
291
+ return parts.map((part, i) => i % 2 === 1 ? part : transform(part)).join("");
292
+ }
293
+ function convertHtmlImages(chunk) {
294
+ return chunk.replace(/<img\b[^>]*\/?>(?:\s*<\/img>)?/gi, (tag) => {
295
+ const src = attr(tag, "src");
296
+ if (!src) return "";
297
+ const alt = attr(tag, "alt");
298
+ return `![${alt}](${src})`;
299
+ });
300
+ }
301
+ function convertIframes(chunk) {
302
+ return chunk.replace(
303
+ /<iframe\b[^>]*>[\s\S]*?<\/iframe>|<iframe\b[^>]*\/>/gi,
304
+ (tag) => {
305
+ const src = attr(tag, "src");
306
+ if (!src) return "";
307
+ const url = embedSrcToWatchUrl(src);
308
+ return `[\u25B6 Watch the video](${url})`;
309
+ }
310
+ );
311
+ }
312
+ function enforceSingleH1(body) {
313
+ const withoutLeading = body.replace(/^\s*#(?!#)[^\n]*\n+/, "");
314
+ return withoutLeading.replace(/^#(?!#)\s?/gm, "## ");
315
+ }
316
+ function articleToMarkdown(article) {
317
+ const converted = outsideCodeFences(
318
+ article.content ?? "",
319
+ (chunk) => convertIframes(convertHtmlImages(chunk))
320
+ );
321
+ const body = enforceSingleH1(converted).trim();
322
+ return `# ${article.title}
323
+
324
+ ${body}
325
+ `;
326
+ }
327
+
328
+ // src/export.ts
329
+ function slugify(input, fallback) {
330
+ const slug = input.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 120);
331
+ return slug || fallback;
332
+ }
333
+ function yamlString(value) {
334
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
335
+ }
336
+ function buildArticleMarkdown(article) {
337
+ const frontmatter = ["---"];
338
+ frontmatter.push(`title: ${yamlString(article.title)}`);
339
+ frontmatter.push(`slug: ${yamlString(article.slug)}`);
340
+ if (article.metaDescription) {
341
+ frontmatter.push(`description: ${yamlString(article.metaDescription)}`);
342
+ }
343
+ if (article.publishedAt) {
344
+ frontmatter.push(`publishedAt: ${yamlString(String(article.publishedAt))}`);
345
+ }
346
+ if (article.topics?.length) {
347
+ frontmatter.push("tags:");
348
+ for (const topic of article.topics) {
349
+ frontmatter.push(` - ${yamlString(topic)}`);
350
+ }
351
+ }
352
+ if (article.bannerUrl) {
353
+ frontmatter.push(`banner: ${yamlString(String(article.bannerUrl))}`);
354
+ }
355
+ if (typeof article.seoScore === "number") {
356
+ frontmatter.push(`seoScore: ${article.seoScore}`);
357
+ }
358
+ if (article.wordCount !== void 0 && article.wordCount !== null) {
359
+ frontmatter.push(`wordCount: ${article.wordCount}`);
360
+ }
361
+ frontmatter.push(`motiblogArticleId: ${yamlString(article.id)}`);
362
+ frontmatter.push("---", "");
363
+ return `${frontmatter.join("\n")}
364
+ ${articleToMarkdown({
365
+ title: article.title,
366
+ content: article.content ?? ""
367
+ })}`;
368
+ }
369
+ async function exportArticlesToDir(outDirInput, articles) {
370
+ const outDir = (0, import_path.resolve)(outDirInput);
371
+ await import_fs.promises.mkdir(outDir, { recursive: true });
372
+ const files = [];
373
+ let skipped = 0;
374
+ for (const article of articles) {
375
+ if (!article.content || !article.content.trim()) {
376
+ skipped += 1;
377
+ continue;
378
+ }
379
+ const slug = slugify(article.slug || article.title, `article-${article.id}`);
380
+ const dir = (0, import_path.join)(outDir, slug);
381
+ await import_fs.promises.mkdir(dir, { recursive: true });
382
+ const file = (0, import_path.join)(dir, "index.md");
383
+ await import_fs.promises.writeFile(file, buildArticleMarkdown(article), "utf8");
384
+ files.push({
385
+ path: file,
386
+ articleId: article.id,
387
+ slug,
388
+ title: article.title
389
+ });
390
+ }
391
+ const manifest = {
392
+ exportedAt: (/* @__PURE__ */ new Date()).toISOString(),
393
+ count: files.length,
394
+ skippedEmptyContent: skipped,
395
+ articles: files.map((f) => ({
396
+ file: f.path,
397
+ articleId: f.articleId,
398
+ slug: f.slug,
399
+ title: f.title
400
+ }))
401
+ };
402
+ await import_fs.promises.writeFile(
403
+ (0, import_path.join)(outDir, "manifest.json"),
404
+ JSON.stringify(manifest, null, 2),
405
+ "utf8"
406
+ );
407
+ return { outDir, files, skipped };
408
+ }
409
+
410
+ // src/tools.ts
411
+ var ARTICLE_STATUSES = [
412
+ "DRAFT",
413
+ "GENERATING",
414
+ "REVIEW",
415
+ "APPROVED",
416
+ "PUBLISHING",
417
+ "PUBLISHED",
418
+ "FAILED",
419
+ "SKIPPED"
420
+ ];
421
+ var projectIdArg = {
422
+ project_id: import_zod.z.string().optional().describe("MotiBlog project id. Omit when MOTIBLOG_PROJECT_ID is configured.")
423
+ };
424
+ function json(value) {
425
+ return { text: JSON.stringify(value, null, 2) };
426
+ }
427
+ function resolveProjectId(ctx, raw) {
428
+ const id = typeof raw === "string" && raw.trim() || ctx.defaultProjectId;
429
+ if (!id) {
430
+ throw new Error(
431
+ "No project specified: pass project_id or set the MOTIBLOG_PROJECT_ID environment variable."
432
+ );
433
+ }
434
+ return id;
435
+ }
436
+ function summarizeArticleForList(a) {
437
+ return {
438
+ id: a.id,
439
+ title: a.title,
440
+ slug: a.slug,
441
+ status: a.status,
442
+ wordCount: a.wordCount,
443
+ seoScore: a.seoScore,
444
+ scheduledFor: a.scheduledFor ?? null,
445
+ publishedAt: a.publishedAt ?? null,
446
+ topics: a.topics ?? []
447
+ };
448
+ }
449
+ function buildDigest(input, now = /* @__PURE__ */ new Date()) {
450
+ const { project, articles, plans } = input;
451
+ const statusCounts = {};
452
+ for (const a of articles) statusCounts[a.status] = (statusCounts[a.status] ?? 0) + 1;
453
+ const reviewQueue = articles.filter((a) => a.status === "REVIEW").map((a) => ({ id: a.id, title: a.title, wordCount: a.wordCount }));
454
+ const sevenDaysAgo = now.getTime() - 7 * 24 * 3600 * 1e3;
455
+ const publishedLast7Days = articles.filter((a) => a.publishedAt && new Date(a.publishedAt).getTime() >= sevenDaysAgo).sort((a, b) => new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()).map((a) => ({ title: a.title, publishedAt: String(a.publishedAt) }));
456
+ const in7Days = now.getTime() + 7 * 24 * 3600 * 1e3;
457
+ const plansNext7Days = plans.filter(
458
+ (p) => p.scheduledDate && new Date(p.scheduledDate).getTime() >= now.getTime() - 24 * 3600 * 1e3 && new Date(p.scheduledDate).getTime() <= in7Days && p.status !== "COMPLETED"
459
+ ).sort((a, b) => String(a.scheduledDate).localeCompare(String(b.scheduledDate))).map((p) => ({
460
+ date: String(p.scheduledDate).slice(0, 10),
461
+ title: p.title,
462
+ status: p.status
463
+ }));
464
+ const latest = publishedLast7Days[0] ?? null;
465
+ return {
466
+ project: {
467
+ id: project.id,
468
+ name: project.name,
469
+ autonomy: project.autoPublish ? "L2 \u2014 auto-publish clean, review exceptions" : "L1 \u2014 drafts daily, publish only via agent review",
470
+ quota: { used: project.articlesUsed ?? 0, limit: project.articlesLimit ?? 0 }
471
+ },
472
+ articles: statusCounts,
473
+ reviewQueue,
474
+ publishedLast7Days,
475
+ plansNext7Days,
476
+ latestPublished: latest ? { title: latest.title, at: latest.publishedAt } : null,
477
+ checkedAt: now.toISOString()
478
+ };
479
+ }
480
+ var toolDefinitions = [
481
+ // ── Discovery ──────────────────────────────────────────────────────────────
482
+ {
483
+ name: "get_digest",
484
+ description: "One-call morning check-in for a project: article counts by lifecycle status, the REVIEW queue (what is waiting to be published, with word counts), posts published in the last 7 days, content plans due in the next 7 days, the latest published post, quota usage, and the project's current autonomy level (L1 = nothing ships without agent review; L2 = clean articles auto-publish). Start every check-in here.",
485
+ inputSchema: { ...projectIdArg },
486
+ annotations: { readOnlyHint: true },
487
+ async handler(args, ctx) {
488
+ const projectId = resolveProjectId(ctx, args.project_id);
489
+ const [project, articles, plans] = await Promise.all([
490
+ ctx.client.getProject(projectId),
491
+ ctx.client.listArticles(projectId),
492
+ ctx.client.listContentPlans(projectId)
493
+ ]);
494
+ return json(
495
+ buildDigest({ project, articles, plans })
496
+ );
497
+ }
498
+ },
499
+ {
500
+ name: "list_projects",
501
+ description: "List every MotiBlog project the API key can access, with settings that matter to agents: requireApproval (REVIEW vs APPROVED after generation), autoPublish, factCheckStrict, selfHostedBlog, and article quota usage.",
502
+ inputSchema: {},
503
+ annotations: { readOnlyHint: true },
504
+ async handler(_args, ctx) {
505
+ return json(await ctx.client.listProjects());
506
+ }
507
+ },
508
+ {
509
+ name: "get_project",
510
+ description: "Get one MotiBlog project in detail: pipeline settings (aiModel, language, internalLinks, banner), positioning inputs (businessProfile, productTruth, manualPositioning) and quota state.",
511
+ inputSchema: { ...projectIdArg },
512
+ annotations: { readOnlyHint: true },
513
+ async handler(args, ctx) {
514
+ return json(await ctx.client.getProject(resolveProjectId(ctx, args.project_id)));
515
+ }
516
+ },
517
+ // ── Pipeline / generation ops ─────────────────────────────────────────────
518
+ {
519
+ name: "start_pipeline",
520
+ description: "Start the autonomous pipeline for a project: site crawl/analysis \u2192 topic selection \u2192 article generation for scheduled plans. Returns immediately; poll get_pipeline_status or list_articles(status=GENERATING) to follow progress.",
521
+ inputSchema: { ...projectIdArg },
522
+ async handler(args, ctx) {
523
+ const projectId = resolveProjectId(ctx, args.project_id);
524
+ return json(await ctx.client.pipelineStart(projectId));
525
+ }
526
+ },
527
+ {
528
+ name: "get_pipeline_status",
529
+ description: "Get current pipeline run state for a project: whether a run is active, its steps, and recent run history. Use after start_pipeline or generate_article.",
530
+ inputSchema: { ...projectIdArg },
531
+ annotations: { readOnlyHint: true },
532
+ async handler(args, ctx) {
533
+ return json(
534
+ await ctx.client.pipelineStatus(resolveProjectId(ctx, args.project_id))
535
+ );
536
+ }
537
+ },
538
+ // ── Topics & planning ─────────────────────────────────────────────────────
539
+ {
540
+ name: "suggest_topics",
541
+ description: "Suggest blog topics for a project by adding entries to its content plan (one call per topic). This is the agent-side 'propose' step: topics land as DRAFT plan entries \u2014 nothing is generated until you approve_content_plan + generate_article. Prefer specific, keyword-like topics.",
542
+ inputSchema: {
543
+ topic: import_zod.z.string().min(1).describe('The proposed topic/title, e.g. "How headless CMSs serve AI agents"'),
544
+ ...projectIdArg
545
+ },
546
+ async handler(args, ctx) {
547
+ const projectId = resolveProjectId(ctx, args.project_id);
548
+ return json(await ctx.client.createContentPlanFromGap(projectId, String(args.topic)));
549
+ }
550
+ },
551
+ {
552
+ name: "list_content_plans",
553
+ description: "List the content plan queue for a project with each entry status: DRAFT (proposed), APPROVED (cleared for generation), IN_PROGRESS, COMPLETED.",
554
+ inputSchema: { ...projectIdArg },
555
+ annotations: { readOnlyHint: true },
556
+ async handler(args, ctx) {
557
+ return json(await ctx.client.listContentPlans(resolveProjectId(ctx, args.project_id)));
558
+ }
559
+ },
560
+ {
561
+ name: "approve_content_plan",
562
+ description: "Approve a DRAFT content-plan entry so it becomes eligible for generation (generate_article or the scheduled pipeline).",
563
+ inputSchema: {
564
+ plan_id: import_zod.z.string().describe("Content plan entry id (from list_content_plans)"),
565
+ ...projectIdArg
566
+ },
567
+ async handler(args, ctx) {
568
+ const projectId = resolveProjectId(ctx, args.project_id);
569
+ return json(await ctx.client.approveContentPlan(projectId, String(args.plan_id)));
570
+ }
571
+ },
572
+ {
573
+ name: "regenerate_content_plan",
574
+ description: "Ask AI to rewrite a content-plan entry (new title/summary/target keyword) \u2014 use when a suggested topic misses the mark. Only works while no article has been generated from it.",
575
+ inputSchema: {
576
+ plan_id: import_zod.z.string().describe("Content plan entry id"),
577
+ ...projectIdArg
578
+ },
579
+ async handler(args, ctx) {
580
+ const projectId = resolveProjectId(ctx, args.project_id);
581
+ return json(await ctx.client.regenerateContentPlan(projectId, String(args.plan_id)));
582
+ }
583
+ },
584
+ {
585
+ name: "generate_article",
586
+ description: "Generate an article from an APPROVED content-plan entry via the full pipeline (research \u2192 outline \u2192 draft \u2192 fact-check \u2192 polish \u2192 scoring). Returns the article in GENERATING state; review later via get_article once status reaches REVIEW or APPROVED.",
587
+ inputSchema: {
588
+ plan_id: import_zod.z.string().describe("Content plan entry id (must be APPROVED)"),
589
+ ...projectIdArg
590
+ },
591
+ async handler(args, ctx) {
592
+ const projectId = resolveProjectId(ctx, args.project_id);
593
+ return json(await ctx.client.generateArticleFromPlan(projectId, String(args.plan_id)));
594
+ }
595
+ },
596
+ {
597
+ name: "get_calendar",
598
+ description: "Get scheduled content-plan entries (with slim article previews) between two ISO dates, e.g. start_date=2026-09-01 end_date=2026-09-30.",
599
+ inputSchema: {
600
+ start_date: import_zod.z.string().describe("ISO date, e.g. 2026-09-01"),
601
+ end_date: import_zod.z.string().describe("ISO date, e.g. 2026-09-30"),
602
+ ...projectIdArg
603
+ },
604
+ annotations: { readOnlyHint: true },
605
+ async handler(args, ctx) {
606
+ const projectId = resolveProjectId(ctx, args.project_id);
607
+ return json(
608
+ await ctx.client.getCalendar(
609
+ projectId,
610
+ String(args.start_date),
611
+ String(args.end_date)
612
+ )
613
+ );
614
+ }
615
+ },
616
+ // ── Review loop ───────────────────────────────────────────────────────────
617
+ {
618
+ name: "list_review_queue",
619
+ description: `List articles awaiting attention, filtered by status. Lifecycle: DRAFT \u2192 GENERATING \u2192 REVIEW \u2192 APPROVED \u2192 PUBLISHING \u2192 PUBLISHED (also FAILED, SKIPPED). Default status=REVIEW (needs human/agent approval); pass APPROVED to see what is cleared for publishing.`,
620
+ inputSchema: {
621
+ status: import_zod.z.enum(ARTICLE_STATUSES).optional().describe("Filter by lifecycle status (default REVIEW)"),
622
+ ...projectIdArg
623
+ },
624
+ annotations: { readOnlyHint: true },
625
+ async handler(args, ctx) {
626
+ const projectId = resolveProjectId(ctx, args.project_id);
627
+ const status = typeof args.status === "string" ? args.status : "REVIEW";
628
+ const articles = await ctx.client.listArticles(projectId, status);
629
+ return json(articles.map(summarizeArticleForList));
630
+ }
631
+ },
632
+ {
633
+ name: "get_article",
634
+ description: "Get the FULL article: markdown content plus quality signals \u2014 factCheckReport (verified/unverified claims with sources), topicGate verdict, seoScore/issues, publishedUrl. Set include_logs=true to append per-phase pipeline telemetry (tokens, cost, phase status). Use this during review before approving publication.",
635
+ inputSchema: {
636
+ article_id: import_zod.z.string().describe("Article id"),
637
+ include_logs: import_zod.z.boolean().optional().describe("Include pipeline phase logs (default false)"),
638
+ ...projectIdArg
639
+ },
640
+ annotations: { readOnlyHint: true },
641
+ async handler(args, ctx) {
642
+ const projectId = resolveProjectId(ctx, args.project_id);
643
+ const article = await ctx.client.getArticle(projectId, String(args.article_id));
644
+ let logs = void 0;
645
+ if (args.include_logs === true) {
646
+ logs = await ctx.client.getPipelineLogs(projectId, String(args.article_id));
647
+ }
648
+ return json(logs ? { article, pipelineLogs: logs } : article);
649
+ }
650
+ },
651
+ {
652
+ name: "update_article",
653
+ description: "Edit an article: replace the markdown content, retitle, change metaDescription, or move status (e.g. fix flagged claims from factCheckReport then set status=APPROVED; or send back to DRAFT). Editing content is how agents answer fact-check flags when no external source exists.",
654
+ inputSchema: {
655
+ article_id: import_zod.z.string().describe("Article id"),
656
+ content: import_zod.z.string().optional().describe("Full replacement markdown body"),
657
+ title: import_zod.z.string().max(500).optional(),
658
+ meta_description: import_zod.z.string().max(320).optional(),
659
+ status: import_zod.z.enum(["DRAFT", "REVIEW", "APPROVED"]).optional(),
660
+ ...projectIdArg
661
+ },
662
+ async handler(args, ctx) {
663
+ const projectId = resolveProjectId(ctx, args.project_id);
664
+ const patch = {};
665
+ if (typeof args.content === "string") patch.content = args.content;
666
+ if (typeof args.title === "string") patch.title = args.title;
667
+ if (typeof args.meta_description === "string") patch.metaDescription = args.meta_description;
668
+ if (typeof args.status === "string") patch.status = args.status;
669
+ return json(await ctx.client.updateArticle(projectId, String(args.article_id), patch));
670
+ }
671
+ },
672
+ {
673
+ name: "approve_publication",
674
+ description: "Approve an article for publication through the governed approval gate. publish_now=true also triggers publishing immediately (to enabled integrations, or the self-hosted blog when selfHostedBlog=true). Otherwise the article sits in APPROVED until the scheduler/autoPublish picks it up or publish_to_integration is called.",
675
+ inputSchema: {
676
+ article_id: import_zod.z.string().describe("Article id (must be in REVIEW/APPROVED/DRAFT-eligible state)"),
677
+ publish_now: import_zod.z.boolean().optional().describe("Also trigger publishing now (default false)"),
678
+ ...projectIdArg
679
+ },
680
+ async handler(args, ctx) {
681
+ const projectId = resolveProjectId(ctx, args.project_id);
682
+ const articleId = String(args.article_id);
683
+ if (args.publish_now === true) {
684
+ return json(await ctx.client.approveAndPublish(projectId, articleId));
685
+ }
686
+ return json(await ctx.client.updateArticle(projectId, articleId, { status: "APPROVED" }));
687
+ }
688
+ },
689
+ {
690
+ name: "schedule_publication",
691
+ description: "Set or clear a per-article scheduled publish time (ISO datetime). Pass null for clear_schedule=true to unschedule. Scheduled APPROVED articles are published automatically by the 15-minute scheduler when autoPublish is on.",
692
+ inputSchema: {
693
+ article_id: import_zod.z.string().describe("Article id"),
694
+ scheduled_for: import_zod.z.string().optional().describe("ISO datetime, e.g. 2026-09-01T09:00:00Z"),
695
+ clear_schedule: import_zod.z.boolean().optional().describe("Clear any scheduled date instead of setting one"),
696
+ ...projectIdArg
697
+ },
698
+ async handler(args, ctx) {
699
+ const projectId = resolveProjectId(ctx, args.project_id);
700
+ const value = args.clear_schedule === true ? null : args.scheduled_for ?? null;
701
+ return json(await ctx.client.scheduleArticle(projectId, String(args.article_id), value));
702
+ }
703
+ },
704
+ // ── Regeneration ops ──────────────────────────────────────────────────────
705
+ {
706
+ name: "regenerate_article",
707
+ description: "Wipe an article and rerun the ENTIRE pipeline from scratch (research \u2192 draft \u2026). Content-destructive: prefer update_article for targeted edits or regenerate_chapter for one section.",
708
+ inputSchema: {
709
+ article_id: import_zod.z.string().describe("Article id"),
710
+ ...projectIdArg
711
+ },
712
+ annotations: { destructiveHint: true },
713
+ async handler(args, ctx) {
714
+ const projectId = resolveProjectId(ctx, args.project_id);
715
+ return json(await ctx.client.regenerateArticle(projectId, String(args.article_id)));
716
+ }
717
+ },
718
+ {
719
+ name: "regenerate_chapter",
720
+ description: "Regenerate ONE chapter (0-based chapter_index) of an article; all other sections stay byte-identical. Cheaper and safer than regenerate_article.",
721
+ inputSchema: {
722
+ article_id: import_zod.z.string().describe("Article id"),
723
+ chapter_index: import_zod.z.number().int().min(0).describe("Zero-based chapter index"),
724
+ ...projectIdArg
725
+ },
726
+ async handler(args, ctx) {
727
+ const projectId = resolveProjectId(ctx, args.project_id);
728
+ return json(
729
+ await ctx.client.regenerateChapter(
730
+ projectId,
731
+ String(args.article_id),
732
+ Number(args.chapter_index)
733
+ )
734
+ );
735
+ }
736
+ },
737
+ {
738
+ name: "get_pipeline_logs",
739
+ description: "Per-phase pipeline telemetry for one article: phase names, statuses, messages, token counts, and USD cost per phase. Diagnose FAILED articles or audit what the generator did.",
740
+ inputSchema: {
741
+ article_id: import_zod.z.string().describe("Article id"),
742
+ ...projectIdArg
743
+ },
744
+ annotations: { readOnlyHint: true },
745
+ async handler(args, ctx) {
746
+ const projectId = resolveProjectId(ctx, args.project_id);
747
+ return json(await ctx.client.getPipelineLogs(projectId, String(args.article_id)));
748
+ }
749
+ },
750
+ {
751
+ name: "list_refresh_suggestions",
752
+ description: "Across ALL your projects: published articles whose Search Console stats suggest they are decaying and would benefit from a refresh. Feed these back into planning via suggest_topics or update_article.",
753
+ inputSchema: {},
754
+ annotations: { readOnlyHint: true },
755
+ async handler(_args, ctx) {
756
+ return json(await ctx.client.listRefreshSuggestions());
757
+ }
758
+ },
759
+ // ── Publishing targets ────────────────────────────────────────────────────
760
+ {
761
+ name: "list_integrations",
762
+ description: "List configured publishing targets (integrations) for a project: WEBHOOK, WORDPRESS, GHOST, WEBFLOW, SHOPIFY, DEVTO, SANITY, CUSTOM_API \u2014 with enabled state.",
763
+ inputSchema: { ...projectIdArg },
764
+ annotations: { readOnlyHint: true },
765
+ async handler(args, ctx) {
766
+ const projectId = resolveProjectId(ctx, args.project_id);
767
+ const integrations = await ctx.client.listIntegrations(projectId);
768
+ return json(integrations.map((i) => ({ ...i, config: "<redacted>" })));
769
+ }
770
+ },
771
+ {
772
+ name: "create_webhook_integration",
773
+ description: "Register a webhook publishing target: MotiBlog POSTs the published article JSON to your endpoint (signed with X-Signature: sha256=<HMAC(secret)> when secret is set). Config shape: { url, secret?, method? }. This is how an agent's own infrastructure receives content pushes.",
774
+ inputSchema: {
775
+ name: import_zod.z.string().min(1).max(100).describe('Display name, e.g. "my-site deploy hook"'),
776
+ url: import_zod.z.string().url().describe("HTTPS endpoint receiving the article payload"),
777
+ secret: import_zod.z.string().optional().describe("HMAC secret used to sign deliveries"),
778
+ method: import_zod.z.enum(["POST", "PUT", "PATCH"]).optional().describe("HTTP method (default POST)"),
779
+ ...projectIdArg
780
+ },
781
+ async handler(args, ctx) {
782
+ const projectId = resolveProjectId(ctx, args.project_id);
783
+ const config = { url: String(args.url) };
784
+ if (typeof args.secret === "string") config.secret = args.secret;
785
+ if (typeof args.method === "string") config.method = args.method;
786
+ return json(
787
+ await ctx.client.createIntegration(projectId, {
788
+ type: "WEBHOOK",
789
+ name: String(args.name),
790
+ config,
791
+ enabled: true
792
+ })
793
+ );
794
+ }
795
+ },
796
+ {
797
+ name: "test_integration",
798
+ description: "Test a publishing target connection. mode=ping (default) checks credentials/reachability only; mode=full may create and clean up a test item on the target.",
799
+ inputSchema: {
800
+ integration_id: import_zod.z.string().describe("Integration id"),
801
+ mode: import_zod.z.enum(["ping", "full"]).optional(),
802
+ ...projectIdArg
803
+ },
804
+ async handler(args, ctx) {
805
+ const projectId = resolveProjectId(ctx, args.project_id);
806
+ return json(
807
+ await ctx.client.testIntegration(
808
+ projectId,
809
+ String(args.integration_id),
810
+ typeof args.mode === "string" ? args.mode : void 0
811
+ )
812
+ );
813
+ }
814
+ },
815
+ {
816
+ name: "publish_to_integration",
817
+ description: "Push an APPROVED article to one specific integration now (enqueues the publish job; transitions APPROVED \u2192 PUBLISHING \u2192 PUBLISHED). Check results via list_publish_logs.",
818
+ inputSchema: {
819
+ article_id: import_zod.z.string().describe("Article id (should be APPROVED)"),
820
+ integration_id: import_zod.z.string().describe("Target integration id"),
821
+ ...projectIdArg
822
+ },
823
+ async handler(args, ctx) {
824
+ const projectId = resolveProjectId(ctx, args.project_id);
825
+ return json(
826
+ await ctx.client.publishToIntegration(
827
+ projectId,
828
+ String(args.article_id),
829
+ String(args.integration_id)
830
+ )
831
+ );
832
+ }
833
+ },
834
+ {
835
+ name: "retry_publish",
836
+ description: "Retry a failed publish attempt for an article/integration pair.",
837
+ inputSchema: {
838
+ article_id: import_zod.z.string(),
839
+ integration_id: import_zod.z.string(),
840
+ ...projectIdArg
841
+ },
842
+ async handler(args, ctx) {
843
+ const projectId = resolveProjectId(ctx, args.project_id);
844
+ return json(
845
+ await ctx.client.retryPublish(
846
+ projectId,
847
+ String(args.article_id),
848
+ String(args.integration_id)
849
+ )
850
+ );
851
+ }
852
+ },
853
+ {
854
+ name: "list_publish_logs",
855
+ description: "Publishing history: SUCCESS/FAILED attempts with publishedUrl, provider error text and timestamps. Pass article_id for one article, or omit for the whole project (limit defaults server-side).",
856
+ inputSchema: {
857
+ article_id: import_zod.z.string().optional(),
858
+ limit: import_zod.z.number().int().min(1).max(200).optional(),
859
+ ...projectIdArg
860
+ },
861
+ annotations: { readOnlyHint: true },
862
+ async handler(args, ctx) {
863
+ const projectId = resolveProjectId(ctx, args.project_id);
864
+ return json(
865
+ await ctx.client.listPublishLogs(
866
+ projectId,
867
+ typeof args.article_id === "string" ? args.article_id : void 0,
868
+ typeof args.limit === "number" ? args.limit : void 0
869
+ )
870
+ );
871
+ }
872
+ },
873
+ // ── Agent-supplied knowledge ───────────────────────────────────────────────
874
+ {
875
+ name: "list_keywords",
876
+ description: "List tracked keywords for a project (search volume, difficulty, intent, status). Keywords drive topic relevance in generation.",
877
+ inputSchema: { ...projectIdArg },
878
+ annotations: { readOnlyHint: true },
879
+ async handler(args, ctx) {
880
+ return json(await ctx.client.listKeywords(resolveProjectId(ctx, args.project_id)));
881
+ }
882
+ },
883
+ {
884
+ name: "add_keyword",
885
+ description: "Track a new SEO keyword for a project so future topic suggestions/generation can target it. intent is one of INFORMATIONAL, NAVIGATIONAL, TRANSACTIONAL, COMMERCIAL.",
886
+ inputSchema: {
887
+ keyword: import_zod.z.string().min(1).describe("The keyword phrase"),
888
+ search_volume: import_zod.z.number().int().min(0).optional(),
889
+ difficulty: import_zod.z.number().int().min(0).max(100).optional(),
890
+ intent: import_zod.z.enum(["INFORMATIONAL", "NAVIGATIONAL", "TRANSACTIONAL", "COMMERCIAL"]).optional(),
891
+ ...projectIdArg
892
+ },
893
+ async handler(args, ctx) {
894
+ const projectId = resolveProjectId(ctx, args.project_id);
895
+ return json(
896
+ await ctx.client.addKeyword(projectId, {
897
+ keyword: String(args.keyword),
898
+ ...typeof args.search_volume === "number" ? { searchVolume: args.search_volume } : {},
899
+ ...typeof args.difficulty === "number" ? { difficulty: args.difficulty } : {},
900
+ ...typeof args.intent === "string" ? { intent: args.intent } : {}
901
+ })
902
+ );
903
+ }
904
+ },
905
+ {
906
+ name: "list_product_facts",
907
+ description: "List the project proprietary facts \u2014 ground-truth statements about the product (features, pricing, integrations) injected into prompts and enforced by strict fact-checking.",
908
+ inputSchema: {
909
+ active_only: import_zod.z.boolean().optional().describe("Only active facts (default false = all)"),
910
+ ...projectIdArg
911
+ },
912
+ annotations: { readOnlyHint: true },
913
+ async handler(args, ctx) {
914
+ const projectId = resolveProjectId(ctx, args.project_id);
915
+ return json(
916
+ await ctx.client.listProductFacts(projectId, args.active_only === true)
917
+ );
918
+ }
919
+ },
920
+ {
921
+ name: "supply_product_fact",
922
+ description: "Supply a ground-truth fact about the customer's product (or update one via fact_id). Strict fact-checking uses these to verify claims; supply facts BEFORE generation for best results. Example: fact='Acme integrates natively with Shopify since v2.3', category=['integrations'].",
923
+ inputSchema: {
924
+ fact: import_zod.z.string().min(1).describe("A single verifiable statement of product truth"),
925
+ category: import_zod.z.array(import_zod.z.string()).optional().describe('Tags, e.g. ["pricing","features"]'),
926
+ fact_id: import_zod.z.string().optional().describe("Existing fact id to UPDATE instead of create"),
927
+ active: import_zod.z.boolean().optional().describe("Set active flag when updating"),
928
+ ...projectIdArg
929
+ },
930
+ async handler(args, ctx) {
931
+ const projectId = resolveProjectId(ctx, args.project_id);
932
+ const category = Array.isArray(args.category) ? args.category.map(String) : void 0;
933
+ if (typeof args.fact_id === "string") {
934
+ return json(
935
+ await ctx.client.updateProductFact(projectId, args.fact_id, {
936
+ ...typeof args.fact === "string" ? { fact: args.fact } : {},
937
+ ...category ? { category } : {},
938
+ ...typeof args.active === "boolean" ? { active: args.active } : {}
939
+ })
940
+ );
941
+ }
942
+ return json(
943
+ await ctx.client.createProductFact(projectId, {
944
+ fact: String(args.fact),
945
+ ...category ? { category } : {}
946
+ })
947
+ );
948
+ }
949
+ },
950
+ // ── Export (deploy-it-yourself) ───────────────────────────────────────────
951
+ {
952
+ name: "export_blog",
953
+ description: "Export articles out of MotiBlog to local files so you can deploy them yourself into any codebase (Astro/Next/Jekyll/Hugo/plain git). Writes {out_dir}/{slug}/index.md with YAML frontmatter (title, slug, description, tags, publishedAt, banner, motiblogArticleId) + portable GitHub-Flavored-Markdown body, plus manifest.json. Default exports PUBLISHED posts; pass status to export drafts for review.",
954
+ inputSchema: {
955
+ out_dir: import_zod.z.string().min(1).describe("Directory to write into (created if missing; relative paths resolve against the MCP server cwd)"),
956
+ status: import_zod.z.enum(ARTICLE_STATUSES).optional().describe("Which lifecycle stage to export (default PUBLISHED)"),
957
+ limit: import_zod.z.number().int().min(1).max(500).optional().describe("Max articles to export (default 100)"),
958
+ ...projectIdArg
959
+ },
960
+ async handler(args, ctx) {
961
+ const projectId = resolveProjectId(ctx, args.project_id);
962
+ const status = typeof args.status === "string" ? args.status : "PUBLISHED";
963
+ const limit = typeof args.limit === "number" ? args.limit : 100;
964
+ const summaries = await ctx.client.listArticles(projectId, status);
965
+ const selected = summaries.slice(0, limit);
966
+ const articles = [];
967
+ for (const summary of selected) {
968
+ articles.push(await ctx.client.getArticle(projectId, summary.id));
969
+ }
970
+ const result = await exportArticlesToDir(String(args.out_dir), articles);
971
+ return json({
972
+ exportedTo: result.outDir,
973
+ count: result.files.length,
974
+ skippedEmpty: result.skipped,
975
+ files: result.files.map((f) => f.path),
976
+ nextStep: "Commit/copy this folder into your target codebase. Each folder is self-contained static content ready for any SSG."
977
+ });
978
+ }
979
+ }
980
+ ];
981
+
982
+ // src/server.ts
983
+ function buildServer(config) {
984
+ const client = new MotiblogApiClient(config.apiBaseUrl, config.apiKey);
985
+ const server = new import_mcp.McpServer(
986
+ { name: "motiblog", version: "0.1.0" },
987
+ {
988
+ instructions: "MotiBlog: agent-operated blog content engine. Typical loop: list_projects \u2192 suggest_topics / list_content_plans \u2192 approve_content_plan \u2192 generate_article \u2192 list_review_queue + get_article (check factCheckReport & seoScore) \u2192 update_article if fixes are needed \u2192 approve_publication \u2192 publish_to_integration or export_blog to ship content to your own codebase. Publication always passes the governed approval gate \u2014 there is no bypass."
989
+ }
990
+ );
991
+ const register = server.registerTool.bind(server);
992
+ for (const tool of toolDefinitions) {
993
+ register(
994
+ tool.name,
995
+ {
996
+ title: tool.name,
997
+ description: tool.description,
998
+ inputSchema: tool.inputSchema,
999
+ ...tool.annotations ? { annotations: tool.annotations } : {}
1000
+ },
1001
+ async (args) => {
1002
+ let result;
1003
+ try {
1004
+ result = await tool.handler(args ?? {}, {
1005
+ client,
1006
+ defaultProjectId: config.defaultProjectId
1007
+ });
1008
+ } catch (err) {
1009
+ result = {
1010
+ text: `Error: ${err instanceof Error ? err.message : String(err)}`,
1011
+ isError: true
1012
+ };
1013
+ }
1014
+ return {
1015
+ content: [{ type: "text", text: result.text }],
1016
+ ...result.isError ? { isError: true } : {}
1017
+ };
1018
+ }
1019
+ );
1020
+ }
1021
+ return server;
1022
+ }
1023
+
1024
+ // src/index.ts
1025
+ async function main() {
1026
+ const config = loadConfig();
1027
+ const server = buildServer(config);
1028
+ await server.connect(new import_stdio.StdioServerTransport());
1029
+ }
1030
+ main().catch((err) => {
1031
+ console.error("[motiblog-mcp] fatal:", err);
1032
+ process.exit(1);
1033
+ });
1034
+ //# sourceMappingURL=index.js.map