@bulkpublishing/mcp-server 1.0.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 +170 -0
- package/dist/ai/engine.d.ts +12 -0
- package/dist/ai/engine.d.ts.map +1 -0
- package/dist/ai/engine.js +397 -0
- package/dist/ai/engine.js.map +1 -0
- package/dist/ai/prompts.d.ts +30 -0
- package/dist/ai/prompts.d.ts.map +1 -0
- package/dist/ai/prompts.js +207 -0
- package/dist/ai/prompts.js.map +1 -0
- package/dist/auth/context.d.ts +81 -0
- package/dist/auth/context.d.ts.map +1 -0
- package/dist/auth/context.js +68 -0
- package/dist/auth/context.js.map +1 -0
- package/dist/auth/validate.d.ts +13 -0
- package/dist/auth/validate.d.ts.map +1 -0
- package/dist/auth/validate.js +87 -0
- package/dist/auth/validate.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -0
- package/dist/tools/csv.d.ts +18 -0
- package/dist/tools/csv.d.ts.map +1 -0
- package/dist/tools/csv.js +673 -0
- package/dist/tools/csv.js.map +1 -0
- package/dist/tools/generation.d.ts +14 -0
- package/dist/tools/generation.d.ts.map +1 -0
- package/dist/tools/generation.js +291 -0
- package/dist/tools/generation.js.map +1 -0
- package/dist/tools/indexing.d.ts +11 -0
- package/dist/tools/indexing.d.ts.map +1 -0
- package/dist/tools/indexing.js +219 -0
- package/dist/tools/indexing.js.map +1 -0
- package/dist/tools/projects.d.ts +11 -0
- package/dist/tools/projects.d.ts.map +1 -0
- package/dist/tools/projects.js +181 -0
- package/dist/tools/projects.js.map +1 -0
- package/dist/tools/research.d.ts +12 -0
- package/dist/tools/research.d.ts.map +1 -0
- package/dist/tools/research.js +88 -0
- package/dist/tools/research.js.map +1 -0
- package/dist/tools/seo.d.ts +12 -0
- package/dist/tools/seo.d.ts.map +1 -0
- package/dist/tools/seo.js +164 -0
- package/dist/tools/seo.js.map +1 -0
- package/dist/tools/wordpress.d.ts +15 -0
- package/dist/tools/wordpress.d.ts.map +1 -0
- package/dist/tools/wordpress.js +447 -0
- package/dist/tools/wordpress.js.map +1 -0
- package/dist/types.d.ts +206 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +9 -0
- package/dist/types.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BPAI MCP Server — CSV Import / Export Tools
|
|
3
|
+
*
|
|
4
|
+
* MCP tool handlers for:
|
|
5
|
+
* - import_csv: Read a local CSV and auto-detect columns
|
|
6
|
+
* - export_csv: Write articles to a CSV file
|
|
7
|
+
* - export_markdown: Write articles as individual .md files
|
|
8
|
+
* - export_json: Write articles as structured JSON
|
|
9
|
+
* - export_bulk: Combined multi-format export with filtering
|
|
10
|
+
*
|
|
11
|
+
* Security:
|
|
12
|
+
* - All operations run on the user's local machine (filesystem only)
|
|
13
|
+
* - No database or remote calls; articles are provided inline by the MCP client
|
|
14
|
+
* - Auth check on every tool call via getUserContext()
|
|
15
|
+
*/
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import * as fs from 'fs/promises';
|
|
18
|
+
import * as path from 'path';
|
|
19
|
+
import { getUserContext } from '../auth/context.js';
|
|
20
|
+
// ============================================
|
|
21
|
+
// Column Auto-Detection
|
|
22
|
+
// ============================================
|
|
23
|
+
/** Canonical field → list of recognized CSV header variations (all lowercase) */
|
|
24
|
+
const COLUMN_ALIASES = {
|
|
25
|
+
keyword: ['keyword', 'target_keyword', 'focus_keyword', 'kw', 'primary_keyword', 'search_term', 'query', 'main_keyword', 'keyphrase', 'focus_keyphrase'],
|
|
26
|
+
title: ['title', 'post_title', 'h1', 'headline', 'article_title', 'page_title', 'heading'],
|
|
27
|
+
meta_description: ['meta', 'meta_description', 'meta_desc', 'description', 'seo_description', 'metadescription'],
|
|
28
|
+
slug: ['slug', 'url_slug', 'permalink', 'url', 'path', 'handle', 'url_handle'],
|
|
29
|
+
category: ['category', 'cat', 'section', 'topic', 'group', 'categories'],
|
|
30
|
+
tone: ['tone', 'voice', 'style', 'writing_style', 'writing_tone'],
|
|
31
|
+
word_count: ['word_count', 'length', 'target_length', 'words', 'target_words', 'wordcount'],
|
|
32
|
+
instructions: ['instructions', 'notes', 'prompt_override', 'custom_prompt', 'additional_context', 'context', 'brief'],
|
|
33
|
+
tags: ['tags', 'keywords', 'labels', 'secondary_keywords'],
|
|
34
|
+
content: ['content', 'body', 'article_content', 'body_html', 'html', 'markdown', 'article_body', 'post_content'],
|
|
35
|
+
status: ['status', 'state', 'publish_status'],
|
|
36
|
+
author: ['author', 'author_name', 'writer'],
|
|
37
|
+
date: ['date', 'publish_date', 'created_at', 'created', 'published_date'],
|
|
38
|
+
};
|
|
39
|
+
function detectColumns(headers) {
|
|
40
|
+
const mapping = {};
|
|
41
|
+
const normalised = headers.map(h => h.trim().toLowerCase().replace(/[\s-]+/g, '_'));
|
|
42
|
+
for (const [field, aliases] of Object.entries(COLUMN_ALIASES)) {
|
|
43
|
+
for (let i = 0; i < normalised.length; i++) {
|
|
44
|
+
if (aliases.includes(normalised[i])) {
|
|
45
|
+
mapping[field] = headers[i].trim();
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return mapping;
|
|
51
|
+
}
|
|
52
|
+
// ============================================
|
|
53
|
+
// CSV Parsing Helpers
|
|
54
|
+
// ============================================
|
|
55
|
+
function parseCSVLine(line, delimiter) {
|
|
56
|
+
const fields = [];
|
|
57
|
+
let current = '';
|
|
58
|
+
let inQuotes = false;
|
|
59
|
+
for (let i = 0; i < line.length; i++) {
|
|
60
|
+
const char = line[i];
|
|
61
|
+
if (inQuotes) {
|
|
62
|
+
if (char === '"') {
|
|
63
|
+
if (i + 1 < line.length && line[i + 1] === '"') {
|
|
64
|
+
current += '"';
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
inQuotes = false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
current += char;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
if (char === '"') {
|
|
77
|
+
inQuotes = true;
|
|
78
|
+
}
|
|
79
|
+
else if (char === delimiter) {
|
|
80
|
+
fields.push(current);
|
|
81
|
+
current = '';
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
current += char;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
fields.push(current);
|
|
89
|
+
return fields;
|
|
90
|
+
}
|
|
91
|
+
function parseCSV(raw, delimiter, skipRows) {
|
|
92
|
+
const lines = raw.split(/\r?\n/).filter(l => l.trim().length > 0);
|
|
93
|
+
const dataLines = lines.slice(skipRows);
|
|
94
|
+
if (dataLines.length === 0)
|
|
95
|
+
return { headers: [], rows: [] };
|
|
96
|
+
const headers = parseCSVLine(dataLines[0], delimiter);
|
|
97
|
+
const rows = [];
|
|
98
|
+
for (let i = 1; i < dataLines.length; i++) {
|
|
99
|
+
const values = parseCSVLine(dataLines[i], delimiter);
|
|
100
|
+
const row = {};
|
|
101
|
+
for (let j = 0; j < headers.length; j++) {
|
|
102
|
+
row[headers[j].trim()] = (values[j] || '').trim();
|
|
103
|
+
}
|
|
104
|
+
rows.push(row);
|
|
105
|
+
}
|
|
106
|
+
return { headers, rows };
|
|
107
|
+
}
|
|
108
|
+
// ============================================
|
|
109
|
+
// CSV Writing Helpers
|
|
110
|
+
// ============================================
|
|
111
|
+
function escapeCSVField(value) {
|
|
112
|
+
if (value.includes('"') || value.includes(',') || value.includes('\n') || value.includes('\r')) {
|
|
113
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
114
|
+
}
|
|
115
|
+
return value;
|
|
116
|
+
}
|
|
117
|
+
// ============================================
|
|
118
|
+
// Shared Article Schema (for export tools)
|
|
119
|
+
// ============================================
|
|
120
|
+
const articleSchema = z.object({
|
|
121
|
+
title: z.string().describe('Article title'),
|
|
122
|
+
slug: z.string().optional().describe('URL slug'),
|
|
123
|
+
keyword: z.string().optional().describe('Target keyword'),
|
|
124
|
+
meta_description: z.string().optional().describe('Meta description'),
|
|
125
|
+
content: z.string().describe('Article content (Markdown or HTML)'),
|
|
126
|
+
word_count: z.number().optional().describe('Word count'),
|
|
127
|
+
model: z.string().optional().describe('AI model used'),
|
|
128
|
+
status: z.string().optional().default('draft').describe('draft, published, etc.'),
|
|
129
|
+
category: z.string().optional().describe('Category name'),
|
|
130
|
+
tags: z.string().optional().describe('Comma-separated tags'),
|
|
131
|
+
author: z.string().optional().describe('Author name'),
|
|
132
|
+
created_at: z.string().optional().describe('Creation date ISO string'),
|
|
133
|
+
research_citations: z.array(z.string()).optional().describe('Source URLs from research'),
|
|
134
|
+
faq_schema: z.string().optional().describe('FAQ structured data'),
|
|
135
|
+
});
|
|
136
|
+
// ============================================
|
|
137
|
+
// Filtering Helper
|
|
138
|
+
// ============================================
|
|
139
|
+
function filterArticles(articles, filters) {
|
|
140
|
+
let result = [...articles];
|
|
141
|
+
if (filters.filter_status && filters.filter_status !== 'all') {
|
|
142
|
+
result = result.filter(a => (a.status || 'draft') === filters.filter_status);
|
|
143
|
+
}
|
|
144
|
+
if (filters.filter_model) {
|
|
145
|
+
result = result.filter(a => a.model && a.model.toLowerCase().includes(filters.filter_model.toLowerCase()));
|
|
146
|
+
}
|
|
147
|
+
if (filters.filter_date_from) {
|
|
148
|
+
const from = new Date(filters.filter_date_from).getTime();
|
|
149
|
+
result = result.filter(a => a.created_at && new Date(a.created_at).getTime() >= from);
|
|
150
|
+
}
|
|
151
|
+
if (filters.filter_date_to) {
|
|
152
|
+
const to = new Date(filters.filter_date_to).getTime();
|
|
153
|
+
result = result.filter(a => a.created_at && new Date(a.created_at).getTime() <= to);
|
|
154
|
+
}
|
|
155
|
+
if (filters.filter_min_words) {
|
|
156
|
+
result = result.filter(a => (a.word_count || 0) >= filters.filter_min_words);
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
// ============================================
|
|
161
|
+
// Export Generators
|
|
162
|
+
// ============================================
|
|
163
|
+
function generateCSVContent(articles, columns, includeHtml) {
|
|
164
|
+
const defaultCols = ['title', 'slug', 'keyword', 'meta_description', 'word_count', 'model', 'status', 'category', 'tags', 'content'];
|
|
165
|
+
const activeCols = columns && columns.length > 0 ? columns : defaultCols;
|
|
166
|
+
const headerRow = activeCols.map(c => escapeCSVField(c)).join(',');
|
|
167
|
+
const dataRows = articles.map(article => {
|
|
168
|
+
return activeCols.map(col => {
|
|
169
|
+
let val = '';
|
|
170
|
+
switch (col) {
|
|
171
|
+
case 'title':
|
|
172
|
+
val = article.title || '';
|
|
173
|
+
break;
|
|
174
|
+
case 'slug':
|
|
175
|
+
val = article.slug || '';
|
|
176
|
+
break;
|
|
177
|
+
case 'keyword':
|
|
178
|
+
val = article.keyword || '';
|
|
179
|
+
break;
|
|
180
|
+
case 'meta_description':
|
|
181
|
+
val = article.meta_description || '';
|
|
182
|
+
break;
|
|
183
|
+
case 'content':
|
|
184
|
+
val = article.content || '';
|
|
185
|
+
break;
|
|
186
|
+
case 'content_html':
|
|
187
|
+
val = includeHtml ? (article.content || '') : '';
|
|
188
|
+
break;
|
|
189
|
+
case 'word_count':
|
|
190
|
+
val = String(article.word_count || '');
|
|
191
|
+
break;
|
|
192
|
+
case 'model':
|
|
193
|
+
val = article.model || '';
|
|
194
|
+
break;
|
|
195
|
+
case 'status':
|
|
196
|
+
val = article.status || 'draft';
|
|
197
|
+
break;
|
|
198
|
+
case 'category':
|
|
199
|
+
val = article.category || '';
|
|
200
|
+
break;
|
|
201
|
+
case 'tags':
|
|
202
|
+
val = article.tags || '';
|
|
203
|
+
break;
|
|
204
|
+
case 'author':
|
|
205
|
+
val = article.author || '';
|
|
206
|
+
break;
|
|
207
|
+
case 'created_at':
|
|
208
|
+
val = article.created_at || '';
|
|
209
|
+
break;
|
|
210
|
+
case 'citations':
|
|
211
|
+
val = (article.research_citations || []).join(' | ');
|
|
212
|
+
break;
|
|
213
|
+
case 'faq_schema':
|
|
214
|
+
val = article.faq_schema || '';
|
|
215
|
+
break;
|
|
216
|
+
default:
|
|
217
|
+
val = article[col] || '';
|
|
218
|
+
break;
|
|
219
|
+
}
|
|
220
|
+
return escapeCSVField(val);
|
|
221
|
+
}).join(',');
|
|
222
|
+
});
|
|
223
|
+
return [headerRow, ...dataRows].join('\n');
|
|
224
|
+
}
|
|
225
|
+
function generateMarkdownFile(article, projectName) {
|
|
226
|
+
const frontmatter = {
|
|
227
|
+
title: article.title,
|
|
228
|
+
};
|
|
229
|
+
if (article.slug)
|
|
230
|
+
frontmatter.slug = article.slug;
|
|
231
|
+
if (article.keyword)
|
|
232
|
+
frontmatter.keyword = article.keyword;
|
|
233
|
+
if (article.meta_description)
|
|
234
|
+
frontmatter.meta_description = article.meta_description;
|
|
235
|
+
if (article.created_at)
|
|
236
|
+
frontmatter.date = article.created_at.split('T')[0];
|
|
237
|
+
if (article.author)
|
|
238
|
+
frontmatter.author = article.author;
|
|
239
|
+
if (projectName)
|
|
240
|
+
frontmatter.project = projectName;
|
|
241
|
+
if (article.word_count)
|
|
242
|
+
frontmatter.word_count = article.word_count;
|
|
243
|
+
if (article.model)
|
|
244
|
+
frontmatter.model = article.model;
|
|
245
|
+
if (article.status)
|
|
246
|
+
frontmatter.status = article.status;
|
|
247
|
+
if (article.category)
|
|
248
|
+
frontmatter.category = article.category;
|
|
249
|
+
if (article.tags) {
|
|
250
|
+
frontmatter.tags = article.tags.split(',').map(t => t.trim()).filter(Boolean);
|
|
251
|
+
}
|
|
252
|
+
const yamlLines = Object.entries(frontmatter).map(([key, value]) => {
|
|
253
|
+
if (Array.isArray(value)) {
|
|
254
|
+
return `${key}:\n${value.map(v => ` - ${v}`).join('\n')}`;
|
|
255
|
+
}
|
|
256
|
+
if (typeof value === 'number')
|
|
257
|
+
return `${key}: ${value}`;
|
|
258
|
+
return `${key}: "${String(value).replace(/"/g, '\\"')}"`;
|
|
259
|
+
});
|
|
260
|
+
return `---\n${yamlLines.join('\n')}\n---\n\n${article.content}`;
|
|
261
|
+
}
|
|
262
|
+
function generateJSONContent(articles, projectName, pretty) {
|
|
263
|
+
const output = {
|
|
264
|
+
exported_at: new Date().toISOString(),
|
|
265
|
+
project: projectName || 'Default',
|
|
266
|
+
total_articles: articles.length,
|
|
267
|
+
articles: articles.map((a, idx) => ({
|
|
268
|
+
id: `art_${String(idx + 1).padStart(3, '0')}`,
|
|
269
|
+
title: a.title,
|
|
270
|
+
slug: a.slug || '',
|
|
271
|
+
keyword: a.keyword || '',
|
|
272
|
+
meta_description: a.meta_description || '',
|
|
273
|
+
content_markdown: a.content,
|
|
274
|
+
word_count: a.word_count || 0,
|
|
275
|
+
model: a.model || '',
|
|
276
|
+
status: a.status || 'draft',
|
|
277
|
+
category: a.category || '',
|
|
278
|
+
tags: a.tags ? a.tags.split(',').map(t => t.trim()) : [],
|
|
279
|
+
created_at: a.created_at || '',
|
|
280
|
+
research_citations: a.research_citations || [],
|
|
281
|
+
})),
|
|
282
|
+
};
|
|
283
|
+
return JSON.stringify(output, null, pretty !== false ? 2 : 0);
|
|
284
|
+
}
|
|
285
|
+
function slugify(text) {
|
|
286
|
+
return text
|
|
287
|
+
.toLowerCase()
|
|
288
|
+
.replace(/[^\w\s-]/g, '')
|
|
289
|
+
.replace(/[\s_]+/g, '-')
|
|
290
|
+
.replace(/-+/g, '-')
|
|
291
|
+
.replace(/^-|-$/g, '')
|
|
292
|
+
.slice(0, 80);
|
|
293
|
+
}
|
|
294
|
+
// ============================================
|
|
295
|
+
// TOOL REGISTRATION
|
|
296
|
+
// ============================================
|
|
297
|
+
export function registerCSVTools(server) {
|
|
298
|
+
// =====================
|
|
299
|
+
// import_csv
|
|
300
|
+
// =====================
|
|
301
|
+
server.tool('import_csv', 'Read a local CSV file and prepare it for batch generation. Auto-detects common column headers (keyword, title, slug, meta_description, etc.) or accepts manual mapping. Use preview_only to inspect the file before committing.', {
|
|
302
|
+
file_path: z.string().describe('Absolute path to the CSV file'),
|
|
303
|
+
column_map: z.record(z.string(), z.string()).optional().describe('Manual column mapping: { canonical_field: "Your CSV Header" }'),
|
|
304
|
+
delimiter: z.string().default(',').describe('CSV delimiter character (, ; or \\t)'),
|
|
305
|
+
encoding: z.string().default('utf-8').describe('File encoding'),
|
|
306
|
+
skip_rows: z.number().default(0).describe('Number of header/info rows to skip before the column headers'),
|
|
307
|
+
max_rows: z.number().optional().describe('Maximum number of rows to import'),
|
|
308
|
+
preview_only: z.boolean().default(false).describe('Preview first 5 rows without full import'),
|
|
309
|
+
}, async (args) => {
|
|
310
|
+
const ctx = getUserContext();
|
|
311
|
+
if (!ctx.authenticated) {
|
|
312
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Not authenticated. Check your BPAI_API_KEY.' }, null, 2) }] };
|
|
313
|
+
}
|
|
314
|
+
try {
|
|
315
|
+
// Read file
|
|
316
|
+
const raw = await fs.readFile(args.file_path, { encoding: args.encoding });
|
|
317
|
+
const { headers, rows } = parseCSV(raw, args.delimiter, args.skip_rows);
|
|
318
|
+
if (headers.length === 0) {
|
|
319
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'No columns found in file. Check the delimiter and skip_rows settings.' }, null, 2) }] };
|
|
320
|
+
}
|
|
321
|
+
// Detect columns
|
|
322
|
+
const autoDetected = detectColumns(headers);
|
|
323
|
+
const columnMapping = args.column_map
|
|
324
|
+
? { ...autoDetected, ...args.column_map }
|
|
325
|
+
: autoDetected;
|
|
326
|
+
// Map rows to canonical fields
|
|
327
|
+
const invertedMap = {};
|
|
328
|
+
for (const [canonical, csvHeader] of Object.entries(columnMapping)) {
|
|
329
|
+
invertedMap[csvHeader] = canonical;
|
|
330
|
+
}
|
|
331
|
+
const mappedRows = rows.map(row => {
|
|
332
|
+
const mapped = {};
|
|
333
|
+
for (const [header, value] of Object.entries(row)) {
|
|
334
|
+
const canonical = invertedMap[header];
|
|
335
|
+
if (canonical) {
|
|
336
|
+
mapped[canonical] = value;
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
mapped[header] = value;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return mapped;
|
|
343
|
+
});
|
|
344
|
+
// Apply limits
|
|
345
|
+
const limitedRows = args.max_rows ? mappedRows.slice(0, args.max_rows) : mappedRows;
|
|
346
|
+
const previewRows = args.preview_only ? limitedRows.slice(0, 5) : limitedRows;
|
|
347
|
+
// Column detection summary
|
|
348
|
+
const detectionSummary = {};
|
|
349
|
+
for (const [field, header] of Object.entries(columnMapping)) {
|
|
350
|
+
const isManual = args.column_map && field in args.column_map;
|
|
351
|
+
detectionSummary[field] = `${header} (${isManual ? 'manual override' : 'auto-detected'})`;
|
|
352
|
+
}
|
|
353
|
+
// Identify unmapped columns
|
|
354
|
+
const mappedHeaders = new Set(Object.values(columnMapping));
|
|
355
|
+
const unmapped = headers.filter(h => !mappedHeaders.has(h.trim()));
|
|
356
|
+
const result = {
|
|
357
|
+
status: 'success',
|
|
358
|
+
file: args.file_path,
|
|
359
|
+
rows_found: rows.length,
|
|
360
|
+
rows_imported: args.preview_only ? 0 : limitedRows.length,
|
|
361
|
+
columns_detected: detectionSummary,
|
|
362
|
+
unmapped_columns: unmapped.length > 0 ? unmapped : undefined,
|
|
363
|
+
preview: previewRows.slice(0, 5),
|
|
364
|
+
ready_for_generation: !!columnMapping.keyword,
|
|
365
|
+
};
|
|
366
|
+
if (args.preview_only) {
|
|
367
|
+
result.note = 'Preview mode — no rows imported. Remove preview_only to import.';
|
|
368
|
+
}
|
|
369
|
+
if (!args.preview_only) {
|
|
370
|
+
result.rows = limitedRows;
|
|
371
|
+
}
|
|
372
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
if (err.code === 'ENOENT') {
|
|
376
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `File not found: ${args.file_path}` }, null, 2) }] };
|
|
377
|
+
}
|
|
378
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `Failed to read CSV: ${err.message}` }, null, 2) }] };
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
// =====================
|
|
382
|
+
// export_csv
|
|
383
|
+
// =====================
|
|
384
|
+
server.tool('export_csv', 'Export articles as a CSV file. Provide the articles array from a previous generation or pass them directly. Supports column selection and status filtering.', {
|
|
385
|
+
output_path: z.string().describe('Absolute path where the CSV will be saved'),
|
|
386
|
+
articles: z.array(articleSchema).describe('Array of articles to export'),
|
|
387
|
+
columns: z.array(z.string()).optional().describe('Which columns to include (defaults to all standard columns)'),
|
|
388
|
+
filter_status: z.string().optional().describe('Filter by status: draft, published, or all'),
|
|
389
|
+
include_html: z.boolean().default(false).describe('Include HTML content column'),
|
|
390
|
+
include_markdown: z.boolean().default(true).describe('Include Markdown content column'),
|
|
391
|
+
}, async (args) => {
|
|
392
|
+
const ctx = getUserContext();
|
|
393
|
+
if (!ctx.authenticated) {
|
|
394
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Not authenticated. Check your BPAI_API_KEY.' }, null, 2) }] };
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const filtered = filterArticles(args.articles, { filter_status: args.filter_status });
|
|
398
|
+
if (filtered.length === 0) {
|
|
399
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'No articles match the given filters.' }, null, 2) }] };
|
|
400
|
+
}
|
|
401
|
+
const csvContent = generateCSVContent(filtered, args.columns, args.include_html);
|
|
402
|
+
// Ensure directory exists
|
|
403
|
+
await fs.mkdir(path.dirname(args.output_path), { recursive: true });
|
|
404
|
+
await fs.writeFile(args.output_path, csvContent, 'utf-8');
|
|
405
|
+
return {
|
|
406
|
+
content: [{
|
|
407
|
+
type: 'text', text: JSON.stringify({
|
|
408
|
+
status: 'success',
|
|
409
|
+
output_file: args.output_path,
|
|
410
|
+
articles_exported: filtered.length,
|
|
411
|
+
file_size: `${(Buffer.byteLength(csvContent) / 1024).toFixed(1)} KB`,
|
|
412
|
+
}, null, 2)
|
|
413
|
+
}],
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `CSV export failed: ${err.message}` }, null, 2) }] };
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
// =====================
|
|
421
|
+
// export_markdown
|
|
422
|
+
// =====================
|
|
423
|
+
server.tool('export_markdown', 'Export articles as individual Markdown files with YAML frontmatter. Each article becomes a separate .md file. Supports flat or nested folder structure.', {
|
|
424
|
+
output_dir: z.string().describe('Directory to write Markdown files to'),
|
|
425
|
+
articles: z.array(articleSchema).describe('Array of articles to export'),
|
|
426
|
+
project_name: z.string().optional().describe('Project name for frontmatter and nested folder structure'),
|
|
427
|
+
structure: z.enum(['flat', 'nested']).default('flat').describe('flat = all files in one folder, nested = grouped in project subfolder'),
|
|
428
|
+
filename_from: z.enum(['slug', 'title', 'keyword']).default('slug').describe('Which field to use for filenames'),
|
|
429
|
+
frontmatter: z.array(z.string()).optional().describe('Which fields to include in frontmatter (defaults to all)'),
|
|
430
|
+
filter_status: z.string().optional().describe('Filter by status: draft, published, or all'),
|
|
431
|
+
}, async (args) => {
|
|
432
|
+
const ctx = getUserContext();
|
|
433
|
+
if (!ctx.authenticated) {
|
|
434
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Not authenticated. Check your BPAI_API_KEY.' }, null, 2) }] };
|
|
435
|
+
}
|
|
436
|
+
try {
|
|
437
|
+
const filtered = filterArticles(args.articles, { filter_status: args.filter_status });
|
|
438
|
+
if (filtered.length === 0) {
|
|
439
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'No articles match the given filters.' }, null, 2) }] };
|
|
440
|
+
}
|
|
441
|
+
// Determine output directory
|
|
442
|
+
const baseDir = args.structure === 'nested' && args.project_name
|
|
443
|
+
? path.join(args.output_dir, slugify(args.project_name))
|
|
444
|
+
: args.output_dir;
|
|
445
|
+
await fs.mkdir(baseDir, { recursive: true });
|
|
446
|
+
const written = [];
|
|
447
|
+
for (const article of filtered) {
|
|
448
|
+
// Generate filename
|
|
449
|
+
let filename;
|
|
450
|
+
switch (args.filename_from) {
|
|
451
|
+
case 'title':
|
|
452
|
+
filename = slugify(article.title);
|
|
453
|
+
break;
|
|
454
|
+
case 'keyword':
|
|
455
|
+
filename = slugify(article.keyword || article.title);
|
|
456
|
+
break;
|
|
457
|
+
default:
|
|
458
|
+
filename = article.slug || slugify(article.title);
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
const filePath = path.join(baseDir, `${filename}.md`);
|
|
462
|
+
const mdContent = generateMarkdownFile(article, args.project_name);
|
|
463
|
+
await fs.writeFile(filePath, mdContent, 'utf-8');
|
|
464
|
+
written.push(filePath);
|
|
465
|
+
}
|
|
466
|
+
// Write manifest
|
|
467
|
+
const manifest = {
|
|
468
|
+
exported_at: new Date().toISOString(),
|
|
469
|
+
project: args.project_name || 'Default',
|
|
470
|
+
total_files: written.length,
|
|
471
|
+
files: written.map(f => path.basename(f)),
|
|
472
|
+
};
|
|
473
|
+
await fs.writeFile(path.join(args.output_dir, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf-8');
|
|
474
|
+
return {
|
|
475
|
+
content: [{
|
|
476
|
+
type: 'text', text: JSON.stringify({
|
|
477
|
+
status: 'success',
|
|
478
|
+
output_directory: baseDir,
|
|
479
|
+
files_written: written.length,
|
|
480
|
+
manifest: path.join(args.output_dir, 'manifest.json'),
|
|
481
|
+
files: written.map(f => path.basename(f)),
|
|
482
|
+
}, null, 2)
|
|
483
|
+
}],
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
catch (err) {
|
|
487
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `Markdown export failed: ${err.message}` }, null, 2) }] };
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
// =====================
|
|
491
|
+
// export_json
|
|
492
|
+
// =====================
|
|
493
|
+
server.tool('export_json', 'Export articles as structured JSON. Useful for custom integrations, static site generators, or API import workflows.', {
|
|
494
|
+
output_path: z.string().describe('File path for JSON output'),
|
|
495
|
+
articles: z.array(articleSchema).describe('Array of articles to export'),
|
|
496
|
+
project_name: z.string().optional().describe('Project name for the export metadata'),
|
|
497
|
+
pretty: z.boolean().default(true).describe('Pretty-print the JSON output'),
|
|
498
|
+
filter_status: z.string().optional().describe('Filter by status: draft, published, or all'),
|
|
499
|
+
}, async (args) => {
|
|
500
|
+
const ctx = getUserContext();
|
|
501
|
+
if (!ctx.authenticated) {
|
|
502
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Not authenticated. Check your BPAI_API_KEY.' }, null, 2) }] };
|
|
503
|
+
}
|
|
504
|
+
try {
|
|
505
|
+
const filtered = filterArticles(args.articles, { filter_status: args.filter_status });
|
|
506
|
+
if (filtered.length === 0) {
|
|
507
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'No articles match the given filters.' }, null, 2) }] };
|
|
508
|
+
}
|
|
509
|
+
const jsonContent = generateJSONContent(filtered, args.project_name, args.pretty);
|
|
510
|
+
await fs.mkdir(path.dirname(args.output_path), { recursive: true });
|
|
511
|
+
await fs.writeFile(args.output_path, jsonContent, 'utf-8');
|
|
512
|
+
return {
|
|
513
|
+
content: [{
|
|
514
|
+
type: 'text', text: JSON.stringify({
|
|
515
|
+
status: 'success',
|
|
516
|
+
output_file: args.output_path,
|
|
517
|
+
articles_exported: filtered.length,
|
|
518
|
+
file_size: `${(Buffer.byteLength(jsonContent) / 1024).toFixed(1)} KB`,
|
|
519
|
+
}, null, 2)
|
|
520
|
+
}],
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
catch (err) {
|
|
524
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `JSON export failed: ${err.message}` }, null, 2) }] };
|
|
525
|
+
}
|
|
526
|
+
});
|
|
527
|
+
// =====================
|
|
528
|
+
// export_bulk
|
|
529
|
+
// =====================
|
|
530
|
+
server.tool('export_bulk', 'Combined multi-format export with advanced filtering. Supports CSV, Markdown, JSON, and HTML output formats. Includes filters for status, model, date range, and minimum word count.', {
|
|
531
|
+
format: z.enum(['csv', 'markdown', 'json', 'html']).describe('Output format: csv, markdown, json, or html'),
|
|
532
|
+
output_path: z.string().describe('Output file path (csv/json/html) or directory (markdown)'),
|
|
533
|
+
articles: z.array(articleSchema).describe('Array of articles to export'),
|
|
534
|
+
project_name: z.string().optional().describe('Project name for metadata and folder structure'),
|
|
535
|
+
structure: z.enum(['flat', 'nested']).default('flat').describe('Folder structure for markdown exports'),
|
|
536
|
+
filter_status: z.string().optional().describe('Filter: draft, published, or all'),
|
|
537
|
+
filter_model: z.string().optional().describe('Filter by model name (partial match)'),
|
|
538
|
+
filter_date_from: z.string().optional().describe('Filter: start date (YYYY-MM-DD)'),
|
|
539
|
+
filter_date_to: z.string().optional().describe('Filter: end date (YYYY-MM-DD)'),
|
|
540
|
+
filter_min_words: z.number().optional().describe('Filter: minimum word count'),
|
|
541
|
+
include_metadata: z.boolean().default(true).describe('Include frontmatter/headers in output'),
|
|
542
|
+
columns: z.array(z.string()).optional().describe('Columns to include (CSV only)'),
|
|
543
|
+
}, async (args) => {
|
|
544
|
+
const ctx = getUserContext();
|
|
545
|
+
if (!ctx.authenticated) {
|
|
546
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: 'Not authenticated. Check your BPAI_API_KEY.' }, null, 2) }] };
|
|
547
|
+
}
|
|
548
|
+
try {
|
|
549
|
+
const filtered = filterArticles(args.articles, {
|
|
550
|
+
filter_status: args.filter_status,
|
|
551
|
+
filter_model: args.filter_model,
|
|
552
|
+
filter_date_from: args.filter_date_from,
|
|
553
|
+
filter_date_to: args.filter_date_to,
|
|
554
|
+
filter_min_words: args.filter_min_words,
|
|
555
|
+
});
|
|
556
|
+
if (filtered.length === 0) {
|
|
557
|
+
return {
|
|
558
|
+
content: [{
|
|
559
|
+
type: 'text', text: JSON.stringify({
|
|
560
|
+
error: 'No articles match the given filters.',
|
|
561
|
+
total_before_filter: args.articles.length,
|
|
562
|
+
filters_applied: {
|
|
563
|
+
status: args.filter_status || 'all',
|
|
564
|
+
model: args.filter_model || 'any',
|
|
565
|
+
date_from: args.filter_date_from || 'none',
|
|
566
|
+
date_to: args.filter_date_to || 'none',
|
|
567
|
+
min_words: args.filter_min_words || 'none',
|
|
568
|
+
},
|
|
569
|
+
}, null, 2)
|
|
570
|
+
}]
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
let outputInfo = {};
|
|
574
|
+
switch (args.format) {
|
|
575
|
+
case 'csv': {
|
|
576
|
+
const csvContent = generateCSVContent(filtered, args.columns);
|
|
577
|
+
await fs.mkdir(path.dirname(args.output_path), { recursive: true });
|
|
578
|
+
await fs.writeFile(args.output_path, csvContent, 'utf-8');
|
|
579
|
+
outputInfo = {
|
|
580
|
+
output_file: args.output_path,
|
|
581
|
+
file_size: `${(Buffer.byteLength(csvContent) / 1024).toFixed(1)} KB`,
|
|
582
|
+
};
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
case 'json': {
|
|
586
|
+
const jsonContent = generateJSONContent(filtered, args.project_name, true);
|
|
587
|
+
await fs.mkdir(path.dirname(args.output_path), { recursive: true });
|
|
588
|
+
await fs.writeFile(args.output_path, jsonContent, 'utf-8');
|
|
589
|
+
outputInfo = {
|
|
590
|
+
output_file: args.output_path,
|
|
591
|
+
file_size: `${(Buffer.byteLength(jsonContent) / 1024).toFixed(1)} KB`,
|
|
592
|
+
};
|
|
593
|
+
break;
|
|
594
|
+
}
|
|
595
|
+
case 'markdown': {
|
|
596
|
+
const baseDir = args.structure === 'nested' && args.project_name
|
|
597
|
+
? path.join(args.output_path, slugify(args.project_name))
|
|
598
|
+
: args.output_path;
|
|
599
|
+
await fs.mkdir(baseDir, { recursive: true });
|
|
600
|
+
const written = [];
|
|
601
|
+
for (const article of filtered) {
|
|
602
|
+
const filename = article.slug || slugify(article.title);
|
|
603
|
+
const filePath = path.join(baseDir, `${filename}.md`);
|
|
604
|
+
const md = generateMarkdownFile(article, args.project_name);
|
|
605
|
+
await fs.writeFile(filePath, md, 'utf-8');
|
|
606
|
+
written.push(path.basename(filePath));
|
|
607
|
+
}
|
|
608
|
+
const manifest = {
|
|
609
|
+
exported_at: new Date().toISOString(),
|
|
610
|
+
project: args.project_name || 'Default',
|
|
611
|
+
total_files: written.length,
|
|
612
|
+
files: written,
|
|
613
|
+
};
|
|
614
|
+
await fs.writeFile(path.join(args.output_path, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf-8');
|
|
615
|
+
outputInfo = {
|
|
616
|
+
output_directory: baseDir,
|
|
617
|
+
files: written,
|
|
618
|
+
manifest: path.join(args.output_path, 'manifest.json'),
|
|
619
|
+
};
|
|
620
|
+
break;
|
|
621
|
+
}
|
|
622
|
+
case 'html': {
|
|
623
|
+
// Generate a single HTML file with all articles
|
|
624
|
+
const htmlParts = filtered.map(a => {
|
|
625
|
+
const meta = args.include_metadata
|
|
626
|
+
? `<div class="article-meta"><span>${a.keyword || ''}</span> | <span>${a.word_count || 0} words</span> | <span>${a.model || ''}</span></div>\n`
|
|
627
|
+
: '';
|
|
628
|
+
return `<article>\n<h1>${a.title}</h1>\n${meta}<div class="content">\n${a.content}\n</div>\n</article>`;
|
|
629
|
+
});
|
|
630
|
+
const htmlContent = `<!DOCTYPE html>
|
|
631
|
+
<html lang="en">
|
|
632
|
+
<head>
|
|
633
|
+
<meta charset="UTF-8">
|
|
634
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
635
|
+
<title>${args.project_name || 'Exported Articles'}</title>
|
|
636
|
+
<style>
|
|
637
|
+
body { font-family: system-ui, sans-serif; max-width: 800px; margin: 0 auto; padding: 2rem; }
|
|
638
|
+
article { margin-bottom: 3rem; padding-bottom: 2rem; border-bottom: 1px solid #e2e8f0; }
|
|
639
|
+
.article-meta { color: #64748b; font-size: 0.875rem; margin-bottom: 1rem; }
|
|
640
|
+
h1 { color: #0f172a; }
|
|
641
|
+
</style>
|
|
642
|
+
</head>
|
|
643
|
+
<body>
|
|
644
|
+
${htmlParts.join('\n\n')}
|
|
645
|
+
</body>
|
|
646
|
+
</html>`;
|
|
647
|
+
await fs.mkdir(path.dirname(args.output_path), { recursive: true });
|
|
648
|
+
await fs.writeFile(args.output_path, htmlContent, 'utf-8');
|
|
649
|
+
outputInfo = {
|
|
650
|
+
output_file: args.output_path,
|
|
651
|
+
file_size: `${(Buffer.byteLength(htmlContent) / 1024).toFixed(1)} KB`,
|
|
652
|
+
};
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return {
|
|
657
|
+
content: [{
|
|
658
|
+
type: 'text', text: JSON.stringify({
|
|
659
|
+
status: 'success',
|
|
660
|
+
format: args.format,
|
|
661
|
+
articles_exported: filtered.length,
|
|
662
|
+
total_before_filter: args.articles.length,
|
|
663
|
+
...outputInfo,
|
|
664
|
+
}, null, 2)
|
|
665
|
+
}],
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
catch (err) {
|
|
669
|
+
return { content: [{ type: 'text', text: JSON.stringify({ error: `Bulk export failed: ${err.message}` }, null, 2) }] };
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
//# sourceMappingURL=csv.js.map
|