pulsemcp-cms-admin-mcp-server 0.9.27 → 0.10.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/build/local/src/index.integration-with-mock.js +20 -6
- package/build/shared/src/pulsemcp-admin-client/lib/create-post.js +5 -1
- package/build/shared/src/pulsemcp-admin-client/lib/get-posts.js +1 -1
- package/build/shared/src/pulsemcp-admin-client/lib/update-post.js +7 -2
- package/build/shared/src/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +8 -5
- package/build/shared/src/tools/draft-newsletter-post.js +8 -4
- package/build/shared/src/tools/get-newsletter-post.js +21 -13
- package/build/shared/src/tools/get-newsletter-posts.js +17 -9
- package/build/shared/src/tools/update-newsletter-post.js +5 -2
- package/package.json +2 -2
- package/shared/pulsemcp-admin-client/lib/create-post.js +5 -1
- package/shared/pulsemcp-admin-client/lib/get-posts.js +1 -1
- package/shared/pulsemcp-admin-client/lib/update-post.js +7 -2
- package/shared/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +8 -5
- package/shared/tools/draft-newsletter-post.js +8 -4
- package/shared/tools/get-newsletter-post.js +21 -13
- package/shared/tools/get-newsletter-posts.js +17 -9
- package/shared/tools/update-newsletter-post.js +5 -2
- package/shared/types.d.ts +4 -4
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Mock data is passed via the PULSEMCP_MOCK_DATA environment variable.
|
|
9
9
|
*/
|
|
10
|
-
import { readFileSync } from 'fs';
|
|
10
|
+
import { existsSync, readFileSync } from 'fs';
|
|
11
11
|
import { dirname, join } from 'path';
|
|
12
12
|
import { fileURLToPath } from 'url';
|
|
13
13
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
@@ -15,11 +15,25 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
15
15
|
import { createMCPServer } from 'pulsemcp-cms-admin-mcp-server-shared';
|
|
16
16
|
// Import the mock client factory from the shared module
|
|
17
17
|
import { createMockPulseMCPAdminClient } from '../../shared/src/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js';
|
|
18
|
-
// Read version from package.json
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
18
|
+
// Read version from package.json. The integration build emits this entry into a
|
|
19
|
+
// nested build/ layout (build/local/src/), so walk up from the module directory
|
|
20
|
+
// to locate the package.json that carries the version rather than assuming a
|
|
21
|
+
// fixed depth.
|
|
22
|
+
function readVersion() {
|
|
23
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
for (let i = 0; i < 6; i++) {
|
|
25
|
+
const candidate = join(dir, 'package.json');
|
|
26
|
+
if (existsSync(candidate)) {
|
|
27
|
+
return JSON.parse(readFileSync(candidate, 'utf-8')).version;
|
|
28
|
+
}
|
|
29
|
+
const parent = dirname(dir);
|
|
30
|
+
if (parent === dir)
|
|
31
|
+
break;
|
|
32
|
+
dir = parent;
|
|
33
|
+
}
|
|
34
|
+
return '0.0.0';
|
|
35
|
+
}
|
|
36
|
+
const VERSION = readVersion();
|
|
23
37
|
async function main() {
|
|
24
38
|
const transport = new StdioServerTransport();
|
|
25
39
|
// Parse mock data from environment variable
|
|
@@ -7,7 +7,11 @@ export async function createPost(apiKey, baseUrl, params) {
|
|
|
7
7
|
formData.append('post[title]', params.title);
|
|
8
8
|
formData.append('post[body]', params.body);
|
|
9
9
|
formData.append('post[slug]', params.slug);
|
|
10
|
-
|
|
10
|
+
// Rails permits only post[author_ids][] — send one entry per author id, in
|
|
11
|
+
// order (index 0 is the primary author).
|
|
12
|
+
params.author_ids.forEach((id) => {
|
|
13
|
+
formData.append('post[author_ids][]', id.toString());
|
|
14
|
+
});
|
|
11
15
|
// Optional fields
|
|
12
16
|
if (params.status)
|
|
13
17
|
formData.append('post[status]', params.status);
|
|
@@ -43,7 +43,7 @@ export async function getPosts(apiKey, baseUrl, params) {
|
|
|
43
43
|
short_description: post.short_description,
|
|
44
44
|
category: post.category,
|
|
45
45
|
status: post.status,
|
|
46
|
-
|
|
46
|
+
author_ids: post.author_ids,
|
|
47
47
|
created_at: post.created_at,
|
|
48
48
|
updated_at: post.updated_at,
|
|
49
49
|
last_updated: post.last_updated,
|
|
@@ -10,8 +10,13 @@ export async function updatePost(apiKey, baseUrl, slug, params) {
|
|
|
10
10
|
formData.append('post[body]', params.body);
|
|
11
11
|
if (params.slug !== undefined)
|
|
12
12
|
formData.append('post[slug]', params.slug);
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// Rails permits only post[author_ids][] — send one entry per author id, in
|
|
14
|
+
// order (index 0 is the primary author).
|
|
15
|
+
if (params.author_ids !== undefined) {
|
|
16
|
+
params.author_ids.forEach((id) => {
|
|
17
|
+
formData.append('post[author_ids][]', id.toString());
|
|
18
|
+
});
|
|
19
|
+
}
|
|
15
20
|
if (params.status !== undefined)
|
|
16
21
|
formData.append('post[status]', params.status);
|
|
17
22
|
if (params.category !== undefined)
|
|
@@ -4,15 +4,18 @@ export function createMockPulseMCPAdminClient(mockData) {
|
|
|
4
4
|
title: 'Test Post',
|
|
5
5
|
body: '<p>Test content</p>',
|
|
6
6
|
slug: 'test-post',
|
|
7
|
-
author_id: 1,
|
|
8
7
|
status: 'draft',
|
|
9
8
|
category: 'newsletter',
|
|
10
9
|
created_at: '2024-01-01T00:00:00Z',
|
|
11
10
|
updated_at: '2024-01-01T00:00:00Z',
|
|
12
|
-
author
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
// Ordered author ids from the list endpoint (no names).
|
|
12
|
+
author_ids: [1, 2],
|
|
13
|
+
// Ordered authors array (id + name) from the show endpoint; source of truth
|
|
14
|
+
// for reads.
|
|
15
|
+
authors: [
|
|
16
|
+
{ id: 1, name: 'Test Author' },
|
|
17
|
+
{ id: 2, name: 'Second Author' },
|
|
18
|
+
],
|
|
16
19
|
};
|
|
17
20
|
return {
|
|
18
21
|
async getPosts(params) {
|
|
@@ -166,10 +166,11 @@ Use cases:
|
|
|
166
166
|
const { author_slug, featured_mcp_server_slugs, featured_mcp_client_slugs, ...otherParams } = validatedArgs;
|
|
167
167
|
// Look up author by slug
|
|
168
168
|
const author = await client.getAuthorBySlug(author_slug);
|
|
169
|
-
// Always create as draft
|
|
169
|
+
// Always create as draft. author_ids is an ordered array; this tool sets
|
|
170
|
+
// a single primary author (length 1).
|
|
170
171
|
const createParams = {
|
|
171
172
|
...otherParams,
|
|
172
|
-
|
|
173
|
+
author_ids: [author.id],
|
|
173
174
|
status: 'draft',
|
|
174
175
|
};
|
|
175
176
|
// Convert slugs to IDs if provided
|
|
@@ -192,8 +193,11 @@ Use cases:
|
|
|
192
193
|
content += `**Slug:** ${post.slug}\n`;
|
|
193
194
|
content += `**Status:** ${post.status}\n`;
|
|
194
195
|
content += `**Category:** ${post.category}\n`;
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
// The ordered `authors` array is the source of truth for authorship.
|
|
197
|
+
const responseAuthors = post.authors ?? [];
|
|
198
|
+
if (responseAuthors.length > 0) {
|
|
199
|
+
const label = responseAuthors.length > 1 ? 'Authors' : 'Author';
|
|
200
|
+
content += `**${label}:** ${responseAuthors.map((a) => a.name).join(', ')}\n`;
|
|
197
201
|
}
|
|
198
202
|
content += `**Created:** ${new Date(post.created_at).toLocaleDateString()}\n\n`;
|
|
199
203
|
if (post.short_description) {
|
|
@@ -12,7 +12,7 @@ export function getNewsletterPost(_server, clientFactory) {
|
|
|
12
12
|
description: `Retrieve complete details for a specific newsletter post by its unique slug identifier. Returns formatted markdown with all post metadata and content.
|
|
13
13
|
|
|
14
14
|
The response is formatted as markdown with sections for:
|
|
15
|
-
- Post title and basic metadata (slug, status, category, author, dates)
|
|
15
|
+
- Post title and basic metadata (slug, status, category, author(s), dates)
|
|
16
16
|
- Summary/short description
|
|
17
17
|
- Full HTML content (body field) as raw HTML
|
|
18
18
|
- Complete metadata including all URLs, SEO tags, and featured items
|
|
@@ -20,7 +20,7 @@ The response is formatted as markdown with sections for:
|
|
|
20
20
|
|
|
21
21
|
All available fields from the post are included:
|
|
22
22
|
- title, slug, status, category
|
|
23
|
-
-
|
|
23
|
+
- authors (ordered list; the primary author is listed first)
|
|
24
24
|
- created_at, updated_at, last_updated
|
|
25
25
|
- short_title, short_description
|
|
26
26
|
- body (raw HTML content)
|
|
@@ -55,28 +55,36 @@ Use cases:
|
|
|
55
55
|
const client = clientFactory();
|
|
56
56
|
try {
|
|
57
57
|
const post = await client.getPost(validatedArgs.slug);
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
// The supervisor show endpoint returns the ordered `authors` array (id +
|
|
59
|
+
// name), which is the source of truth for authorship.
|
|
60
|
+
const baseAuthors = post.authors ?? [];
|
|
61
|
+
// Enrich each author with its slug (the authors array only carries id +
|
|
62
|
+
// name). Preserve position order so the primary author renders first.
|
|
63
|
+
const authorLines = [];
|
|
64
|
+
for (const a of baseAuthors) {
|
|
65
|
+
let slug;
|
|
66
|
+
let name = a.name;
|
|
62
67
|
try {
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
const fullAuthor = await client.getAuthorById(a.id);
|
|
69
|
+
if (fullAuthor) {
|
|
70
|
+
slug = fullAuthor.slug;
|
|
71
|
+
if (!name)
|
|
72
|
+
name = fullAuthor.name;
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
75
|
catch (error) {
|
|
70
|
-
// If we can't fetch author,
|
|
76
|
+
// If we can't fetch the author, fall back to whatever name we have.
|
|
71
77
|
console.error('Failed to fetch author details:', error);
|
|
72
78
|
}
|
|
79
|
+
authorLines.push(slug ? `${name} (${slug}, ID: ${a.id})` : `${name} (ID: ${a.id})`);
|
|
73
80
|
}
|
|
74
81
|
// Format the response for MCP
|
|
75
82
|
let content = `# ${post.title}\n\n`;
|
|
76
83
|
content += `**Slug:** ${post.slug}\n`;
|
|
77
84
|
content += `**Status:** ${post.status} | **Category:** ${post.category}\n`;
|
|
78
|
-
if (
|
|
79
|
-
|
|
85
|
+
if (authorLines.length > 0) {
|
|
86
|
+
const label = authorLines.length > 1 ? 'Authors' : 'Author';
|
|
87
|
+
content += `**${label}:** ${authorLines.join(', ')}\n`;
|
|
80
88
|
}
|
|
81
89
|
content += `**Created:** ${new Date(post.created_at).toLocaleDateString()}\n`;
|
|
82
90
|
content += `**Updated:** ${new Date(post.updated_at).toLocaleDateString()}\n`;
|
|
@@ -75,17 +75,25 @@ Use cases:
|
|
|
75
75
|
for (const [index, post] of response.posts.entries()) {
|
|
76
76
|
content += `${index + 1}. **${post.title}** (${post.slug})\n`;
|
|
77
77
|
content += ` Status: ${post.status} | Category: ${post.category}\n`;
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
// The list endpoint returns ordered author ids (no names); resolve each
|
|
79
|
+
// to a name, preserving order so the primary author renders first.
|
|
80
|
+
if (post.author_ids && post.author_ids.length > 0) {
|
|
81
|
+
const authorInfo = [];
|
|
82
|
+
for (const authorId of post.author_ids) {
|
|
83
|
+
try {
|
|
84
|
+
const author = await client.getAuthorById(authorId);
|
|
85
|
+
if (author) {
|
|
86
|
+
authorInfo.push(`${author.name} (${author.slug}, ID: ${author.id})`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
// Skip showing this author if we can't fetch it
|
|
91
|
+
console.error(`Failed to fetch author ${authorId}:`, error);
|
|
84
92
|
}
|
|
85
93
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
if (authorInfo.length > 0) {
|
|
95
|
+
const label = authorInfo.length > 1 ? 'Authors' : 'Author';
|
|
96
|
+
content += ` ${label}: ${authorInfo.join(', ')}\n`;
|
|
89
97
|
}
|
|
90
98
|
}
|
|
91
99
|
content += ` Created: ${new Date(post.created_at).toLocaleDateString()}\n`;
|
|
@@ -187,8 +187,11 @@ Use cases:
|
|
|
187
187
|
content += `**Slug:** ${post.slug}\n`;
|
|
188
188
|
content += `**Status:** ${post.status}\n`;
|
|
189
189
|
content += `**Category:** ${post.category}\n`;
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
// The ordered `authors` array is the source of truth for authorship.
|
|
191
|
+
const responseAuthors = post.authors ?? [];
|
|
192
|
+
if (responseAuthors.length > 0) {
|
|
193
|
+
const label = responseAuthors.length > 1 ? 'Authors' : 'Author';
|
|
194
|
+
content += `**${label}:** ${responseAuthors.map((a) => a.name).join(', ')}\n`;
|
|
192
195
|
}
|
|
193
196
|
content += `**Updated:** ${new Date(post.updated_at).toLocaleDateString()}\n\n`;
|
|
194
197
|
// Show what was updated
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pulsemcp-cms-admin-mcp-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Local implementation of PulseMCP CMS Admin MCP server",
|
|
5
|
-
"mcpName": "com.pulsemcp
|
|
5
|
+
"mcpName": "com.pulsemcp/pulsemcp-cms-admin",
|
|
6
6
|
"main": "build/index.js",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"bin": {
|
|
@@ -7,7 +7,11 @@ export async function createPost(apiKey, baseUrl, params) {
|
|
|
7
7
|
formData.append('post[title]', params.title);
|
|
8
8
|
formData.append('post[body]', params.body);
|
|
9
9
|
formData.append('post[slug]', params.slug);
|
|
10
|
-
|
|
10
|
+
// Rails permits only post[author_ids][] — send one entry per author id, in
|
|
11
|
+
// order (index 0 is the primary author).
|
|
12
|
+
params.author_ids.forEach((id) => {
|
|
13
|
+
formData.append('post[author_ids][]', id.toString());
|
|
14
|
+
});
|
|
11
15
|
// Optional fields
|
|
12
16
|
if (params.status)
|
|
13
17
|
formData.append('post[status]', params.status);
|
|
@@ -43,7 +43,7 @@ export async function getPosts(apiKey, baseUrl, params) {
|
|
|
43
43
|
short_description: post.short_description,
|
|
44
44
|
category: post.category,
|
|
45
45
|
status: post.status,
|
|
46
|
-
|
|
46
|
+
author_ids: post.author_ids,
|
|
47
47
|
created_at: post.created_at,
|
|
48
48
|
updated_at: post.updated_at,
|
|
49
49
|
last_updated: post.last_updated,
|
|
@@ -10,8 +10,13 @@ export async function updatePost(apiKey, baseUrl, slug, params) {
|
|
|
10
10
|
formData.append('post[body]', params.body);
|
|
11
11
|
if (params.slug !== undefined)
|
|
12
12
|
formData.append('post[slug]', params.slug);
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
// Rails permits only post[author_ids][] — send one entry per author id, in
|
|
14
|
+
// order (index 0 is the primary author).
|
|
15
|
+
if (params.author_ids !== undefined) {
|
|
16
|
+
params.author_ids.forEach((id) => {
|
|
17
|
+
formData.append('post[author_ids][]', id.toString());
|
|
18
|
+
});
|
|
19
|
+
}
|
|
15
20
|
if (params.status !== undefined)
|
|
16
21
|
formData.append('post[status]', params.status);
|
|
17
22
|
if (params.category !== undefined)
|
|
@@ -4,15 +4,18 @@ export function createMockPulseMCPAdminClient(mockData) {
|
|
|
4
4
|
title: 'Test Post',
|
|
5
5
|
body: '<p>Test content</p>',
|
|
6
6
|
slug: 'test-post',
|
|
7
|
-
author_id: 1,
|
|
8
7
|
status: 'draft',
|
|
9
8
|
category: 'newsletter',
|
|
10
9
|
created_at: '2024-01-01T00:00:00Z',
|
|
11
10
|
updated_at: '2024-01-01T00:00:00Z',
|
|
12
|
-
author
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
// Ordered author ids from the list endpoint (no names).
|
|
12
|
+
author_ids: [1, 2],
|
|
13
|
+
// Ordered authors array (id + name) from the show endpoint; source of truth
|
|
14
|
+
// for reads.
|
|
15
|
+
authors: [
|
|
16
|
+
{ id: 1, name: 'Test Author' },
|
|
17
|
+
{ id: 2, name: 'Second Author' },
|
|
18
|
+
],
|
|
16
19
|
};
|
|
17
20
|
return {
|
|
18
21
|
async getPosts(params) {
|
|
@@ -166,10 +166,11 @@ Use cases:
|
|
|
166
166
|
const { author_slug, featured_mcp_server_slugs, featured_mcp_client_slugs, ...otherParams } = validatedArgs;
|
|
167
167
|
// Look up author by slug
|
|
168
168
|
const author = await client.getAuthorBySlug(author_slug);
|
|
169
|
-
// Always create as draft
|
|
169
|
+
// Always create as draft. author_ids is an ordered array; this tool sets
|
|
170
|
+
// a single primary author (length 1).
|
|
170
171
|
const createParams = {
|
|
171
172
|
...otherParams,
|
|
172
|
-
|
|
173
|
+
author_ids: [author.id],
|
|
173
174
|
status: 'draft',
|
|
174
175
|
};
|
|
175
176
|
// Convert slugs to IDs if provided
|
|
@@ -192,8 +193,11 @@ Use cases:
|
|
|
192
193
|
content += `**Slug:** ${post.slug}\n`;
|
|
193
194
|
content += `**Status:** ${post.status}\n`;
|
|
194
195
|
content += `**Category:** ${post.category}\n`;
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
// The ordered `authors` array is the source of truth for authorship.
|
|
197
|
+
const responseAuthors = post.authors ?? [];
|
|
198
|
+
if (responseAuthors.length > 0) {
|
|
199
|
+
const label = responseAuthors.length > 1 ? 'Authors' : 'Author';
|
|
200
|
+
content += `**${label}:** ${responseAuthors.map((a) => a.name).join(', ')}\n`;
|
|
197
201
|
}
|
|
198
202
|
content += `**Created:** ${new Date(post.created_at).toLocaleDateString()}\n\n`;
|
|
199
203
|
if (post.short_description) {
|
|
@@ -12,7 +12,7 @@ export function getNewsletterPost(_server, clientFactory) {
|
|
|
12
12
|
description: `Retrieve complete details for a specific newsletter post by its unique slug identifier. Returns formatted markdown with all post metadata and content.
|
|
13
13
|
|
|
14
14
|
The response is formatted as markdown with sections for:
|
|
15
|
-
- Post title and basic metadata (slug, status, category, author, dates)
|
|
15
|
+
- Post title and basic metadata (slug, status, category, author(s), dates)
|
|
16
16
|
- Summary/short description
|
|
17
17
|
- Full HTML content (body field) as raw HTML
|
|
18
18
|
- Complete metadata including all URLs, SEO tags, and featured items
|
|
@@ -20,7 +20,7 @@ The response is formatted as markdown with sections for:
|
|
|
20
20
|
|
|
21
21
|
All available fields from the post are included:
|
|
22
22
|
- title, slug, status, category
|
|
23
|
-
-
|
|
23
|
+
- authors (ordered list; the primary author is listed first)
|
|
24
24
|
- created_at, updated_at, last_updated
|
|
25
25
|
- short_title, short_description
|
|
26
26
|
- body (raw HTML content)
|
|
@@ -55,28 +55,36 @@ Use cases:
|
|
|
55
55
|
const client = clientFactory();
|
|
56
56
|
try {
|
|
57
57
|
const post = await client.getPost(validatedArgs.slug);
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
// The supervisor show endpoint returns the ordered `authors` array (id +
|
|
59
|
+
// name), which is the source of truth for authorship.
|
|
60
|
+
const baseAuthors = post.authors ?? [];
|
|
61
|
+
// Enrich each author with its slug (the authors array only carries id +
|
|
62
|
+
// name). Preserve position order so the primary author renders first.
|
|
63
|
+
const authorLines = [];
|
|
64
|
+
for (const a of baseAuthors) {
|
|
65
|
+
let slug;
|
|
66
|
+
let name = a.name;
|
|
62
67
|
try {
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
const fullAuthor = await client.getAuthorById(a.id);
|
|
69
|
+
if (fullAuthor) {
|
|
70
|
+
slug = fullAuthor.slug;
|
|
71
|
+
if (!name)
|
|
72
|
+
name = fullAuthor.name;
|
|
67
73
|
}
|
|
68
74
|
}
|
|
69
75
|
catch (error) {
|
|
70
|
-
// If we can't fetch author,
|
|
76
|
+
// If we can't fetch the author, fall back to whatever name we have.
|
|
71
77
|
console.error('Failed to fetch author details:', error);
|
|
72
78
|
}
|
|
79
|
+
authorLines.push(slug ? `${name} (${slug}, ID: ${a.id})` : `${name} (ID: ${a.id})`);
|
|
73
80
|
}
|
|
74
81
|
// Format the response for MCP
|
|
75
82
|
let content = `# ${post.title}\n\n`;
|
|
76
83
|
content += `**Slug:** ${post.slug}\n`;
|
|
77
84
|
content += `**Status:** ${post.status} | **Category:** ${post.category}\n`;
|
|
78
|
-
if (
|
|
79
|
-
|
|
85
|
+
if (authorLines.length > 0) {
|
|
86
|
+
const label = authorLines.length > 1 ? 'Authors' : 'Author';
|
|
87
|
+
content += `**${label}:** ${authorLines.join(', ')}\n`;
|
|
80
88
|
}
|
|
81
89
|
content += `**Created:** ${new Date(post.created_at).toLocaleDateString()}\n`;
|
|
82
90
|
content += `**Updated:** ${new Date(post.updated_at).toLocaleDateString()}\n`;
|
|
@@ -75,17 +75,25 @@ Use cases:
|
|
|
75
75
|
for (const [index, post] of response.posts.entries()) {
|
|
76
76
|
content += `${index + 1}. **${post.title}** (${post.slug})\n`;
|
|
77
77
|
content += ` Status: ${post.status} | Category: ${post.category}\n`;
|
|
78
|
-
//
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
78
|
+
// The list endpoint returns ordered author ids (no names); resolve each
|
|
79
|
+
// to a name, preserving order so the primary author renders first.
|
|
80
|
+
if (post.author_ids && post.author_ids.length > 0) {
|
|
81
|
+
const authorInfo = [];
|
|
82
|
+
for (const authorId of post.author_ids) {
|
|
83
|
+
try {
|
|
84
|
+
const author = await client.getAuthorById(authorId);
|
|
85
|
+
if (author) {
|
|
86
|
+
authorInfo.push(`${author.name} (${author.slug}, ID: ${author.id})`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
// Skip showing this author if we can't fetch it
|
|
91
|
+
console.error(`Failed to fetch author ${authorId}:`, error);
|
|
84
92
|
}
|
|
85
93
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
if (authorInfo.length > 0) {
|
|
95
|
+
const label = authorInfo.length > 1 ? 'Authors' : 'Author';
|
|
96
|
+
content += ` ${label}: ${authorInfo.join(', ')}\n`;
|
|
89
97
|
}
|
|
90
98
|
}
|
|
91
99
|
content += ` Created: ${new Date(post.created_at).toLocaleDateString()}\n`;
|
|
@@ -187,8 +187,11 @@ Use cases:
|
|
|
187
187
|
content += `**Slug:** ${post.slug}\n`;
|
|
188
188
|
content += `**Status:** ${post.status}\n`;
|
|
189
189
|
content += `**Category:** ${post.category}\n`;
|
|
190
|
-
|
|
191
|
-
|
|
190
|
+
// The ordered `authors` array is the source of truth for authorship.
|
|
191
|
+
const responseAuthors = post.authors ?? [];
|
|
192
|
+
if (responseAuthors.length > 0) {
|
|
193
|
+
const label = responseAuthors.length > 1 ? 'Authors' : 'Author';
|
|
194
|
+
content += `**${label}:** ${responseAuthors.map((a) => a.name).join(', ')}\n`;
|
|
192
195
|
}
|
|
193
196
|
content += `**Updated:** ${new Date(post.updated_at).toLocaleDateString()}\n\n`;
|
|
194
197
|
// Show what was updated
|
package/shared/types.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export interface Post {
|
|
|
3
3
|
title: string;
|
|
4
4
|
body?: string;
|
|
5
5
|
slug: string;
|
|
6
|
-
|
|
6
|
+
author_ids?: number[];
|
|
7
7
|
status: 'draft' | 'live' | string;
|
|
8
8
|
category: 'newsletter' | 'other' | string;
|
|
9
9
|
image_url?: string;
|
|
@@ -19,10 +19,10 @@ export interface Post {
|
|
|
19
19
|
featured_mcp_client_ids?: number[];
|
|
20
20
|
created_at: string;
|
|
21
21
|
updated_at: string;
|
|
22
|
-
|
|
22
|
+
authors?: Array<{
|
|
23
23
|
id: number;
|
|
24
24
|
name: string;
|
|
25
|
-
}
|
|
25
|
+
}>;
|
|
26
26
|
}
|
|
27
27
|
export interface PostsResponse {
|
|
28
28
|
posts: Post[];
|
|
@@ -36,7 +36,7 @@ export interface CreatePostParams {
|
|
|
36
36
|
title: string;
|
|
37
37
|
body: string;
|
|
38
38
|
slug: string;
|
|
39
|
-
|
|
39
|
+
author_ids: number[];
|
|
40
40
|
status?: 'draft' | 'live';
|
|
41
41
|
category?: 'newsletter' | 'other';
|
|
42
42
|
image_url?: string;
|