pulsemcp-cms-admin-mcp-server 0.6.0 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/README.md +78 -34
  2. package/build/index.js +9 -1
  3. package/build/local/src/index.integration-with-mock.js +9 -1
  4. package/build/shared/src/pulsemcp-admin-client/lib/create-mcp-implementation.js +179 -0
  5. package/build/shared/src/pulsemcp-admin-client/lib/get-unified-mcp-server.js +93 -0
  6. package/build/shared/src/pulsemcp-admin-client/lib/get-unified-mcp-servers.js +51 -0
  7. package/build/shared/src/pulsemcp-admin-client/lib/save-mcp-implementation.js +27 -0
  8. package/build/shared/src/pulsemcp-admin-client/lib/unified-mcp-server-mapper.js +81 -0
  9. package/build/shared/src/pulsemcp-admin-client/lib/update-unified-mcp-server.js +90 -0
  10. package/build/shared/src/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +73 -0
  11. package/build/shared/src/server.js +19 -2
  12. package/build/shared/src/tools/get-mcp-server.js +246 -0
  13. package/build/shared/src/tools/list-mcp-servers.js +146 -0
  14. package/build/shared/src/tools/save-mcp-implementation.js +157 -92
  15. package/build/shared/src/tools/update-mcp-server.js +340 -0
  16. package/build/shared/src/tools.js +15 -0
  17. package/package.json +1 -1
  18. package/shared/index.d.ts +1 -1
  19. package/shared/pulsemcp-admin-client/lib/create-mcp-implementation.d.ts +3 -0
  20. package/shared/pulsemcp-admin-client/lib/create-mcp-implementation.js +179 -0
  21. package/shared/pulsemcp-admin-client/lib/get-unified-mcp-server.d.ts +7 -0
  22. package/shared/pulsemcp-admin-client/lib/get-unified-mcp-server.js +93 -0
  23. package/shared/pulsemcp-admin-client/lib/get-unified-mcp-servers.d.ts +9 -0
  24. package/shared/pulsemcp-admin-client/lib/get-unified-mcp-servers.js +51 -0
  25. package/shared/pulsemcp-admin-client/lib/save-mcp-implementation.js +27 -0
  26. package/shared/pulsemcp-admin-client/lib/unified-mcp-server-mapper.d.ts +72 -0
  27. package/shared/pulsemcp-admin-client/lib/unified-mcp-server-mapper.js +81 -0
  28. package/shared/pulsemcp-admin-client/lib/update-unified-mcp-server.d.ts +7 -0
  29. package/shared/pulsemcp-admin-client/lib/update-unified-mcp-server.js +90 -0
  30. package/shared/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +73 -0
  31. package/shared/server.d.ts +25 -2
  32. package/shared/server.js +19 -2
  33. package/shared/tools/get-mcp-server.d.ts +30 -0
  34. package/shared/tools/get-mcp-server.js +246 -0
  35. package/shared/tools/list-mcp-servers.d.ts +50 -0
  36. package/shared/tools/list-mcp-servers.js +146 -0
  37. package/shared/tools/save-mcp-implementation.d.ts +12 -13
  38. package/shared/tools/save-mcp-implementation.js +157 -92
  39. package/shared/tools/update-mcp-server.d.ts +199 -0
  40. package/shared/tools/update-mcp-server.js +340 -0
  41. package/shared/tools.d.ts +4 -1
  42. package/shared/tools.js +15 -0
  43. package/shared/types.d.ts +152 -0
package/README.md CHANGED
@@ -40,26 +40,43 @@ 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 | 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. |
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
+ | `get_unofficial_mirrors` | unofficial_mirrors | read | List unofficial mirrors with search, pagination, and MCP server filtering. |
64
+ | `get_unofficial_mirror` | unofficial_mirrors | read | Get detailed unofficial mirror info by ID or name. |
65
+ | `create_unofficial_mirror` | unofficial_mirrors | write | Create a new unofficial mirror entry with JSON data. |
66
+ | `update_unofficial_mirror` | unofficial_mirrors | write | Update an existing unofficial mirror by ID. |
67
+ | `delete_unofficial_mirror` | unofficial_mirrors | write | Delete an unofficial mirror by ID (irreversible). |
68
+ | `get_official_mirrors` | official_mirrors | read | List official mirrors with search, status, and processing filters. |
69
+ | `get_official_mirror` | official_mirrors | read | Get detailed official mirror info by ID or name. |
70
+ | `get_tenants` | tenants | read | List tenants with search and admin status filtering. |
71
+ | `get_tenant` | tenants | read | Get detailed tenant info by ID or slug. |
72
+ | `get_mcp_jsons` | mcp_jsons | read | List MCP JSON configs with mirror and server filtering. |
73
+ | `get_mcp_json` | mcp_jsons | read | Get a single MCP JSON configuration by ID. |
74
+ | `create_mcp_json` | mcp_jsons | write | Create a new MCP JSON configuration for an unofficial mirror. |
75
+ | `update_mcp_json` | mcp_jsons | write | Update an existing MCP JSON configuration by ID. |
76
+ | `delete_mcp_json` | mcp_jsons | write | Delete an MCP JSON configuration by ID (irreversible). |
77
+ | `list_mcp_servers` | mcp_servers | read | List/search MCP servers with filtering by status, classification, pagination. |
78
+ | `get_mcp_server` | mcp_servers | read | Get detailed MCP server info by slug (unified view of all admin UI fields). |
79
+ | `update_mcp_server` | mcp_servers | write | Update an MCP server's fields (all admin UI fields supported). |
63
80
 
64
81
  # Tool Groups
65
82
 
@@ -70,14 +87,24 @@ This server organizes tools into groups that can be selectively enabled or disab
70
87
 
71
88
  ## Available Groups
72
89
 
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 |
90
+ | Group | Tools | Description |
91
+ | ----------------------------- | ----- | -------------------------------------------- |
92
+ | `newsletter` | 6 | Full newsletter management (read + write) |
93
+ | `newsletter_readonly` | 3 | Newsletter read-only (get posts, authors) |
94
+ | `server_queue` | 5 | Full MCP implementation queue (read + write) |
95
+ | `server_queue_readonly` | 3 | MCP implementation queue read-only |
96
+ | `official_queue` | 7 | Full official mirror queue (read + write) |
97
+ | `official_queue_readonly` | 2 | Official mirror queue read-only |
98
+ | `unofficial_mirrors` | 5 | Full unofficial mirrors CRUD (read + write) |
99
+ | `unofficial_mirrors_readonly` | 2 | Unofficial mirrors read-only |
100
+ | `official_mirrors` | 2 | Official mirrors REST API (read-only) |
101
+ | `official_mirrors_readonly` | 2 | Official mirrors read-only (alias) |
102
+ | `tenants` | 2 | Tenants REST API (read-only) |
103
+ | `tenants_readonly` | 2 | Tenants read-only (alias) |
104
+ | `mcp_jsons` | 5 | Full MCP JSON configurations (read + write) |
105
+ | `mcp_jsons_readonly` | 2 | MCP JSON configurations read-only |
106
+ | `mcp_servers` | 3 | Full MCP servers management (read + write) |
107
+ | `mcp_servers_readonly` | 2 | MCP servers read-only (list, get) |
81
108
 
82
109
  ### Tools by Group
83
110
 
@@ -90,12 +117,25 @@ This server organizes tools into groups that can be selectively enabled or disab
90
117
  - **official_queue** / **official_queue_readonly**:
91
118
  - Read-only: `get_official_mirror_queue_items`, `get_official_mirror_queue_item`
92
119
  - 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`
120
+ - **unofficial_mirrors** / **unofficial_mirrors_readonly**:
121
+ - Read-only: `get_unofficial_mirrors`, `get_unofficial_mirror`
122
+ - Write: `create_unofficial_mirror`, `update_unofficial_mirror`, `delete_unofficial_mirror`
123
+ - **official_mirrors** / **official_mirrors_readonly**:
124
+ - Read-only: `get_official_mirrors`, `get_official_mirror`
125
+ - **tenants** / **tenants_readonly**:
126
+ - Read-only: `get_tenants`, `get_tenant`
127
+ - **mcp_jsons** / **mcp_jsons_readonly**:
128
+ - Read-only: `get_mcp_jsons`, `get_mcp_json`
129
+ - Write: `create_mcp_json`, `update_mcp_json`, `delete_mcp_json`
130
+ - **mcp_servers** / **mcp_servers_readonly**:
131
+ - Read-only: `list_mcp_servers`, `get_mcp_server`
132
+ - Write: `update_mcp_server`
93
133
 
94
134
  ## Environment Variables
95
135
 
96
- | Variable | Description | Default |
97
- | ------------- | ------------------------------------------- | ---------------------------------------------------------- |
98
- | `TOOL_GROUPS` | Comma-separated list of enabled tool groups | `newsletter,server_queue,official_queue` (all base groups) |
136
+ | Variable | Description | Default |
137
+ | ------------- | ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
138
+ | `TOOL_GROUPS` | Comma-separated list of enabled tool groups | `newsletter,server_queue,official_queue,unofficial_mirrors,official_mirrors,tenants,mcp_jsons,mcp_servers` (all base groups) |
99
139
 
100
140
  ## Examples
101
141
 
@@ -120,7 +160,7 @@ TOOL_GROUPS=server_queue_readonly
120
160
  Enable all groups with read-only access:
121
161
 
122
162
  ```bash
123
- TOOL_GROUPS=newsletter_readonly,server_queue_readonly,official_queue_readonly
163
+ TOOL_GROUPS=newsletter_readonly,server_queue_readonly,official_queue_readonly,unofficial_mirrors_readonly,official_mirrors_readonly,tenants_readonly,mcp_jsons_readonly,mcp_servers_readonly
124
164
  ```
125
165
 
126
166
  Mix full and read-only access per group:
@@ -146,6 +186,10 @@ TOOL_GROUPS=newsletter,server_queue_readonly
146
186
  - Use the `canonical` array parameter in `save_mcp_implementation` to set canonical URLs with scope (domain, subdomain, subfolder, or url)
147
187
  - Remote endpoints allow specifying how MCP servers can be accessed (direct URL, setup URL, authentication method, cost, etc.)
148
188
  - When updating existing remotes, include the remote `id` (number from `get_draft_mcp_implementations`) in the remote object
189
+ - Use `list_mcp_servers` to browse MCP servers with filtering by status (draft/live/archived) and classification (official/community)
190
+ - Use `get_mcp_server` to get a unified view of all server data including provider, source code, canonicals, remotes, and tags
191
+ - Use `update_mcp_server` to update any admin UI field: name, description, provider, source code, package info, tags, canonicals, remotes, etc.
192
+ - The `mcp_servers` tools abstract away the MCPImplementation → MCPServer data model complexity
149
193
 
150
194
  # Examples
151
195
 
@@ -252,7 +296,7 @@ Add to your Claude Desktop configuration:
252
296
  "args": ["/path/to/pulsemcp-cms-admin/local/build/index.js"],
253
297
  "env": {
254
298
  "PULSEMCP_ADMIN_API_KEY": "your-api-key-here",
255
- "TOOL_GROUPS": "newsletter,server_queue,official_queue"
299
+ "TOOL_GROUPS": "newsletter,server_queue,official_queue,unofficial_mirrors,official_mirrors,tenants,mcp_jsons,mcp_servers"
256
300
  }
257
301
  }
258
302
  }
@@ -269,7 +313,7 @@ For read-only access:
269
313
  "args": ["/path/to/pulsemcp-cms-admin/local/build/index.js"],
270
314
  "env": {
271
315
  "PULSEMCP_ADMIN_API_KEY": "your-api-key-here",
272
- "TOOL_GROUPS": "newsletter_readonly,server_queue_readonly,official_queue_readonly"
316
+ "TOOL_GROUPS": "newsletter_readonly,server_queue_readonly,official_queue_readonly,unofficial_mirrors_readonly,official_mirrors_readonly,tenants_readonly,mcp_jsons_readonly,mcp_servers_readonly"
273
317
  }
274
318
  }
275
319
  }
package/build/index.js CHANGED
@@ -1,7 +1,15 @@
1
1
  #!/usr/bin/env node
2
+ import { readFileSync } from 'fs';
3
+ import { dirname, join } from 'path';
4
+ import { fileURLToPath } from 'url';
2
5
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
6
  import { createMCPServer } from '../shared/index.js';
4
7
  import { logServerStart, logError } from '../shared/logging.js';
8
+ // Read version from package.json
9
+ const __dirname = dirname(fileURLToPath(import.meta.url));
10
+ const packageJsonPath = join(__dirname, '..', 'package.json');
11
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
12
+ const VERSION = packageJson.version;
5
13
  // Validate required environment variables before starting
6
14
  function validateEnvironment() {
7
15
  const required = [
@@ -33,7 +41,7 @@ async function main() {
33
41
  // Validate environment variables first
34
42
  validateEnvironment();
35
43
  // Create server using factory
36
- const { server, registerHandlers } = createMCPServer();
44
+ const { server, registerHandlers } = createMCPServer({ version: VERSION });
37
45
  // Register all handlers (resources and tools)
38
46
  await registerHandlers(server);
39
47
  // Start server
@@ -7,11 +7,19 @@
7
7
  *
8
8
  * Mock data is passed via the PULSEMCP_MOCK_DATA environment variable.
9
9
  */
10
+ import { readFileSync } from 'fs';
11
+ import { dirname, join } from 'path';
12
+ import { fileURLToPath } from 'url';
10
13
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
11
14
  // IMPORTANT: This uses the package name pattern, not a relative path
12
15
  import { createMCPServer } from 'pulsemcp-cms-admin-mcp-server-shared';
13
16
  // Import the mock client factory from the shared module
14
17
  import { createMockPulseMCPAdminClient } from '../../shared/src/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js';
18
+ // Read version from package.json
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const packageJsonPath = join(__dirname, '..', 'package.json');
21
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
22
+ const VERSION = packageJson.version;
15
23
  async function main() {
16
24
  const transport = new StdioServerTransport();
17
25
  // Parse mock data from environment variable
@@ -26,7 +34,7 @@ async function main() {
26
34
  }
27
35
  // Create client factory that returns our mock
28
36
  const clientFactory = () => createMockPulseMCPAdminClient(mockData);
29
- const { server, registerHandlers } = createMCPServer();
37
+ const { server, registerHandlers } = createMCPServer({ version: VERSION });
30
38
  await registerHandlers(server, clientFactory);
31
39
  await server.connect(transport);
32
40
  }
@@ -0,0 +1,179 @@
1
+ export async function createMCPImplementation(apiKey, baseUrl, params) {
2
+ const url = new URL(`/api/implementations`, baseUrl);
3
+ // Build form data for the POST request
4
+ const formData = new URLSearchParams();
5
+ // Required fields for creation
6
+ formData.append('mcp_implementation[name]', params.name);
7
+ formData.append('mcp_implementation[type]', params.type);
8
+ // Optional fields
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.status !== undefined) {
16
+ formData.append('mcp_implementation[status]', params.status);
17
+ }
18
+ if (params.slug !== undefined) {
19
+ formData.append('mcp_implementation[slug]', params.slug);
20
+ }
21
+ if (params.url !== undefined) {
22
+ // Backend expects 'marketing_url' field, but tool exposes it as 'url' for better UX
23
+ formData.append('mcp_implementation[marketing_url]', params.url);
24
+ }
25
+ if (params.provider_name !== undefined) {
26
+ formData.append('mcp_implementation[provider_name]', params.provider_name);
27
+ }
28
+ if (params.github_stars !== undefined) {
29
+ formData.append('mcp_implementation[github_stars]', params.github_stars === null ? '' : params.github_stars.toString());
30
+ }
31
+ if (params.classification !== undefined) {
32
+ formData.append('mcp_implementation[classification]', params.classification);
33
+ }
34
+ if (params.implementation_language !== undefined) {
35
+ formData.append('mcp_implementation[implementation_language]', params.implementation_language);
36
+ }
37
+ if (params.mcp_server_id !== undefined) {
38
+ formData.append('mcp_implementation[mcp_server_id]', params.mcp_server_id === null ? '' : params.mcp_server_id.toString());
39
+ }
40
+ if (params.mcp_client_id !== undefined) {
41
+ formData.append('mcp_implementation[mcp_client_id]', params.mcp_client_id === null ? '' : params.mcp_client_id.toString());
42
+ }
43
+ // Provider creation/linking
44
+ if (params.provider_id !== undefined) {
45
+ formData.append('mcp_implementation[provider_id]', params.provider_id.toString());
46
+ }
47
+ if (params.provider_slug !== undefined) {
48
+ formData.append('mcp_implementation[provider_slug]', params.provider_slug);
49
+ }
50
+ if (params.provider_url !== undefined) {
51
+ formData.append('mcp_implementation[provider_url]', params.provider_url);
52
+ }
53
+ // GitHub repository fields
54
+ if (params.github_owner !== undefined) {
55
+ formData.append('mcp_implementation[github_owner]', params.github_owner);
56
+ }
57
+ if (params.github_repo !== undefined) {
58
+ formData.append('mcp_implementation[github_repo]', params.github_repo);
59
+ }
60
+ if (params.github_subfolder !== undefined) {
61
+ formData.append('mcp_implementation[github_subfolder]', params.github_subfolder);
62
+ }
63
+ // Package registry fields
64
+ if (params.package_registry !== undefined) {
65
+ formData.append('mcp_implementation[package_registry]', params.package_registry);
66
+ }
67
+ if (params.package_name !== undefined) {
68
+ formData.append('mcp_implementation[package_name]', params.package_name);
69
+ }
70
+ // Flags
71
+ if (params.recommended !== undefined) {
72
+ formData.append('mcp_implementation[recommended]', params.recommended.toString());
73
+ }
74
+ // Date overrides
75
+ if (params.created_on_override !== undefined) {
76
+ formData.append('mcp_implementation[created_on_override]', params.created_on_override);
77
+ }
78
+ // Tags
79
+ if (params.tags !== undefined) {
80
+ if (params.tags.length > 0) {
81
+ params.tags.forEach((tagSlug, index) => {
82
+ formData.append(`mcp_implementation[tags][${index}]`, tagSlug);
83
+ });
84
+ }
85
+ else {
86
+ // Empty array explicitly provided - send empty array marker to Rails
87
+ formData.append('mcp_implementation[tags]', '[]');
88
+ }
89
+ }
90
+ // Internal notes
91
+ if (params.internal_notes !== undefined) {
92
+ formData.append('mcp_implementation[internal_notes]', params.internal_notes);
93
+ }
94
+ // Remote endpoints
95
+ // Rails expects nested attributes to use the _attributes suffix for has_many associations
96
+ if (params.remote !== undefined) {
97
+ if (params.remote.length > 0) {
98
+ params.remote.forEach((remote, index) => {
99
+ if (remote.id !== undefined) {
100
+ formData.append(`mcp_implementation[remote_attributes][${index}][id]`, remote.id.toString());
101
+ }
102
+ if (remote.url_direct !== undefined) {
103
+ formData.append(`mcp_implementation[remote_attributes][${index}][url_direct]`, remote.url_direct);
104
+ }
105
+ if (remote.url_setup !== undefined) {
106
+ formData.append(`mcp_implementation[remote_attributes][${index}][url_setup]`, remote.url_setup);
107
+ }
108
+ if (remote.transport !== undefined) {
109
+ formData.append(`mcp_implementation[remote_attributes][${index}][transport]`, remote.transport);
110
+ }
111
+ if (remote.host_platform !== undefined) {
112
+ formData.append(`mcp_implementation[remote_attributes][${index}][host_platform]`, remote.host_platform);
113
+ }
114
+ if (remote.host_infrastructure !== undefined) {
115
+ formData.append(`mcp_implementation[remote_attributes][${index}][host_infrastructure]`, remote.host_infrastructure);
116
+ }
117
+ if (remote.authentication_method !== undefined) {
118
+ formData.append(`mcp_implementation[remote_attributes][${index}][authentication_method]`, remote.authentication_method);
119
+ }
120
+ if (remote.cost !== undefined) {
121
+ formData.append(`mcp_implementation[remote_attributes][${index}][cost]`, remote.cost);
122
+ }
123
+ if (remote.status !== undefined) {
124
+ formData.append(`mcp_implementation[remote_attributes][${index}][status]`, remote.status);
125
+ }
126
+ if (remote.display_name !== undefined) {
127
+ formData.append(`mcp_implementation[remote_attributes][${index}][display_name]`, remote.display_name);
128
+ }
129
+ if (remote.internal_notes !== undefined) {
130
+ formData.append(`mcp_implementation[remote_attributes][${index}][internal_notes]`, remote.internal_notes);
131
+ }
132
+ });
133
+ }
134
+ }
135
+ // Canonical URLs
136
+ // Rails expects nested attributes to use the _attributes suffix for has_many associations
137
+ if (params.canonical !== undefined) {
138
+ if (params.canonical.length > 0) {
139
+ params.canonical.forEach((canonicalUrl, index) => {
140
+ formData.append(`mcp_implementation[canonical_attributes][${index}][url]`, canonicalUrl.url);
141
+ formData.append(`mcp_implementation[canonical_attributes][${index}][scope]`, canonicalUrl.scope);
142
+ if (canonicalUrl.note !== undefined) {
143
+ formData.append(`mcp_implementation[canonical_attributes][${index}][note]`, canonicalUrl.note);
144
+ }
145
+ });
146
+ }
147
+ }
148
+ const response = await fetch(url.toString(), {
149
+ method: 'POST',
150
+ headers: {
151
+ 'X-API-Key': apiKey,
152
+ 'Content-Type': 'application/x-www-form-urlencoded',
153
+ Accept: 'application/json',
154
+ },
155
+ body: formData.toString(),
156
+ });
157
+ if (!response.ok) {
158
+ if (response.status === 401) {
159
+ throw new Error('Invalid API key');
160
+ }
161
+ if (response.status === 403) {
162
+ throw new Error('User lacks admin privileges');
163
+ }
164
+ if (response.status === 422) {
165
+ const errorData = (await response.json());
166
+ // Handle both array format and single error string format from Rails
167
+ // Also handle empty arrays - an empty array should fall back to the default message
168
+ const errors = errorData.errors && errorData.errors.length > 0
169
+ ? errorData.errors
170
+ : errorData.error
171
+ ? [errorData.error]
172
+ : ['Unknown validation error'];
173
+ throw new Error(`Validation failed: ${errors.join(', ')}`);
174
+ }
175
+ throw new Error(`Failed to create MCP implementation: ${response.status} ${response.statusText}`);
176
+ }
177
+ const data = await response.json();
178
+ return data;
179
+ }
@@ -0,0 +1,93 @@
1
+ import { mapToUnifiedServer } from './unified-mcp-server-mapper.js';
2
+ /**
3
+ * Get a unified MCP server by its slug.
4
+ * This fetches the MCPServer and its associated MCPImplementation data.
5
+ */
6
+ export async function getUnifiedMCPServer(apiKey, baseUrl, slug) {
7
+ // First, get the MCPServer to get its ID
8
+ const serverUrl = new URL(`/supervisor/mcp_servers/${slug}`, baseUrl);
9
+ const serverResponse = await fetch(serverUrl.toString(), {
10
+ method: 'GET',
11
+ headers: {
12
+ 'X-API-Key': apiKey,
13
+ Accept: 'application/json',
14
+ },
15
+ });
16
+ if (!serverResponse.ok) {
17
+ if (serverResponse.status === 401) {
18
+ throw new Error('Invalid API key');
19
+ }
20
+ if (serverResponse.status === 403) {
21
+ throw new Error('User lacks admin privileges');
22
+ }
23
+ if (serverResponse.status === 404) {
24
+ throw new Error(`MCP server not found: ${slug}`);
25
+ }
26
+ throw new Error(`Failed to fetch MCP server: ${serverResponse.status} ${serverResponse.statusText}`);
27
+ }
28
+ const mcpServerData = (await serverResponse.json());
29
+ // Now search for the implementation that references this MCPServer
30
+ // Use a search that includes the slug to narrow down results
31
+ // Don't pass status param to get all statuses (API doesn't support 'all' as a value)
32
+ const searchUrl = new URL('/api/implementations/search', baseUrl);
33
+ searchUrl.searchParams.append('q', slug);
34
+ searchUrl.searchParams.append('type', 'server');
35
+ searchUrl.searchParams.append('limit', '50');
36
+ const searchResponse = await fetch(searchUrl.toString(), {
37
+ method: 'GET',
38
+ headers: {
39
+ 'X-API-Key': apiKey,
40
+ Accept: 'application/json',
41
+ },
42
+ });
43
+ if (!searchResponse.ok) {
44
+ throw new Error(`Failed to fetch implementation data: ${searchResponse.status} ${searchResponse.statusText}`);
45
+ }
46
+ const searchData = (await searchResponse.json());
47
+ // Find the implementation that matches this MCPServer's ID
48
+ const matchingImpl = searchData.data.find((impl) => impl.mcp_server_id === mcpServerData.id);
49
+ if (!matchingImpl) {
50
+ // If no matching implementation found, construct a minimal unified server from MCPServer data
51
+ return {
52
+ id: mcpServerData.id,
53
+ slug: mcpServerData.slug,
54
+ implementation_id: null, // No implementation exists for this server
55
+ name: mcpServerData.slug, // Use slug as name fallback
56
+ status: 'draft',
57
+ classification: mcpServerData.classification,
58
+ implementation_language: mcpServerData.implementation_language,
59
+ remotes: mcpServerData.remotes?.map((r) => ({
60
+ id: r.id,
61
+ display_name: r.display_name,
62
+ url_direct: r.url_direct,
63
+ url_setup: r.url_setup,
64
+ transport: r.transport,
65
+ host_platform: r.host_platform,
66
+ host_infrastructure: r.host_infrastructure,
67
+ authentication_method: r.authentication_method,
68
+ cost: r.cost,
69
+ status: r.status,
70
+ internal_notes: r.internal_notes,
71
+ })),
72
+ tags: mcpServerData.tags,
73
+ registry_package_id: mcpServerData.registry_package_id,
74
+ registry_package_soft_verified: mcpServerData.registry_package_soft_verified,
75
+ downloads_estimate_last_7_days: mcpServerData.downloads_estimate_last_7_days,
76
+ downloads_estimate_last_30_days: mcpServerData.downloads_estimate_last_30_days,
77
+ downloads_estimate_total: mcpServerData.downloads_estimate_total,
78
+ created_at: mcpServerData.created_at,
79
+ updated_at: mcpServerData.updated_at,
80
+ };
81
+ }
82
+ // Merge MCPServer data with implementation data for complete picture
83
+ matchingImpl.mcp_server = {
84
+ ...mcpServerData,
85
+ remotes: mcpServerData.remotes,
86
+ tags: mcpServerData.tags,
87
+ };
88
+ const unified = mapToUnifiedServer(matchingImpl);
89
+ if (!unified) {
90
+ throw new Error(`Failed to map MCP server data for: ${slug}`);
91
+ }
92
+ return unified;
93
+ }
@@ -0,0 +1,51 @@
1
+ import { mapToUnifiedServer } from './unified-mcp-server-mapper.js';
2
+ export async function getUnifiedMCPServers(apiKey, baseUrl, params) {
3
+ const url = new URL('/api/implementations/search', baseUrl);
4
+ // Always filter to servers only
5
+ url.searchParams.append('type', 'server');
6
+ // The API requires a search query - use '*' wildcard if none provided
7
+ url.searchParams.append('q', params?.q || '*');
8
+ if (params?.status && params.status !== 'all') {
9
+ url.searchParams.append('status', params.status);
10
+ }
11
+ if (params?.classification) {
12
+ url.searchParams.append('classification', params.classification);
13
+ }
14
+ if (params?.limit) {
15
+ url.searchParams.append('limit', params.limit.toString());
16
+ }
17
+ if (params?.offset) {
18
+ url.searchParams.append('offset', params.offset.toString());
19
+ }
20
+ const response = await fetch(url.toString(), {
21
+ method: 'GET',
22
+ headers: {
23
+ 'X-API-Key': apiKey,
24
+ Accept: 'application/json',
25
+ },
26
+ });
27
+ if (!response.ok) {
28
+ if (response.status === 401) {
29
+ throw new Error('Invalid API key');
30
+ }
31
+ if (response.status === 403) {
32
+ throw new Error('User lacks admin privileges');
33
+ }
34
+ throw new Error(`Failed to fetch MCP servers: ${response.status} ${response.statusText}`);
35
+ }
36
+ const data = (await response.json());
37
+ // Map implementations to unified servers, filtering out those without an MCPServer
38
+ const servers = data.data
39
+ .map(mapToUnifiedServer)
40
+ .filter((s) => s !== null);
41
+ return {
42
+ servers,
43
+ pagination: {
44
+ current_page: data.meta.current_page,
45
+ total_pages: data.meta.total_pages,
46
+ total_count: data.meta.total_count,
47
+ has_next: data.meta.has_next,
48
+ limit: data.meta.limit,
49
+ },
50
+ };
51
+ }
@@ -63,6 +63,33 @@ export async function saveMCPImplementation(apiKey, baseUrl, id, params) {
63
63
  if (params.github_subfolder !== undefined) {
64
64
  formData.append('mcp_implementation[github_subfolder]', params.github_subfolder);
65
65
  }
66
+ // Package registry fields
67
+ if (params.package_registry !== undefined) {
68
+ formData.append('mcp_implementation[package_registry]', params.package_registry);
69
+ }
70
+ if (params.package_name !== undefined) {
71
+ formData.append('mcp_implementation[package_name]', params.package_name);
72
+ }
73
+ // Flags
74
+ if (params.recommended !== undefined) {
75
+ formData.append('mcp_implementation[recommended]', params.recommended.toString());
76
+ }
77
+ // Date overrides
78
+ if (params.created_on_override !== undefined) {
79
+ formData.append('mcp_implementation[created_on_override]', params.created_on_override);
80
+ }
81
+ // Tags
82
+ if (params.tags !== undefined) {
83
+ if (params.tags.length > 0) {
84
+ params.tags.forEach((tagSlug, index) => {
85
+ formData.append(`mcp_implementation[tags][${index}]`, tagSlug);
86
+ });
87
+ }
88
+ else {
89
+ // Empty array explicitly provided - send empty array marker to Rails
90
+ formData.append('mcp_implementation[tags]', '[]');
91
+ }
92
+ }
66
93
  // Internal notes
67
94
  if (params.internal_notes !== undefined) {
68
95
  formData.append('mcp_implementation[internal_notes]', params.internal_notes);