pulsemcp-cms-admin-mcp-server 0.0.2

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.
Files changed (69) hide show
  1. package/README.md +169 -0
  2. package/build/index.js +48 -0
  3. package/build/shared/src/index.js +2 -0
  4. package/build/shared/src/logging.js +34 -0
  5. package/build/shared/src/pulsemcp-admin-client/lib/create-post.js +69 -0
  6. package/build/shared/src/pulsemcp-admin-client/lib/get-author-by-slug.js +26 -0
  7. package/build/shared/src/pulsemcp-admin-client/lib/get-authors.js +38 -0
  8. package/build/shared/src/pulsemcp-admin-client/lib/get-mcp-client-by-slug.js +26 -0
  9. package/build/shared/src/pulsemcp-admin-client/lib/get-mcp-server-by-slug.js +26 -0
  10. package/build/shared/src/pulsemcp-admin-client/lib/get-post.js +26 -0
  11. package/build/shared/src/pulsemcp-admin-client/lib/get-posts.js +62 -0
  12. package/build/shared/src/pulsemcp-admin-client/lib/update-post.js +75 -0
  13. package/build/shared/src/pulsemcp-admin-client/lib/upload-image.js +36 -0
  14. package/build/shared/src/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +158 -0
  15. package/build/shared/src/pulsemcp-admin-client/pulsemcp-admin-client.js +1 -0
  16. package/build/shared/src/server.js +71 -0
  17. package/build/shared/src/tools/draft-newsletter-post.js +225 -0
  18. package/build/shared/src/tools/get-authors.js +115 -0
  19. package/build/shared/src/tools/get-newsletter-post.js +127 -0
  20. package/build/shared/src/tools/get-newsletter-posts.js +127 -0
  21. package/build/shared/src/tools/update-newsletter-post.js +232 -0
  22. package/build/shared/src/tools/upload-image.js +106 -0
  23. package/build/shared/src/tools.js +58 -0
  24. package/build/shared/src/types.js +1 -0
  25. package/package.json +43 -0
  26. package/shared/index.d.ts +4 -0
  27. package/shared/index.js +2 -0
  28. package/shared/logging.d.ts +20 -0
  29. package/shared/logging.js +34 -0
  30. package/shared/pulsemcp-admin-client/lib/create-post.d.ts +3 -0
  31. package/shared/pulsemcp-admin-client/lib/create-post.js +69 -0
  32. package/shared/pulsemcp-admin-client/lib/get-author-by-slug.d.ts +3 -0
  33. package/shared/pulsemcp-admin-client/lib/get-author-by-slug.js +26 -0
  34. package/shared/pulsemcp-admin-client/lib/get-authors.d.ts +6 -0
  35. package/shared/pulsemcp-admin-client/lib/get-authors.js +38 -0
  36. package/shared/pulsemcp-admin-client/lib/get-mcp-client-by-slug.d.ts +3 -0
  37. package/shared/pulsemcp-admin-client/lib/get-mcp-client-by-slug.js +26 -0
  38. package/shared/pulsemcp-admin-client/lib/get-mcp-server-by-slug.d.ts +3 -0
  39. package/shared/pulsemcp-admin-client/lib/get-mcp-server-by-slug.js +26 -0
  40. package/shared/pulsemcp-admin-client/lib/get-post.d.ts +3 -0
  41. package/shared/pulsemcp-admin-client/lib/get-post.js +26 -0
  42. package/shared/pulsemcp-admin-client/lib/get-posts.d.ts +8 -0
  43. package/shared/pulsemcp-admin-client/lib/get-posts.js +62 -0
  44. package/shared/pulsemcp-admin-client/lib/update-post.d.ts +3 -0
  45. package/shared/pulsemcp-admin-client/lib/update-post.js +75 -0
  46. package/shared/pulsemcp-admin-client/lib/upload-image.d.ts +3 -0
  47. package/shared/pulsemcp-admin-client/lib/upload-image.js +36 -0
  48. package/shared/pulsemcp-admin-client/pulsemcp-admin-client.d.ts +3 -0
  49. package/shared/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.d.ts +27 -0
  50. package/shared/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +158 -0
  51. package/shared/pulsemcp-admin-client/pulsemcp-admin-client.js +1 -0
  52. package/shared/server.d.ts +71 -0
  53. package/shared/server.js +71 -0
  54. package/shared/tools/draft-newsletter-post.d.ts +97 -0
  55. package/shared/tools/draft-newsletter-post.js +225 -0
  56. package/shared/tools/get-authors.d.ts +34 -0
  57. package/shared/tools/get-authors.js +115 -0
  58. package/shared/tools/get-newsletter-post.d.ts +30 -0
  59. package/shared/tools/get-newsletter-post.js +127 -0
  60. package/shared/tools/get-newsletter-posts.d.ts +42 -0
  61. package/shared/tools/get-newsletter-posts.js +127 -0
  62. package/shared/tools/update-newsletter-post.d.ts +92 -0
  63. package/shared/tools/update-newsletter-post.js +232 -0
  64. package/shared/tools/upload-image.d.ts +38 -0
  65. package/shared/tools/upload-image.js +106 -0
  66. package/shared/tools.d.ts +15 -0
  67. package/shared/tools.js +58 -0
  68. package/shared/types.d.ts +100 -0
  69. package/shared/types.js +1 -0
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ # PulseMCP CMS Admin MCP Server
2
+
3
+ > **Note**: This package is part of the [MCP Servers](https://github.com/pulsemcp/mcp-servers) monorepo. For the latest updates and full source code, visit the [PulseMCP CMS Admin MCP Server directory](https://github.com/pulsemcp/mcp-servers/tree/main/experimental/pulsemcp-cms-admin).
4
+
5
+
6
+ Haven't heard about MCP yet? The easiest way to keep up-to-date is to read our [weekly newsletter at PulseMCP](https://www.pulsemcp.com/).
7
+
8
+ ---
9
+
10
+ This is an MCP ([Model Context Protocol](https://modelcontextprotocol.io/)) Server for managing PulseMCP's content management system. It provides tools for newsletter management and content operations through direct integration with the [PulseMCP Admin API](https://admin.pulsemcp.com).
11
+
12
+ **Note**: This is an internal tool for the PulseMCP team. The source code is public for reference purposes, but the server requires API keys that are not publicly available.
13
+
14
+ # Table of Contents
15
+
16
+ - [Highlights](#highlights)
17
+ - [Capabilities](#capabilities)
18
+ - [Usage Tips](#usage-tips)
19
+ - [Examples](#examples)
20
+ - [Setup](#setup)
21
+ - [Cheatsheet](#cheatsheet)
22
+ - [Claude Desktop](#claude-desktop)
23
+ - [Manual Setup](#manual-setup)
24
+
25
+ # Highlights
26
+
27
+ **Newsletter Management**: Create, update, and retrieve newsletter posts with full content control.
28
+
29
+ **Image Uploads**: Upload images to cloud storage and attach them to newsletter posts.
30
+
31
+ **Content Search**: Find newsletter posts with powerful search and pagination capabilities.
32
+
33
+ **Draft Control**: Manage draft posts before publishing to the newsletter.
34
+
35
+ # Capabilities
36
+
37
+ This server is built and tested on macOS with Claude Desktop. It should work with other MCP clients as well.
38
+
39
+ | Tool Name | Description |
40
+ | ------------------------ | -------------------------------------------------------------------------- |
41
+ | `get_newsletter_posts` | List newsletter posts with search, sorting, and pagination options. |
42
+ | `get_newsletter_post` | Retrieve a specific newsletter post by its unique slug. |
43
+ | `draft_newsletter_post` | Create a new draft newsletter post with title, body, and metadata. |
44
+ | `update_newsletter_post` | Update an existing newsletter post's content and metadata (except status). |
45
+ | `upload_image` | Upload an image and attach it to a specific newsletter post. |
46
+ | `get_authors` | Get a list of authors with optional search and pagination. |
47
+
48
+ # Usage Tips
49
+
50
+ - Use `get_newsletter_posts` to browse existing content before creating new posts
51
+ - When uploading images, both `post_slug` and `file_name` are required
52
+ - Draft posts allow you to prepare content without immediately publishing
53
+ - Search functionality supports finding posts by title, content, or metadata
54
+ - The update tool allows modifying all post metadata except status
55
+ - Use author slugs when creating posts (e.g., "sarah-chen", "john-doe")
56
+ - Use MCP server/client slugs for featured content (e.g., "github-mcp", "claude-desktop")
57
+
58
+ # Examples
59
+
60
+ ## Search for Newsletter Posts
61
+
62
+ ```
63
+ User: Can you find all newsletter posts about MCP servers?
64
+ Assistant: I'll search for newsletter posts about MCP servers.
65
+
66
+ [Calls get_newsletter_posts with search="MCP servers"]
67
+
68
+ I found 3 newsletter posts about MCP servers:
69
+
70
+ 1. **"Top 10 MCP Servers This Week"** (2024-01-15)
71
+ - Weekly roundup of the most popular MCP servers
72
+
73
+ 2. **"Building Your First MCP Server"** (2024-01-08)
74
+ - Tutorial on creating custom MCP servers
75
+
76
+ 3. **"MCP Server Security Best Practices"** (2023-12-28)
77
+ - Guide to securing MCP server deployments
78
+ ```
79
+
80
+ ## Create a Draft Post
81
+
82
+ ```
83
+ User: Create a draft newsletter post about the new weather MCP server
84
+
85
+ Assistant: I'll create a draft newsletter post about the new weather MCP server.
86
+
87
+ [Calls draft_newsletter_post with title, body, slug, and author_slug]
88
+
89
+ I've created a draft newsletter post titled "New Weather MCP Server: Real-time Forecasts in Your LLM" with the slug "weather-mcp-server-launch". The draft has been saved and can be edited or published later.
90
+ ```
91
+
92
+ ## Upload and Attach an Image
93
+
94
+ ```
95
+ User: Upload this screenshot to the weather server post I just created
96
+
97
+ Assistant: I'll upload the screenshot and attach it to your weather server post.
98
+
99
+ [Calls upload_image with post_slug="weather-mcp-server-launch" and file_name="weather-dashboard.png"]
100
+
101
+ The screenshot has been successfully uploaded and attached to the "weather-mcp-server-launch" post. The image is now available at: https://storage.pulsemcp.com/images/newsletter/weather-dashboard.png
102
+ ```
103
+
104
+ ## Update an Existing Post
105
+
106
+ ```
107
+ User: Update the weather post to include information about the new temperature alerts feature
108
+
109
+ Assistant: I'll update the weather MCP server post to include information about the temperature alerts feature.
110
+
111
+ [Calls update_newsletter_post with slug="weather-mcp-server-launch" and updated content]
112
+
113
+ I've successfully updated the "New Weather MCP Server" post to include a section about the temperature alerts feature. The post now covers both the real-time forecasts and the new alert system.
114
+ ```
115
+
116
+ # Setup
117
+
118
+ ## Cheatsheet
119
+
120
+ Quick setup:
121
+
122
+ ```bash
123
+ # Install dependencies
124
+ npm run install-all
125
+
126
+ # Build the server
127
+ npm run build
128
+
129
+ # Set your API key
130
+ export PULSEMCP_ADMIN_API_KEY="your-api-key-here"
131
+
132
+ # Run the server
133
+ cd local && npm start
134
+ ```
135
+
136
+ ## Claude Desktop
137
+
138
+ Add to your Claude Desktop configuration:
139
+
140
+ ### macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
141
+
142
+ ### Windows: `%APPDATA%\Claude\claude_desktop_config.json`
143
+
144
+ ```json
145
+ {
146
+ "mcpServers": {
147
+ "pulsemcp-cms-admin": {
148
+ "command": "node",
149
+ "args": ["/path/to/pulsemcp-cms-admin/local/build/index.js"],
150
+ "env": {
151
+ "PULSEMCP_ADMIN_API_KEY": "your-api-key-here"
152
+ }
153
+ }
154
+ }
155
+ }
156
+ ```
157
+
158
+ ### Manual Setup
159
+
160
+ If you prefer to run the server manually:
161
+
162
+ ```bash
163
+ cd /path/to/pulsemcp-cms-admin/local
164
+ PULSEMCP_ADMIN_API_KEY="your-api-key-here" node build/index.js
165
+ ```
166
+
167
+ ## License
168
+
169
+ MIT
package/build/index.js ADDED
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env node
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { createMCPServer } from '../shared/index.js';
4
+ import { logServerStart, logError } from '../shared/logging.js';
5
+ // Validate required environment variables before starting
6
+ function validateEnvironment() {
7
+ const required = [
8
+ {
9
+ name: 'PULSEMCP_ADMIN_API_KEY',
10
+ description: 'API key for PulseMCP admin API authentication',
11
+ },
12
+ ];
13
+ const optional = [];
14
+ const missing = required.filter(({ name }) => !process.env[name]);
15
+ if (missing.length > 0) {
16
+ logError('validateEnvironment', 'Missing required environment variables:');
17
+ missing.forEach(({ name, description }) => {
18
+ console.error(` - ${name}: ${description}`);
19
+ });
20
+ if (optional.length > 0) {
21
+ console.error('\nOptional environment variables:');
22
+ optional.forEach(({ name, description }) => {
23
+ console.error(` - ${name}: ${description}`);
24
+ });
25
+ }
26
+ console.error('\nPlease set the required environment variables and try again.');
27
+ console.error('Example:');
28
+ console.error(' export PULSEMCP_ADMIN_API_KEY="your-api-key"');
29
+ process.exit(1);
30
+ }
31
+ }
32
+ async function main() {
33
+ // Validate environment variables first
34
+ validateEnvironment();
35
+ // Create server using factory
36
+ const { server, registerHandlers } = createMCPServer();
37
+ // Register all handlers (resources and tools)
38
+ await registerHandlers(server);
39
+ // Start server
40
+ const transport = new StdioServerTransport();
41
+ await server.connect(transport);
42
+ logServerStart('pulsemcp-cms-admin');
43
+ }
44
+ // Run the server
45
+ main().catch((error) => {
46
+ logError('main', error);
47
+ process.exit(1);
48
+ });
@@ -0,0 +1,2 @@
1
+ export { registerTools, createRegisterTools } from './tools.js';
2
+ export { createMCPServer, PulseMCPAdminClient, } from './server.js';
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Logging utilities for consistent output across MCP servers
3
+ */
4
+ /**
5
+ * Log server startup message
6
+ */
7
+ export function logServerStart(serverName, transport = 'stdio') {
8
+ console.error(`MCP server ${serverName} running on ${transport}`);
9
+ }
10
+ /**
11
+ * Log an error with context
12
+ */
13
+ export function logError(context, error) {
14
+ const message = error instanceof Error ? error.message : String(error);
15
+ const stack = error instanceof Error ? error.stack : undefined;
16
+ console.error(`[ERROR] ${context}: ${message}`);
17
+ if (stack) {
18
+ console.error(stack);
19
+ }
20
+ }
21
+ /**
22
+ * Log a warning
23
+ */
24
+ export function logWarning(context, message) {
25
+ console.error(`[WARN] ${context}: ${message}`);
26
+ }
27
+ /**
28
+ * Log debug information (only in development)
29
+ */
30
+ export function logDebug(context, message) {
31
+ if (process.env.NODE_ENV === 'development' || process.env.DEBUG) {
32
+ console.error(`[DEBUG] ${context}: ${message}`);
33
+ }
34
+ }
@@ -0,0 +1,69 @@
1
+ export async function createPost(apiKey, baseUrl, params) {
2
+ const url = new URL('/posts', baseUrl);
3
+ // Build form data for the POST request
4
+ const formData = new URLSearchParams();
5
+ // Required fields
6
+ formData.append('post[title]', params.title);
7
+ formData.append('post[body]', params.body);
8
+ formData.append('post[slug]', params.slug);
9
+ formData.append('post[author_id]', params.author_id.toString());
10
+ // Optional fields
11
+ if (params.status)
12
+ formData.append('post[status]', params.status);
13
+ if (params.category)
14
+ formData.append('post[category]', params.category);
15
+ if (params.image_url)
16
+ formData.append('post[image_url]', params.image_url);
17
+ if (params.preview_image_url)
18
+ formData.append('post[preview_image_url]', params.preview_image_url);
19
+ if (params.share_image)
20
+ formData.append('post[share_image]', params.share_image);
21
+ if (params.title_tag)
22
+ formData.append('post[title_tag]', params.title_tag);
23
+ if (params.short_title)
24
+ formData.append('post[short_title]', params.short_title);
25
+ if (params.short_description)
26
+ formData.append('post[short_description]', params.short_description);
27
+ if (params.description_tag)
28
+ formData.append('post[description_tag]', params.description_tag);
29
+ if (params.last_updated)
30
+ formData.append('post[last_updated]', params.last_updated);
31
+ if (params.table_of_contents)
32
+ formData.append('post[table_of_contents]', JSON.stringify(params.table_of_contents));
33
+ // Handle arrays for featured servers/clients
34
+ if (params.featured_mcp_server_ids) {
35
+ params.featured_mcp_server_ids.forEach((id) => {
36
+ formData.append('post[featured_mcp_server_ids][]', id.toString());
37
+ });
38
+ }
39
+ if (params.featured_mcp_client_ids) {
40
+ params.featured_mcp_client_ids.forEach((id) => {
41
+ formData.append('post[featured_mcp_client_ids][]', id.toString());
42
+ });
43
+ }
44
+ const response = await fetch(url.toString(), {
45
+ method: 'POST',
46
+ headers: {
47
+ 'X-API-Key': apiKey,
48
+ 'Content-Type': 'application/x-www-form-urlencoded',
49
+ Accept: 'application/json',
50
+ },
51
+ body: formData.toString(),
52
+ });
53
+ if (!response.ok) {
54
+ if (response.status === 401) {
55
+ throw new Error('Invalid API key');
56
+ }
57
+ if (response.status === 403) {
58
+ throw new Error('User lacks admin privileges');
59
+ }
60
+ if (response.status === 422) {
61
+ const errorData = (await response.json());
62
+ const errors = errorData.errors || ['Validation failed'];
63
+ throw new Error(`Validation failed: ${errors.join(', ')}`);
64
+ }
65
+ throw new Error(`Failed to create post: ${response.status} ${response.statusText}`);
66
+ }
67
+ const data = await response.json();
68
+ return data;
69
+ }
@@ -0,0 +1,26 @@
1
+ export async function getAuthorBySlug(apiKey, baseUrl, slug) {
2
+ // Use the supervisor endpoint which supports JSON
3
+ const url = new URL(`/supervisor/authors/${slug}`, baseUrl);
4
+ const response = await fetch(url.toString(), {
5
+ method: 'GET',
6
+ headers: {
7
+ 'X-API-Key': apiKey,
8
+ Accept: 'application/json',
9
+ },
10
+ });
11
+ if (!response.ok) {
12
+ if (response.status === 401) {
13
+ throw new Error('Invalid API key');
14
+ }
15
+ if (response.status === 403) {
16
+ throw new Error('User lacks admin privileges');
17
+ }
18
+ if (response.status === 404) {
19
+ throw new Error(`Author not found: ${slug}`);
20
+ }
21
+ throw new Error(`Failed to fetch author: ${response.status} ${response.statusText}`);
22
+ }
23
+ const data = await response.json();
24
+ // The supervisor endpoint returns the author object directly
25
+ return data;
26
+ }
@@ -0,0 +1,38 @@
1
+ export async function getAuthors(apiKey, baseUrl, params) {
2
+ // Use the supervisor endpoint which supports JSON
3
+ const url = new URL('/supervisor/authors', baseUrl);
4
+ // Add query parameters if provided
5
+ if (params?.search) {
6
+ url.searchParams.append('search', params.search);
7
+ }
8
+ if (params?.page) {
9
+ url.searchParams.append('page', params.page.toString());
10
+ }
11
+ const response = await fetch(url.toString(), {
12
+ method: 'GET',
13
+ headers: {
14
+ 'X-API-Key': apiKey,
15
+ Accept: 'application/json',
16
+ },
17
+ });
18
+ if (!response.ok) {
19
+ if (response.status === 401) {
20
+ throw new Error('Invalid API key');
21
+ }
22
+ if (response.status === 403) {
23
+ throw new Error('User lacks admin privileges');
24
+ }
25
+ throw new Error(`Failed to fetch authors: ${response.status} ${response.statusText}`);
26
+ }
27
+ const data = (await response.json());
28
+ return {
29
+ authors: data.data || [],
30
+ pagination: data.meta
31
+ ? {
32
+ current_page: data.meta.current_page,
33
+ total_pages: data.meta.total_pages,
34
+ total_count: data.meta.total_count,
35
+ }
36
+ : undefined,
37
+ };
38
+ }
@@ -0,0 +1,26 @@
1
+ export async function getMCPClientBySlug(apiKey, baseUrl, slug) {
2
+ // Use the supervisor endpoint which supports JSON
3
+ const url = new URL(`/supervisor/mcp_clients/${slug}`, baseUrl);
4
+ const response = await fetch(url.toString(), {
5
+ method: 'GET',
6
+ headers: {
7
+ 'X-API-Key': apiKey,
8
+ Accept: 'application/json',
9
+ },
10
+ });
11
+ if (!response.ok) {
12
+ if (response.status === 401) {
13
+ throw new Error('Invalid API key');
14
+ }
15
+ if (response.status === 403) {
16
+ throw new Error('User lacks admin privileges');
17
+ }
18
+ if (response.status === 404) {
19
+ throw new Error(`MCP client not found: ${slug}`);
20
+ }
21
+ throw new Error(`Failed to fetch MCP client: ${response.status} ${response.statusText}`);
22
+ }
23
+ const data = await response.json();
24
+ // The supervisor endpoint returns the MCP client object directly
25
+ return data;
26
+ }
@@ -0,0 +1,26 @@
1
+ export async function getMCPServerBySlug(apiKey, baseUrl, slug) {
2
+ // Use the supervisor endpoint which supports JSON
3
+ const url = new URL(`/supervisor/mcp_servers/${slug}`, baseUrl);
4
+ const response = await fetch(url.toString(), {
5
+ method: 'GET',
6
+ headers: {
7
+ 'X-API-Key': apiKey,
8
+ Accept: 'application/json',
9
+ },
10
+ });
11
+ if (!response.ok) {
12
+ if (response.status === 401) {
13
+ throw new Error('Invalid API key');
14
+ }
15
+ if (response.status === 403) {
16
+ throw new Error('User lacks admin privileges');
17
+ }
18
+ if (response.status === 404) {
19
+ throw new Error(`MCP server not found: ${slug}`);
20
+ }
21
+ throw new Error(`Failed to fetch MCP server: ${response.status} ${response.statusText}`);
22
+ }
23
+ const data = await response.json();
24
+ // The supervisor endpoint returns the MCP server object directly
25
+ return data;
26
+ }
@@ -0,0 +1,26 @@
1
+ export async function getPost(apiKey, baseUrl, slug) {
2
+ // Use the supervisor endpoint which supports JSON and returns full post data including body
3
+ const url = new URL(`/supervisor/posts/${slug}`, baseUrl);
4
+ const response = await fetch(url.toString(), {
5
+ method: 'GET',
6
+ headers: {
7
+ 'X-API-Key': apiKey,
8
+ Accept: 'application/json',
9
+ },
10
+ });
11
+ if (!response.ok) {
12
+ if (response.status === 401) {
13
+ throw new Error('Invalid API key');
14
+ }
15
+ if (response.status === 403) {
16
+ throw new Error('User lacks admin privileges');
17
+ }
18
+ if (response.status === 404) {
19
+ throw new Error(`Post not found: ${slug}`);
20
+ }
21
+ throw new Error(`Failed to fetch post: ${response.status} ${response.statusText}`);
22
+ }
23
+ const data = await response.json();
24
+ // The supervisor endpoint returns the full post object with all fields including body
25
+ return data;
26
+ }
@@ -0,0 +1,62 @@
1
+ export async function getPosts(apiKey, baseUrl, params) {
2
+ const url = new URL('/posts', baseUrl);
3
+ // Add query parameters if provided
4
+ if (params?.search) {
5
+ url.searchParams.append('search', params.search);
6
+ }
7
+ if (params?.sort) {
8
+ url.searchParams.append('sort', params.sort);
9
+ }
10
+ if (params?.direction) {
11
+ url.searchParams.append('direction', params.direction);
12
+ }
13
+ if (params?.page) {
14
+ url.searchParams.append('page', params.page.toString());
15
+ }
16
+ const response = await fetch(url.toString(), {
17
+ method: 'GET',
18
+ headers: {
19
+ 'X-API-Key': apiKey,
20
+ Accept: 'application/json',
21
+ },
22
+ });
23
+ if (!response.ok) {
24
+ if (response.status === 401) {
25
+ throw new Error('Invalid API key');
26
+ }
27
+ if (response.status === 403) {
28
+ throw new Error('User lacks admin privileges');
29
+ }
30
+ throw new Error(`Failed to fetch posts: ${response.status} ${response.statusText}`);
31
+ }
32
+ // Parse the JSON response
33
+ const data = (await response.json());
34
+ // Handle the Rails JSON structure with data and meta
35
+ if (data.data && data.meta) {
36
+ return {
37
+ posts: data.data.map((post) => ({
38
+ id: post.id,
39
+ slug: post.slug,
40
+ title: post.title,
41
+ short_title: post.short_title,
42
+ short_description: post.short_description,
43
+ category: post.category,
44
+ status: post.status,
45
+ author_id: post.author_id,
46
+ created_at: post.created_at,
47
+ updated_at: post.updated_at,
48
+ last_updated: post.last_updated,
49
+ })),
50
+ pagination: {
51
+ current_page: data.meta.current_page,
52
+ total_pages: data.meta.total_pages,
53
+ total_count: data.meta.total_count,
54
+ },
55
+ };
56
+ }
57
+ // Fallback for unexpected response format
58
+ return {
59
+ posts: [],
60
+ pagination: undefined,
61
+ };
62
+ }
@@ -0,0 +1,75 @@
1
+ export async function updatePost(apiKey, baseUrl, slug, params) {
2
+ const url = new URL(`/posts/${slug}`, baseUrl);
3
+ // Build form data for the PUT request
4
+ const formData = new URLSearchParams();
5
+ // Add all provided fields
6
+ if (params.title !== undefined)
7
+ formData.append('post[title]', params.title);
8
+ if (params.body !== undefined)
9
+ formData.append('post[body]', params.body);
10
+ if (params.slug !== undefined)
11
+ formData.append('post[slug]', params.slug);
12
+ if (params.author_id !== undefined)
13
+ formData.append('post[author_id]', params.author_id.toString());
14
+ if (params.status !== undefined)
15
+ formData.append('post[status]', params.status);
16
+ if (params.category !== undefined)
17
+ formData.append('post[category]', params.category);
18
+ if (params.image_url !== undefined)
19
+ formData.append('post[image_url]', params.image_url);
20
+ if (params.preview_image_url !== undefined)
21
+ formData.append('post[preview_image_url]', params.preview_image_url);
22
+ if (params.share_image !== undefined)
23
+ formData.append('post[share_image]', params.share_image);
24
+ if (params.title_tag !== undefined)
25
+ formData.append('post[title_tag]', params.title_tag);
26
+ if (params.short_title !== undefined)
27
+ formData.append('post[short_title]', params.short_title);
28
+ if (params.short_description !== undefined)
29
+ formData.append('post[short_description]', params.short_description);
30
+ if (params.description_tag !== undefined)
31
+ formData.append('post[description_tag]', params.description_tag);
32
+ if (params.last_updated !== undefined)
33
+ formData.append('post[last_updated]', params.last_updated);
34
+ if (params.table_of_contents !== undefined)
35
+ formData.append('post[table_of_contents]', JSON.stringify(params.table_of_contents));
36
+ // Handle arrays for featured servers/clients
37
+ if (params.featured_mcp_server_ids !== undefined) {
38
+ params.featured_mcp_server_ids.forEach((id) => {
39
+ formData.append('post[featured_mcp_server_ids][]', id.toString());
40
+ });
41
+ }
42
+ if (params.featured_mcp_client_ids !== undefined) {
43
+ params.featured_mcp_client_ids.forEach((id) => {
44
+ formData.append('post[featured_mcp_client_ids][]', id.toString());
45
+ });
46
+ }
47
+ const response = await fetch(url.toString(), {
48
+ method: 'PUT',
49
+ headers: {
50
+ 'X-API-Key': apiKey,
51
+ 'Content-Type': 'application/x-www-form-urlencoded',
52
+ Accept: 'application/json',
53
+ },
54
+ body: formData.toString(),
55
+ });
56
+ if (!response.ok) {
57
+ if (response.status === 401) {
58
+ throw new Error('Invalid API key');
59
+ }
60
+ if (response.status === 403) {
61
+ throw new Error('User lacks admin privileges');
62
+ }
63
+ if (response.status === 404) {
64
+ throw new Error(`Post not found: ${slug}`);
65
+ }
66
+ if (response.status === 422) {
67
+ const errorData = (await response.json());
68
+ const errors = errorData.errors || ['Validation failed'];
69
+ throw new Error(`Validation failed: ${errors.join(', ')}`);
70
+ }
71
+ throw new Error(`Failed to update post: ${response.status} ${response.statusText}`);
72
+ }
73
+ const data = await response.json();
74
+ return data;
75
+ }
@@ -0,0 +1,36 @@
1
+ export async function uploadImage(apiKey, baseUrl, postSlug, fileName, fileData) {
2
+ const url = new URL('/upload_image', baseUrl);
3
+ // Create form data for multipart upload
4
+ const formData = new FormData();
5
+ // Create a blob from the buffer
6
+ const blob = new Blob([fileData], { type: 'image/png' }); // Default to PNG, adjust as needed
7
+ // Add file to form data
8
+ formData.append('file', blob, fileName);
9
+ // Add folder path that includes the post slug
10
+ formData.append('folder', `newsletter/${postSlug}`);
11
+ // Add the full filepath
12
+ formData.append('filepath', `newsletter/${postSlug}/${fileName}`);
13
+ const response = await fetch(url.toString(), {
14
+ method: 'POST',
15
+ headers: {
16
+ 'X-API-Key': apiKey,
17
+ // Don't set Content-Type for FormData - let the browser set it with boundary
18
+ },
19
+ body: formData,
20
+ });
21
+ if (!response.ok) {
22
+ if (response.status === 401) {
23
+ throw new Error('Invalid API key');
24
+ }
25
+ if (response.status === 403) {
26
+ throw new Error('User lacks admin privileges');
27
+ }
28
+ if (response.status === 422) {
29
+ const errorData = await response.text();
30
+ throw new Error(`Validation failed: ${errorData}`);
31
+ }
32
+ throw new Error(`Failed to upload image: ${response.status} ${response.statusText}`);
33
+ }
34
+ const data = (await response.json());
35
+ return data;
36
+ }