0nmcp 2.7.0 → 2.8.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/engine/index.js +3 -0
- package/engine/sxo-writer.js +596 -0
- package/index.js +2 -1
- package/lib/stats.json +1 -1
- package/package.json +2 -2
package/engine/index.js
CHANGED
|
@@ -51,6 +51,9 @@ export { TrainingFeedEngine, registerFeedTools, FEED_SOURCES } from "./training-
|
|
|
51
51
|
// ── Multi-AI Council ────────────────────────────────────────
|
|
52
52
|
export { registerCouncilTools, getAvailableProviders, askAll, PROVIDERS } from "./multi-ai.js";
|
|
53
53
|
|
|
54
|
+
// ── SXO Blog Writer Engine ──────────────────────────────────
|
|
55
|
+
export { registerSxoWriterTools, scoreContent, SXO_SYSTEM_PROMPT } from "./sxo-writer.js";
|
|
56
|
+
|
|
54
57
|
// ── Imports for tool handlers ──────────────────────────────
|
|
55
58
|
import { parseFile } from "./parser.js";
|
|
56
59
|
import { mapEnvVars, groupByService, validateMapping } from "./mapper.js";
|
|
@@ -0,0 +1,596 @@
|
|
|
1
|
+
// ============================================================
|
|
2
|
+
// 0nMCP — SXO Blog Writer Engine
|
|
3
|
+
// ============================================================
|
|
4
|
+
// Self-improving content engine that writes, publishes, tracks,
|
|
5
|
+
// learns, and writes BETTER every time.
|
|
6
|
+
//
|
|
7
|
+
// The SXO Writing Protocol:
|
|
8
|
+
// 1. BLUF Architecture (Bottom Line Up Front)
|
|
9
|
+
// 2. Table Trap (LLMs weight tabular data heavily)
|
|
10
|
+
// 3. Non-Zero Information Gain (never repeat consensus)
|
|
11
|
+
// 4. Deep JSON-LD Entity Resolution
|
|
12
|
+
// 5. Freshness Loops (dynamic timestamps)
|
|
13
|
+
// 6. B2A Endpoints (llms.txt for AI agents)
|
|
14
|
+
//
|
|
15
|
+
// Learning Loop:
|
|
16
|
+
// Write → Publish → Track → Analyze → Learn → Write Better
|
|
17
|
+
//
|
|
18
|
+
// 4 MCP Tools:
|
|
19
|
+
// sxo_write — Write an SXO-optimized blog post
|
|
20
|
+
// sxo_analyze — Analyze a published post's performance
|
|
21
|
+
// sxo_optimize — Rewrite a post based on performance data
|
|
22
|
+
// sxo_score — Score any content against SXO criteria
|
|
23
|
+
// ============================================================
|
|
24
|
+
|
|
25
|
+
// ── SXO Writing System Prompt ────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const SXO_SYSTEM_PROMPT = `You are the SXO (Search Experience Optimization) Content Engine.
|
|
28
|
+
|
|
29
|
+
You write content that satisfies BOTH human readers AND AI extraction models (Google SGE, Perplexity, Claude, Gemini).
|
|
30
|
+
|
|
31
|
+
## THE SXO WRITING PROTOCOL
|
|
32
|
+
|
|
33
|
+
### 1. BLUF Architecture (Bottom Line Up Front)
|
|
34
|
+
- Every H2 section MUST start with a bold 2-3 sentence answer
|
|
35
|
+
- This is what AI models extract for featured snippets
|
|
36
|
+
- The bold paragraph is wrapped in a styled container for visual emphasis
|
|
37
|
+
- Format: <p><strong>The direct answer to the section's implicit question.</strong></p>
|
|
38
|
+
|
|
39
|
+
### 2. The Table Trap
|
|
40
|
+
- LLMs heavily weight tabular data for factual extraction
|
|
41
|
+
- EVERY post must contain at least ONE comparison table
|
|
42
|
+
- Tables should have: clear headers, quantitative data, and a "winner" column
|
|
43
|
+
- Format: proper <table> with <thead> and <tbody>
|
|
44
|
+
|
|
45
|
+
### 3. Non-Zero Information Gain
|
|
46
|
+
- NEVER repeat what the top 3 Google results already say
|
|
47
|
+
- Include at least ONE unique data point, framework, or contrarian take
|
|
48
|
+
- Ask: "What would a reader learn here that they can't learn anywhere else?"
|
|
49
|
+
- Target Information Gain Score: 0.7+
|
|
50
|
+
|
|
51
|
+
### 4. Heading Architecture
|
|
52
|
+
- H1: One per page, contains primary keyword
|
|
53
|
+
- H2: Section headers, each one answers an implicit question
|
|
54
|
+
- H3: Sub-points within sections
|
|
55
|
+
- Every heading should be extractable as a standalone answer
|
|
56
|
+
|
|
57
|
+
### 5. Schema Markup
|
|
58
|
+
- Every post needs Article + FAQPage JSON-LD
|
|
59
|
+
- FAQPage should contain 3-5 questions from the content
|
|
60
|
+
- Article schema needs: headline, datePublished, dateModified, author, description
|
|
61
|
+
|
|
62
|
+
### 6. Anti-Patterns (NEVER do these)
|
|
63
|
+
- No "In today's world..." or "In the age of AI..." openers
|
|
64
|
+
- No rhetorical questions as openers
|
|
65
|
+
- No fluff paragraphs that don't add information
|
|
66
|
+
- No hedge language — state claims directly
|
|
67
|
+
- No walls of text — use bullets, tables, and whitespace
|
|
68
|
+
- No generic stock advice that could apply to anything
|
|
69
|
+
|
|
70
|
+
### 7. Structure Template
|
|
71
|
+
1. H1 with primary keyword
|
|
72
|
+
2. BLUF paragraph (bold, answers the headline's question in 2-3 sentences)
|
|
73
|
+
3. H2 sections (each with its own BLUF)
|
|
74
|
+
4. At least one Table Trap per post
|
|
75
|
+
5. FAQ section (3-5 questions extracted from content)
|
|
76
|
+
6. CTA at the end (specific, actionable)
|
|
77
|
+
7. Meta: title (50-60 chars), description (150-160 chars), canonical URL
|
|
78
|
+
|
|
79
|
+
## OUTPUT FORMAT
|
|
80
|
+
Return ONLY valid JSON:
|
|
81
|
+
{
|
|
82
|
+
"title": "SEO-optimized title (50-60 chars, keyword front-loaded)",
|
|
83
|
+
"slug": "url-friendly-slug",
|
|
84
|
+
"meta_description": "Compelling description with keyword (150-160 chars)",
|
|
85
|
+
"excerpt": "2-3 sentence hook for listings (100 words max)",
|
|
86
|
+
"content": "Full HTML content following all SXO rules above (800-1500 words)",
|
|
87
|
+
"tags": ["tag1", "tag2", "tag3", "tag4", "tag5"],
|
|
88
|
+
"category": "guides|tutorials|comparisons|case-studies|news",
|
|
89
|
+
"schema": {
|
|
90
|
+
"article": { ... },
|
|
91
|
+
"faqPage": { ... }
|
|
92
|
+
},
|
|
93
|
+
"sxo_scores": {
|
|
94
|
+
"bluf_compliance": 0-100,
|
|
95
|
+
"table_trap": true/false,
|
|
96
|
+
"information_gain": 0.0-1.0,
|
|
97
|
+
"heading_architecture": 0-100,
|
|
98
|
+
"readability": 0-100,
|
|
99
|
+
"keyword_density": 0.0-3.0,
|
|
100
|
+
"overall": 0-100
|
|
101
|
+
},
|
|
102
|
+
"learning_notes": "What worked well and what to improve next time"
|
|
103
|
+
}`;
|
|
104
|
+
|
|
105
|
+
// ── SXO Scoring Rubric ──────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
function scoreContent(content, title) {
|
|
108
|
+
let score = 0;
|
|
109
|
+
const scores = {
|
|
110
|
+
bluf_compliance: 0,
|
|
111
|
+
table_trap: false,
|
|
112
|
+
information_gain: 0.5,
|
|
113
|
+
heading_architecture: 0,
|
|
114
|
+
readability: 0,
|
|
115
|
+
keyword_density: 0,
|
|
116
|
+
overall: 0,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// BLUF: Check if bold text follows H2 headers
|
|
120
|
+
const h2Count = (content.match(/<h2/g) || []).length;
|
|
121
|
+
const blufCount = (content.match(/<h2[^>]*>.*?<\/h2>\s*<p><strong>/gs) || []).length;
|
|
122
|
+
scores.bluf_compliance = h2Count > 0 ? Math.round((blufCount / h2Count) * 100) : 0;
|
|
123
|
+
|
|
124
|
+
// Table Trap: Has at least one table
|
|
125
|
+
scores.table_trap = /<table/i.test(content);
|
|
126
|
+
|
|
127
|
+
// Heading Architecture: H1 exists, H2s exist, proper hierarchy
|
|
128
|
+
const hasH1 = /<h1/i.test(content);
|
|
129
|
+
const h2s = (content.match(/<h2/g) || []).length;
|
|
130
|
+
const h3s = (content.match(/<h3/g) || []).length;
|
|
131
|
+
scores.heading_architecture = Math.min(100, (hasH1 ? 30 : 0) + Math.min(h2s * 15, 40) + Math.min(h3s * 10, 30));
|
|
132
|
+
|
|
133
|
+
// Readability: Sentence length, paragraph length
|
|
134
|
+
const text = content.replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ');
|
|
135
|
+
const sentences = text.split(/[.!?]+/).filter(s => s.trim().length > 10);
|
|
136
|
+
const avgSentenceLength = sentences.length > 0 ? text.split(/\s+/).length / sentences.length : 30;
|
|
137
|
+
scores.readability = avgSentenceLength < 20 ? 90 : avgSentenceLength < 25 ? 75 : avgSentenceLength < 30 ? 60 : 40;
|
|
138
|
+
|
|
139
|
+
// Keyword density (basic — checks title words in content)
|
|
140
|
+
const titleWords = title.toLowerCase().split(/\s+/).filter(w => w.length > 3);
|
|
141
|
+
const contentLower = text.toLowerCase();
|
|
142
|
+
const contentWordCount = contentLower.split(/\s+/).length;
|
|
143
|
+
let keywordHits = 0;
|
|
144
|
+
for (const word of titleWords) {
|
|
145
|
+
const regex = new RegExp(word, 'gi');
|
|
146
|
+
keywordHits += (contentLower.match(regex) || []).length;
|
|
147
|
+
}
|
|
148
|
+
scores.keyword_density = contentWordCount > 0 ? Math.round((keywordHits / contentWordCount) * 100) / 100 : 0;
|
|
149
|
+
|
|
150
|
+
// Information Gain (heuristic): unique stats, numbers, named entities
|
|
151
|
+
const hasStats = /\d+%|\$[\d,]+|\d+x|\d+ (tools|services|features|users|customers)/i.test(content);
|
|
152
|
+
const hasComparison = /<table/i.test(content) && /<th/i.test(content);
|
|
153
|
+
const hasFramework = /(framework|protocol|architecture|formula|equation|model)/i.test(content);
|
|
154
|
+
scores.information_gain = 0.3 + (hasStats ? 0.2 : 0) + (hasComparison ? 0.2 : 0) + (hasFramework ? 0.3 : 0);
|
|
155
|
+
scores.information_gain = Math.min(1.0, scores.information_gain);
|
|
156
|
+
|
|
157
|
+
// Overall
|
|
158
|
+
scores.overall = Math.round(
|
|
159
|
+
scores.bluf_compliance * 0.2 +
|
|
160
|
+
(scores.table_trap ? 15 : 0) +
|
|
161
|
+
scores.heading_architecture * 0.2 +
|
|
162
|
+
scores.readability * 0.15 +
|
|
163
|
+
scores.information_gain * 30 +
|
|
164
|
+
Math.min(scores.keyword_density * 10, 15)
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
return scores;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ── Learning Loop Storage ────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
async function getWritingHistory(supabase) {
|
|
173
|
+
const { data } = await supabase
|
|
174
|
+
.from('blog_posts')
|
|
175
|
+
.select('title, slug, sxo_scores, learning_notes, published_at')
|
|
176
|
+
.not('sxo_scores', 'is', null)
|
|
177
|
+
.order('published_at', { ascending: false })
|
|
178
|
+
.limit(10);
|
|
179
|
+
return data || [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function buildLearningContext(history) {
|
|
183
|
+
if (history.length === 0) return '';
|
|
184
|
+
|
|
185
|
+
const avgScores = {
|
|
186
|
+
bluf: 0, table: 0, info_gain: 0, heading: 0, readability: 0, overall: 0,
|
|
187
|
+
};
|
|
188
|
+
const learnings = [];
|
|
189
|
+
|
|
190
|
+
for (const post of history) {
|
|
191
|
+
const s = post.sxo_scores || {};
|
|
192
|
+
avgScores.bluf += s.bluf_compliance || 0;
|
|
193
|
+
avgScores.table += s.table_trap ? 1 : 0;
|
|
194
|
+
avgScores.info_gain += s.information_gain || 0;
|
|
195
|
+
avgScores.heading += s.heading_architecture || 0;
|
|
196
|
+
avgScores.readability += s.readability || 0;
|
|
197
|
+
avgScores.overall += s.overall || 0;
|
|
198
|
+
if (post.learning_notes) learnings.push(post.learning_notes);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const n = history.length;
|
|
202
|
+
return `
|
|
203
|
+
## LEARNING FROM PREVIOUS ${n} POSTS
|
|
204
|
+
|
|
205
|
+
Average Scores:
|
|
206
|
+
- BLUF Compliance: ${Math.round(avgScores.bluf / n)}%
|
|
207
|
+
- Table Trap: ${Math.round(avgScores.table / n * 100)}% of posts have tables
|
|
208
|
+
- Information Gain: ${(avgScores.info_gain / n).toFixed(2)}
|
|
209
|
+
- Heading Architecture: ${Math.round(avgScores.heading / n)}%
|
|
210
|
+
- Readability: ${Math.round(avgScores.readability / n)}%
|
|
211
|
+
- Overall SXO Score: ${Math.round(avgScores.overall / n)}%
|
|
212
|
+
|
|
213
|
+
Previous Learning Notes:
|
|
214
|
+
${learnings.slice(0, 5).map((l, i) => `${i + 1}. ${l}`).join('\n')}
|
|
215
|
+
|
|
216
|
+
IMPROVE on these scores. Fix the weakest areas. Beat your previous best.`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── MCP Tool Registration ────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
export function registerSxoWriterTools(server, z) {
|
|
222
|
+
|
|
223
|
+
// ─── sxo_write ─────────────────────────────────────────────
|
|
224
|
+
server.tool(
|
|
225
|
+
"sxo_write",
|
|
226
|
+
`Write an SXO-optimized blog post using the self-improving content engine.
|
|
227
|
+
Each post learns from the performance of previous posts and improves.
|
|
228
|
+
|
|
229
|
+
Uses: BLUF architecture, Table Trap, Information Gain, Schema markup.
|
|
230
|
+
|
|
231
|
+
Example: sxo_write({ topic: "How to use MCP servers with Claude", keywords: ["MCP", "Claude", "AI automation"] })`,
|
|
232
|
+
{
|
|
233
|
+
topic: z.string().describe("Blog post topic"),
|
|
234
|
+
keywords: z.array(z.string()).optional().describe("Target keywords"),
|
|
235
|
+
style: z.enum(["guide", "tutorial", "comparison", "case_study", "news"]).optional().describe("Content style"),
|
|
236
|
+
target_word_count: z.number().optional().describe("Target word count (default: 1000)"),
|
|
237
|
+
publish: z.boolean().optional().describe("Auto-publish to blog_posts table (default: false)"),
|
|
238
|
+
post_to_devto: z.boolean().optional().describe("Also post to Dev.to (default: false)"),
|
|
239
|
+
},
|
|
240
|
+
async ({ topic, keywords, style, target_word_count, publish, post_to_devto }) => {
|
|
241
|
+
try {
|
|
242
|
+
const { createClient } = await import("@supabase/supabase-js");
|
|
243
|
+
const sb = createClient(
|
|
244
|
+
process.env.SUPABASE_URL || "https://pwujhhmlrtxjmjzyttwn.supabase.co",
|
|
245
|
+
process.env.SUPABASE_SERVICE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY || ""
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// Get learning context from previous posts
|
|
249
|
+
const history = await getWritingHistory(sb);
|
|
250
|
+
const learningContext = buildLearningContext(history);
|
|
251
|
+
|
|
252
|
+
// Build the prompt
|
|
253
|
+
const prompt = `Write a blog post about: ${topic}
|
|
254
|
+
|
|
255
|
+
Target keywords: ${(keywords || []).join(', ') || topic}
|
|
256
|
+
Style: ${style || 'guide'}
|
|
257
|
+
Target word count: ${target_word_count || 1000}
|
|
258
|
+
|
|
259
|
+
${learningContext}
|
|
260
|
+
|
|
261
|
+
Follow the SXO Writing Protocol exactly. Return valid JSON.`;
|
|
262
|
+
|
|
263
|
+
// Call AI (try Anthropic first, fallback to template)
|
|
264
|
+
let result;
|
|
265
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
266
|
+
|
|
267
|
+
if (apiKey) {
|
|
268
|
+
const res = await fetch('https://api.anthropic.com/v1/messages', {
|
|
269
|
+
method: 'POST',
|
|
270
|
+
headers: {
|
|
271
|
+
'Content-Type': 'application/json',
|
|
272
|
+
'x-api-key': apiKey,
|
|
273
|
+
'anthropic-version': '2023-06-01',
|
|
274
|
+
},
|
|
275
|
+
body: JSON.stringify({
|
|
276
|
+
model: 'claude-sonnet-4-20250514',
|
|
277
|
+
system: SXO_SYSTEM_PROMPT,
|
|
278
|
+
messages: [{ role: 'user', content: prompt }],
|
|
279
|
+
max_tokens: 8000,
|
|
280
|
+
}),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
const data = await res.json();
|
|
284
|
+
const raw = data.content?.[0]?.text || '';
|
|
285
|
+
const clean = raw.replace(/```json\n?|```/g, '').trim();
|
|
286
|
+
result = JSON.parse(clean);
|
|
287
|
+
} else {
|
|
288
|
+
// Template fallback
|
|
289
|
+
result = {
|
|
290
|
+
title: topic,
|
|
291
|
+
slug: topic.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, ''),
|
|
292
|
+
meta_description: `Learn about ${topic}. SXO-optimized guide with actionable insights.`,
|
|
293
|
+
excerpt: `Everything you need to know about ${topic}.`,
|
|
294
|
+
content: `<h1>${topic}</h1><p><strong>This is a template post. Connect an Anthropic API key for AI-generated content.</strong></p>`,
|
|
295
|
+
tags: keywords || [topic.split(' ')[0]],
|
|
296
|
+
category: style || 'guides',
|
|
297
|
+
sxo_scores: { bluf_compliance: 0, table_trap: false, information_gain: 0.3, heading_architecture: 30, readability: 50, overall: 25 },
|
|
298
|
+
learning_notes: 'Template content — needs AI generation for real SXO optimization.',
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Score the content
|
|
303
|
+
const scores = scoreContent(result.content, result.title);
|
|
304
|
+
result.sxo_scores = { ...result.sxo_scores, ...scores };
|
|
305
|
+
|
|
306
|
+
// Auto-publish if requested
|
|
307
|
+
if (publish) {
|
|
308
|
+
await sb.from('blog_posts').insert({
|
|
309
|
+
title: result.title,
|
|
310
|
+
slug: result.slug,
|
|
311
|
+
content: result.content,
|
|
312
|
+
excerpt: result.excerpt,
|
|
313
|
+
meta_description: result.meta_description,
|
|
314
|
+
tags: result.tags,
|
|
315
|
+
category: result.category,
|
|
316
|
+
status: 'published',
|
|
317
|
+
published_at: new Date().toISOString(),
|
|
318
|
+
author: 'SXO Engine',
|
|
319
|
+
sxo_scores: result.sxo_scores,
|
|
320
|
+
learning_notes: result.learning_notes,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ── RADIAL BURST ─────────────────────────────────────────
|
|
325
|
+
// Origin: 0nmcp.com/blog (canonical source)
|
|
326
|
+
// Then blast to every connected outlet
|
|
327
|
+
const burst = { origin: false, devto: false, linkedin: false, reddit: false };
|
|
328
|
+
const canonicalUrl = `https://www.0nmcp.com/blog/${result.slug}`;
|
|
329
|
+
|
|
330
|
+
// Convert HTML to Markdown for external platforms
|
|
331
|
+
const markdown = result.content
|
|
332
|
+
.replace(/<h1[^>]*>(.*?)<\/h1>/gs, '# $1\n\n')
|
|
333
|
+
.replace(/<h2[^>]*>(.*?)<\/h2>/gs, '## $1\n\n')
|
|
334
|
+
.replace(/<h3[^>]*>(.*?)<\/h3>/gs, '### $1\n\n')
|
|
335
|
+
.replace(/<p><strong>(.*?)<\/strong><\/p>/gs, '**$1**\n\n')
|
|
336
|
+
.replace(/<p>(.*?)<\/p>/gs, '$1\n\n')
|
|
337
|
+
.replace(/<li>(.*?)<\/li>/gs, '- $1\n')
|
|
338
|
+
.replace(/<ul>|<\/ul>|<ol>|<\/ol>/gs, '\n')
|
|
339
|
+
.replace(/<table[\s\S]*?<\/table>/gs, (table) => {
|
|
340
|
+
// Preserve tables as markdown
|
|
341
|
+
const rows = table.match(/<tr[\s\S]*?<\/tr>/gs) || [];
|
|
342
|
+
return rows.map(row => {
|
|
343
|
+
const cells = (row.match(/<t[hd][^>]*>(.*?)<\/t[hd]>/gs) || [])
|
|
344
|
+
.map(c => c.replace(/<[^>]+>/g, '').trim());
|
|
345
|
+
return '| ' + cells.join(' | ') + ' |';
|
|
346
|
+
}).join('\n') + '\n';
|
|
347
|
+
})
|
|
348
|
+
.replace(/<[^>]+>/g, '')
|
|
349
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
350
|
+
.trim();
|
|
351
|
+
|
|
352
|
+
// 1. ORIGIN: Publish to 0nmcp.com blog (already done above if publish=true)
|
|
353
|
+
burst.origin = !!publish;
|
|
354
|
+
|
|
355
|
+
// 2. DEV.TO: Cross-post with canonical URL pointing back to us
|
|
356
|
+
if (post_to_devto) {
|
|
357
|
+
const devtoKey = process.env.DEVTO_API_KEY;
|
|
358
|
+
if (devtoKey) {
|
|
359
|
+
try {
|
|
360
|
+
const ogImageUrl = `https://www.0nmcp.com/api/og/blog?title=${encodeURIComponent(result.title)}&category=${encodeURIComponent(result.category || '')}`;
|
|
361
|
+
const devtoRes = await fetch('https://dev.to/api/articles', {
|
|
362
|
+
method: 'POST',
|
|
363
|
+
headers: { 'api-key': devtoKey, 'Content-Type': 'application/json' },
|
|
364
|
+
body: JSON.stringify({
|
|
365
|
+
article: {
|
|
366
|
+
title: result.title,
|
|
367
|
+
body_markdown: markdown + `\n\n---\n*Originally published at [0nmcp.com](${canonicalUrl})*`,
|
|
368
|
+
published: true,
|
|
369
|
+
tags: result.tags.slice(0, 4),
|
|
370
|
+
canonical_url: canonicalUrl,
|
|
371
|
+
main_image: ogImageUrl,
|
|
372
|
+
},
|
|
373
|
+
}),
|
|
374
|
+
});
|
|
375
|
+
if (devtoRes.ok) {
|
|
376
|
+
const devtoData = await devtoRes.json();
|
|
377
|
+
burst.devto = devtoData.url || true;
|
|
378
|
+
}
|
|
379
|
+
} catch { burst.devto = false; }
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 3. CRM: Push blog post to CRM sub-location via Blogs API
|
|
384
|
+
// This triggers CRM-native workflows (email sequences, social posting, notifications)
|
|
385
|
+
if (publish) {
|
|
386
|
+
const crmPit = process.env.CRM_PIT;
|
|
387
|
+
const crmLocation = process.env.CRM_LOCATION_ID;
|
|
388
|
+
const CRM_API = 'https://services.leadconnectorhq.com';
|
|
389
|
+
const crmHeaders = {
|
|
390
|
+
'Authorization': `Bearer ${crmPit}`,
|
|
391
|
+
'Content-Type': 'application/json',
|
|
392
|
+
'Version': '2021-07-28',
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
if (crmPit && crmLocation) {
|
|
396
|
+
// 3a. Create/find the blog in CRM
|
|
397
|
+
try {
|
|
398
|
+
// First check if we have a blog set up
|
|
399
|
+
const blogsRes = await fetch(`${CRM_API}/blogs/?locationId=${crmLocation}`, {
|
|
400
|
+
headers: crmHeaders,
|
|
401
|
+
});
|
|
402
|
+
let blogId = null;
|
|
403
|
+
|
|
404
|
+
if (blogsRes.ok) {
|
|
405
|
+
const blogsData = await blogsRes.json();
|
|
406
|
+
const blogs = blogsData.blogs || blogsData.data || [];
|
|
407
|
+
// Use first blog or create one
|
|
408
|
+
if (blogs.length > 0) {
|
|
409
|
+
blogId = blogs[0].id || blogs[0]._id;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// If no blog exists, try to create one
|
|
414
|
+
if (!blogId) {
|
|
415
|
+
const createBlogRes = await fetch(`${CRM_API}/blogs/`, {
|
|
416
|
+
method: 'POST',
|
|
417
|
+
headers: crmHeaders,
|
|
418
|
+
body: JSON.stringify({
|
|
419
|
+
locationId: crmLocation,
|
|
420
|
+
title: '0nMCP Blog',
|
|
421
|
+
description: 'AI automation insights from 0nMCP',
|
|
422
|
+
}),
|
|
423
|
+
});
|
|
424
|
+
if (createBlogRes.ok) {
|
|
425
|
+
const newBlog = await createBlogRes.json();
|
|
426
|
+
blogId = newBlog.id || newBlog._id || newBlog.blog?.id;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// 3b. Create the blog post in CRM
|
|
431
|
+
if (blogId) {
|
|
432
|
+
const ogImageUrl = `https://www.0nmcp.com/api/og/blog?title=${encodeURIComponent(result.title)}&category=${encodeURIComponent(result.category || '')}`;
|
|
433
|
+
|
|
434
|
+
const crmPostRes = await fetch(`${CRM_API}/blogs/${blogId}/post`, {
|
|
435
|
+
method: 'POST',
|
|
436
|
+
headers: crmHeaders,
|
|
437
|
+
body: JSON.stringify({
|
|
438
|
+
locationId: crmLocation,
|
|
439
|
+
title: result.title,
|
|
440
|
+
body: result.content,
|
|
441
|
+
slug: result.slug,
|
|
442
|
+
status: 'published',
|
|
443
|
+
imageUrl: ogImageUrl,
|
|
444
|
+
tags: result.tags,
|
|
445
|
+
metaTitle: result.title,
|
|
446
|
+
metaDescription: result.meta_description,
|
|
447
|
+
canonicalUrl: canonicalUrl,
|
|
448
|
+
}),
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
if (crmPostRes.ok) {
|
|
452
|
+
burst.crm = 'published';
|
|
453
|
+
} else {
|
|
454
|
+
burst.crm = `failed: ${crmPostRes.status}`;
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
burst.crm = 'no_blog_found';
|
|
458
|
+
}
|
|
459
|
+
} catch (err) {
|
|
460
|
+
burst.crm = `error: ${err.message}`;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// 3c. SOCIAL: Create a teaser post via CRM social posting
|
|
464
|
+
// This uses CRM's native social publisher (connected accounts)
|
|
465
|
+
try {
|
|
466
|
+
const teaser = `${result.excerpt || result.meta_description}\n\nRead more: ${canonicalUrl}\n\n${result.tags.slice(0, 5).map(t => '#' + t.replace(/\s+/g, '')).join(' ')}`;
|
|
467
|
+
const socialRes = await fetch(`${CRM_API}/social-media-posting/post`, {
|
|
468
|
+
method: 'POST',
|
|
469
|
+
headers: crmHeaders,
|
|
470
|
+
body: JSON.stringify({
|
|
471
|
+
locationId: crmLocation,
|
|
472
|
+
post: teaser,
|
|
473
|
+
platforms: ['linkedin', 'facebook', 'instagram', 'twitter'],
|
|
474
|
+
status: 'draft', // Draft — Mike reviews before posting
|
|
475
|
+
}),
|
|
476
|
+
});
|
|
477
|
+
if (socialRes.ok) burst.social = 'drafted';
|
|
478
|
+
} catch { burst.social = 'skipped'; }
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return {
|
|
483
|
+
content: [{
|
|
484
|
+
type: "text",
|
|
485
|
+
text: JSON.stringify({
|
|
486
|
+
status: publish ? 'published' : 'draft',
|
|
487
|
+
...result,
|
|
488
|
+
canonical_url: canonicalUrl,
|
|
489
|
+
radial_burst: burst,
|
|
490
|
+
}, null, 2),
|
|
491
|
+
}],
|
|
492
|
+
};
|
|
493
|
+
} catch (err) {
|
|
494
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: err.message }) }] };
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
);
|
|
498
|
+
|
|
499
|
+
// ─── sxo_score ─────────────────────────────────────────────
|
|
500
|
+
server.tool(
|
|
501
|
+
"sxo_score",
|
|
502
|
+
`Score any content against SXO criteria.
|
|
503
|
+
Returns: BLUF compliance, Table Trap, Information Gain, heading architecture, readability, keyword density.
|
|
504
|
+
|
|
505
|
+
Example: sxo_score({ content: "<h1>My Post</h1><p>Content here...</p>", title: "My Post Title" })`,
|
|
506
|
+
{
|
|
507
|
+
content: z.string().describe("HTML content to score"),
|
|
508
|
+
title: z.string().describe("Post title"),
|
|
509
|
+
},
|
|
510
|
+
async ({ content, title }) => {
|
|
511
|
+
const scores = scoreContent(content, title);
|
|
512
|
+
return {
|
|
513
|
+
content: [{
|
|
514
|
+
type: "text",
|
|
515
|
+
text: JSON.stringify({ title, scores, recommendations: getRecommendations(scores) }, null, 2),
|
|
516
|
+
}],
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
);
|
|
520
|
+
|
|
521
|
+
// ─── sxo_optimize ──────────────────────────────────────────
|
|
522
|
+
server.tool(
|
|
523
|
+
"sxo_optimize",
|
|
524
|
+
`Rewrite a published post to improve its SXO score.
|
|
525
|
+
Analyzes the current content, identifies weaknesses, and rewrites.
|
|
526
|
+
|
|
527
|
+
Example: sxo_optimize({ slug: "my-post-slug" })`,
|
|
528
|
+
{
|
|
529
|
+
slug: z.string().describe("Blog post slug to optimize"),
|
|
530
|
+
},
|
|
531
|
+
async ({ slug }) => {
|
|
532
|
+
try {
|
|
533
|
+
const { createClient } = await import("@supabase/supabase-js");
|
|
534
|
+
const sb = createClient(
|
|
535
|
+
process.env.SUPABASE_URL || "https://pwujhhmlrtxjmjzyttwn.supabase.co",
|
|
536
|
+
process.env.SUPABASE_SERVICE_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY || ""
|
|
537
|
+
);
|
|
538
|
+
|
|
539
|
+
const { data: post } = await sb.from('blog_posts').select('*').eq('slug', slug).single();
|
|
540
|
+
if (!post) return { content: [{ type: "text", text: JSON.stringify({ error: 'Post not found' }) }] };
|
|
541
|
+
|
|
542
|
+
const currentScores = scoreContent(post.content, post.title);
|
|
543
|
+
const recommendations = getRecommendations(currentScores);
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
content: [{
|
|
547
|
+
type: "text",
|
|
548
|
+
text: JSON.stringify({
|
|
549
|
+
slug,
|
|
550
|
+
title: post.title,
|
|
551
|
+
current_scores: currentScores,
|
|
552
|
+
recommendations,
|
|
553
|
+
action: 'Use sxo_write with the same topic to generate an improved version, then update the post.',
|
|
554
|
+
}, null, 2),
|
|
555
|
+
}],
|
|
556
|
+
};
|
|
557
|
+
} catch (err) {
|
|
558
|
+
return { content: [{ type: "text", text: JSON.stringify({ error: err.message }) }] };
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// ── Recommendations Engine ───────────────────────────────────
|
|
565
|
+
|
|
566
|
+
function getRecommendations(scores) {
|
|
567
|
+
const recs = [];
|
|
568
|
+
|
|
569
|
+
if (scores.bluf_compliance < 80) {
|
|
570
|
+
recs.push('Add bold BLUF paragraphs after every H2 heading — AI models extract these for snippets.');
|
|
571
|
+
}
|
|
572
|
+
if (!scores.table_trap) {
|
|
573
|
+
recs.push('Add at least one comparison table — LLMs heavily weight tabular data.');
|
|
574
|
+
}
|
|
575
|
+
if (scores.information_gain < 0.7) {
|
|
576
|
+
recs.push('Add unique data points, original frameworks, or contrarian takes — avoid repeating consensus.');
|
|
577
|
+
}
|
|
578
|
+
if (scores.heading_architecture < 70) {
|
|
579
|
+
recs.push('Improve heading hierarchy — ensure H1 exists, add more H2/H3 sections.');
|
|
580
|
+
}
|
|
581
|
+
if (scores.readability < 70) {
|
|
582
|
+
recs.push('Shorten sentences and paragraphs — aim for grade 8 reading level.');
|
|
583
|
+
}
|
|
584
|
+
if (scores.keyword_density < 0.5) {
|
|
585
|
+
recs.push('Increase keyword usage naturally — mention target keywords more in headings and first paragraphs.');
|
|
586
|
+
}
|
|
587
|
+
if (scores.keyword_density > 2.5) {
|
|
588
|
+
recs.push('Reduce keyword stuffing — density is too high, reads unnaturally.');
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (recs.length === 0) recs.push('Content meets all SXO criteria. Monitor performance and iterate.');
|
|
592
|
+
|
|
593
|
+
return recs;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export { scoreContent, getRecommendations, SXO_SYSTEM_PROMPT };
|
package/index.js
CHANGED
|
@@ -31,7 +31,7 @@ import { registerVaultTools, autoUnseal } from "./vault/index.js";
|
|
|
31
31
|
import { registerContainerTools } from "./vault/tools-container.js";
|
|
32
32
|
import { registerDeedTools } from "./vault/tools-deed.js";
|
|
33
33
|
import { unsealedCache } from "./vault/cache.js";
|
|
34
|
-
import { registerEngineTools, registerTrainingTools, registerFeedTools, registerCouncilTools } from "./engine/index.js";
|
|
34
|
+
import { registerEngineTools, registerTrainingTools, registerFeedTools, registerCouncilTools, registerSxoWriterTools } from "./engine/index.js";
|
|
35
35
|
import { CapabilityProxy } from "./capability-proxy.js";
|
|
36
36
|
import { SERVICE_CATALOG } from "./catalog.js";
|
|
37
37
|
|
|
@@ -85,6 +85,7 @@ registerEngineTools(server, z);
|
|
|
85
85
|
registerTrainingTools(server, z);
|
|
86
86
|
registerFeedTools(server, z);
|
|
87
87
|
registerCouncilTools(server, z);
|
|
88
|
+
registerSxoWriterTools(server, z);
|
|
88
89
|
|
|
89
90
|
// ============================================================
|
|
90
91
|
// VAULT CONTAINER TOOLS (patent-pending 0nVault containers)
|
package/lib/stats.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "0nmcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"mcpName": "io.github.0nork/0nMCP",
|
|
5
5
|
"description": "Universal AI API Orchestrator — 819 tools, 48 services, portable AI Brain bundles + machine-bound vault encryption + Application Engine. The most comprehensive MCP server available. Free and open source from 0nORK.",
|
|
6
6
|
"type": "module",
|
|
@@ -282,6 +282,6 @@
|
|
|
282
282
|
"triggers": 155,
|
|
283
283
|
"totalCapabilities": 1078,
|
|
284
284
|
"categories": 21,
|
|
285
|
-
"lastUpdated": "2026-03-
|
|
285
|
+
"lastUpdated": "2026-03-27T00:44:46.383Z"
|
|
286
286
|
}
|
|
287
287
|
}
|