pulsemcp-cms-admin-mcp-server 0.1.0 → 0.1.1
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 +50 -18
- package/build/local/src/index.integration-with-mock.js +36 -0
- package/build/shared/src/pulsemcp-admin-client/lib/get-draft-mcp-implementations.js +61 -0
- package/build/shared/src/pulsemcp-admin-client/lib/save-mcp-implementation.js +73 -0
- package/build/shared/src/pulsemcp-admin-client/lib/search-mcp-implementations.js +51 -0
- package/build/shared/src/pulsemcp-admin-client/lib/send-email.js +46 -0
- package/build/shared/src/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +120 -0
- package/build/shared/src/server.js +16 -0
- package/build/shared/src/tools/get-draft-mcp-implementations.js +167 -0
- package/build/shared/src/tools/save-mcp-implementation.js +223 -0
- package/build/shared/src/tools/search-mcp-implementations.js +155 -0
- package/build/shared/src/tools/send-mcp-implementation-posting-notification.js +207 -0
- package/build/shared/src/tools.js +56 -9
- package/package.json +1 -1
- package/shared/pulsemcp-admin-client/lib/get-draft-mcp-implementations.d.ts +6 -0
- package/shared/pulsemcp-admin-client/lib/get-draft-mcp-implementations.js +61 -0
- package/shared/pulsemcp-admin-client/lib/save-mcp-implementation.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/save-mcp-implementation.js +73 -0
- package/shared/pulsemcp-admin-client/lib/search-mcp-implementations.d.ts +9 -0
- package/shared/pulsemcp-admin-client/lib/search-mcp-implementations.js +51 -0
- package/shared/pulsemcp-admin-client/lib/send-email.d.ts +25 -0
- package/shared/pulsemcp-admin-client/lib/send-email.js +46 -0
- package/shared/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.d.ts +8 -1
- package/shared/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +120 -0
- package/shared/server.d.ts +65 -1
- package/shared/server.js +16 -0
- package/shared/tools/get-draft-mcp-implementations.d.ts +33 -0
- package/shared/tools/get-draft-mcp-implementations.js +167 -0
- package/shared/tools/save-mcp-implementation.d.ts +85 -0
- package/shared/tools/save-mcp-implementation.js +223 -0
- package/shared/tools/search-mcp-implementations.d.ts +48 -0
- package/shared/tools/search-mcp-implementations.js +155 -0
- package/shared/tools/send-mcp-implementation-posting-notification.d.ts +46 -0
- package/shared/tools/send-mcp-implementation-posting-notification.js +207 -0
- package/shared/tools.d.ts +25 -1
- package/shared/tools.js +56 -9
- package/shared/types.d.ts +46 -0
package/README.md
CHANGED
|
@@ -30,20 +30,38 @@ This is an MCP ([Model Context Protocol](https://modelcontextprotocol.io/)) Serv
|
|
|
30
30
|
|
|
31
31
|
**Content Search**: Find newsletter posts with powerful search and pagination capabilities.
|
|
32
32
|
|
|
33
|
+
**MCP Implementation Search**: Search for MCP servers and clients in the PulseMCP registry.
|
|
34
|
+
|
|
35
|
+
**Toolgroups**: Enable/disable tool groups (newsletter, server_queue_readonly, server_queue_all) via environment variable.
|
|
36
|
+
|
|
33
37
|
**Draft Control**: Manage draft posts before publishing to the newsletter.
|
|
34
38
|
|
|
35
39
|
# Capabilities
|
|
36
40
|
|
|
37
41
|
This server is built and tested on macOS with Claude Desktop. It should work with other MCP clients as well.
|
|
38
42
|
|
|
39
|
-
| Tool Name
|
|
40
|
-
|
|
|
41
|
-
| `get_newsletter_posts`
|
|
42
|
-
| `get_newsletter_post`
|
|
43
|
-
| `draft_newsletter_post`
|
|
44
|
-
| `update_newsletter_post` | Update an existing newsletter post's content and metadata (except status).
|
|
45
|
-
| `upload_image`
|
|
46
|
-
| `get_authors` | Get a list of authors with optional search and pagination.
|
|
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
|
+
| `save_mcp_implementation` | server_queue_all | Update an MCP implementation (replicates Admin panel "Save Changes" button). |
|
|
54
|
+
| `send_mcp_implementation_posting_notification` | server_queue_all | Send email notification when MCP implementation goes live. |
|
|
55
|
+
|
|
56
|
+
# Tool Groups
|
|
57
|
+
|
|
58
|
+
This server organizes tools into groups that can be selectively enabled or disabled:
|
|
59
|
+
|
|
60
|
+
- **newsletter** (6 tools): Newsletter management, image uploads, and author retrieval
|
|
61
|
+
- **server_queue_readonly** (2 tools): Read-only MCP implementation tools (search, draft retrieval)
|
|
62
|
+
- **server_queue_all** (4 tools): All MCP implementation tools including write operations (search, draft retrieval, update, and email notification)
|
|
63
|
+
|
|
64
|
+
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.
|
|
47
65
|
|
|
48
66
|
# Usage Tips
|
|
49
67
|
|
|
@@ -54,6 +72,8 @@ This server is built and tested on macOS with Claude Desktop. It should work wit
|
|
|
54
72
|
- The update tool allows modifying all post metadata except status
|
|
55
73
|
- Use author slugs when creating posts (e.g., "sarah-chen", "john-doe")
|
|
56
74
|
- Use MCP server/client slugs for featured content (e.g., "github-mcp", "claude-desktop")
|
|
75
|
+
- Use `search_mcp_implementations` to discover MCP servers and clients in the PulseMCP registry
|
|
76
|
+
- Enable or disable specific toolgroups by setting `PULSEMCP_ADMIN_ENABLED_TOOLGROUPS` environment variable
|
|
57
77
|
|
|
58
78
|
# Examples
|
|
59
79
|
|
|
@@ -80,9 +100,7 @@ I found 3 newsletter posts about MCP servers:
|
|
|
80
100
|
## Create a Draft Post
|
|
81
101
|
|
|
82
102
|
```
|
|
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.
|
|
103
|
+
User: Create a draft newsletter post about the new weather MCP serverAssistant: I'll create a draft newsletter post about the new weather MCP server.
|
|
86
104
|
|
|
87
105
|
[Calls draft_newsletter_post with title, body, slug, and author_slug]
|
|
88
106
|
|
|
@@ -92,9 +110,7 @@ I've created a draft newsletter post titled "New Weather MCP Server: Real-time F
|
|
|
92
110
|
## Upload and Attach an Image
|
|
93
111
|
|
|
94
112
|
```
|
|
95
|
-
User: Upload this screenshot to the weather server post I just
|
|
96
|
-
|
|
97
|
-
Assistant: I'll upload the screenshot and attach it to your weather server post.
|
|
113
|
+
User: Upload this screenshot to the weather server post I just createdAssistant: I'll upload the screenshot and attach it to your weather server post.
|
|
98
114
|
|
|
99
115
|
[Calls upload_image with post_slug="weather-mcp-server-launch" and file_name="weather-dashboard.png"]
|
|
100
116
|
|
|
@@ -104,15 +120,30 @@ The screenshot has been successfully uploaded and attached to the "weather-mcp-s
|
|
|
104
120
|
## Update an Existing Post
|
|
105
121
|
|
|
106
122
|
```
|
|
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.
|
|
123
|
+
User: Update the weather post to include information about the new temperature alerts featureAssistant: I'll update the weather MCP server post to include information about the temperature alerts feature.
|
|
110
124
|
|
|
111
125
|
[Calls update_newsletter_post with slug="weather-mcp-server-launch" and updated content]
|
|
112
126
|
|
|
113
127
|
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
128
|
```
|
|
115
129
|
|
|
130
|
+
## Search for MCP Implementations
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
User: Find MCP servers related to weather or climate
|
|
134
|
+
Assistant: I'll search for MCP servers related to weather and climate.
|
|
135
|
+
|
|
136
|
+
[Calls search_mcp_implementations with search="weather climate"]
|
|
137
|
+
|
|
138
|
+
I found 2 MCP servers related to weather and climate:
|
|
139
|
+
|
|
140
|
+
1. **Weather MCP Server** (weather-mcp)
|
|
141
|
+
- Real-time weather data and forecasting capabilities
|
|
142
|
+
|
|
143
|
+
2. **Climate Data MCP Client** (climate-data-mcp)
|
|
144
|
+
- Access climate research and environmental data
|
|
145
|
+
```
|
|
146
|
+
|
|
116
147
|
# Setup
|
|
117
148
|
|
|
118
149
|
## Cheatsheet
|
|
@@ -148,7 +179,8 @@ Add to your Claude Desktop configuration:
|
|
|
148
179
|
"command": "node",
|
|
149
180
|
"args": ["/path/to/pulsemcp-cms-admin/local/build/index.js"],
|
|
150
181
|
"env": {
|
|
151
|
-
"PULSEMCP_ADMIN_API_KEY": "your-api-key-here"
|
|
182
|
+
"PULSEMCP_ADMIN_API_KEY": "your-api-key-here",
|
|
183
|
+
"PULSEMCP_ADMIN_ENABLED_TOOLGROUPS": "newsletter,server_queue_readonly,server_queue_all"
|
|
152
184
|
}
|
|
153
185
|
}
|
|
154
186
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Integration Test Server Entry Point with Mock Client Factory
|
|
4
|
+
*
|
|
5
|
+
* This file is used for integration testing with a mocked PulseMCPAdminClient.
|
|
6
|
+
* It uses the real MCP server but injects a mock client factory.
|
|
7
|
+
*
|
|
8
|
+
* Mock data is passed via the PULSEMCP_MOCK_DATA environment variable.
|
|
9
|
+
*/
|
|
10
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
11
|
+
// IMPORTANT: This uses the package name pattern, not a relative path
|
|
12
|
+
import { createMCPServer } from 'pulsemcp-cms-admin-mcp-server-shared';
|
|
13
|
+
// Import the mock client factory from the shared module
|
|
14
|
+
import { createMockPulseMCPAdminClient } from '../../shared/src/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js';
|
|
15
|
+
async function main() {
|
|
16
|
+
const transport = new StdioServerTransport();
|
|
17
|
+
// Parse mock data from environment variable
|
|
18
|
+
let mockData = {};
|
|
19
|
+
if (process.env.PULSEMCP_MOCK_DATA) {
|
|
20
|
+
try {
|
|
21
|
+
mockData = JSON.parse(process.env.PULSEMCP_MOCK_DATA);
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
console.error('Failed to parse PULSEMCP_MOCK_DATA:', e);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
// Create client factory that returns our mock
|
|
28
|
+
const clientFactory = () => createMockPulseMCPAdminClient(mockData);
|
|
29
|
+
const { server, registerHandlers } = createMCPServer();
|
|
30
|
+
await registerHandlers(server, clientFactory);
|
|
31
|
+
await server.connect(transport);
|
|
32
|
+
}
|
|
33
|
+
main().catch((error) => {
|
|
34
|
+
console.error('Server error:', error);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
export async function getDraftMCPImplementations(apiKey, baseUrl, params) {
|
|
2
|
+
const url = new URL('/api/implementations/drafts', baseUrl);
|
|
3
|
+
// Add query parameters if provided
|
|
4
|
+
if (params?.page) {
|
|
5
|
+
url.searchParams.append('page', params.page.toString());
|
|
6
|
+
}
|
|
7
|
+
if (params?.search) {
|
|
8
|
+
url.searchParams.append('search', params.search);
|
|
9
|
+
}
|
|
10
|
+
const response = await fetch(url.toString(), {
|
|
11
|
+
method: 'GET',
|
|
12
|
+
headers: {
|
|
13
|
+
'X-API-Key': apiKey,
|
|
14
|
+
Accept: 'application/json',
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
if (!response.ok) {
|
|
18
|
+
if (response.status === 401) {
|
|
19
|
+
throw new Error('Invalid API key');
|
|
20
|
+
}
|
|
21
|
+
if (response.status === 403) {
|
|
22
|
+
throw new Error('User lacks admin privileges');
|
|
23
|
+
}
|
|
24
|
+
throw new Error(`Failed to fetch draft implementations: ${response.status} ${response.statusText}`);
|
|
25
|
+
}
|
|
26
|
+
// Parse the JSON response
|
|
27
|
+
const data = (await response.json());
|
|
28
|
+
// Handle the Rails JSON structure with data and meta
|
|
29
|
+
if (data.data && data.meta) {
|
|
30
|
+
return {
|
|
31
|
+
implementations: data.data.map((impl) => ({
|
|
32
|
+
id: impl.id,
|
|
33
|
+
name: impl.name,
|
|
34
|
+
short_description: impl.short_description,
|
|
35
|
+
description: impl.description,
|
|
36
|
+
type: impl.type,
|
|
37
|
+
status: impl.status,
|
|
38
|
+
slug: impl.slug,
|
|
39
|
+
url: impl.url,
|
|
40
|
+
provider_name: impl.provider_name,
|
|
41
|
+
github_stars: impl.github_stars,
|
|
42
|
+
classification: impl.classification,
|
|
43
|
+
implementation_language: impl.implementation_language,
|
|
44
|
+
mcp_server_id: impl.mcp_server_id,
|
|
45
|
+
mcp_client_id: impl.mcp_client_id,
|
|
46
|
+
created_at: impl.created_at,
|
|
47
|
+
updated_at: impl.updated_at,
|
|
48
|
+
})),
|
|
49
|
+
pagination: {
|
|
50
|
+
current_page: data.meta.current_page,
|
|
51
|
+
total_pages: data.meta.total_pages,
|
|
52
|
+
total_count: data.meta.total_count,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// Fallback for unexpected response format
|
|
57
|
+
return {
|
|
58
|
+
implementations: [],
|
|
59
|
+
pagination: undefined,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export async function saveMCPImplementation(apiKey, baseUrl, id, params) {
|
|
2
|
+
const url = new URL(`/api/implementations/${id}`, baseUrl);
|
|
3
|
+
// Build form data for the PUT request
|
|
4
|
+
const formData = new URLSearchParams();
|
|
5
|
+
// Add all provided fields using Rails conventions
|
|
6
|
+
if (params.name !== undefined) {
|
|
7
|
+
formData.append('mcp_implementation[name]', params.name);
|
|
8
|
+
}
|
|
9
|
+
if (params.short_description !== undefined) {
|
|
10
|
+
formData.append('mcp_implementation[short_description]', params.short_description);
|
|
11
|
+
}
|
|
12
|
+
if (params.description !== undefined) {
|
|
13
|
+
formData.append('mcp_implementation[description]', params.description);
|
|
14
|
+
}
|
|
15
|
+
if (params.type !== undefined) {
|
|
16
|
+
formData.append('mcp_implementation[type]', params.type);
|
|
17
|
+
}
|
|
18
|
+
if (params.status !== undefined) {
|
|
19
|
+
formData.append('mcp_implementation[status]', params.status);
|
|
20
|
+
}
|
|
21
|
+
if (params.slug !== undefined) {
|
|
22
|
+
formData.append('mcp_implementation[slug]', params.slug);
|
|
23
|
+
}
|
|
24
|
+
if (params.url !== undefined) {
|
|
25
|
+
formData.append('mcp_implementation[url]', params.url);
|
|
26
|
+
}
|
|
27
|
+
if (params.provider_name !== undefined) {
|
|
28
|
+
formData.append('mcp_implementation[provider_name]', params.provider_name);
|
|
29
|
+
}
|
|
30
|
+
if (params.github_stars !== undefined) {
|
|
31
|
+
formData.append('mcp_implementation[github_stars]', params.github_stars.toString());
|
|
32
|
+
}
|
|
33
|
+
if (params.classification !== undefined) {
|
|
34
|
+
formData.append('mcp_implementation[classification]', params.classification);
|
|
35
|
+
}
|
|
36
|
+
if (params.implementation_language !== undefined) {
|
|
37
|
+
formData.append('mcp_implementation[implementation_language]', params.implementation_language);
|
|
38
|
+
}
|
|
39
|
+
if (params.mcp_server_id !== undefined) {
|
|
40
|
+
formData.append('mcp_implementation[mcp_server_id]', params.mcp_server_id === null ? '' : params.mcp_server_id.toString());
|
|
41
|
+
}
|
|
42
|
+
if (params.mcp_client_id !== undefined) {
|
|
43
|
+
formData.append('mcp_implementation[mcp_client_id]', params.mcp_client_id === null ? '' : params.mcp_client_id.toString());
|
|
44
|
+
}
|
|
45
|
+
const response = await fetch(url.toString(), {
|
|
46
|
+
method: 'PUT',
|
|
47
|
+
headers: {
|
|
48
|
+
'X-API-Key': apiKey,
|
|
49
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
50
|
+
Accept: 'application/json',
|
|
51
|
+
},
|
|
52
|
+
body: formData.toString(),
|
|
53
|
+
});
|
|
54
|
+
if (!response.ok) {
|
|
55
|
+
if (response.status === 401) {
|
|
56
|
+
throw new Error('Invalid API key');
|
|
57
|
+
}
|
|
58
|
+
if (response.status === 403) {
|
|
59
|
+
throw new Error('User lacks admin privileges');
|
|
60
|
+
}
|
|
61
|
+
if (response.status === 404) {
|
|
62
|
+
throw new Error(`MCP implementation not found: ${id}`);
|
|
63
|
+
}
|
|
64
|
+
if (response.status === 422) {
|
|
65
|
+
const errorData = (await response.json());
|
|
66
|
+
const errors = errorData.errors || ['Validation failed'];
|
|
67
|
+
throw new Error(`Validation failed: ${errors.join(', ')}`);
|
|
68
|
+
}
|
|
69
|
+
throw new Error(`Failed to save MCP implementation: ${response.status} ${response.statusText}`);
|
|
70
|
+
}
|
|
71
|
+
const data = await response.json();
|
|
72
|
+
return data;
|
|
73
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export async function searchMCPImplementations(apiKey, baseUrl, params) {
|
|
2
|
+
// Endpoint implemented at: /api/implementations/search
|
|
3
|
+
// Requires admin authentication via X-API-Key header
|
|
4
|
+
const url = new URL('/api/implementations/search', baseUrl);
|
|
5
|
+
// Add query parameters
|
|
6
|
+
url.searchParams.append('q', params.query);
|
|
7
|
+
if (params.type && params.type !== 'all') {
|
|
8
|
+
url.searchParams.append('type', params.type);
|
|
9
|
+
}
|
|
10
|
+
if (params.status && params.status !== 'all') {
|
|
11
|
+
url.searchParams.append('status', params.status);
|
|
12
|
+
}
|
|
13
|
+
if (params.limit) {
|
|
14
|
+
url.searchParams.append('limit', params.limit.toString());
|
|
15
|
+
}
|
|
16
|
+
if (params.offset) {
|
|
17
|
+
url.searchParams.append('offset', params.offset.toString());
|
|
18
|
+
}
|
|
19
|
+
const response = await fetch(url.toString(), {
|
|
20
|
+
method: 'GET',
|
|
21
|
+
headers: {
|
|
22
|
+
'X-API-Key': apiKey,
|
|
23
|
+
Accept: 'application/json',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
if (response.status === 401) {
|
|
28
|
+
throw new Error('Invalid API key');
|
|
29
|
+
}
|
|
30
|
+
if (response.status === 403) {
|
|
31
|
+
throw new Error('User lacks admin privileges');
|
|
32
|
+
}
|
|
33
|
+
if (response.status === 404) {
|
|
34
|
+
throw new Error('MCP implementations search endpoint not found');
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Failed to search MCP implementations: ${response.status} ${response.statusText}`);
|
|
37
|
+
}
|
|
38
|
+
const data = (await response.json());
|
|
39
|
+
return {
|
|
40
|
+
implementations: data.data || [],
|
|
41
|
+
pagination: data.meta
|
|
42
|
+
? {
|
|
43
|
+
current_page: data.meta.current_page,
|
|
44
|
+
total_pages: data.meta.total_pages,
|
|
45
|
+
total_count: data.meta.total_count,
|
|
46
|
+
has_next: data.meta.has_next,
|
|
47
|
+
limit: data.meta.limit,
|
|
48
|
+
}
|
|
49
|
+
: undefined,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Send an email via the PulseMCP Admin API
|
|
3
|
+
*/
|
|
4
|
+
export async function sendEmail(apiKey, baseUrl, params) {
|
|
5
|
+
const url = new URL('/emails', baseUrl);
|
|
6
|
+
// Create form data for Rails-style parameters
|
|
7
|
+
const formData = new URLSearchParams();
|
|
8
|
+
formData.append('email[from_email_address]', params.from_email_address);
|
|
9
|
+
formData.append('email[from_name]', params.from_name);
|
|
10
|
+
formData.append('email[reply_to_email_address]', params.reply_to_email_address);
|
|
11
|
+
formData.append('email[to_email_address]', params.to_email_address);
|
|
12
|
+
formData.append('email[subject]', params.subject);
|
|
13
|
+
formData.append('email[content]', params.content);
|
|
14
|
+
const response = await fetch(url.toString(), {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: {
|
|
17
|
+
'X-API-Key': apiKey,
|
|
18
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
19
|
+
Accept: 'application/json',
|
|
20
|
+
},
|
|
21
|
+
body: formData.toString(),
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
const errorBody = await response.text();
|
|
25
|
+
let errorMessage;
|
|
26
|
+
try {
|
|
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);
|
|
32
|
+
}
|
|
33
|
+
else if (errorData.error) {
|
|
34
|
+
errorMessage = errorData.error;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
errorMessage = errorBody;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
errorMessage = errorBody || `HTTP ${response.status}: ${response.statusText}`;
|
|
42
|
+
}
|
|
43
|
+
throw new Error(`Failed to send email: ${errorMessage}`);
|
|
44
|
+
}
|
|
45
|
+
return response.json();
|
|
46
|
+
}
|
|
@@ -201,5 +201,125 @@ export function createMockPulseMCPAdminClient(mockData) {
|
|
|
201
201
|
}
|
|
202
202
|
return null;
|
|
203
203
|
},
|
|
204
|
+
async searchMCPImplementations(params) {
|
|
205
|
+
if (mockData.errors?.searchMCPImplementations) {
|
|
206
|
+
throw mockData.errors.searchMCPImplementations;
|
|
207
|
+
}
|
|
208
|
+
if (mockData.implementationsResponse) {
|
|
209
|
+
return mockData.implementationsResponse;
|
|
210
|
+
}
|
|
211
|
+
let implementations = mockData.implementations || [
|
|
212
|
+
{
|
|
213
|
+
id: 1,
|
|
214
|
+
name: 'Test MCP Server',
|
|
215
|
+
slug: 'test-mcp-server',
|
|
216
|
+
type: 'server',
|
|
217
|
+
status: 'live',
|
|
218
|
+
short_description: 'A test MCP server implementation',
|
|
219
|
+
classification: 'community',
|
|
220
|
+
implementation_language: 'TypeScript',
|
|
221
|
+
},
|
|
222
|
+
];
|
|
223
|
+
// Apply search filter
|
|
224
|
+
if (params.query) {
|
|
225
|
+
const query = params.query.toLowerCase();
|
|
226
|
+
implementations = implementations.filter((impl) => impl.name.toLowerCase().includes(query) ||
|
|
227
|
+
impl.slug.toLowerCase().includes(query) ||
|
|
228
|
+
(impl.short_description && impl.short_description.toLowerCase().includes(query)) ||
|
|
229
|
+
(impl.description && impl.description.toLowerCase().includes(query)) ||
|
|
230
|
+
(impl.provider_name && impl.provider_name.toLowerCase().includes(query)));
|
|
231
|
+
}
|
|
232
|
+
// Apply type filter
|
|
233
|
+
if (params.type && params.type !== 'all') {
|
|
234
|
+
implementations = implementations.filter((impl) => impl.type === params.type);
|
|
235
|
+
}
|
|
236
|
+
// Apply status filter
|
|
237
|
+
if (params.status && params.status !== 'all') {
|
|
238
|
+
implementations = implementations.filter((impl) => impl.status === params.status);
|
|
239
|
+
}
|
|
240
|
+
// Apply pagination
|
|
241
|
+
const offset = params.offset || 0;
|
|
242
|
+
const limit = params.limit || 30;
|
|
243
|
+
const totalCount = implementations.length;
|
|
244
|
+
const paginatedImpls = implementations.slice(offset, offset + limit);
|
|
245
|
+
return {
|
|
246
|
+
implementations: paginatedImpls,
|
|
247
|
+
pagination: {
|
|
248
|
+
current_page: Math.floor(offset / limit) + 1,
|
|
249
|
+
total_pages: Math.ceil(totalCount / limit),
|
|
250
|
+
total_count: totalCount,
|
|
251
|
+
has_next: offset + limit < totalCount,
|
|
252
|
+
limit: limit,
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
},
|
|
256
|
+
async getDraftMCPImplementations(params) {
|
|
257
|
+
if (mockData.errors?.getDraftMCPImplementations) {
|
|
258
|
+
throw mockData.errors.getDraftMCPImplementations;
|
|
259
|
+
}
|
|
260
|
+
if (mockData.draftImplementationsResponse) {
|
|
261
|
+
return mockData.draftImplementationsResponse;
|
|
262
|
+
}
|
|
263
|
+
// Filter to only draft implementations
|
|
264
|
+
let implementations = (mockData.implementations || []).filter((impl) => impl.status === 'draft');
|
|
265
|
+
// Apply search filter if provided
|
|
266
|
+
if (params?.search) {
|
|
267
|
+
const searchLower = params.search.toLowerCase();
|
|
268
|
+
implementations = implementations.filter((impl) => impl.name.toLowerCase().includes(searchLower) ||
|
|
269
|
+
impl.short_description?.toLowerCase().includes(searchLower) ||
|
|
270
|
+
impl.description?.toLowerCase().includes(searchLower));
|
|
271
|
+
}
|
|
272
|
+
// Apply pagination
|
|
273
|
+
const page = params?.page || 1;
|
|
274
|
+
const perPage = 20; // Match Rails default
|
|
275
|
+
const offset = (page - 1) * perPage;
|
|
276
|
+
const paginatedImpls = implementations.slice(offset, offset + perPage);
|
|
277
|
+
return {
|
|
278
|
+
implementations: paginatedImpls,
|
|
279
|
+
pagination: {
|
|
280
|
+
current_page: page,
|
|
281
|
+
total_pages: Math.ceil(implementations.length / perPage),
|
|
282
|
+
total_count: implementations.length,
|
|
283
|
+
},
|
|
284
|
+
};
|
|
285
|
+
},
|
|
286
|
+
async saveMCPImplementation(id, params) {
|
|
287
|
+
if (mockData.errors?.saveMCPImplementation) {
|
|
288
|
+
throw mockData.errors.saveMCPImplementation;
|
|
289
|
+
}
|
|
290
|
+
// Find existing implementation in mock data
|
|
291
|
+
const implementations = mockData.implementations || [];
|
|
292
|
+
const existingImpl = implementations.find((impl) => impl.id === id);
|
|
293
|
+
if (!existingImpl) {
|
|
294
|
+
throw new Error(`MCP implementation not found: ${id}`);
|
|
295
|
+
}
|
|
296
|
+
// Merge params with existing implementation
|
|
297
|
+
const updatedImpl = {
|
|
298
|
+
...existingImpl,
|
|
299
|
+
...params,
|
|
300
|
+
id: id, // Ensure ID doesn't change
|
|
301
|
+
updated_at: new Date().toISOString(),
|
|
302
|
+
};
|
|
303
|
+
return updatedImpl;
|
|
304
|
+
},
|
|
305
|
+
async sendEmail(params) {
|
|
306
|
+
if (mockData.errors?.sendEmail) {
|
|
307
|
+
throw mockData.errors.sendEmail;
|
|
308
|
+
}
|
|
309
|
+
// Mock successful email response
|
|
310
|
+
return {
|
|
311
|
+
id: Math.floor(Math.random() * 10000),
|
|
312
|
+
sender_provider: 'sendgrid',
|
|
313
|
+
send_timestamp_utc: new Date().toISOString(),
|
|
314
|
+
from_email_address: params.from_email_address,
|
|
315
|
+
to_email_address: params.to_email_address,
|
|
316
|
+
subject: params.subject,
|
|
317
|
+
content_text: params.content,
|
|
318
|
+
content_html: `<html><body>${params.content.replace(/\n/g, '<br>')}</body></html>`,
|
|
319
|
+
campaign_identifier: `admin-api-email-${Date.now()}`,
|
|
320
|
+
created_at: new Date().toISOString(),
|
|
321
|
+
updated_at: new Date().toISOString(),
|
|
322
|
+
};
|
|
323
|
+
},
|
|
204
324
|
};
|
|
205
325
|
}
|
|
@@ -55,6 +55,22 @@ export class PulseMCPAdminClient {
|
|
|
55
55
|
const { getMCPClientById } = await import('./pulsemcp-admin-client/lib/get-mcp-client-by-id.js');
|
|
56
56
|
return getMCPClientById(this.apiKey, this.baseUrl, id);
|
|
57
57
|
}
|
|
58
|
+
async searchMCPImplementations(params) {
|
|
59
|
+
const { searchMCPImplementations } = await import('./pulsemcp-admin-client/lib/search-mcp-implementations.js');
|
|
60
|
+
return searchMCPImplementations(this.apiKey, this.baseUrl, params);
|
|
61
|
+
}
|
|
62
|
+
async getDraftMCPImplementations(params) {
|
|
63
|
+
const { getDraftMCPImplementations } = await import('./pulsemcp-admin-client/lib/get-draft-mcp-implementations.js');
|
|
64
|
+
return getDraftMCPImplementations(this.apiKey, this.baseUrl, params);
|
|
65
|
+
}
|
|
66
|
+
async saveMCPImplementation(id, params) {
|
|
67
|
+
const { saveMCPImplementation } = await import('./pulsemcp-admin-client/lib/save-mcp-implementation.js');
|
|
68
|
+
return saveMCPImplementation(this.apiKey, this.baseUrl, id, params);
|
|
69
|
+
}
|
|
70
|
+
async sendEmail(params) {
|
|
71
|
+
const { sendEmail } = await import('./pulsemcp-admin-client/lib/send-email.js');
|
|
72
|
+
return sendEmail(this.apiKey, this.baseUrl, params);
|
|
73
|
+
}
|
|
58
74
|
}
|
|
59
75
|
export function createMCPServer() {
|
|
60
76
|
const server = new Server({
|