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.
- package/README.md +169 -0
- package/build/index.js +48 -0
- package/build/shared/src/index.js +2 -0
- package/build/shared/src/logging.js +34 -0
- package/build/shared/src/pulsemcp-admin-client/lib/create-post.js +69 -0
- package/build/shared/src/pulsemcp-admin-client/lib/get-author-by-slug.js +26 -0
- package/build/shared/src/pulsemcp-admin-client/lib/get-authors.js +38 -0
- package/build/shared/src/pulsemcp-admin-client/lib/get-mcp-client-by-slug.js +26 -0
- package/build/shared/src/pulsemcp-admin-client/lib/get-mcp-server-by-slug.js +26 -0
- package/build/shared/src/pulsemcp-admin-client/lib/get-post.js +26 -0
- package/build/shared/src/pulsemcp-admin-client/lib/get-posts.js +62 -0
- package/build/shared/src/pulsemcp-admin-client/lib/update-post.js +75 -0
- package/build/shared/src/pulsemcp-admin-client/lib/upload-image.js +36 -0
- package/build/shared/src/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +158 -0
- package/build/shared/src/pulsemcp-admin-client/pulsemcp-admin-client.js +1 -0
- package/build/shared/src/server.js +71 -0
- package/build/shared/src/tools/draft-newsletter-post.js +225 -0
- package/build/shared/src/tools/get-authors.js +115 -0
- package/build/shared/src/tools/get-newsletter-post.js +127 -0
- package/build/shared/src/tools/get-newsletter-posts.js +127 -0
- package/build/shared/src/tools/update-newsletter-post.js +232 -0
- package/build/shared/src/tools/upload-image.js +106 -0
- package/build/shared/src/tools.js +58 -0
- package/build/shared/src/types.js +1 -0
- package/package.json +43 -0
- package/shared/index.d.ts +4 -0
- package/shared/index.js +2 -0
- package/shared/logging.d.ts +20 -0
- package/shared/logging.js +34 -0
- package/shared/pulsemcp-admin-client/lib/create-post.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/create-post.js +69 -0
- package/shared/pulsemcp-admin-client/lib/get-author-by-slug.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/get-author-by-slug.js +26 -0
- package/shared/pulsemcp-admin-client/lib/get-authors.d.ts +6 -0
- package/shared/pulsemcp-admin-client/lib/get-authors.js +38 -0
- package/shared/pulsemcp-admin-client/lib/get-mcp-client-by-slug.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/get-mcp-client-by-slug.js +26 -0
- package/shared/pulsemcp-admin-client/lib/get-mcp-server-by-slug.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/get-mcp-server-by-slug.js +26 -0
- package/shared/pulsemcp-admin-client/lib/get-post.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/get-post.js +26 -0
- package/shared/pulsemcp-admin-client/lib/get-posts.d.ts +8 -0
- package/shared/pulsemcp-admin-client/lib/get-posts.js +62 -0
- package/shared/pulsemcp-admin-client/lib/update-post.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/update-post.js +75 -0
- package/shared/pulsemcp-admin-client/lib/upload-image.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/upload-image.js +36 -0
- package/shared/pulsemcp-admin-client/pulsemcp-admin-client.d.ts +3 -0
- package/shared/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.d.ts +27 -0
- package/shared/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +158 -0
- package/shared/pulsemcp-admin-client/pulsemcp-admin-client.js +1 -0
- package/shared/server.d.ts +71 -0
- package/shared/server.js +71 -0
- package/shared/tools/draft-newsletter-post.d.ts +97 -0
- package/shared/tools/draft-newsletter-post.js +225 -0
- package/shared/tools/get-authors.d.ts +34 -0
- package/shared/tools/get-authors.js +115 -0
- package/shared/tools/get-newsletter-post.d.ts +30 -0
- package/shared/tools/get-newsletter-post.js +127 -0
- package/shared/tools/get-newsletter-posts.d.ts +42 -0
- package/shared/tools/get-newsletter-posts.js +127 -0
- package/shared/tools/update-newsletter-post.d.ts +92 -0
- package/shared/tools/update-newsletter-post.js +232 -0
- package/shared/tools/upload-image.d.ts +38 -0
- package/shared/tools/upload-image.js +106 -0
- package/shared/tools.d.ts +15 -0
- package/shared/tools.js +58 -0
- package/shared/types.d.ts +100 -0
- 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,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
|
+
}
|