pulsemcp-cms-admin-mcp-server 0.4.3 → 0.5.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 CHANGED
@@ -32,7 +32,7 @@ This is an MCP ([Model Context Protocol](https://modelcontextprotocol.io/)) Serv
32
32
 
33
33
  **MCP Implementation Search**: Search for MCP servers and clients in the PulseMCP registry.
34
34
 
35
- **Toolgroups**: Enable/disable tool groups (newsletter, server_queue_readonly, server_queue_all, official_queue_readonly, official_queue_all) via environment variable.
35
+ **Tool Groups**: Enable/disable tool groups via `TOOL_GROUPS` environment variable. Each group has a base variant (full access) and a `_readonly` variant (read-only access).
36
36
 
37
37
  **Draft Control**: Manage draft posts before publishing to the newsletter.
38
38
 
@@ -40,38 +40,95 @@ This is an MCP ([Model Context Protocol](https://modelcontextprotocol.io/)) Serv
40
40
 
41
41
  This server is built and tested on macOS with Claude Desktop. It should work with other MCP clients as well.
42
42
 
43
- | Tool Name | Tool Group | Description |
44
- | -------------------------------------- | ----------------------- | ---------------------------------------------------------------------------- |
45
- | `get_newsletter_posts` | newsletter | List newsletter posts with search, sorting, and pagination options. |
46
- | `get_newsletter_post` | newsletter | Retrieve a specific newsletter post by its unique slug. |
47
- | `draft_newsletter_post` | newsletter | Create a new draft newsletter post with title, body, and metadata. |
48
- | `update_newsletter_post` | newsletter | Update an existing newsletter post's content and metadata (except status). |
49
- | `upload_image` | newsletter | Upload an image and attach it to a specific newsletter post. |
50
- | `get_authors` | newsletter | Get a list of authors with optional search and pagination. |
51
- | `search_mcp_implementations` | server_queue_readonly | Search for MCP servers and clients in the PulseMCP registry. |
52
- | `get_draft_mcp_implementations` | server_queue_readonly | Retrieve paginated list of draft MCP implementations needing review. |
53
- | `find_providers` | server_queue_readonly | Search for providers by ID, name, URL, or slug. |
54
- | `save_mcp_implementation` | server_queue_all | Update an MCP implementation (replicates Admin panel "Save Changes" button). |
55
- | `send_impl_posted_notif` | server_queue_all | Send email notification when MCP implementation goes live. |
56
- | `get_official_mirror_queue_items` | official_queue_readonly | List and filter official mirror queue entries with pagination and search. |
57
- | `get_official_mirror_queue_item` | official_queue_readonly | Get detailed information about a single official mirror queue entry. |
58
- | `approve_official_mirror_queue_item` | official_queue_all | Approve a queue entry and link it to an existing MCP server (async). |
59
- | `approve_mirror_no_modify` | official_queue_all | Approve without updating the linked server. |
60
- | `reject_official_mirror_queue_item` | official_queue_all | Reject a queue entry (async operation). |
61
- | `add_official_mirror_to_regular_queue` | official_queue_all | Convert a queue entry to a draft MCP implementation (async). |
62
- | `unlink_official_mirror_queue_item` | official_queue_all | Unlink a queue entry from its linked MCP server. |
43
+ | Tool Name | Tool Group | Read/Write | Description |
44
+ | -------------------------------------- | -------------- | ---------- | ---------------------------------------------------------------------------- |
45
+ | `get_newsletter_posts` | newsletter | read | List newsletter posts with search, sorting, and pagination options. |
46
+ | `get_newsletter_post` | newsletter | read | Retrieve a specific newsletter post by its unique slug. |
47
+ | `draft_newsletter_post` | newsletter | write | Create a new draft newsletter post with title, body, and metadata. |
48
+ | `update_newsletter_post` | newsletter | write | Update an existing newsletter post's content and metadata (except status). |
49
+ | `upload_image` | newsletter | write | Upload an image and attach it to a specific newsletter post. |
50
+ | `get_authors` | newsletter | read | Get a list of authors with optional search and pagination. |
51
+ | `search_mcp_implementations` | server_queue | read | Search for MCP servers and clients in the PulseMCP registry. |
52
+ | `get_draft_mcp_implementations` | server_queue | read | Retrieve paginated list of draft MCP implementations needing review. |
53
+ | `find_providers` | server_queue | read | Search for providers by ID, name, URL, or slug. |
54
+ | `save_mcp_implementation` | server_queue | write | Update an MCP implementation (replicates Admin panel "Save Changes" button). |
55
+ | `send_impl_posted_notif` | server_queue | write | Send email notification when MCP implementation goes live. |
56
+ | `get_official_mirror_queue_items` | official_queue | read | List and filter official mirror queue entries with pagination and search. |
57
+ | `get_official_mirror_queue_item` | official_queue | read | Get detailed information about a single official mirror queue entry. |
58
+ | `approve_official_mirror_queue_item` | official_queue | write | Approve a queue entry and link it to an existing MCP server (async). |
59
+ | `approve_mirror_no_modify` | official_queue | write | Approve without updating the linked server. |
60
+ | `reject_official_mirror_queue_item` | official_queue | write | Reject a queue entry (async operation). |
61
+ | `add_official_mirror_to_regular_queue` | official_queue | write | Convert a queue entry to a draft MCP implementation (async). |
62
+ | `unlink_official_mirror_queue_item` | official_queue | write | Unlink a queue entry from its linked MCP server. |
63
63
 
64
64
  # Tool Groups
65
65
 
66
- This server organizes tools into groups that can be selectively enabled or disabled:
66
+ This server organizes tools into groups that can be selectively enabled or disabled. Each group has two variants:
67
67
 
68
- - **newsletter** (6 tools): Newsletter management, image uploads, and author retrieval
69
- - **server_queue_readonly** (3 tools): Read-only MCP implementation tools (search, draft retrieval, provider lookup)
70
- - **server_queue_all** (5 tools): All MCP implementation tools including write operations (search, draft retrieval, provider lookup, update, and email notification)
71
- - **official_queue_readonly** (2 tools): Read-only official mirror queue tools (list, get details)
72
- - **official_queue_all** (7 tools): All official mirror queue tools including approve, reject, unlink, and add to regular queue
68
+ - **Base group** (e.g., `newsletter`): Full read + write access
69
+ - **Readonly group** (e.g., `newsletter_readonly`): Read-only access
73
70
 
74
- You can control which tool groups are available by setting the `PULSEMCP_ADMIN_ENABLED_TOOLGROUPS` environment variable as a comma-separated list (e.g., `newsletter,server_queue_readonly`). If not set, all tool groups are enabled by default.
71
+ ## Available Groups
72
+
73
+ | Group | Tools | Description |
74
+ | ------------------------- | ----- | -------------------------------------------- |
75
+ | `newsletter` | 6 | Full newsletter management (read + write) |
76
+ | `newsletter_readonly` | 3 | Newsletter read-only (get posts, authors) |
77
+ | `server_queue` | 5 | Full MCP implementation queue (read + write) |
78
+ | `server_queue_readonly` | 3 | MCP implementation queue read-only |
79
+ | `official_queue` | 7 | Full official mirror queue (read + write) |
80
+ | `official_queue_readonly` | 2 | Official mirror queue read-only |
81
+
82
+ ### Tools by Group
83
+
84
+ - **newsletter** / **newsletter_readonly**:
85
+ - Read-only: `get_newsletter_posts`, `get_newsletter_post`, `get_authors`
86
+ - Write: `draft_newsletter_post`, `update_newsletter_post`, `upload_image`
87
+ - **server_queue** / **server_queue_readonly**:
88
+ - Read-only: `search_mcp_implementations`, `get_draft_mcp_implementations`, `find_providers`
89
+ - Write: `save_mcp_implementation`, `send_impl_posted_notif`
90
+ - **official_queue** / **official_queue_readonly**:
91
+ - Read-only: `get_official_mirror_queue_items`, `get_official_mirror_queue_item`
92
+ - Write: `approve_official_mirror_queue_item`, `approve_mirror_no_modify`, `reject_official_mirror_queue_item`, `add_official_mirror_to_regular_queue`, `unlink_official_mirror_queue_item`
93
+
94
+ ## Environment Variables
95
+
96
+ | Variable | Description | Default |
97
+ | ------------- | ------------------------------------------- | ---------------------------------------------------------- |
98
+ | `TOOL_GROUPS` | Comma-separated list of enabled tool groups | `newsletter,server_queue,official_queue` (all base groups) |
99
+
100
+ ## Examples
101
+
102
+ Enable all tools with full access (default):
103
+
104
+ ```bash
105
+ # No environment variables needed - all base groups enabled
106
+ ```
107
+
108
+ Enable only newsletter tools:
109
+
110
+ ```bash
111
+ TOOL_GROUPS=newsletter
112
+ ```
113
+
114
+ Enable server_queue with read-only access:
115
+
116
+ ```bash
117
+ TOOL_GROUPS=server_queue_readonly
118
+ ```
119
+
120
+ Enable all groups with read-only access:
121
+
122
+ ```bash
123
+ TOOL_GROUPS=newsletter_readonly,server_queue_readonly,official_queue_readonly
124
+ ```
125
+
126
+ Mix full and read-only access per group:
127
+
128
+ ```bash
129
+ # Full newsletter access, read-only server_queue, no official_queue
130
+ TOOL_GROUPS=newsletter,server_queue_readonly
131
+ ```
75
132
 
76
133
  # Usage Tips
77
134
 
@@ -83,7 +140,8 @@ You can control which tool groups are available by setting the `PULSEMCP_ADMIN_E
83
140
  - Use author slugs when creating posts (e.g., "sarah-chen", "john-doe")
84
141
  - Use MCP server/client slugs for featured content (e.g., "github-mcp", "claude-desktop")
85
142
  - Use `search_mcp_implementations` to discover MCP servers and clients in the PulseMCP registry
86
- - Enable or disable specific toolgroups by setting `PULSEMCP_ADMIN_ENABLED_TOOLGROUPS` environment variable
143
+ - Enable or disable specific tool groups by setting `TOOL_GROUPS` environment variable
144
+ - Use `_readonly` suffixes to restrict groups to read-only operations (e.g., `server_queue_readonly`)
87
145
  - Use the `remote` array parameter in `save_mcp_implementation` to configure remote endpoints for MCP servers (transport, host_platform, authentication_method, etc.)
88
146
  - Use the `canonical` array parameter in `save_mcp_implementation` to set canonical URLs with scope (domain, subdomain, subfolder, or url)
89
147
  - Remote endpoints allow specifying how MCP servers can be accessed (direct URL, setup URL, authentication method, cost, etc.)
@@ -194,7 +252,24 @@ Add to your Claude Desktop configuration:
194
252
  "args": ["/path/to/pulsemcp-cms-admin/local/build/index.js"],
195
253
  "env": {
196
254
  "PULSEMCP_ADMIN_API_KEY": "your-api-key-here",
197
- "PULSEMCP_ADMIN_ENABLED_TOOLGROUPS": "newsletter,server_queue_readonly,server_queue_all,official_queue_readonly,official_queue_all"
255
+ "TOOL_GROUPS": "newsletter,server_queue,official_queue"
256
+ }
257
+ }
258
+ }
259
+ }
260
+ ```
261
+
262
+ For read-only access:
263
+
264
+ ```json
265
+ {
266
+ "mcpServers": {
267
+ "pulsemcp-cms-admin-readonly": {
268
+ "command": "node",
269
+ "args": ["/path/to/pulsemcp-cms-admin/local/build/index.js"],
270
+ "env": {
271
+ "PULSEMCP_ADMIN_API_KEY": "your-api-key-here",
272
+ "TOOL_GROUPS": "newsletter_readonly,server_queue_readonly,official_queue_readonly"
198
273
  }
199
274
  }
200
275
  }
@@ -65,7 +65,13 @@ export async function createPost(apiKey, baseUrl, params) {
65
65
  }
66
66
  if (response.status === 422) {
67
67
  const errorData = (await response.json());
68
- const errors = errorData.errors || ['Validation failed'];
68
+ // Handle both array format and single error string format from Rails
69
+ // Also handle empty arrays - an empty array should fall back to the default message
70
+ const errors = errorData.errors && errorData.errors.length > 0
71
+ ? errorData.errors
72
+ : errorData.error
73
+ ? [errorData.error]
74
+ : ['Unknown validation error'];
69
75
  throw new Error(`Validation failed: ${errors.join(', ')}`);
70
76
  }
71
77
  throw new Error(`Failed to create post: ${response.status} ${response.statusText}`);
@@ -154,7 +154,13 @@ export async function saveMCPImplementation(apiKey, baseUrl, id, params) {
154
154
  }
155
155
  if (response.status === 422) {
156
156
  const errorData = (await response.json());
157
- const errors = errorData.errors || ['Validation failed'];
157
+ // Handle both array format and single error string format from Rails
158
+ // Also handle empty arrays - an empty array should fall back to the default message
159
+ const errors = errorData.errors && errorData.errors.length > 0
160
+ ? errorData.errors
161
+ : errorData.error
162
+ ? [errorData.error]
163
+ : ['Unknown validation error'];
158
164
  throw new Error(`Validation failed: ${errors.join(', ')}`);
159
165
  }
160
166
  throw new Error(`Failed to save MCP implementation: ${response.status} ${response.statusText}`);
@@ -25,16 +25,19 @@ export async function sendEmail(apiKey, baseUrl, params) {
25
25
  let errorMessage;
26
26
  try {
27
27
  const errorData = JSON.parse(errorBody);
28
- if (errorData.errors) {
29
- errorMessage = Array.isArray(errorData.errors)
30
- ? errorData.errors.join(', ')
31
- : JSON.stringify(errorData.errors);
28
+ // Handle both array format and single error string format from Rails
29
+ // Also handle empty arrays - an empty array should fall back to checking other fields
30
+ if (Array.isArray(errorData.errors) && errorData.errors.length > 0) {
31
+ errorMessage = errorData.errors.join(', ');
32
+ }
33
+ else if (errorData.errors && typeof errorData.errors === 'object') {
34
+ errorMessage = JSON.stringify(errorData.errors);
32
35
  }
33
36
  else if (errorData.error) {
34
37
  errorMessage = errorData.error;
35
38
  }
36
39
  else {
37
- errorMessage = errorBody;
40
+ errorMessage = errorBody || 'Unknown validation error';
38
41
  }
39
42
  }
40
43
  catch {
@@ -71,7 +71,13 @@ export async function updatePost(apiKey, baseUrl, slug, params) {
71
71
  }
72
72
  if (response.status === 422) {
73
73
  const errorData = (await response.json());
74
- const errors = errorData.errors || ['Validation failed'];
74
+ // Handle both array format and single error string format from Rails
75
+ // Also handle empty arrays - an empty array should fall back to the default message
76
+ const errors = errorData.errors && errorData.errors.length > 0
77
+ ? errorData.errors
78
+ : errorData.error
79
+ ? [errorData.error]
80
+ : ['Unknown validation error'];
75
81
  throw new Error(`Validation failed: ${errors.join(', ')}`);
76
82
  }
77
83
  throw new Error(`Failed to update post: ${response.status} ${response.statusText}`);
@@ -18,65 +18,94 @@ import { rejectOfficialMirrorQueueItem } from './tools/reject-official-mirror-qu
18
18
  import { addOfficialMirrorToRegularQueue } from './tools/add-official-mirror-to-regular-queue.js';
19
19
  import { unlinkOfficialMirrorQueueItem } from './tools/unlink-official-mirror-queue-item.js';
20
20
  const ALL_TOOLS = [
21
- { factory: getNewsletterPosts, groups: ['newsletter'] },
22
- { factory: getNewsletterPost, groups: ['newsletter'] },
23
- { factory: draftNewsletterPost, groups: ['newsletter'] },
24
- { factory: updateNewsletterPost, groups: ['newsletter'] },
25
- { factory: uploadImage, groups: ['newsletter'] },
26
- { factory: getAuthors, groups: ['newsletter'] },
27
- { factory: searchMCPImplementations, groups: ['server_queue_readonly', 'server_queue_all'] },
28
- { factory: getDraftMCPImplementations, groups: ['server_queue_readonly', 'server_queue_all'] },
29
- { factory: saveMCPImplementation, groups: ['server_queue_all'] },
30
- { factory: sendMCPImplementationPostingNotification, groups: ['server_queue_all'] },
31
- { factory: findProviders, groups: ['server_queue_readonly', 'server_queue_all'] },
32
- // Official mirror queue tools
21
+ // Newsletter tools (all are write operations except get_newsletter_posts/post and get_authors)
22
+ { factory: getNewsletterPosts, group: 'newsletter', isWriteOperation: false },
23
+ { factory: getNewsletterPost, group: 'newsletter', isWriteOperation: false },
24
+ { factory: draftNewsletterPost, group: 'newsletter', isWriteOperation: true },
25
+ { factory: updateNewsletterPost, group: 'newsletter', isWriteOperation: true },
26
+ { factory: uploadImage, group: 'newsletter', isWriteOperation: true },
27
+ { factory: getAuthors, group: 'newsletter', isWriteOperation: false },
28
+ // Server queue tools
29
+ { factory: searchMCPImplementations, group: 'server_queue', isWriteOperation: false },
30
+ { factory: getDraftMCPImplementations, group: 'server_queue', isWriteOperation: false },
31
+ { factory: saveMCPImplementation, group: 'server_queue', isWriteOperation: true },
33
32
  {
34
- factory: getOfficialMirrorQueueItems,
35
- groups: ['official_queue_readonly', 'official_queue_all'],
33
+ factory: sendMCPImplementationPostingNotification,
34
+ group: 'server_queue',
35
+ isWriteOperation: true,
36
36
  },
37
+ { factory: findProviders, group: 'server_queue', isWriteOperation: false },
38
+ // Official mirror queue tools
39
+ { factory: getOfficialMirrorQueueItems, group: 'official_queue', isWriteOperation: false },
40
+ { factory: getOfficialMirrorQueueItem, group: 'official_queue', isWriteOperation: false },
41
+ { factory: approveOfficialMirrorQueueItem, group: 'official_queue', isWriteOperation: true },
37
42
  {
38
- factory: getOfficialMirrorQueueItem,
39
- groups: ['official_queue_readonly', 'official_queue_all'],
43
+ factory: approveOfficialMirrorQueueItemWithoutModifying,
44
+ group: 'official_queue',
45
+ isWriteOperation: true,
40
46
  },
41
- { factory: approveOfficialMirrorQueueItem, groups: ['official_queue_all'] },
42
- { factory: approveOfficialMirrorQueueItemWithoutModifying, groups: ['official_queue_all'] },
43
- { factory: rejectOfficialMirrorQueueItem, groups: ['official_queue_all'] },
44
- { factory: addOfficialMirrorToRegularQueue, groups: ['official_queue_all'] },
45
- { factory: unlinkOfficialMirrorQueueItem, groups: ['official_queue_all'] },
47
+ { factory: rejectOfficialMirrorQueueItem, group: 'official_queue', isWriteOperation: true },
48
+ { factory: addOfficialMirrorToRegularQueue, group: 'official_queue', isWriteOperation: true },
49
+ { factory: unlinkOfficialMirrorQueueItem, group: 'official_queue', isWriteOperation: true },
50
+ ];
51
+ /**
52
+ * All valid tool groups (base groups and their _readonly variants)
53
+ */
54
+ const VALID_TOOL_GROUPS = [
55
+ 'newsletter',
56
+ 'newsletter_readonly',
57
+ 'server_queue',
58
+ 'server_queue_readonly',
59
+ 'official_queue',
60
+ 'official_queue_readonly',
46
61
  ];
62
+ /**
63
+ * Base groups (without _readonly suffix) - used for default "all groups" behavior
64
+ */
65
+ const BASE_TOOL_GROUPS = ['newsletter', 'server_queue', 'official_queue'];
47
66
  /**
48
67
  * Parse enabled tool groups from environment variable or parameter
49
- * @param enabledGroupsParam - Comma-separated list of tool groups (e.g., "newsletter,server_queue_all")
68
+ * @param enabledGroupsParam - Comma-separated list of tool groups (e.g., "newsletter,server_queue_readonly")
50
69
  * @returns Array of enabled tool groups
51
70
  */
52
71
  export function parseEnabledToolGroups(enabledGroupsParam) {
53
- const groupsStr = enabledGroupsParam || process.env.PULSEMCP_ADMIN_ENABLED_TOOLGROUPS || '';
72
+ const groupsStr = enabledGroupsParam || process.env.TOOL_GROUPS || '';
54
73
  if (!groupsStr) {
55
- // Default: all groups enabled
56
- return [
57
- 'newsletter',
58
- 'server_queue_readonly',
59
- 'server_queue_all',
60
- 'official_queue_readonly',
61
- 'official_queue_all',
62
- ];
74
+ // Default: all base groups enabled (full read+write access)
75
+ return [...BASE_TOOL_GROUPS];
63
76
  }
64
77
  const groups = groupsStr.split(',').map((g) => g.trim());
65
78
  const validGroups = [];
66
79
  for (const group of groups) {
67
- if (group === 'newsletter' ||
68
- group === 'server_queue_readonly' ||
69
- group === 'server_queue_all' ||
70
- group === 'official_queue_readonly' ||
71
- group === 'official_queue_all') {
80
+ if (VALID_TOOL_GROUPS.includes(group) &&
81
+ !validGroups.includes(group)) {
72
82
  validGroups.push(group);
73
83
  }
74
- else {
84
+ else if (!VALID_TOOL_GROUPS.includes(group)) {
75
85
  console.warn(`Unknown tool group: ${group}`);
76
86
  }
77
87
  }
78
88
  return validGroups;
79
89
  }
90
+ /**
91
+ * Check if a tool should be included based on enabled groups
92
+ * @param toolDef - The tool definition to check
93
+ * @param enabledGroups - Array of enabled tool groups
94
+ * @returns true if the tool should be included
95
+ */
96
+ function shouldIncludeTool(toolDef, enabledGroups) {
97
+ const baseGroup = toolDef.group;
98
+ const readonlyGroup = `${baseGroup}_readonly`;
99
+ // Check if the base group (full access) is enabled
100
+ if (enabledGroups.includes(baseGroup)) {
101
+ return true;
102
+ }
103
+ // Check if the readonly group is enabled (only include read operations)
104
+ if (enabledGroups.includes(readonlyGroup) && !toolDef.isWriteOperation) {
105
+ return true;
106
+ }
107
+ return false;
108
+ }
80
109
  /**
81
110
  * Creates a function to register all tools with the server.
82
111
  * This pattern uses individual tool files for better modularity and testability.
@@ -84,16 +113,17 @@ export function parseEnabledToolGroups(enabledGroupsParam) {
84
113
  * Each tool is defined in its own file under the `tools/` directory and follows
85
114
  * a factory pattern that accepts the server and clientFactory as parameters.
86
115
  *
87
- * Tool groups can be enabled/disabled via the PULSEMCP_ADMIN_ENABLED_TOOLGROUPS
88
- * environment variable (comma-separated list, e.g., "newsletter,server_queue_readonly").
89
- * If not set, all tool groups are enabled by default.
116
+ * Tool groups can be enabled/disabled via the TOOL_GROUPS environment variable
117
+ * (comma-separated list, e.g., "newsletter,server_queue_readonly"). If not set, all
118
+ * base tool groups are enabled by default (full read+write access).
90
119
  *
91
120
  * Available tool groups:
92
- * - newsletter: All newsletter-related tools
93
- * - server_queue_readonly: Read-only server queue tools (search, get drafts)
94
- * - server_queue_all: All server queue tools including write operations
95
- * - official_queue_readonly: Read-only official mirror queue tools (list, get)
96
- * - official_queue_all: All official mirror queue tools including write operations
121
+ * - newsletter: All newsletter-related tools (read + write)
122
+ * - newsletter_readonly: Newsletter tools (read only)
123
+ * - server_queue: MCP implementation queue tools (read + write)
124
+ * - server_queue_readonly: MCP implementation queue tools (read only)
125
+ * - official_queue: Official mirror queue tools (read + write)
126
+ * - official_queue_readonly: Official mirror queue tools (read only)
97
127
  *
98
128
  * @param clientFactory - Factory function that creates client instances
99
129
  * @param enabledGroups - Optional comma-separated list of enabled tool groups (overrides env var)
@@ -103,7 +133,7 @@ export function createRegisterTools(clientFactory, enabledGroups) {
103
133
  return (server) => {
104
134
  const enabledToolGroups = parseEnabledToolGroups(enabledGroups);
105
135
  // Filter tools based on enabled groups
106
- const enabledTools = ALL_TOOLS.filter((toolDef) => toolDef.groups.some((group) => enabledToolGroups.includes(group)));
136
+ const enabledTools = ALL_TOOLS.filter((toolDef) => shouldIncludeTool(toolDef, enabledToolGroups));
107
137
  // Create tool instances
108
138
  const tools = enabledTools.map((toolDef) => toolDef.factory(server, clientFactory));
109
139
  // List available tools
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pulsemcp-cms-admin-mcp-server",
3
- "version": "0.4.3",
3
+ "version": "0.5.0",
4
4
  "description": "Local implementation of PulseMCP CMS Admin MCP server",
5
5
  "mcpName": "com.pulsemcp.servers/pulsemcp-cms-admin",
6
6
  "main": "build/index.js",
@@ -65,7 +65,13 @@ export async function createPost(apiKey, baseUrl, params) {
65
65
  }
66
66
  if (response.status === 422) {
67
67
  const errorData = (await response.json());
68
- const errors = errorData.errors || ['Validation failed'];
68
+ // Handle both array format and single error string format from Rails
69
+ // Also handle empty arrays - an empty array should fall back to the default message
70
+ const errors = errorData.errors && errorData.errors.length > 0
71
+ ? errorData.errors
72
+ : errorData.error
73
+ ? [errorData.error]
74
+ : ['Unknown validation error'];
69
75
  throw new Error(`Validation failed: ${errors.join(', ')}`);
70
76
  }
71
77
  throw new Error(`Failed to create post: ${response.status} ${response.statusText}`);
@@ -154,7 +154,13 @@ export async function saveMCPImplementation(apiKey, baseUrl, id, params) {
154
154
  }
155
155
  if (response.status === 422) {
156
156
  const errorData = (await response.json());
157
- const errors = errorData.errors || ['Validation failed'];
157
+ // Handle both array format and single error string format from Rails
158
+ // Also handle empty arrays - an empty array should fall back to the default message
159
+ const errors = errorData.errors && errorData.errors.length > 0
160
+ ? errorData.errors
161
+ : errorData.error
162
+ ? [errorData.error]
163
+ : ['Unknown validation error'];
158
164
  throw new Error(`Validation failed: ${errors.join(', ')}`);
159
165
  }
160
166
  throw new Error(`Failed to save MCP implementation: ${response.status} ${response.statusText}`);
@@ -25,16 +25,19 @@ export async function sendEmail(apiKey, baseUrl, params) {
25
25
  let errorMessage;
26
26
  try {
27
27
  const errorData = JSON.parse(errorBody);
28
- if (errorData.errors) {
29
- errorMessage = Array.isArray(errorData.errors)
30
- ? errorData.errors.join(', ')
31
- : JSON.stringify(errorData.errors);
28
+ // Handle both array format and single error string format from Rails
29
+ // Also handle empty arrays - an empty array should fall back to checking other fields
30
+ if (Array.isArray(errorData.errors) && errorData.errors.length > 0) {
31
+ errorMessage = errorData.errors.join(', ');
32
+ }
33
+ else if (errorData.errors && typeof errorData.errors === 'object') {
34
+ errorMessage = JSON.stringify(errorData.errors);
32
35
  }
33
36
  else if (errorData.error) {
34
37
  errorMessage = errorData.error;
35
38
  }
36
39
  else {
37
- errorMessage = errorBody;
40
+ errorMessage = errorBody || 'Unknown validation error';
38
41
  }
39
42
  }
40
43
  catch {
@@ -71,7 +71,13 @@ export async function updatePost(apiKey, baseUrl, slug, params) {
71
71
  }
72
72
  if (response.status === 422) {
73
73
  const errorData = (await response.json());
74
- const errors = errorData.errors || ['Validation failed'];
74
+ // Handle both array format and single error string format from Rails
75
+ // Also handle empty arrays - an empty array should fall back to the default message
76
+ const errors = errorData.errors && errorData.errors.length > 0
77
+ ? errorData.errors
78
+ : errorData.error
79
+ ? [errorData.error]
80
+ : ['Unknown validation error'];
75
81
  throw new Error(`Validation failed: ${errors.join(', ')}`);
76
82
  }
77
83
  throw new Error(`Failed to update post: ${response.status} ${response.statusText}`);
package/shared/tools.d.ts CHANGED
@@ -3,16 +3,19 @@ import { ClientFactory } from './server.js';
3
3
  /**
4
4
  * Tool group definitions - groups of related tools that can be enabled/disabled together
5
5
  *
6
- * - newsletter: All newsletter-related tools (posts, authors, images)
7
- * - server_queue_readonly: Read-only server queue tools (search, get drafts)
8
- * - server_queue_all: All server queue tools including write operations (search, get drafts, save)
9
- * - official_queue_readonly: Read-only official mirror queue tools (list, get)
10
- * - official_queue_all: All official mirror queue tools including write operations (approve, reject, unlink)
6
+ * Each group has two variants:
7
+ * - Base group (e.g., 'newsletter'): Includes all tools (read + write operations)
8
+ * - Readonly group (e.g., 'newsletter_readonly'): Includes only read operations
9
+ *
10
+ * Groups:
11
+ * - newsletter / newsletter_readonly: Newsletter-related tools (posts, authors, images)
12
+ * - server_queue / server_queue_readonly: Server queue tools (search, drafts, providers, save, notifications)
13
+ * - official_queue / official_queue_readonly: Official mirror queue tools (list, get, approve, reject, unlink)
11
14
  */
12
- export type ToolGroup = 'newsletter' | 'server_queue_readonly' | 'server_queue_all' | 'official_queue_readonly' | 'official_queue_all';
15
+ export type ToolGroup = 'newsletter' | 'newsletter_readonly' | 'server_queue' | 'server_queue_readonly' | 'official_queue' | 'official_queue_readonly';
13
16
  /**
14
17
  * Parse enabled tool groups from environment variable or parameter
15
- * @param enabledGroupsParam - Comma-separated list of tool groups (e.g., "newsletter,server_queue_all")
18
+ * @param enabledGroupsParam - Comma-separated list of tool groups (e.g., "newsletter,server_queue_readonly")
16
19
  * @returns Array of enabled tool groups
17
20
  */
18
21
  export declare function parseEnabledToolGroups(enabledGroupsParam?: string): ToolGroup[];
@@ -23,16 +26,17 @@ export declare function parseEnabledToolGroups(enabledGroupsParam?: string): Too
23
26
  * Each tool is defined in its own file under the `tools/` directory and follows
24
27
  * a factory pattern that accepts the server and clientFactory as parameters.
25
28
  *
26
- * Tool groups can be enabled/disabled via the PULSEMCP_ADMIN_ENABLED_TOOLGROUPS
27
- * environment variable (comma-separated list, e.g., "newsletter,server_queue_readonly").
28
- * If not set, all tool groups are enabled by default.
29
+ * Tool groups can be enabled/disabled via the TOOL_GROUPS environment variable
30
+ * (comma-separated list, e.g., "newsletter,server_queue_readonly"). If not set, all
31
+ * base tool groups are enabled by default (full read+write access).
29
32
  *
30
33
  * Available tool groups:
31
- * - newsletter: All newsletter-related tools
32
- * - server_queue_readonly: Read-only server queue tools (search, get drafts)
33
- * - server_queue_all: All server queue tools including write operations
34
- * - official_queue_readonly: Read-only official mirror queue tools (list, get)
35
- * - official_queue_all: All official mirror queue tools including write operations
34
+ * - newsletter: All newsletter-related tools (read + write)
35
+ * - newsletter_readonly: Newsletter tools (read only)
36
+ * - server_queue: MCP implementation queue tools (read + write)
37
+ * - server_queue_readonly: MCP implementation queue tools (read only)
38
+ * - official_queue: Official mirror queue tools (read + write)
39
+ * - official_queue_readonly: Official mirror queue tools (read only)
36
40
  *
37
41
  * @param clientFactory - Factory function that creates client instances
38
42
  * @param enabledGroups - Optional comma-separated list of enabled tool groups (overrides env var)
package/shared/tools.js CHANGED
@@ -18,65 +18,94 @@ import { rejectOfficialMirrorQueueItem } from './tools/reject-official-mirror-qu
18
18
  import { addOfficialMirrorToRegularQueue } from './tools/add-official-mirror-to-regular-queue.js';
19
19
  import { unlinkOfficialMirrorQueueItem } from './tools/unlink-official-mirror-queue-item.js';
20
20
  const ALL_TOOLS = [
21
- { factory: getNewsletterPosts, groups: ['newsletter'] },
22
- { factory: getNewsletterPost, groups: ['newsletter'] },
23
- { factory: draftNewsletterPost, groups: ['newsletter'] },
24
- { factory: updateNewsletterPost, groups: ['newsletter'] },
25
- { factory: uploadImage, groups: ['newsletter'] },
26
- { factory: getAuthors, groups: ['newsletter'] },
27
- { factory: searchMCPImplementations, groups: ['server_queue_readonly', 'server_queue_all'] },
28
- { factory: getDraftMCPImplementations, groups: ['server_queue_readonly', 'server_queue_all'] },
29
- { factory: saveMCPImplementation, groups: ['server_queue_all'] },
30
- { factory: sendMCPImplementationPostingNotification, groups: ['server_queue_all'] },
31
- { factory: findProviders, groups: ['server_queue_readonly', 'server_queue_all'] },
32
- // Official mirror queue tools
21
+ // Newsletter tools (all are write operations except get_newsletter_posts/post and get_authors)
22
+ { factory: getNewsletterPosts, group: 'newsletter', isWriteOperation: false },
23
+ { factory: getNewsletterPost, group: 'newsletter', isWriteOperation: false },
24
+ { factory: draftNewsletterPost, group: 'newsletter', isWriteOperation: true },
25
+ { factory: updateNewsletterPost, group: 'newsletter', isWriteOperation: true },
26
+ { factory: uploadImage, group: 'newsletter', isWriteOperation: true },
27
+ { factory: getAuthors, group: 'newsletter', isWriteOperation: false },
28
+ // Server queue tools
29
+ { factory: searchMCPImplementations, group: 'server_queue', isWriteOperation: false },
30
+ { factory: getDraftMCPImplementations, group: 'server_queue', isWriteOperation: false },
31
+ { factory: saveMCPImplementation, group: 'server_queue', isWriteOperation: true },
33
32
  {
34
- factory: getOfficialMirrorQueueItems,
35
- groups: ['official_queue_readonly', 'official_queue_all'],
33
+ factory: sendMCPImplementationPostingNotification,
34
+ group: 'server_queue',
35
+ isWriteOperation: true,
36
36
  },
37
+ { factory: findProviders, group: 'server_queue', isWriteOperation: false },
38
+ // Official mirror queue tools
39
+ { factory: getOfficialMirrorQueueItems, group: 'official_queue', isWriteOperation: false },
40
+ { factory: getOfficialMirrorQueueItem, group: 'official_queue', isWriteOperation: false },
41
+ { factory: approveOfficialMirrorQueueItem, group: 'official_queue', isWriteOperation: true },
37
42
  {
38
- factory: getOfficialMirrorQueueItem,
39
- groups: ['official_queue_readonly', 'official_queue_all'],
43
+ factory: approveOfficialMirrorQueueItemWithoutModifying,
44
+ group: 'official_queue',
45
+ isWriteOperation: true,
40
46
  },
41
- { factory: approveOfficialMirrorQueueItem, groups: ['official_queue_all'] },
42
- { factory: approveOfficialMirrorQueueItemWithoutModifying, groups: ['official_queue_all'] },
43
- { factory: rejectOfficialMirrorQueueItem, groups: ['official_queue_all'] },
44
- { factory: addOfficialMirrorToRegularQueue, groups: ['official_queue_all'] },
45
- { factory: unlinkOfficialMirrorQueueItem, groups: ['official_queue_all'] },
47
+ { factory: rejectOfficialMirrorQueueItem, group: 'official_queue', isWriteOperation: true },
48
+ { factory: addOfficialMirrorToRegularQueue, group: 'official_queue', isWriteOperation: true },
49
+ { factory: unlinkOfficialMirrorQueueItem, group: 'official_queue', isWriteOperation: true },
50
+ ];
51
+ /**
52
+ * All valid tool groups (base groups and their _readonly variants)
53
+ */
54
+ const VALID_TOOL_GROUPS = [
55
+ 'newsletter',
56
+ 'newsletter_readonly',
57
+ 'server_queue',
58
+ 'server_queue_readonly',
59
+ 'official_queue',
60
+ 'official_queue_readonly',
46
61
  ];
62
+ /**
63
+ * Base groups (without _readonly suffix) - used for default "all groups" behavior
64
+ */
65
+ const BASE_TOOL_GROUPS = ['newsletter', 'server_queue', 'official_queue'];
47
66
  /**
48
67
  * Parse enabled tool groups from environment variable or parameter
49
- * @param enabledGroupsParam - Comma-separated list of tool groups (e.g., "newsletter,server_queue_all")
68
+ * @param enabledGroupsParam - Comma-separated list of tool groups (e.g., "newsletter,server_queue_readonly")
50
69
  * @returns Array of enabled tool groups
51
70
  */
52
71
  export function parseEnabledToolGroups(enabledGroupsParam) {
53
- const groupsStr = enabledGroupsParam || process.env.PULSEMCP_ADMIN_ENABLED_TOOLGROUPS || '';
72
+ const groupsStr = enabledGroupsParam || process.env.TOOL_GROUPS || '';
54
73
  if (!groupsStr) {
55
- // Default: all groups enabled
56
- return [
57
- 'newsletter',
58
- 'server_queue_readonly',
59
- 'server_queue_all',
60
- 'official_queue_readonly',
61
- 'official_queue_all',
62
- ];
74
+ // Default: all base groups enabled (full read+write access)
75
+ return [...BASE_TOOL_GROUPS];
63
76
  }
64
77
  const groups = groupsStr.split(',').map((g) => g.trim());
65
78
  const validGroups = [];
66
79
  for (const group of groups) {
67
- if (group === 'newsletter' ||
68
- group === 'server_queue_readonly' ||
69
- group === 'server_queue_all' ||
70
- group === 'official_queue_readonly' ||
71
- group === 'official_queue_all') {
80
+ if (VALID_TOOL_GROUPS.includes(group) &&
81
+ !validGroups.includes(group)) {
72
82
  validGroups.push(group);
73
83
  }
74
- else {
84
+ else if (!VALID_TOOL_GROUPS.includes(group)) {
75
85
  console.warn(`Unknown tool group: ${group}`);
76
86
  }
77
87
  }
78
88
  return validGroups;
79
89
  }
90
+ /**
91
+ * Check if a tool should be included based on enabled groups
92
+ * @param toolDef - The tool definition to check
93
+ * @param enabledGroups - Array of enabled tool groups
94
+ * @returns true if the tool should be included
95
+ */
96
+ function shouldIncludeTool(toolDef, enabledGroups) {
97
+ const baseGroup = toolDef.group;
98
+ const readonlyGroup = `${baseGroup}_readonly`;
99
+ // Check if the base group (full access) is enabled
100
+ if (enabledGroups.includes(baseGroup)) {
101
+ return true;
102
+ }
103
+ // Check if the readonly group is enabled (only include read operations)
104
+ if (enabledGroups.includes(readonlyGroup) && !toolDef.isWriteOperation) {
105
+ return true;
106
+ }
107
+ return false;
108
+ }
80
109
  /**
81
110
  * Creates a function to register all tools with the server.
82
111
  * This pattern uses individual tool files for better modularity and testability.
@@ -84,16 +113,17 @@ export function parseEnabledToolGroups(enabledGroupsParam) {
84
113
  * Each tool is defined in its own file under the `tools/` directory and follows
85
114
  * a factory pattern that accepts the server and clientFactory as parameters.
86
115
  *
87
- * Tool groups can be enabled/disabled via the PULSEMCP_ADMIN_ENABLED_TOOLGROUPS
88
- * environment variable (comma-separated list, e.g., "newsletter,server_queue_readonly").
89
- * If not set, all tool groups are enabled by default.
116
+ * Tool groups can be enabled/disabled via the TOOL_GROUPS environment variable
117
+ * (comma-separated list, e.g., "newsletter,server_queue_readonly"). If not set, all
118
+ * base tool groups are enabled by default (full read+write access).
90
119
  *
91
120
  * Available tool groups:
92
- * - newsletter: All newsletter-related tools
93
- * - server_queue_readonly: Read-only server queue tools (search, get drafts)
94
- * - server_queue_all: All server queue tools including write operations
95
- * - official_queue_readonly: Read-only official mirror queue tools (list, get)
96
- * - official_queue_all: All official mirror queue tools including write operations
121
+ * - newsletter: All newsletter-related tools (read + write)
122
+ * - newsletter_readonly: Newsletter tools (read only)
123
+ * - server_queue: MCP implementation queue tools (read + write)
124
+ * - server_queue_readonly: MCP implementation queue tools (read only)
125
+ * - official_queue: Official mirror queue tools (read + write)
126
+ * - official_queue_readonly: Official mirror queue tools (read only)
97
127
  *
98
128
  * @param clientFactory - Factory function that creates client instances
99
129
  * @param enabledGroups - Optional comma-separated list of enabled tool groups (overrides env var)
@@ -103,7 +133,7 @@ export function createRegisterTools(clientFactory, enabledGroups) {
103
133
  return (server) => {
104
134
  const enabledToolGroups = parseEnabledToolGroups(enabledGroups);
105
135
  // Filter tools based on enabled groups
106
- const enabledTools = ALL_TOOLS.filter((toolDef) => toolDef.groups.some((group) => enabledToolGroups.includes(group)));
136
+ const enabledTools = ALL_TOOLS.filter((toolDef) => shouldIncludeTool(toolDef, enabledToolGroups));
107
137
  // Create tool instances
108
138
  const tools = enabledTools.map((toolDef) => toolDef.factory(server, clientFactory));
109
139
  // List available tools