pulsemcp-cms-admin-mcp-server 0.9.17 → 0.9.26
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 +93 -74
- package/build/index.js +53 -1
- package/build/shared/src/elicitation-config.js +69 -0
- package/build/shared/src/pulsemcp-admin-client/lib/bulk-update-tenant-servers.js +41 -0
- package/build/shared/src/pulsemcp-admin-client/lib/create-mcp-implementation.js +7 -0
- package/build/shared/src/pulsemcp-admin-client/lib/delete-api-key.js +25 -0
- package/build/shared/src/pulsemcp-admin-client/lib/delete-tenant.js +31 -0
- package/build/shared/src/pulsemcp-admin-client/lib/get-unified-mcp-server.js +5 -1
- package/build/shared/src/pulsemcp-admin-client/lib/list-tenant-servers.js +57 -0
- package/build/shared/src/pulsemcp-admin-client/lib/save-mcp-implementation.js +8 -0
- package/build/shared/src/pulsemcp-admin-client/lib/set-known-missing-init-tools-list.js +42 -0
- package/build/shared/src/pulsemcp-admin-client/lib/unified-mcp-server-mapper.js +3 -0
- package/build/shared/src/pulsemcp-admin-client/lib/update-unified-mcp-server.js +14 -0
- package/build/shared/src/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +39 -0
- package/build/shared/src/server.js +20 -0
- package/build/shared/src/tools/add-servers-to-tenant.js +133 -0
- package/build/shared/src/tools/delete-api-key.js +119 -0
- package/build/shared/src/tools/delete-tenant.js +129 -0
- package/build/shared/src/tools/get-mcp-server.js +26 -8
- package/build/shared/src/tools/list-mcp-servers.js +3 -0
- package/build/shared/src/tools/list-tenant-servers.js +89 -0
- package/build/shared/src/tools/remove-servers-from-tenant.js +92 -0
- package/build/shared/src/tools/revoke-api-key.js +119 -0
- package/build/shared/src/tools/save-mcp-implementation.js +89 -2
- package/build/shared/src/tools/set-known-missing-init-tools-list.js +75 -0
- package/build/shared/src/tools/update-mcp-server.js +19 -0
- package/build/shared/src/tools.js +30 -1
- package/node_modules/@pulsemcp/mcp-elicitation/build/config.d.ts +15 -0
- package/node_modules/@pulsemcp/mcp-elicitation/build/config.js +41 -0
- package/node_modules/@pulsemcp/mcp-elicitation/build/elicitation.d.ts +24 -0
- package/node_modules/@pulsemcp/mcp-elicitation/build/elicitation.js +175 -0
- package/node_modules/@pulsemcp/mcp-elicitation/build/index.d.ts +3 -0
- package/node_modules/@pulsemcp/mcp-elicitation/build/index.js +2 -0
- package/node_modules/@pulsemcp/mcp-elicitation/build/types.d.ts +114 -0
- package/node_modules/@pulsemcp/mcp-elicitation/build/types.js +1 -0
- package/node_modules/@pulsemcp/mcp-elicitation/package.json +28 -0
- package/package.json +7 -1
- package/shared/elicitation-config.d.ts +61 -0
- package/shared/elicitation-config.js +69 -0
- package/shared/pulsemcp-admin-client/lib/bulk-update-tenant-servers.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/bulk-update-tenant-servers.js +41 -0
- package/shared/pulsemcp-admin-client/lib/create-mcp-implementation.js +7 -0
- package/shared/pulsemcp-admin-client/lib/delete-api-key.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/delete-api-key.js +25 -0
- package/shared/pulsemcp-admin-client/lib/delete-tenant.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/delete-tenant.js +31 -0
- package/shared/pulsemcp-admin-client/lib/get-unified-mcp-server.js +5 -1
- package/shared/pulsemcp-admin-client/lib/list-tenant-servers.d.ts +7 -0
- package/shared/pulsemcp-admin-client/lib/list-tenant-servers.js +57 -0
- package/shared/pulsemcp-admin-client/lib/save-mcp-implementation.js +8 -0
- package/shared/pulsemcp-admin-client/lib/set-known-missing-init-tools-list.d.ts +3 -0
- package/shared/pulsemcp-admin-client/lib/set-known-missing-init-tools-list.js +42 -0
- package/shared/pulsemcp-admin-client/lib/unified-mcp-server-mapper.d.ts +2 -0
- package/shared/pulsemcp-admin-client/lib/unified-mcp-server-mapper.js +3 -0
- package/shared/pulsemcp-admin-client/lib/update-unified-mcp-server.js +14 -0
- package/shared/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +39 -0
- package/shared/server.d.ts +19 -1
- package/shared/server.js +20 -0
- package/shared/tools/add-servers-to-tenant.d.ts +57 -0
- package/shared/tools/add-servers-to-tenant.js +133 -0
- package/shared/tools/delete-api-key.d.ts +30 -0
- package/shared/tools/delete-api-key.js +119 -0
- package/shared/tools/delete-tenant.d.ts +36 -0
- package/shared/tools/delete-tenant.js +129 -0
- package/shared/tools/get-mcp-server.js +26 -8
- package/shared/tools/list-mcp-servers.js +3 -0
- package/shared/tools/list-tenant-servers.d.ts +45 -0
- package/shared/tools/list-tenant-servers.js +89 -0
- package/shared/tools/remove-servers-from-tenant.d.ts +42 -0
- package/shared/tools/remove-servers-from-tenant.js +92 -0
- package/shared/tools/revoke-api-key.d.ts +30 -0
- package/shared/tools/revoke-api-key.js +119 -0
- package/shared/tools/save-mcp-implementation.d.ts +9 -1
- package/shared/tools/save-mcp-implementation.js +89 -2
- package/shared/tools/set-known-missing-init-tools-list.d.ts +38 -0
- package/shared/tools/set-known-missing-init-tools-list.js +75 -0
- package/shared/tools/update-mcp-server.d.ts +6 -0
- package/shared/tools/update-mcp-server.js +19 -0
- package/shared/tools.d.ts +5 -3
- package/shared/tools.js +30 -1
- package/shared/types.d.ts +89 -0
package/README.md
CHANGED
|
@@ -40,72 +40,75 @@ 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
|
|
44
|
-
| -------------------------------------- |
|
|
45
|
-
| `get_newsletter_posts` | newsletter
|
|
46
|
-
| `get_newsletter_post` | newsletter
|
|
47
|
-
| `draft_newsletter_post` | newsletter
|
|
48
|
-
| `update_newsletter_post` | newsletter
|
|
49
|
-
| `upload_image` | newsletter
|
|
50
|
-
| `get_authors` | newsletter
|
|
51
|
-
| `search_mcp_implementations` | server_directory
|
|
52
|
-
| `get_draft_mcp_implementations` | server_directory
|
|
53
|
-
| `find_providers` | server_directory
|
|
54
|
-
| `save_mcp_implementation` | server_directory
|
|
55
|
-
| `send_impl_posted_notif` | server_directory
|
|
56
|
-
| `get_official_mirror_queue_items` | official_queue
|
|
57
|
-
| `get_official_mirror_queue_item` | official_queue
|
|
58
|
-
| `approve_official_mirror_queue_item` | official_queue
|
|
59
|
-
| `approve_mirror_no_modify` | official_queue
|
|
60
|
-
| `reject_official_mirror_queue_item` | official_queue
|
|
61
|
-
| `add_official_mirror_to_regular_queue` | official_queue
|
|
62
|
-
| `unlink_official_mirror_queue_item` | official_queue
|
|
63
|
-
| `get_unofficial_mirrors` | unofficial_mirrors
|
|
64
|
-
| `get_unofficial_mirror` | unofficial_mirrors
|
|
65
|
-
| `create_unofficial_mirror` | unofficial_mirrors
|
|
66
|
-
| `update_unofficial_mirror` | unofficial_mirrors
|
|
67
|
-
| `delete_unofficial_mirror` | unofficial_mirrors
|
|
68
|
-
| `get_official_mirrors` | official_mirrors
|
|
69
|
-
| `get_official_mirror` | official_mirrors
|
|
70
|
-
| `get_tenants` | tenants
|
|
71
|
-
| `get_tenant` | tenants
|
|
72
|
-
| `create_tenant` | tenants
|
|
73
|
-
| `create_api_key` | tenants
|
|
74
|
-
| `
|
|
75
|
-
| `
|
|
76
|
-
| `
|
|
77
|
-
| `
|
|
78
|
-
| `
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
82
|
-
| `
|
|
83
|
-
| `
|
|
84
|
-
| `
|
|
85
|
-
| `
|
|
86
|
-
| `
|
|
87
|
-
| `
|
|
88
|
-
| `
|
|
89
|
-
| `
|
|
90
|
-
| `
|
|
91
|
-
| `
|
|
92
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
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_directory | read | Search for MCP servers and clients in the PulseMCP registry. |
|
|
52
|
+
| `get_draft_mcp_implementations` | server_directory | read | Retrieve paginated list of draft MCP implementations needing review. |
|
|
53
|
+
| `find_providers` | server_directory | read | Search for providers by ID, name, URL, or slug. |
|
|
54
|
+
| `save_mcp_implementation` | server_directory | write | Update an MCP implementation (replicates Admin panel "Save Changes" button). |
|
|
55
|
+
| `send_impl_posted_notif` | server_directory | 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
|
+
| `create_tenant` | tenants | write | Create a new tenant for sub-registry provisioning. |
|
|
73
|
+
| `create_api_key` | tenants | write | Create an API key for a tenant. Returns the raw key (only available at creation time). |
|
|
74
|
+
| `revoke_api_key` | tenants | write | Revoke an API key by ID, immediately invalidating it. Idempotent. Requires elicitation approval. |
|
|
75
|
+
| `delete_tenant` | tenants_destructive | write | Permanently delete a tenant. Requires elicitation approval. With `force: true`, cascades to dependents. |
|
|
76
|
+
| `delete_api_key` | tenants_destructive | write | Permanently revoke (delete) an API key. Idempotent. Requires elicitation approval. |
|
|
77
|
+
| `get_mcp_jsons` | mcp_jsons | read | List MCP JSON configs with mirror and server filtering. |
|
|
78
|
+
| `get_mcp_json` | mcp_jsons | read | Get a single MCP JSON configuration by ID. |
|
|
79
|
+
| `create_mcp_json` | mcp_jsons | write | Create a new MCP JSON configuration for an unofficial mirror. |
|
|
80
|
+
| `update_mcp_json` | mcp_jsons | write | Update an existing MCP JSON configuration by ID. |
|
|
81
|
+
| `delete_mcp_json` | mcp_jsons | write | Delete an MCP JSON configuration by ID (irreversible). |
|
|
82
|
+
| `list_mcp_servers` | mcp_servers | read | List/search MCP servers with filtering by status, classification, pagination. |
|
|
83
|
+
| `get_mcp_server` | mcp_servers | read | Get detailed MCP server info by slug (unified view of all admin UI fields). |
|
|
84
|
+
| `update_mcp_server` | mcp_servers | write | Update an MCP server's fields (all admin UI fields supported). |
|
|
85
|
+
| `recache_mcp_server` | mcp_servers | write | Refresh the cache for a specific MCP server (show page, cards, canonicals, parent pages). |
|
|
86
|
+
| `get_redirects` | redirects | read | List URL redirects with search, status filtering, and pagination. |
|
|
87
|
+
| `get_redirect` | redirects | read | Get detailed redirect info by ID. |
|
|
88
|
+
| `create_redirect` | redirects | write | Create a new URL redirect entry. |
|
|
89
|
+
| `update_redirect` | redirects | write | Update an existing URL redirect by ID. |
|
|
90
|
+
| `delete_redirect` | redirects | write | Delete a URL redirect by ID (irreversible). |
|
|
91
|
+
| `list_good_jobs` | good_jobs | read | List and filter background jobs by queue, status, job class, and date range. |
|
|
92
|
+
| `get_good_job` | good_jobs | read | Get detailed information about a specific background job. |
|
|
93
|
+
| `list_good_job_cron_schedules` | good_jobs | read | List all configured cron schedules. |
|
|
94
|
+
| `list_good_job_processes` | good_jobs | read | List active worker processes. |
|
|
95
|
+
| `get_good_job_queue_statistics` | good_jobs | read | Get aggregate job statistics by status. |
|
|
96
|
+
| `retry_good_job` | good_jobs | write | Retry a failed or discarded background job. |
|
|
97
|
+
| `discard_good_job` | good_jobs | write | Discard a background job to prevent retries. |
|
|
98
|
+
| `reschedule_good_job` | good_jobs | write | Reschedule a background job to a new time. |
|
|
99
|
+
| `force_trigger_good_job_cron` | good_jobs | write | Force trigger a cron schedule immediately. |
|
|
100
|
+
| `cleanup_good_jobs` | good_jobs | write | Clean up old background jobs by status and age. |
|
|
101
|
+
| `run_exam_for_mirror` | proctor | write | Run proctor exams against unofficial mirrors via Fly Machines. Returns truncated summary with `result_id`. |
|
|
102
|
+
| `get_exam_result` | proctor | read | Retrieve full untruncated exam results by `result_id`, with optional section/mirror filtering. |
|
|
103
|
+
| `save_results_for_mirror` | proctor | write | Save proctor exam results via `result_id` from `run_exam_for_mirror`. |
|
|
104
|
+
| `list_proctor_runs` | proctor | read | List proctor runs with filtering by name, recommended status, and tenant IDs. |
|
|
105
|
+
| `get_proctor_metadata` | proctor | read | Get available proctor runtimes and exam types. |
|
|
106
|
+
| `list_discovered_urls` | discovered_urls | read | List discovered URLs with status filtering and pagination. |
|
|
107
|
+
| `mark_discovered_url_processed` | discovered_urls | write | Mark a discovered URL as processed with a result status. |
|
|
108
|
+
| `get_discovered_url_stats` | discovered_urls | read | Get summary statistics for discovered URLs pipeline. |
|
|
109
|
+
| `get_moz_metrics` | moz | read | Fetch live URL metrics from the MOZ API (page authority, domain authority, spam score, link counts). |
|
|
110
|
+
| `get_moz_backlinks` | moz | read | Fetch live backlink data from the MOZ API (source pages, anchor text, domain authority). |
|
|
111
|
+
| `get_moz_stored_metrics` | moz | read | List stored/historical MOZ data for a server's canonicals with pagination. |
|
|
109
112
|
|
|
110
113
|
# Tool Groups
|
|
111
114
|
|
|
@@ -128,8 +131,9 @@ This server organizes tools into groups that can be selectively enabled or disab
|
|
|
128
131
|
| `unofficial_mirrors_readonly` | 2 | Unofficial mirrors read-only |
|
|
129
132
|
| `official_mirrors` | 2 | Official mirrors REST API (read-only) |
|
|
130
133
|
| `official_mirrors_readonly` | 2 | Official mirrors read-only (alias) |
|
|
131
|
-
| `tenants` |
|
|
134
|
+
| `tenants` | 5 | Tenant management including API key provisioning and revocation (read + write) |
|
|
132
135
|
| `tenants_readonly` | 2 | Tenants read-only (list, get) |
|
|
136
|
+
| `tenants_destructive` | 2 | **Opt-in only** (NOT in default groups). Destructive tenant tools — `delete_tenant`, `delete_api_key`. Requires MCP elicitation approval per call. |
|
|
133
137
|
| `mcp_jsons` | 5 | Full MCP JSON configurations (read + write) |
|
|
134
138
|
| `mcp_jsons_readonly` | 2 | MCP JSON configurations read-only |
|
|
135
139
|
| `mcp_servers` | 4 | Full MCP servers management (read + write) |
|
|
@@ -163,7 +167,9 @@ This server organizes tools into groups that can be selectively enabled or disab
|
|
|
163
167
|
- Read-only: `get_official_mirrors`, `get_official_mirror`
|
|
164
168
|
- **tenants** / **tenants_readonly**:
|
|
165
169
|
- Read-only: `get_tenants`, `get_tenant`
|
|
166
|
-
- Write: `create_tenant`, `create_api_key`
|
|
170
|
+
- Write: `create_tenant`, `create_api_key`, `revoke_api_key` (`revoke_api_key` requires MCP elicitation approval before each call)
|
|
171
|
+
- **tenants_destructive** (opt-in only — NOT enabled by default; not part of `tenants` or `tenants_readonly`):
|
|
172
|
+
- Write: `delete_tenant`, `delete_api_key` — both require MCP elicitation approval before each call
|
|
167
173
|
- **mcp_jsons** / **mcp_jsons_readonly**:
|
|
168
174
|
- Read-only: `get_mcp_jsons`, `get_mcp_json`
|
|
169
175
|
- Write: `create_mcp_json`, `update_mcp_json`, `delete_mcp_json`
|
|
@@ -187,12 +193,25 @@ This server organizes tools into groups that can be selectively enabled or disab
|
|
|
187
193
|
|
|
188
194
|
## Environment Variables
|
|
189
195
|
|
|
190
|
-
| Variable
|
|
191
|
-
|
|
|
192
|
-
| `PULSEMCP_ADMIN_API_KEY`
|
|
193
|
-
| `PULSEMCP_ADMIN_API_URL`
|
|
194
|
-
| `PULSEMCP_ADMIN_INTERNAL_SECRET`
|
|
195
|
-
| `TOOL_GROUPS`
|
|
196
|
+
| Variable | Description | Default |
|
|
197
|
+
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
198
|
+
| `PULSEMCP_ADMIN_API_KEY` | API key sent as `X-API-Key` for admin-API authentication. Required. | — |
|
|
199
|
+
| `PULSEMCP_ADMIN_API_URL` | Base URL for the admin API. | `https://admin.pulsemcp.com` |
|
|
200
|
+
| `PULSEMCP_ADMIN_INTERNAL_SECRET` | Optional shared secret sent as the `X-PulseMCP-Internal` header. A Cloudflare custom rule uses it to route trusted internal traffic past the WAF and rate limiter in front of `admin.pulsemcp.com`. Needed in production to avoid spurious 403s; leave unset locally and against staging (staging is grey-clouded, no WAF). | unset |
|
|
201
|
+
| `TOOL_GROUPS` | Comma-separated list of enabled tool groups. `tenants_destructive` is **NOT** included in the default; opt in explicitly. | `newsletter,server_directory,official_queue,unofficial_mirrors,official_mirrors,tenants,mcp_jsons,mcp_servers,redirects,good_jobs,proctor,discovered_urls,moz` (all base groups) |
|
|
202
|
+
| `DANGEROUSLY_SKIP_ELICITATIONS` | When set to `true`, skips elicitation prompts for destructive tools (`delete_tenant`, `delete_api_key`, `revoke_api_key`). Use only when running in trusted automation that already has its own approval gate. | unset (false) |
|
|
203
|
+
| `PULSEMCP_CMS_ADMIN_ELICITATION_DESTRUCTIVE` | Per-action override. Set to `false` to skip elicitation for destructive tools while leaving other elicitation behavior unchanged. Implied false when `DANGEROUSLY_SKIP_ELICITATIONS=true`. | `true` |
|
|
204
|
+
| `ELICITATION_REQUEST_URL` / `ELICITATION_POLL_URL` | HTTP fallback elicitation endpoints (used when the MCP client doesn't support native elicitation). Both must be set together. Required at startup if `tenants_destructive` is enabled and `DANGEROUSLY_SKIP_ELICITATIONS` is not set. | unset |
|
|
205
|
+
|
|
206
|
+
### Destructive tools and elicitation
|
|
207
|
+
|
|
208
|
+
When `tenants_destructive` is enabled, the server validates at startup that elicitation is reachable. It will refuse to start with a remediation message unless one of the following is true:
|
|
209
|
+
|
|
210
|
+
- The MCP client supports native elicitation (use `ELICITATION_REQUEST_URL`/`ELICITATION_POLL_URL` HTTP fallback to make this check pass at startup), **or**
|
|
211
|
+
- `ELICITATION_REQUEST_URL` and `ELICITATION_POLL_URL` are both set, **or**
|
|
212
|
+
- `DANGEROUSLY_SKIP_ELICITATIONS=true` is set (operator opts out — at their own risk).
|
|
213
|
+
|
|
214
|
+
By default (`PULSEMCP_CMS_ADMIN_ELICITATION_DESTRUCTIVE=true`), each call to `delete_tenant`, `delete_api_key`, or `revoke_api_key` requires the user to explicitly confirm via the [MCP elicitation protocol](https://modelcontextprotocol.io/specification/draft/client/elicitation) before the request is sent. The startup safety check guards only `tenants_destructive`, since `revoke_api_key` lives in the regular `tenants` group used by deployments that may not have HTTP-fallback elicitation configured.
|
|
196
215
|
|
|
197
216
|
## Examples
|
|
198
217
|
|
package/build/index.js
CHANGED
|
@@ -4,7 +4,9 @@ import { dirname, join } from 'path';
|
|
|
4
4
|
import { fileURLToPath } from 'url';
|
|
5
5
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
6
6
|
import { createMCPServer } from '../shared/index.js';
|
|
7
|
-
import { logServerStart, logError } from '../shared/logging.js';
|
|
7
|
+
import { logServerStart, logError, logWarning } from '../shared/logging.js';
|
|
8
|
+
import { checkElicitationSafety } from '../shared/elicitation-config.js';
|
|
9
|
+
import { parseEnabledToolGroups } from '../shared/tools.js';
|
|
8
10
|
// Read version from package.json
|
|
9
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
12
|
const packageJsonPath = join(__dirname, '..', 'package.json');
|
|
@@ -37,9 +39,59 @@ function validateEnvironment() {
|
|
|
37
39
|
process.exit(1);
|
|
38
40
|
}
|
|
39
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Ensures the server cannot start with destructive tools enabled unless elicitation
|
|
44
|
+
* is reachable (HTTP fallback) or explicitly disabled via DANGEROUSLY_SKIP_ELICITATIONS=true.
|
|
45
|
+
*
|
|
46
|
+
* Native MCP elicitation can't be detected at startup — it requires a connected client —
|
|
47
|
+
* so without HTTP fallback URLs there is no guarantee a confirmation prompt will reach
|
|
48
|
+
* the user. The check is only enforced when the operator has opted into the
|
|
49
|
+
* `tenants_destructive` tool group; when that group is not enabled there are no
|
|
50
|
+
* destructive tools to gate.
|
|
51
|
+
*/
|
|
52
|
+
function validateElicitationSafety() {
|
|
53
|
+
const enabledGroups = parseEnabledToolGroups();
|
|
54
|
+
const destructiveGroupEnabled = enabledGroups.includes('tenants_destructive');
|
|
55
|
+
if (!destructiveGroupEnabled) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const result = checkElicitationSafety();
|
|
59
|
+
if (result.safe && result.reason === 'dangerously_skip') {
|
|
60
|
+
logWarning('security', 'DANGEROUSLY_SKIP_ELICITATIONS=true — destructive tenant tools will execute without user confirmation.');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (result.safe) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
logError('security', 'Server cannot start: tenants_destructive is enabled but no elicitation mechanism is configured.');
|
|
67
|
+
console.error('');
|
|
68
|
+
console.error('The tenants_destructive tool group enables delete_tenant and delete_api_key,');
|
|
69
|
+
console.error('which require user confirmation prompts (elicitation) before destroying admin data.');
|
|
70
|
+
console.error('Without elicitation, those tools could be invoked without any human approval.');
|
|
71
|
+
console.error('');
|
|
72
|
+
console.error('To fix this, choose one of the following options:');
|
|
73
|
+
console.error('');
|
|
74
|
+
console.error(' Option 1: Configure HTTP elicitation fallback (recommended):');
|
|
75
|
+
console.error(' export ELICITATION_REQUEST_URL="https://your-endpoint/request"');
|
|
76
|
+
console.error(' export ELICITATION_POLL_URL="https://your-endpoint/poll"');
|
|
77
|
+
console.error('');
|
|
78
|
+
console.error(' Option 2: Drop the destructive group from TOOL_GROUPS (safest):');
|
|
79
|
+
console.error(' unset TOOL_GROUPS # default has no destructive tools');
|
|
80
|
+
console.error('');
|
|
81
|
+
console.error(' Option 3: Explicitly opt out of elicitation (use with extreme caution):');
|
|
82
|
+
console.error(' export DANGEROUSLY_SKIP_ELICITATIONS=true');
|
|
83
|
+
console.error('');
|
|
84
|
+
console.error('Note: native MCP client elicitation support cannot be verified at startup,');
|
|
85
|
+
console.error('so even clients that support it (e.g., Claude Desktop) need HTTP fallback URLs configured');
|
|
86
|
+
console.error('or DANGEROUSLY_SKIP_ELICITATIONS=true if the risk is accepted.');
|
|
87
|
+
console.error('');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
40
90
|
async function main() {
|
|
41
91
|
// Validate environment variables first
|
|
42
92
|
validateEnvironment();
|
|
93
|
+
// Validate elicitation safety when destructive tools are enabled
|
|
94
|
+
validateElicitationSafety();
|
|
43
95
|
// Create server using factory
|
|
44
96
|
const { server, registerHandlers } = createMCPServer({ version: VERSION });
|
|
45
97
|
// Register all handlers (resources and tools)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { readElicitationConfig } from '@pulsemcp/mcp-elicitation';
|
|
2
|
+
/**
|
|
3
|
+
* Parse a boolean environment variable with a default.
|
|
4
|
+
* Accepts "true"/"false" (case-insensitive). Unset = default.
|
|
5
|
+
*/
|
|
6
|
+
function parseBooleanEnv(value, defaultValue) {
|
|
7
|
+
if (value === undefined)
|
|
8
|
+
return defaultValue;
|
|
9
|
+
return value.toLowerCase() !== 'false';
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Check whether DANGEROUSLY_SKIP_ELICITATIONS is explicitly set to "true".
|
|
13
|
+
*/
|
|
14
|
+
export function isDangerouslySkipElicitations(env = process.env) {
|
|
15
|
+
return env.DANGEROUSLY_SKIP_ELICITATIONS?.toLowerCase() === 'true';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Check whether HTTP fallback elicitation URLs are configured.
|
|
19
|
+
*/
|
|
20
|
+
export function hasHttpElicitationFallback(env = process.env) {
|
|
21
|
+
return !!(env.ELICITATION_REQUEST_URL?.trim() && env.ELICITATION_POLL_URL?.trim());
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Read the full pulsemcp-cms-admin elicitation configuration from environment variables.
|
|
25
|
+
*
|
|
26
|
+
* Environment variables:
|
|
27
|
+
* DANGEROUSLY_SKIP_ELICITATIONS - Must be explicitly "true" to bypass all elicitation.
|
|
28
|
+
* PULSEMCP_CMS_ADMIN_ELICITATION_DESTRUCTIVE - Override for destructive operations (default: follows base enabled state)
|
|
29
|
+
*
|
|
30
|
+
* Plus all standard elicitation env vars (ELICITATION_REQUEST_URL, etc.)
|
|
31
|
+
*/
|
|
32
|
+
export function readCmsAdminElicitationConfig(env = process.env) {
|
|
33
|
+
// Map DANGEROUSLY_SKIP_ELICITATIONS to the base library's enabled flag.
|
|
34
|
+
// The cms-admin server does not use ELICITATION_ENABLED — the only way to disable
|
|
35
|
+
// destructive elicitation is via DANGEROUSLY_SKIP_ELICITATIONS=true.
|
|
36
|
+
const dangerouslySkip = isDangerouslySkipElicitations(env);
|
|
37
|
+
const base = readElicitationConfig({
|
|
38
|
+
...env,
|
|
39
|
+
ELICITATION_ENABLED: dangerouslySkip ? 'false' : 'true',
|
|
40
|
+
});
|
|
41
|
+
const destructiveElicitationEnabled = base.enabled
|
|
42
|
+
? parseBooleanEnv(env.PULSEMCP_CMS_ADMIN_ELICITATION_DESTRUCTIVE, true)
|
|
43
|
+
: false;
|
|
44
|
+
return {
|
|
45
|
+
base,
|
|
46
|
+
destructiveElicitationEnabled,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Check whether the elicitation configuration is safe to start the server when
|
|
51
|
+
* destructive tools are enabled.
|
|
52
|
+
*
|
|
53
|
+
* The server is "safe" if either:
|
|
54
|
+
* - DANGEROUSLY_SKIP_ELICITATIONS=true is explicitly set (operator opted out), or
|
|
55
|
+
* - HTTP fallback URLs are configured (so elicitation will be reachable).
|
|
56
|
+
*
|
|
57
|
+
* Native MCP elicitation support cannot be detected at startup (it requires an
|
|
58
|
+
* active client connection), so callers relying solely on native elicitation
|
|
59
|
+
* should also configure HTTP fallback URLs.
|
|
60
|
+
*/
|
|
61
|
+
export function checkElicitationSafety(env = process.env) {
|
|
62
|
+
if (isDangerouslySkipElicitations(env)) {
|
|
63
|
+
return { safe: true, reason: 'dangerously_skip' };
|
|
64
|
+
}
|
|
65
|
+
if (hasHttpElicitationFallback(env)) {
|
|
66
|
+
return { safe: true, reason: 'http_fallback' };
|
|
67
|
+
}
|
|
68
|
+
return { safe: false, reason: 'no_elicitation_configured' };
|
|
69
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { adminFetch } from './admin-fetch.js';
|
|
2
|
+
export async function bulkUpdateTenantServers(apiKey, baseUrl, idOrSlug, params) {
|
|
3
|
+
const url = new URL(`/api/tenants/${idOrSlug}/servers/bulk_update`, baseUrl);
|
|
4
|
+
const body = {};
|
|
5
|
+
if (params.add_server_identifiers && params.add_server_identifiers.length > 0) {
|
|
6
|
+
body.add_server_identifiers = params.add_server_identifiers;
|
|
7
|
+
}
|
|
8
|
+
if (params.remove_server_identifiers && params.remove_server_identifiers.length > 0) {
|
|
9
|
+
body.remove_server_identifiers = params.remove_server_identifiers;
|
|
10
|
+
}
|
|
11
|
+
if (params.restore_association_ids && params.restore_association_ids.length > 0) {
|
|
12
|
+
body.restore_association_ids = params.restore_association_ids;
|
|
13
|
+
}
|
|
14
|
+
const response = await adminFetch(url.toString(), {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
headers: {
|
|
17
|
+
'X-API-Key': apiKey,
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
Accept: 'application/json',
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify(body),
|
|
22
|
+
});
|
|
23
|
+
if (!response.ok) {
|
|
24
|
+
if (response.status === 401) {
|
|
25
|
+
throw new Error('Invalid API key');
|
|
26
|
+
}
|
|
27
|
+
if (response.status === 403) {
|
|
28
|
+
throw new Error('User lacks write privileges');
|
|
29
|
+
}
|
|
30
|
+
if (response.status === 404) {
|
|
31
|
+
throw new Error(`Tenant with ID/slug ${idOrSlug} not found`);
|
|
32
|
+
}
|
|
33
|
+
if (response.status === 422) {
|
|
34
|
+
const errorData = (await response.json());
|
|
35
|
+
const detail = errorData.message || errorData.error_code || 'Unknown error';
|
|
36
|
+
throw new Error(`Bulk update failed: ${detail}`);
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Failed to bulk-update tenant servers: ${response.status} ${response.statusText}`);
|
|
39
|
+
}
|
|
40
|
+
return (await response.json());
|
|
41
|
+
}
|
|
@@ -88,6 +88,13 @@ export async function createMCPImplementation(apiKey, baseUrl, params) {
|
|
|
88
88
|
if (params.internal_notes !== undefined) {
|
|
89
89
|
formData.append('mcp_implementation[internal_notes]', params.internal_notes);
|
|
90
90
|
}
|
|
91
|
+
// Owner tenant linking. Slug wins over id on the Rails side when both are sent.
|
|
92
|
+
if (params.owner_tenant_slug !== undefined) {
|
|
93
|
+
formData.append('mcp_implementation[owner_tenant_slug]', params.owner_tenant_slug === null ? '' : params.owner_tenant_slug);
|
|
94
|
+
}
|
|
95
|
+
if (params.owner_tenant_id !== undefined) {
|
|
96
|
+
formData.append('mcp_implementation[owner_tenant_id]', params.owner_tenant_id === null ? '' : params.owner_tenant_id.toString());
|
|
97
|
+
}
|
|
91
98
|
// Remote endpoints
|
|
92
99
|
// Rails expects nested attributes to use the _attributes suffix for has_many associations
|
|
93
100
|
if (params.remote !== undefined) {
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { adminFetch } from './admin-fetch.js';
|
|
2
|
+
export async function deleteApiKey(apiKey, baseUrl, id) {
|
|
3
|
+
const url = new URL(`/api/api_keys/${id}`, baseUrl);
|
|
4
|
+
const response = await adminFetch(url.toString(), {
|
|
5
|
+
method: 'DELETE',
|
|
6
|
+
headers: {
|
|
7
|
+
'X-API-Key': apiKey,
|
|
8
|
+
Accept: 'application/json',
|
|
9
|
+
},
|
|
10
|
+
});
|
|
11
|
+
if (!response.ok) {
|
|
12
|
+
if (response.status === 401) {
|
|
13
|
+
throw new Error('Invalid API key');
|
|
14
|
+
}
|
|
15
|
+
if (response.status === 403) {
|
|
16
|
+
throw new Error('User lacks write privileges');
|
|
17
|
+
}
|
|
18
|
+
if (response.status === 422) {
|
|
19
|
+
const errorData = (await response.json().catch(() => ({})));
|
|
20
|
+
throw new Error(`Cannot delete API key: ${errorData.errors?.join(', ') || 'validation failed'}`);
|
|
21
|
+
}
|
|
22
|
+
throw new Error(`Failed to delete API key: ${response.status} ${response.statusText}`);
|
|
23
|
+
}
|
|
24
|
+
return (await response.json());
|
|
25
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { adminFetch } from './admin-fetch.js';
|
|
2
|
+
export async function deleteTenant(apiKey, baseUrl, params) {
|
|
3
|
+
const url = new URL(`/api/tenants/${encodeURIComponent(String(params.id_or_slug))}`, baseUrl);
|
|
4
|
+
if (params.force) {
|
|
5
|
+
url.searchParams.set('force', 'true');
|
|
6
|
+
}
|
|
7
|
+
const response = await adminFetch(url.toString(), {
|
|
8
|
+
method: 'DELETE',
|
|
9
|
+
headers: {
|
|
10
|
+
'X-API-Key': apiKey,
|
|
11
|
+
Accept: 'application/json',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
if (!response.ok) {
|
|
15
|
+
if (response.status === 401) {
|
|
16
|
+
throw new Error('Invalid API key');
|
|
17
|
+
}
|
|
18
|
+
if (response.status === 403) {
|
|
19
|
+
throw new Error('User lacks write privileges');
|
|
20
|
+
}
|
|
21
|
+
if (response.status === 404) {
|
|
22
|
+
throw new Error(`Tenant ${params.id_or_slug} not found`);
|
|
23
|
+
}
|
|
24
|
+
if (response.status === 422) {
|
|
25
|
+
const errorData = (await response.json().catch(() => ({})));
|
|
26
|
+
throw new Error(`Cannot delete tenant: ${errorData.errors?.join(', ') || 'validation failed'}`);
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`Failed to delete tenant: ${response.status} ${response.statusText}`);
|
|
29
|
+
}
|
|
30
|
+
return (await response.json());
|
|
31
|
+
}
|
|
@@ -80,8 +80,12 @@ export async function getUnifiedMCPServer(apiKey, baseUrl, slug) {
|
|
|
80
80
|
updated_at: mcpServerData.updated_at,
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
-
// Merge MCPServer data with implementation data for complete picture
|
|
83
|
+
// Merge MCPServer data with implementation data for complete picture.
|
|
84
|
+
// Spread the search-response mcp_server first so fields only present there
|
|
85
|
+
// (e.g. owner_tenant_id/owner_tenant_slug) are preserved when the supervisor
|
|
86
|
+
// endpoint omits them. Supervisor data wins for the fields it does provide.
|
|
84
87
|
matchingImpl.mcp_server = {
|
|
88
|
+
...matchingImpl.mcp_server,
|
|
85
89
|
...mcpServerData,
|
|
86
90
|
remotes: mcpServerData.remotes,
|
|
87
91
|
tags: mcpServerData.tags,
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { adminFetch } from './admin-fetch.js';
|
|
2
|
+
function mapTenantServer(row) {
|
|
3
|
+
return {
|
|
4
|
+
id: row.id,
|
|
5
|
+
tenant_id: row.tenant_id,
|
|
6
|
+
mcp_server_id: row.mcp_server_id,
|
|
7
|
+
mcp_server_slug: row.mcp_server_slug,
|
|
8
|
+
status: row.status,
|
|
9
|
+
server_json_selection: row.server_json_selection,
|
|
10
|
+
first_touched_at: row.first_touched_at,
|
|
11
|
+
touched: row.touched,
|
|
12
|
+
created_at: row.created_at,
|
|
13
|
+
updated_at: row.updated_at,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export async function listTenantServers(apiKey, baseUrl, idOrSlug, params) {
|
|
17
|
+
const url = new URL(`/api/tenants/${idOrSlug}/servers`, baseUrl);
|
|
18
|
+
if (params?.status) {
|
|
19
|
+
url.searchParams.append('status', params.status);
|
|
20
|
+
}
|
|
21
|
+
if (params?.limit !== undefined) {
|
|
22
|
+
url.searchParams.append('limit', params.limit.toString());
|
|
23
|
+
}
|
|
24
|
+
if (params?.offset !== undefined) {
|
|
25
|
+
url.searchParams.append('offset', params.offset.toString());
|
|
26
|
+
}
|
|
27
|
+
const response = await adminFetch(url.toString(), {
|
|
28
|
+
method: 'GET',
|
|
29
|
+
headers: {
|
|
30
|
+
'X-API-Key': apiKey,
|
|
31
|
+
Accept: 'application/json',
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
if (response.status === 401) {
|
|
36
|
+
throw new Error('Invalid API key');
|
|
37
|
+
}
|
|
38
|
+
if (response.status === 403) {
|
|
39
|
+
throw new Error('User lacks admin privileges');
|
|
40
|
+
}
|
|
41
|
+
if (response.status === 404) {
|
|
42
|
+
throw new Error(`Tenant with ID/slug ${idOrSlug} not found`);
|
|
43
|
+
}
|
|
44
|
+
throw new Error(`Failed to list tenant servers: ${response.status} ${response.statusText}`);
|
|
45
|
+
}
|
|
46
|
+
const data = (await response.json());
|
|
47
|
+
return {
|
|
48
|
+
data: data.data.map(mapTenantServer),
|
|
49
|
+
pagination: {
|
|
50
|
+
current_page: data.meta.current_page,
|
|
51
|
+
total_pages: data.meta.total_pages,
|
|
52
|
+
total_count: data.meta.total_count,
|
|
53
|
+
has_next: data.meta.has_next,
|
|
54
|
+
limit: data.meta.limit,
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -98,6 +98,14 @@ export async function saveMCPImplementation(apiKey, baseUrl, id, params) {
|
|
|
98
98
|
if (params.internal_notes !== undefined) {
|
|
99
99
|
formData.append('mcp_implementation[internal_notes]', params.internal_notes);
|
|
100
100
|
}
|
|
101
|
+
// Owner tenant linking. Slug wins over id on the Rails side when both are sent.
|
|
102
|
+
// null/empty value clears the link (Rails uses .presence semantics).
|
|
103
|
+
if (params.owner_tenant_slug !== undefined) {
|
|
104
|
+
formData.append('mcp_implementation[owner_tenant_slug]', params.owner_tenant_slug === null ? '' : params.owner_tenant_slug);
|
|
105
|
+
}
|
|
106
|
+
if (params.owner_tenant_id !== undefined) {
|
|
107
|
+
formData.append('mcp_implementation[owner_tenant_id]', params.owner_tenant_id === null ? '' : params.owner_tenant_id.toString());
|
|
108
|
+
}
|
|
101
109
|
// Remote endpoints
|
|
102
110
|
// Rails expects nested attributes to use the _attributes suffix for has_many associations
|
|
103
111
|
if (params.remote !== undefined) {
|