pulsemcp-cms-admin-mcp-server 0.10.4 → 0.10.6
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 +83 -79
- package/build/shared/src/pulsemcp-admin-client/lib/create-secret.js +41 -0
- package/build/shared/src/pulsemcp-admin-client/lib/get-secret.js +28 -0
- package/build/shared/src/pulsemcp-admin-client/lib/link-secret-to-server.js +50 -0
- package/build/shared/src/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +53 -0
- package/build/shared/src/server.js +13 -0
- package/build/shared/src/tools/link-secret-to-mcp-server.js +140 -0
- package/build/shared/src/tools.js +6 -0
- package/node_modules/@pulsemcp/mcp-elicitation/package.json +1 -1
- package/package.json +1 -1
- package/shared/pulsemcp-admin-client/lib/create-secret.d.ts +7 -0
- package/shared/pulsemcp-admin-client/lib/create-secret.js +41 -0
- package/shared/pulsemcp-admin-client/lib/get-secret.d.ts +7 -0
- package/shared/pulsemcp-admin-client/lib/get-secret.js +28 -0
- package/shared/pulsemcp-admin-client/lib/link-secret-to-server.d.ts +12 -0
- package/shared/pulsemcp-admin-client/lib/link-secret-to-server.js +50 -0
- package/shared/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.d.ts +7 -1
- package/shared/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +53 -0
- package/shared/server.d.ts +7 -1
- package/shared/server.js +13 -0
- package/shared/tools/link-secret-to-mcp-server.d.ts +50 -0
- package/shared/tools/link-secret-to-mcp-server.js +140 -0
- package/shared/tools.d.ts +2 -1
- package/shared/tools.js +6 -0
- package/shared/types.d.ts +31 -0
package/README.md
CHANGED
|
@@ -40,76 +40,77 @@ 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_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
|
-
| `set_github_repository_classification` | mcp_servers | write | Set a GitHub repository's `classification` (e.g. `other` to drop a non-MCP-driven platform repo from the gh_stars popularity path).
|
|
87
|
-
| `get_redirects` | redirects | read | List URL redirects with search, status filtering, and pagination.
|
|
88
|
-
| `get_redirect` | redirects | read | Get detailed redirect info by ID.
|
|
89
|
-
| `create_redirect` | redirects | write | Create a new URL redirect entry.
|
|
90
|
-
| `update_redirect` | redirects | write | Update an existing URL redirect by ID.
|
|
91
|
-
| `delete_redirect` | redirects | write | Delete a URL redirect by ID (irreversible).
|
|
92
|
-
| `list_good_jobs` | good_jobs | read | List and filter background jobs by queue, status, job class, and date range.
|
|
93
|
-
| `get_good_job` | good_jobs | read | Get detailed information about a specific background job.
|
|
94
|
-
| `list_good_job_cron_schedules` | good_jobs | read | List all configured cron schedules.
|
|
95
|
-
| `list_good_job_processes` | good_jobs | read | List active worker processes.
|
|
96
|
-
| `get_good_job_queue_statistics` | good_jobs | read | Get aggregate job statistics by status.
|
|
97
|
-
| `retry_good_job` | good_jobs | write | Retry a failed or discarded background job.
|
|
98
|
-
| `discard_good_job` | good_jobs | write | Discard a background job to prevent retries.
|
|
99
|
-
| `reschedule_good_job` | good_jobs | write | Reschedule a background job to a new time.
|
|
100
|
-
| `force_trigger_good_job_cron` | good_jobs | write | Force trigger a cron schedule immediately.
|
|
101
|
-
| `cleanup_good_jobs` | good_jobs | write | Clean up old background jobs by status and age.
|
|
102
|
-
| `run_exam_for_mirror` | proctor | write | Run proctor exams against unofficial mirrors via Fly Machines. Returns truncated summary with `result_id`.
|
|
103
|
-
| `get_exam_result` | proctor | read | Retrieve full untruncated exam results by `result_id`, with optional section/mirror filtering.
|
|
104
|
-
| `save_results_for_mirror` | proctor | write | Save proctor exam results via `result_id` from `run_exam_for_mirror`.
|
|
105
|
-
| `list_proctor_runs` | proctor | read | List proctor runs with filtering by name, recommended status, and tenant IDs.
|
|
106
|
-
| `get_proctor_metadata` | proctor | read | Get available proctor runtimes and exam types.
|
|
107
|
-
| `list_discovered_urls` | discovered_urls | read | List discovered URLs with status filtering and pagination.
|
|
108
|
-
| `mark_discovered_url_processed` | discovered_urls | write | Mark a discovered URL as processed with a result status.
|
|
109
|
-
| `get_discovered_url_stats` | discovered_urls | read | Get summary statistics for discovered URLs pipeline.
|
|
110
|
-
| `get_moz_metrics` | moz | read | Fetch live URL metrics from the MOZ API (page authority, domain authority, spam score, link counts).
|
|
111
|
-
| `get_moz_backlinks` | moz | read | Fetch live backlink data from the MOZ API (source pages, anchor text, domain authority).
|
|
112
|
-
| `get_moz_stored_metrics` | moz | read | List stored/historical MOZ data for a server's canonicals with pagination.
|
|
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
|
+
| `set_github_repository_classification` | mcp_servers | write | Set a GitHub repository's `classification` (e.g. `other` to drop a non-MCP-driven platform repo from the gh_stars popularity path). |
|
|
87
|
+
| `get_redirects` | redirects | read | List URL redirects with search, status filtering, and pagination. |
|
|
88
|
+
| `get_redirect` | redirects | read | Get detailed redirect info by ID. |
|
|
89
|
+
| `create_redirect` | redirects | write | Create a new URL redirect entry. |
|
|
90
|
+
| `update_redirect` | redirects | write | Update an existing URL redirect by ID. |
|
|
91
|
+
| `delete_redirect` | redirects | write | Delete a URL redirect by ID (irreversible). |
|
|
92
|
+
| `list_good_jobs` | good_jobs | read | List and filter background jobs by queue, status, job class, and date range. |
|
|
93
|
+
| `get_good_job` | good_jobs | read | Get detailed information about a specific background job. |
|
|
94
|
+
| `list_good_job_cron_schedules` | good_jobs | read | List all configured cron schedules. |
|
|
95
|
+
| `list_good_job_processes` | good_jobs | read | List active worker processes. |
|
|
96
|
+
| `get_good_job_queue_statistics` | good_jobs | read | Get aggregate job statistics by status. |
|
|
97
|
+
| `retry_good_job` | good_jobs | write | Retry a failed or discarded background job. |
|
|
98
|
+
| `discard_good_job` | good_jobs | write | Discard a background job to prevent retries. |
|
|
99
|
+
| `reschedule_good_job` | good_jobs | write | Reschedule a background job to a new time. |
|
|
100
|
+
| `force_trigger_good_job_cron` | good_jobs | write | Force trigger a cron schedule immediately. |
|
|
101
|
+
| `cleanup_good_jobs` | good_jobs | write | Clean up old background jobs by status and age. |
|
|
102
|
+
| `run_exam_for_mirror` | proctor | write | Run proctor exams against unofficial mirrors via Fly Machines. Returns truncated summary with `result_id`. |
|
|
103
|
+
| `get_exam_result` | proctor | read | Retrieve full untruncated exam results by `result_id`, with optional section/mirror filtering. |
|
|
104
|
+
| `save_results_for_mirror` | proctor | write | Save proctor exam results via `result_id` from `run_exam_for_mirror`. |
|
|
105
|
+
| `list_proctor_runs` | proctor | read | List proctor runs with filtering by name, recommended status, and tenant IDs. |
|
|
106
|
+
| `get_proctor_metadata` | proctor | read | Get available proctor runtimes and exam types. |
|
|
107
|
+
| `list_discovered_urls` | discovered_urls | read | List discovered URLs with status filtering and pagination. |
|
|
108
|
+
| `mark_discovered_url_processed` | discovered_urls | write | Mark a discovered URL as processed with a result status. |
|
|
109
|
+
| `get_discovered_url_stats` | discovered_urls | read | Get summary statistics for discovered URLs pipeline. |
|
|
110
|
+
| `get_moz_metrics` | moz | read | Fetch live URL metrics from the MOZ API (page authority, domain authority, spam score, link counts). |
|
|
111
|
+
| `get_moz_backlinks` | moz | read | Fetch live backlink data from the MOZ API (source pages, anchor text, domain authority). |
|
|
112
|
+
| `get_moz_stored_metrics` | moz | read | List stored/historical MOZ data for a server's canonicals with pagination. |
|
|
113
|
+
| `link_secret_to_mcp_server` | secrets | write | Make an auth secret available to an MCP server: upsert a Secret (referencing a 1Password item) and write the server↔secret link Proctor reads. The raw secret value never passes through. |
|
|
113
114
|
|
|
114
115
|
# Tool Groups
|
|
115
116
|
|
|
@@ -149,6 +150,7 @@ This server organizes tools into groups that can be selectively enabled or disab
|
|
|
149
150
|
| `discovered_urls_readonly` | 2 | Discovered URL tools read-only (list, stats) |
|
|
150
151
|
| `moz` | 3 | MOZ SEO metrics — live URL metrics, backlinks, and stored historical data (all read-only) |
|
|
151
152
|
| `moz_readonly` | 3 | MOZ tools read-only (alias — all MOZ tools are read-only) |
|
|
153
|
+
| `secrets` | 1 | Auth-secret ↔ MCP server linking. Write-only (no `_readonly` variant): upserts a 1Password-referenced Secret and links it to a server so Proctor can inject it at runtime. |
|
|
152
154
|
|
|
153
155
|
### Tools by Group
|
|
154
156
|
|
|
@@ -191,18 +193,20 @@ This server organizes tools into groups that can be selectively enabled or disab
|
|
|
191
193
|
- Write: `mark_discovered_url_processed`
|
|
192
194
|
- **moz** / **moz_readonly**:
|
|
193
195
|
- Read-only: `get_moz_metrics`, `get_moz_backlinks`, `get_moz_stored_metrics`
|
|
196
|
+
- **secrets** (write-only — no `_readonly` variant):
|
|
197
|
+
- Write: `link_secret_to_mcp_server`
|
|
194
198
|
|
|
195
199
|
## Environment Variables
|
|
196
200
|
|
|
197
|
-
| Variable | Description | Default
|
|
198
|
-
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
199
|
-
| `PULSEMCP_ADMIN_API_KEY` | API key sent as `X-API-Key` for admin-API authentication. Required. | —
|
|
200
|
-
| `PULSEMCP_ADMIN_API_URL` | Base URL for the admin API. | `https://admin.pulsemcp.com`
|
|
201
|
-
| `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
|
|
202
|
-
| `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) |
|
|
203
|
-
| `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)
|
|
204
|
-
| `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`
|
|
205
|
-
| `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
|
|
201
|
+
| Variable | Description | Default |
|
|
202
|
+
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
203
|
+
| `PULSEMCP_ADMIN_API_KEY` | API key sent as `X-API-Key` for admin-API authentication. Required. | — |
|
|
204
|
+
| `PULSEMCP_ADMIN_API_URL` | Base URL for the admin API. | `https://admin.pulsemcp.com` |
|
|
205
|
+
| `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 |
|
|
206
|
+
| `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,secrets` (all base groups) |
|
|
207
|
+
| `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) |
|
|
208
|
+
| `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` |
|
|
209
|
+
| `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 |
|
|
206
210
|
|
|
207
211
|
### Destructive tools and elicitation
|
|
208
212
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { adminFetch } from './admin-fetch.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a secret. The secret's value stays in 1Password — this only records
|
|
4
|
+
* the `onepassword_item_id` reference plus metadata.
|
|
5
|
+
*/
|
|
6
|
+
export async function createSecret(apiKey, baseUrl, params) {
|
|
7
|
+
const url = new URL('/api/secrets', baseUrl);
|
|
8
|
+
const body = {
|
|
9
|
+
slug: params.slug,
|
|
10
|
+
onepassword_item_id: params.onepassword_item_id,
|
|
11
|
+
};
|
|
12
|
+
if (params.title !== undefined) {
|
|
13
|
+
body.title = params.title;
|
|
14
|
+
}
|
|
15
|
+
if (params.description !== undefined) {
|
|
16
|
+
body.description = params.description;
|
|
17
|
+
}
|
|
18
|
+
const response = await adminFetch(url.toString(), {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: {
|
|
21
|
+
'X-API-Key': apiKey,
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
Accept: 'application/json',
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify(body),
|
|
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 write privileges');
|
|
33
|
+
}
|
|
34
|
+
if (response.status === 422) {
|
|
35
|
+
const errorData = (await response.json());
|
|
36
|
+
throw new Error(`Validation failed: ${errorData.errors?.join(', ') || 'Unknown error'}`);
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Failed to create secret: ${response.status} ${response.statusText}`);
|
|
39
|
+
}
|
|
40
|
+
return (await response.json());
|
|
41
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { adminFetch } from './admin-fetch.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fetch a secret by id or slug. Returns null when the secret does not exist
|
|
4
|
+
* (HTTP 404) so callers can branch on create-vs-reuse without catching errors.
|
|
5
|
+
*/
|
|
6
|
+
export async function getSecret(apiKey, baseUrl, idOrSlug) {
|
|
7
|
+
const url = new URL(`/api/secrets/${encodeURIComponent(String(idOrSlug))}`, baseUrl);
|
|
8
|
+
const response = await adminFetch(url.toString(), {
|
|
9
|
+
method: 'GET',
|
|
10
|
+
headers: {
|
|
11
|
+
'X-API-Key': apiKey,
|
|
12
|
+
Accept: 'application/json',
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
if (response.status === 404) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (response.status === 401) {
|
|
20
|
+
throw new Error('Invalid API key');
|
|
21
|
+
}
|
|
22
|
+
if (response.status === 403) {
|
|
23
|
+
throw new Error('User lacks admin privileges');
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`Failed to fetch secret: ${response.status} ${response.statusText}`);
|
|
26
|
+
}
|
|
27
|
+
return (await response.json());
|
|
28
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { adminFetch } from './admin-fetch.js';
|
|
2
|
+
/**
|
|
3
|
+
* Link an MCP server to a secret by writing the mcp_servers_secrets join row.
|
|
4
|
+
* This join is what Proctor reads to inject the secret value at runtime, so it
|
|
5
|
+
* is the operation that actually scopes a stored secret to a server.
|
|
6
|
+
*
|
|
7
|
+
* Idempotent on the backend: relinking an already-linked server returns the
|
|
8
|
+
* existing join (updating onepassword_tag when a new value is supplied) instead
|
|
9
|
+
* of erroring or duplicating.
|
|
10
|
+
*/
|
|
11
|
+
export async function linkSecretToServer(apiKey, baseUrl, params) {
|
|
12
|
+
const url = new URL(`/api/secrets/${encodeURIComponent(String(params.secret))}/servers`, baseUrl);
|
|
13
|
+
const body = {};
|
|
14
|
+
if (params.mcp_server_id !== undefined) {
|
|
15
|
+
body.mcp_server_id = params.mcp_server_id;
|
|
16
|
+
}
|
|
17
|
+
if (params.mcp_server_slug !== undefined) {
|
|
18
|
+
body.mcp_server_slug = params.mcp_server_slug;
|
|
19
|
+
}
|
|
20
|
+
if (params.onepassword_tag !== undefined) {
|
|
21
|
+
body.onepassword_tag = params.onepassword_tag;
|
|
22
|
+
}
|
|
23
|
+
const response = await adminFetch(url.toString(), {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'X-API-Key': apiKey,
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
Accept: 'application/json',
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify(body),
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
if (response.status === 401) {
|
|
34
|
+
throw new Error('Invalid API key');
|
|
35
|
+
}
|
|
36
|
+
if (response.status === 403) {
|
|
37
|
+
throw new Error('User lacks write privileges');
|
|
38
|
+
}
|
|
39
|
+
if (response.status === 404) {
|
|
40
|
+
const errorData = (await response.json().catch(() => ({})));
|
|
41
|
+
throw new Error(errorData.error || 'Secret or MCP server not found');
|
|
42
|
+
}
|
|
43
|
+
if (response.status === 422) {
|
|
44
|
+
const errorData = (await response.json());
|
|
45
|
+
throw new Error(`Validation failed: ${errorData.errors?.join(', ') || 'Unknown error'}`);
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Failed to link secret to server: ${response.status} ${response.statusText}`);
|
|
48
|
+
}
|
|
49
|
+
return (await response.json());
|
|
50
|
+
}
|
|
@@ -1105,5 +1105,58 @@ export function createMockPulseMCPAdminClient(mockData) {
|
|
|
1105
1105
|
classification,
|
|
1106
1106
|
};
|
|
1107
1107
|
},
|
|
1108
|
+
// Secret REST API methods
|
|
1109
|
+
async getSecret(idOrSlug) {
|
|
1110
|
+
if (mockData.errors?.getSecret) {
|
|
1111
|
+
throw mockData.errors.getSecret;
|
|
1112
|
+
}
|
|
1113
|
+
const key = String(idOrSlug);
|
|
1114
|
+
return mockData.secretsBySlug?.[key] ?? null;
|
|
1115
|
+
},
|
|
1116
|
+
async createSecret(params) {
|
|
1117
|
+
if (mockData.errors?.createSecret) {
|
|
1118
|
+
throw mockData.errors.createSecret;
|
|
1119
|
+
}
|
|
1120
|
+
if (mockData.createSecretResponse) {
|
|
1121
|
+
return mockData.createSecretResponse;
|
|
1122
|
+
}
|
|
1123
|
+
return {
|
|
1124
|
+
id: 1,
|
|
1125
|
+
slug: params.slug,
|
|
1126
|
+
onepassword_item_id: params.onepassword_item_id,
|
|
1127
|
+
title: params.title ?? null,
|
|
1128
|
+
description: params.description ?? null,
|
|
1129
|
+
mcp_servers_count: 0,
|
|
1130
|
+
mcp_server_slugs: [],
|
|
1131
|
+
created_at: new Date().toISOString(),
|
|
1132
|
+
updated_at: new Date().toISOString(),
|
|
1133
|
+
};
|
|
1134
|
+
},
|
|
1135
|
+
async linkSecretToServer(params) {
|
|
1136
|
+
if (mockData.errors?.linkSecretToServer) {
|
|
1137
|
+
throw mockData.errors.linkSecretToServer;
|
|
1138
|
+
}
|
|
1139
|
+
if (mockData.linkSecretToServerResponse) {
|
|
1140
|
+
return mockData.linkSecretToServerResponse;
|
|
1141
|
+
}
|
|
1142
|
+
const slug = typeof params.secret === 'string' ? params.secret : `secret-${params.secret}`;
|
|
1143
|
+
const serverSlug = params.mcp_server_slug ?? 'mock-server';
|
|
1144
|
+
return {
|
|
1145
|
+
id: 1,
|
|
1146
|
+
slug,
|
|
1147
|
+
onepassword_item_id: 'op://Vault/Item/credential',
|
|
1148
|
+
title: null,
|
|
1149
|
+
description: null,
|
|
1150
|
+
mcp_servers_count: 1,
|
|
1151
|
+
mcp_server_slugs: [serverSlug],
|
|
1152
|
+
created_at: new Date().toISOString(),
|
|
1153
|
+
updated_at: new Date().toISOString(),
|
|
1154
|
+
link: {
|
|
1155
|
+
mcp_server_id: params.mcp_server_id ?? 1,
|
|
1156
|
+
mcp_server_slug: serverSlug,
|
|
1157
|
+
onepassword_tag: params.onepassword_tag ?? null,
|
|
1158
|
+
},
|
|
1159
|
+
};
|
|
1160
|
+
},
|
|
1108
1161
|
};
|
|
1109
1162
|
}
|
|
@@ -51,6 +51,9 @@ import { getRedirect } from './pulsemcp-admin-client/lib/get-redirect.js';
|
|
|
51
51
|
import { createRedirect } from './pulsemcp-admin-client/lib/create-redirect.js';
|
|
52
52
|
import { updateRedirect } from './pulsemcp-admin-client/lib/update-redirect.js';
|
|
53
53
|
import { deleteRedirect } from './pulsemcp-admin-client/lib/delete-redirect.js';
|
|
54
|
+
import { getSecret } from './pulsemcp-admin-client/lib/get-secret.js';
|
|
55
|
+
import { createSecret } from './pulsemcp-admin-client/lib/create-secret.js';
|
|
56
|
+
import { linkSecretToServer } from './pulsemcp-admin-client/lib/link-secret-to-server.js';
|
|
54
57
|
import { getGoodJobs } from './pulsemcp-admin-client/lib/get-good-jobs.js';
|
|
55
58
|
import { getGoodJob } from './pulsemcp-admin-client/lib/get-good-job.js';
|
|
56
59
|
import { getGoodJobCronSchedules } from './pulsemcp-admin-client/lib/get-good-job-cron-schedules.js';
|
|
@@ -270,6 +273,16 @@ export class PulseMCPAdminClient {
|
|
|
270
273
|
async deleteRedirect(id) {
|
|
271
274
|
return deleteRedirect(this.apiKey, this.baseUrl, id);
|
|
272
275
|
}
|
|
276
|
+
// Secret REST API methods
|
|
277
|
+
async getSecret(idOrSlug) {
|
|
278
|
+
return getSecret(this.apiKey, this.baseUrl, idOrSlug);
|
|
279
|
+
}
|
|
280
|
+
async createSecret(params) {
|
|
281
|
+
return createSecret(this.apiKey, this.baseUrl, params);
|
|
282
|
+
}
|
|
283
|
+
async linkSecretToServer(params) {
|
|
284
|
+
return linkSecretToServer(this.apiKey, this.baseUrl, params);
|
|
285
|
+
}
|
|
273
286
|
// GoodJob REST API methods
|
|
274
287
|
async getGoodJobs(params) {
|
|
275
288
|
return getGoodJobs(this.apiKey, this.baseUrl, params);
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const PARAM_DESCRIPTIONS = {
|
|
3
|
+
slug: 'Unique slug identifier for the secret (lowercase, hyphenated, e.g. "linear-api-key"). Used to look up an existing secret or to create a new one.',
|
|
4
|
+
onepassword_item_id: 'The 1Password item reference the secret value lives in (e.g. "op://Vault/Item/credential"). The raw secret value is NEVER passed through this tool — only this reference. If a secret with this slug already exists, this must match its stored reference.',
|
|
5
|
+
mcp_server_slug: 'Slug of the MCP server to grant the secret to (e.g. "linear"). The link is what Proctor reads to inject the secret value when running that server.',
|
|
6
|
+
title: 'Optional human-readable title for the secret (only used when creating a new secret).',
|
|
7
|
+
description: 'Optional description of what the secret is for (only used when creating a new secret).',
|
|
8
|
+
onepassword_tag: 'Optional tag scoping which field of the 1Password item to inject for this server (e.g. "production"). Stored on the server↔secret link, so the same secret can inject different fields per server.',
|
|
9
|
+
};
|
|
10
|
+
const LinkSecretToMcpServerSchema = z.object({
|
|
11
|
+
slug: z.string().describe(PARAM_DESCRIPTIONS.slug),
|
|
12
|
+
onepassword_item_id: z.string().describe(PARAM_DESCRIPTIONS.onepassword_item_id),
|
|
13
|
+
mcp_server_slug: z.string().describe(PARAM_DESCRIPTIONS.mcp_server_slug),
|
|
14
|
+
title: z.string().optional().describe(PARAM_DESCRIPTIONS.title),
|
|
15
|
+
description: z.string().optional().describe(PARAM_DESCRIPTIONS.description),
|
|
16
|
+
onepassword_tag: z.string().optional().describe(PARAM_DESCRIPTIONS.onepassword_tag),
|
|
17
|
+
});
|
|
18
|
+
export function linkSecretToMcpServer(_server, clientFactory) {
|
|
19
|
+
return {
|
|
20
|
+
name: 'link_secret_to_mcp_server',
|
|
21
|
+
description: `Make an auth secret available to an MCP server. This upserts a Secret (by slug) that references a 1Password item, then writes the server↔secret link that Proctor reads to inject the secret value at runtime.
|
|
22
|
+
|
|
23
|
+
The raw secret value stays in 1Password — this tool only deals in the 1Password item reference ("onepassword_item_id") and slug, never a credential value.
|
|
24
|
+
|
|
25
|
+
Behavior:
|
|
26
|
+
- If no secret with "slug" exists, it is created from "onepassword_item_id" (plus optional title/description).
|
|
27
|
+
- If a secret with "slug" already exists, it is reused. Its stored "onepassword_item_id" must match the one you pass, otherwise the tool errors instead of silently rebinding a shared secret.
|
|
28
|
+
- The link is idempotent: re-running for an already-linked server updates the "onepassword_tag" (when provided) without creating a duplicate.
|
|
29
|
+
|
|
30
|
+
Example request:
|
|
31
|
+
{
|
|
32
|
+
"slug": "linear-api-key",
|
|
33
|
+
"onepassword_item_id": "op://Shared/Linear API Key/credential",
|
|
34
|
+
"mcp_server_slug": "linear",
|
|
35
|
+
"onepassword_tag": "production"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Use cases:
|
|
39
|
+
- Onboarding an auth-gated MCP server: register its credential reference and scope it to the server so Proctor can run it.
|
|
40
|
+
- Granting an already-registered secret to an additional server.`,
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
slug: { type: 'string', description: PARAM_DESCRIPTIONS.slug },
|
|
45
|
+
onepassword_item_id: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
description: PARAM_DESCRIPTIONS.onepassword_item_id,
|
|
48
|
+
},
|
|
49
|
+
mcp_server_slug: { type: 'string', description: PARAM_DESCRIPTIONS.mcp_server_slug },
|
|
50
|
+
title: { type: 'string', description: PARAM_DESCRIPTIONS.title },
|
|
51
|
+
description: { type: 'string', description: PARAM_DESCRIPTIONS.description },
|
|
52
|
+
onepassword_tag: { type: 'string', description: PARAM_DESCRIPTIONS.onepassword_tag },
|
|
53
|
+
},
|
|
54
|
+
required: ['slug', 'onepassword_item_id', 'mcp_server_slug'],
|
|
55
|
+
},
|
|
56
|
+
handler: async (args) => {
|
|
57
|
+
const validatedArgs = LinkSecretToMcpServerSchema.parse(args);
|
|
58
|
+
const client = clientFactory();
|
|
59
|
+
try {
|
|
60
|
+
const existing = await client.getSecret(validatedArgs.slug);
|
|
61
|
+
if (existing && existing.onepassword_item_id !== validatedArgs.onepassword_item_id) {
|
|
62
|
+
return {
|
|
63
|
+
content: [
|
|
64
|
+
{
|
|
65
|
+
type: 'text',
|
|
66
|
+
text: `Error: a secret with slug "${validatedArgs.slug}" already exists but references a different 1Password item ` +
|
|
67
|
+
`("${existing.onepassword_item_id}", not "${validatedArgs.onepassword_item_id}"). ` +
|
|
68
|
+
`Refusing to rebind it, since other servers may depend on it. ` +
|
|
69
|
+
`Use a different slug, or update the existing secret explicitly if the reference really changed.`,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
isError: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
let created = false;
|
|
76
|
+
if (!existing) {
|
|
77
|
+
try {
|
|
78
|
+
await client.createSecret({
|
|
79
|
+
slug: validatedArgs.slug,
|
|
80
|
+
onepassword_item_id: validatedArgs.onepassword_item_id,
|
|
81
|
+
title: validatedArgs.title,
|
|
82
|
+
description: validatedArgs.description,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (createError) {
|
|
86
|
+
// The backend also enforces uniqueness on onepassword_item_id, so the
|
|
87
|
+
// item may already be registered under a *different* slug than the one
|
|
88
|
+
// requested. getSecret(slug) can't surface that (it looks up by slug),
|
|
89
|
+
// so translate the opaque uniqueness 422 into actionable guidance
|
|
90
|
+
// instead of letting the raw validation message reach the agent.
|
|
91
|
+
const message = createError instanceof Error ? createError.message : String(createError);
|
|
92
|
+
if (/onepassword item/i.test(message) && /taken/i.test(message)) {
|
|
93
|
+
return {
|
|
94
|
+
content: [
|
|
95
|
+
{
|
|
96
|
+
type: 'text',
|
|
97
|
+
text: `Error: the 1Password item "${validatedArgs.onepassword_item_id}" is already registered under a different secret slug, ` +
|
|
98
|
+
`so it cannot be created again as "${validatedArgs.slug}". ` +
|
|
99
|
+
`To link it to "${validatedArgs.mcp_server_slug}", re-run this tool with the existing secret's slug ` +
|
|
100
|
+
`(find it via the secrets list), or register a different 1Password item under "${validatedArgs.slug}".`,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
isError: true,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
throw createError;
|
|
107
|
+
}
|
|
108
|
+
created = true;
|
|
109
|
+
}
|
|
110
|
+
const result = await client.linkSecretToServer({
|
|
111
|
+
secret: validatedArgs.slug,
|
|
112
|
+
mcp_server_slug: validatedArgs.mcp_server_slug,
|
|
113
|
+
onepassword_tag: validatedArgs.onepassword_tag,
|
|
114
|
+
});
|
|
115
|
+
let content = `Successfully linked secret "${result.slug}" to MCP server "${result.link.mcp_server_slug}".\n\n`;
|
|
116
|
+
content += `**Secret:** ${result.slug} (id ${result.id}) — ${created ? 'created' : 'reused existing'}\n`;
|
|
117
|
+
content += `**1Password item:** ${result.onepassword_item_id}\n`;
|
|
118
|
+
content += `**Linked server:** ${result.link.mcp_server_slug} (id ${result.link.mcp_server_id})\n`;
|
|
119
|
+
if (result.link.onepassword_tag) {
|
|
120
|
+
content += `**1Password tag:** ${result.link.onepassword_tag}\n`;
|
|
121
|
+
}
|
|
122
|
+
if (result.mcp_server_slugs && result.mcp_server_slugs.length > 0) {
|
|
123
|
+
content += `**All servers using this secret:** ${result.mcp_server_slugs.join(', ')}\n`;
|
|
124
|
+
}
|
|
125
|
+
return { content: [{ type: 'text', text: content }] };
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return {
|
|
129
|
+
content: [
|
|
130
|
+
{
|
|
131
|
+
type: 'text',
|
|
132
|
+
text: `Error linking secret to MCP server: ${error instanceof Error ? error.message : String(error)}`,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
isError: true,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -55,6 +55,8 @@ import { getRedirect } from './tools/get-redirect.js';
|
|
|
55
55
|
import { createRedirect } from './tools/create-redirect.js';
|
|
56
56
|
import { updateRedirect } from './tools/update-redirect.js';
|
|
57
57
|
import { deleteRedirect } from './tools/delete-redirect.js';
|
|
58
|
+
// Secret tools
|
|
59
|
+
import { linkSecretToMcpServer } from './tools/link-secret-to-mcp-server.js';
|
|
58
60
|
// GoodJob tools
|
|
59
61
|
import { listGoodJobs } from './tools/list-good-jobs.js';
|
|
60
62
|
import { getGoodJob } from './tools/get-good-job.js';
|
|
@@ -254,6 +256,8 @@ const ALL_TOOLS = [
|
|
|
254
256
|
{ factory: createRedirect, groups: ['redirects'], isWriteOperation: true },
|
|
255
257
|
{ factory: updateRedirect, groups: ['redirects'], isWriteOperation: true },
|
|
256
258
|
{ factory: deleteRedirect, groups: ['redirects'], isWriteOperation: true },
|
|
259
|
+
// Secret tools (write-only: upsert a secret + write the mcp_servers_secrets join Proctor reads)
|
|
260
|
+
{ factory: linkSecretToMcpServer, groups: ['secrets'], isWriteOperation: true },
|
|
257
261
|
// GoodJob tools
|
|
258
262
|
{ factory: listGoodJobs, groups: ['good_jobs'], isWriteOperation: false },
|
|
259
263
|
{ factory: getGoodJob, groups: ['good_jobs'], isWriteOperation: false },
|
|
@@ -328,6 +332,7 @@ const VALID_TOOL_GROUPS = [
|
|
|
328
332
|
'mcp_servers_readonly',
|
|
329
333
|
'redirects',
|
|
330
334
|
'redirects_readonly',
|
|
335
|
+
'secrets',
|
|
331
336
|
'good_jobs',
|
|
332
337
|
'good_jobs_readonly',
|
|
333
338
|
'proctor',
|
|
@@ -351,6 +356,7 @@ const BASE_TOOL_GROUPS = [
|
|
|
351
356
|
'mcp_jsons',
|
|
352
357
|
'mcp_servers',
|
|
353
358
|
'redirects',
|
|
359
|
+
'secrets',
|
|
354
360
|
'good_jobs',
|
|
355
361
|
'proctor',
|
|
356
362
|
'discovered_urls',
|
package/package.json
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Secret, CreateSecretParams } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a secret. The secret's value stays in 1Password — this only records
|
|
4
|
+
* the `onepassword_item_id` reference plus metadata.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createSecret(apiKey: string, baseUrl: string, params: CreateSecretParams): Promise<Secret>;
|
|
7
|
+
//# sourceMappingURL=create-secret.d.ts.map
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { adminFetch } from './admin-fetch.js';
|
|
2
|
+
/**
|
|
3
|
+
* Create a secret. The secret's value stays in 1Password — this only records
|
|
4
|
+
* the `onepassword_item_id` reference plus metadata.
|
|
5
|
+
*/
|
|
6
|
+
export async function createSecret(apiKey, baseUrl, params) {
|
|
7
|
+
const url = new URL('/api/secrets', baseUrl);
|
|
8
|
+
const body = {
|
|
9
|
+
slug: params.slug,
|
|
10
|
+
onepassword_item_id: params.onepassword_item_id,
|
|
11
|
+
};
|
|
12
|
+
if (params.title !== undefined) {
|
|
13
|
+
body.title = params.title;
|
|
14
|
+
}
|
|
15
|
+
if (params.description !== undefined) {
|
|
16
|
+
body.description = params.description;
|
|
17
|
+
}
|
|
18
|
+
const response = await adminFetch(url.toString(), {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: {
|
|
21
|
+
'X-API-Key': apiKey,
|
|
22
|
+
'Content-Type': 'application/json',
|
|
23
|
+
Accept: 'application/json',
|
|
24
|
+
},
|
|
25
|
+
body: JSON.stringify(body),
|
|
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 write privileges');
|
|
33
|
+
}
|
|
34
|
+
if (response.status === 422) {
|
|
35
|
+
const errorData = (await response.json());
|
|
36
|
+
throw new Error(`Validation failed: ${errorData.errors?.join(', ') || 'Unknown error'}`);
|
|
37
|
+
}
|
|
38
|
+
throw new Error(`Failed to create secret: ${response.status} ${response.statusText}`);
|
|
39
|
+
}
|
|
40
|
+
return (await response.json());
|
|
41
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { Secret } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fetch a secret by id or slug. Returns null when the secret does not exist
|
|
4
|
+
* (HTTP 404) so callers can branch on create-vs-reuse without catching errors.
|
|
5
|
+
*/
|
|
6
|
+
export declare function getSecret(apiKey: string, baseUrl: string, idOrSlug: string | number): Promise<Secret | null>;
|
|
7
|
+
//# sourceMappingURL=get-secret.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { adminFetch } from './admin-fetch.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fetch a secret by id or slug. Returns null when the secret does not exist
|
|
4
|
+
* (HTTP 404) so callers can branch on create-vs-reuse without catching errors.
|
|
5
|
+
*/
|
|
6
|
+
export async function getSecret(apiKey, baseUrl, idOrSlug) {
|
|
7
|
+
const url = new URL(`/api/secrets/${encodeURIComponent(String(idOrSlug))}`, baseUrl);
|
|
8
|
+
const response = await adminFetch(url.toString(), {
|
|
9
|
+
method: 'GET',
|
|
10
|
+
headers: {
|
|
11
|
+
'X-API-Key': apiKey,
|
|
12
|
+
Accept: 'application/json',
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
if (!response.ok) {
|
|
16
|
+
if (response.status === 404) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (response.status === 401) {
|
|
20
|
+
throw new Error('Invalid API key');
|
|
21
|
+
}
|
|
22
|
+
if (response.status === 403) {
|
|
23
|
+
throw new Error('User lacks admin privileges');
|
|
24
|
+
}
|
|
25
|
+
throw new Error(`Failed to fetch secret: ${response.status} ${response.statusText}`);
|
|
26
|
+
}
|
|
27
|
+
return (await response.json());
|
|
28
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LinkSecretToServerParams, SecretWithLink } from '../../types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Link an MCP server to a secret by writing the mcp_servers_secrets join row.
|
|
4
|
+
* This join is what Proctor reads to inject the secret value at runtime, so it
|
|
5
|
+
* is the operation that actually scopes a stored secret to a server.
|
|
6
|
+
*
|
|
7
|
+
* Idempotent on the backend: relinking an already-linked server returns the
|
|
8
|
+
* existing join (updating onepassword_tag when a new value is supplied) instead
|
|
9
|
+
* of erroring or duplicating.
|
|
10
|
+
*/
|
|
11
|
+
export declare function linkSecretToServer(apiKey: string, baseUrl: string, params: LinkSecretToServerParams): Promise<SecretWithLink>;
|
|
12
|
+
//# sourceMappingURL=link-secret-to-server.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { adminFetch } from './admin-fetch.js';
|
|
2
|
+
/**
|
|
3
|
+
* Link an MCP server to a secret by writing the mcp_servers_secrets join row.
|
|
4
|
+
* This join is what Proctor reads to inject the secret value at runtime, so it
|
|
5
|
+
* is the operation that actually scopes a stored secret to a server.
|
|
6
|
+
*
|
|
7
|
+
* Idempotent on the backend: relinking an already-linked server returns the
|
|
8
|
+
* existing join (updating onepassword_tag when a new value is supplied) instead
|
|
9
|
+
* of erroring or duplicating.
|
|
10
|
+
*/
|
|
11
|
+
export async function linkSecretToServer(apiKey, baseUrl, params) {
|
|
12
|
+
const url = new URL(`/api/secrets/${encodeURIComponent(String(params.secret))}/servers`, baseUrl);
|
|
13
|
+
const body = {};
|
|
14
|
+
if (params.mcp_server_id !== undefined) {
|
|
15
|
+
body.mcp_server_id = params.mcp_server_id;
|
|
16
|
+
}
|
|
17
|
+
if (params.mcp_server_slug !== undefined) {
|
|
18
|
+
body.mcp_server_slug = params.mcp_server_slug;
|
|
19
|
+
}
|
|
20
|
+
if (params.onepassword_tag !== undefined) {
|
|
21
|
+
body.onepassword_tag = params.onepassword_tag;
|
|
22
|
+
}
|
|
23
|
+
const response = await adminFetch(url.toString(), {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
'X-API-Key': apiKey,
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
Accept: 'application/json',
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify(body),
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
if (response.status === 401) {
|
|
34
|
+
throw new Error('Invalid API key');
|
|
35
|
+
}
|
|
36
|
+
if (response.status === 403) {
|
|
37
|
+
throw new Error('User lacks write privileges');
|
|
38
|
+
}
|
|
39
|
+
if (response.status === 404) {
|
|
40
|
+
const errorData = (await response.json().catch(() => ({})));
|
|
41
|
+
throw new Error(errorData.error || 'Secret or MCP server not found');
|
|
42
|
+
}
|
|
43
|
+
if (response.status === 422) {
|
|
44
|
+
const errorData = (await response.json());
|
|
45
|
+
throw new Error(`Validation failed: ${errorData.errors?.join(', ') || 'Unknown error'}`);
|
|
46
|
+
}
|
|
47
|
+
throw new Error(`Failed to link secret to server: ${response.status} ${response.statusText}`);
|
|
48
|
+
}
|
|
49
|
+
return (await response.json());
|
|
50
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IPulseMCPAdminClient } from '../server.js';
|
|
2
|
-
import type { Post, ImageUploadResponse, Author, MCPServer, MCPClient, MCPImplementation, MCPImplementationsResponse, Provider, ProvidersResponse, OfficialMirrorQueueItem, OfficialMirrorQueueItemDetail, OfficialMirrorQueueResponse, OfficialMirrorQueueActionResponse } from '../types.js';
|
|
2
|
+
import type { Post, ImageUploadResponse, Author, MCPServer, MCPClient, MCPImplementation, MCPImplementationsResponse, Provider, ProvidersResponse, OfficialMirrorQueueItem, OfficialMirrorQueueItemDetail, OfficialMirrorQueueResponse, OfficialMirrorQueueActionResponse, Secret, SecretWithLink } from '../types.js';
|
|
3
3
|
interface MockData {
|
|
4
4
|
posts?: Post[];
|
|
5
5
|
postsBySlug?: Record<string, Post>;
|
|
@@ -19,6 +19,9 @@ interface MockData {
|
|
|
19
19
|
officialMirrorQueueItemsResponse?: OfficialMirrorQueueResponse;
|
|
20
20
|
officialMirrorQueueItemDetail?: OfficialMirrorQueueItemDetail;
|
|
21
21
|
officialMirrorQueueActionResponse?: OfficialMirrorQueueActionResponse;
|
|
22
|
+
secretsBySlug?: Record<string, Secret>;
|
|
23
|
+
createSecretResponse?: Secret;
|
|
24
|
+
linkSecretToServerResponse?: SecretWithLink;
|
|
22
25
|
errors?: {
|
|
23
26
|
getPosts?: Error;
|
|
24
27
|
getPost?: Error;
|
|
@@ -42,6 +45,9 @@ interface MockData {
|
|
|
42
45
|
rejectOfficialMirrorQueueItem?: Error;
|
|
43
46
|
addOfficialMirrorToRegularQueue?: Error;
|
|
44
47
|
unlinkOfficialMirrorQueueItem?: Error;
|
|
48
|
+
getSecret?: Error;
|
|
49
|
+
createSecret?: Error;
|
|
50
|
+
linkSecretToServer?: Error;
|
|
45
51
|
};
|
|
46
52
|
}
|
|
47
53
|
export declare function createMockPulseMCPAdminClient(mockData: MockData): IPulseMCPAdminClient;
|
|
@@ -1105,5 +1105,58 @@ export function createMockPulseMCPAdminClient(mockData) {
|
|
|
1105
1105
|
classification,
|
|
1106
1106
|
};
|
|
1107
1107
|
},
|
|
1108
|
+
// Secret REST API methods
|
|
1109
|
+
async getSecret(idOrSlug) {
|
|
1110
|
+
if (mockData.errors?.getSecret) {
|
|
1111
|
+
throw mockData.errors.getSecret;
|
|
1112
|
+
}
|
|
1113
|
+
const key = String(idOrSlug);
|
|
1114
|
+
return mockData.secretsBySlug?.[key] ?? null;
|
|
1115
|
+
},
|
|
1116
|
+
async createSecret(params) {
|
|
1117
|
+
if (mockData.errors?.createSecret) {
|
|
1118
|
+
throw mockData.errors.createSecret;
|
|
1119
|
+
}
|
|
1120
|
+
if (mockData.createSecretResponse) {
|
|
1121
|
+
return mockData.createSecretResponse;
|
|
1122
|
+
}
|
|
1123
|
+
return {
|
|
1124
|
+
id: 1,
|
|
1125
|
+
slug: params.slug,
|
|
1126
|
+
onepassword_item_id: params.onepassword_item_id,
|
|
1127
|
+
title: params.title ?? null,
|
|
1128
|
+
description: params.description ?? null,
|
|
1129
|
+
mcp_servers_count: 0,
|
|
1130
|
+
mcp_server_slugs: [],
|
|
1131
|
+
created_at: new Date().toISOString(),
|
|
1132
|
+
updated_at: new Date().toISOString(),
|
|
1133
|
+
};
|
|
1134
|
+
},
|
|
1135
|
+
async linkSecretToServer(params) {
|
|
1136
|
+
if (mockData.errors?.linkSecretToServer) {
|
|
1137
|
+
throw mockData.errors.linkSecretToServer;
|
|
1138
|
+
}
|
|
1139
|
+
if (mockData.linkSecretToServerResponse) {
|
|
1140
|
+
return mockData.linkSecretToServerResponse;
|
|
1141
|
+
}
|
|
1142
|
+
const slug = typeof params.secret === 'string' ? params.secret : `secret-${params.secret}`;
|
|
1143
|
+
const serverSlug = params.mcp_server_slug ?? 'mock-server';
|
|
1144
|
+
return {
|
|
1145
|
+
id: 1,
|
|
1146
|
+
slug,
|
|
1147
|
+
onepassword_item_id: 'op://Vault/Item/credential',
|
|
1148
|
+
title: null,
|
|
1149
|
+
description: null,
|
|
1150
|
+
mcp_servers_count: 1,
|
|
1151
|
+
mcp_server_slugs: [serverSlug],
|
|
1152
|
+
created_at: new Date().toISOString(),
|
|
1153
|
+
updated_at: new Date().toISOString(),
|
|
1154
|
+
link: {
|
|
1155
|
+
mcp_server_id: params.mcp_server_id ?? 1,
|
|
1156
|
+
mcp_server_slug: serverSlug,
|
|
1157
|
+
onepassword_tag: params.onepassword_tag ?? null,
|
|
1158
|
+
},
|
|
1159
|
+
};
|
|
1160
|
+
},
|
|
1108
1161
|
};
|
|
1109
1162
|
}
|
package/shared/server.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
-
import type { Post, PostsResponse, CreatePostParams, UpdatePostParams, ImageUploadResponse, Author, AuthorsResponse, MCPServer, MCPClient, MCPImplementation, MCPImplementationsResponse, SaveMCPImplementationParams, CreateMCPImplementationParams, Provider, ProvidersResponse, OfficialMirrorQueueStatus, OfficialMirrorQueueResponse, OfficialMirrorQueueItemDetail, OfficialMirrorQueueActionResponse, UnofficialMirror, UnofficialMirrorsResponse, CreateUnofficialMirrorParams, UpdateUnofficialMirrorParams, OfficialMirrorRest, OfficialMirrorsResponse, Tenant, TenantsResponse, ListTenantServersResponse, BulkUpdateTenantServersParams, BulkUpdateTenantServersResponse, McpJson, McpJsonsResponse, CreateMcpJsonParams, UpdateMcpJsonParams, UnifiedMCPServer, UnifiedMCPServersResponse, UpdateUnifiedMCPServerParams, Redirect, RedirectsResponse, RedirectStatus, CreateRedirectParams, UpdateRedirectParams, GoodJob, GoodJobsResponse, GoodJobStatus, GoodJobCronSchedule, GoodJobProcess, GoodJobStatistics, GoodJobActionResponse, GoodJobCleanupResponse, PopularityDropBypassStatus, ProctorRunExamParams, ProctorRunExamResponse, ProctorSaveResultsParams, ProctorSaveResultsResponse, ProctorRunsResponse, GetProctorRunsParams, ProctorMetadataResponse, DiscoveredUrlsResponse, MarkDiscoveredUrlProcessedParams, MarkDiscoveredUrlProcessedResponse, DiscoveredUrlStats, MozMetricsResponse, MozBacklinksResponse, MozStoredMetricsResponse, CreateTenantParams, ApiKey, CreateApiKeyParams, DeleteTenantParams, DeleteTenantResponse, DeleteApiKeyResponse, RecacheMCPServerResponse, SetKnownMissingInitToolsListResponse, GithubRepositoryClassification, SetGithubRepositoryClassificationResponse } from './types.js';
|
|
2
|
+
import type { Post, PostsResponse, CreatePostParams, UpdatePostParams, ImageUploadResponse, Author, AuthorsResponse, MCPServer, MCPClient, MCPImplementation, MCPImplementationsResponse, SaveMCPImplementationParams, CreateMCPImplementationParams, Provider, ProvidersResponse, OfficialMirrorQueueStatus, OfficialMirrorQueueResponse, OfficialMirrorQueueItemDetail, OfficialMirrorQueueActionResponse, UnofficialMirror, UnofficialMirrorsResponse, CreateUnofficialMirrorParams, UpdateUnofficialMirrorParams, OfficialMirrorRest, OfficialMirrorsResponse, Tenant, TenantsResponse, ListTenantServersResponse, BulkUpdateTenantServersParams, BulkUpdateTenantServersResponse, McpJson, McpJsonsResponse, CreateMcpJsonParams, UpdateMcpJsonParams, UnifiedMCPServer, UnifiedMCPServersResponse, UpdateUnifiedMCPServerParams, Redirect, RedirectsResponse, RedirectStatus, CreateRedirectParams, UpdateRedirectParams, Secret, CreateSecretParams, LinkSecretToServerParams, SecretWithLink, GoodJob, GoodJobsResponse, GoodJobStatus, GoodJobCronSchedule, GoodJobProcess, GoodJobStatistics, GoodJobActionResponse, GoodJobCleanupResponse, PopularityDropBypassStatus, ProctorRunExamParams, ProctorRunExamResponse, ProctorSaveResultsParams, ProctorSaveResultsResponse, ProctorRunsResponse, GetProctorRunsParams, ProctorMetadataResponse, DiscoveredUrlsResponse, MarkDiscoveredUrlProcessedParams, MarkDiscoveredUrlProcessedResponse, DiscoveredUrlStats, MozMetricsResponse, MozBacklinksResponse, MozStoredMetricsResponse, CreateTenantParams, ApiKey, CreateApiKeyParams, DeleteTenantParams, DeleteTenantResponse, DeleteApiKeyResponse, RecacheMCPServerResponse, SetKnownMissingInitToolsListResponse, GithubRepositoryClassification, SetGithubRepositoryClassificationResponse } from './types.js';
|
|
3
3
|
export interface IPulseMCPAdminClient {
|
|
4
4
|
getPosts(params?: {
|
|
5
5
|
search?: string;
|
|
@@ -150,6 +150,9 @@ export interface IPulseMCPAdminClient {
|
|
|
150
150
|
success: boolean;
|
|
151
151
|
message: string;
|
|
152
152
|
}>;
|
|
153
|
+
getSecret(idOrSlug: string | number): Promise<Secret | null>;
|
|
154
|
+
createSecret(params: CreateSecretParams): Promise<Secret>;
|
|
155
|
+
linkSecretToServer(params: LinkSecretToServerParams): Promise<SecretWithLink>;
|
|
153
156
|
getGoodJobs(params?: {
|
|
154
157
|
queue_name?: string;
|
|
155
158
|
status?: GoodJobStatus;
|
|
@@ -352,6 +355,9 @@ export declare class PulseMCPAdminClient implements IPulseMCPAdminClient {
|
|
|
352
355
|
success: boolean;
|
|
353
356
|
message: string;
|
|
354
357
|
}>;
|
|
358
|
+
getSecret(idOrSlug: string | number): Promise<Secret | null>;
|
|
359
|
+
createSecret(params: CreateSecretParams): Promise<Secret>;
|
|
360
|
+
linkSecretToServer(params: LinkSecretToServerParams): Promise<SecretWithLink>;
|
|
355
361
|
getGoodJobs(params?: {
|
|
356
362
|
queue_name?: string;
|
|
357
363
|
status?: GoodJobStatus;
|
package/shared/server.js
CHANGED
|
@@ -51,6 +51,9 @@ import { getRedirect } from './pulsemcp-admin-client/lib/get-redirect.js';
|
|
|
51
51
|
import { createRedirect } from './pulsemcp-admin-client/lib/create-redirect.js';
|
|
52
52
|
import { updateRedirect } from './pulsemcp-admin-client/lib/update-redirect.js';
|
|
53
53
|
import { deleteRedirect } from './pulsemcp-admin-client/lib/delete-redirect.js';
|
|
54
|
+
import { getSecret } from './pulsemcp-admin-client/lib/get-secret.js';
|
|
55
|
+
import { createSecret } from './pulsemcp-admin-client/lib/create-secret.js';
|
|
56
|
+
import { linkSecretToServer } from './pulsemcp-admin-client/lib/link-secret-to-server.js';
|
|
54
57
|
import { getGoodJobs } from './pulsemcp-admin-client/lib/get-good-jobs.js';
|
|
55
58
|
import { getGoodJob } from './pulsemcp-admin-client/lib/get-good-job.js';
|
|
56
59
|
import { getGoodJobCronSchedules } from './pulsemcp-admin-client/lib/get-good-job-cron-schedules.js';
|
|
@@ -270,6 +273,16 @@ export class PulseMCPAdminClient {
|
|
|
270
273
|
async deleteRedirect(id) {
|
|
271
274
|
return deleteRedirect(this.apiKey, this.baseUrl, id);
|
|
272
275
|
}
|
|
276
|
+
// Secret REST API methods
|
|
277
|
+
async getSecret(idOrSlug) {
|
|
278
|
+
return getSecret(this.apiKey, this.baseUrl, idOrSlug);
|
|
279
|
+
}
|
|
280
|
+
async createSecret(params) {
|
|
281
|
+
return createSecret(this.apiKey, this.baseUrl, params);
|
|
282
|
+
}
|
|
283
|
+
async linkSecretToServer(params) {
|
|
284
|
+
return linkSecretToServer(this.apiKey, this.baseUrl, params);
|
|
285
|
+
}
|
|
273
286
|
// GoodJob REST API methods
|
|
274
287
|
async getGoodJobs(params) {
|
|
275
288
|
return getGoodJobs(this.apiKey, this.baseUrl, params);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import type { ClientFactory } from '../server.js';
|
|
3
|
+
export declare function linkSecretToMcpServer(_server: Server, clientFactory: ClientFactory): {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
inputSchema: {
|
|
7
|
+
type: string;
|
|
8
|
+
properties: {
|
|
9
|
+
slug: {
|
|
10
|
+
type: string;
|
|
11
|
+
description: "Unique slug identifier for the secret (lowercase, hyphenated, e.g. \"linear-api-key\"). Used to look up an existing secret or to create a new one.";
|
|
12
|
+
};
|
|
13
|
+
onepassword_item_id: {
|
|
14
|
+
type: string;
|
|
15
|
+
description: "The 1Password item reference the secret value lives in (e.g. \"op://Vault/Item/credential\"). The raw secret value is NEVER passed through this tool — only this reference. If a secret with this slug already exists, this must match its stored reference.";
|
|
16
|
+
};
|
|
17
|
+
mcp_server_slug: {
|
|
18
|
+
type: string;
|
|
19
|
+
description: "Slug of the MCP server to grant the secret to (e.g. \"linear\"). The link is what Proctor reads to inject the secret value when running that server.";
|
|
20
|
+
};
|
|
21
|
+
title: {
|
|
22
|
+
type: string;
|
|
23
|
+
description: "Optional human-readable title for the secret (only used when creating a new secret).";
|
|
24
|
+
};
|
|
25
|
+
description: {
|
|
26
|
+
type: string;
|
|
27
|
+
description: "Optional description of what the secret is for (only used when creating a new secret).";
|
|
28
|
+
};
|
|
29
|
+
onepassword_tag: {
|
|
30
|
+
type: string;
|
|
31
|
+
description: "Optional tag scoping which field of the 1Password item to inject for this server (e.g. \"production\"). Stored on the server↔secret link, so the same secret can inject different fields per server.";
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
required: string[];
|
|
35
|
+
};
|
|
36
|
+
handler: (args: unknown) => Promise<{
|
|
37
|
+
content: {
|
|
38
|
+
type: string;
|
|
39
|
+
text: string;
|
|
40
|
+
}[];
|
|
41
|
+
isError: boolean;
|
|
42
|
+
} | {
|
|
43
|
+
content: {
|
|
44
|
+
type: string;
|
|
45
|
+
text: string;
|
|
46
|
+
}[];
|
|
47
|
+
isError?: undefined;
|
|
48
|
+
}>;
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=link-secret-to-mcp-server.d.ts.map
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const PARAM_DESCRIPTIONS = {
|
|
3
|
+
slug: 'Unique slug identifier for the secret (lowercase, hyphenated, e.g. "linear-api-key"). Used to look up an existing secret or to create a new one.',
|
|
4
|
+
onepassword_item_id: 'The 1Password item reference the secret value lives in (e.g. "op://Vault/Item/credential"). The raw secret value is NEVER passed through this tool — only this reference. If a secret with this slug already exists, this must match its stored reference.',
|
|
5
|
+
mcp_server_slug: 'Slug of the MCP server to grant the secret to (e.g. "linear"). The link is what Proctor reads to inject the secret value when running that server.',
|
|
6
|
+
title: 'Optional human-readable title for the secret (only used when creating a new secret).',
|
|
7
|
+
description: 'Optional description of what the secret is for (only used when creating a new secret).',
|
|
8
|
+
onepassword_tag: 'Optional tag scoping which field of the 1Password item to inject for this server (e.g. "production"). Stored on the server↔secret link, so the same secret can inject different fields per server.',
|
|
9
|
+
};
|
|
10
|
+
const LinkSecretToMcpServerSchema = z.object({
|
|
11
|
+
slug: z.string().describe(PARAM_DESCRIPTIONS.slug),
|
|
12
|
+
onepassword_item_id: z.string().describe(PARAM_DESCRIPTIONS.onepassword_item_id),
|
|
13
|
+
mcp_server_slug: z.string().describe(PARAM_DESCRIPTIONS.mcp_server_slug),
|
|
14
|
+
title: z.string().optional().describe(PARAM_DESCRIPTIONS.title),
|
|
15
|
+
description: z.string().optional().describe(PARAM_DESCRIPTIONS.description),
|
|
16
|
+
onepassword_tag: z.string().optional().describe(PARAM_DESCRIPTIONS.onepassword_tag),
|
|
17
|
+
});
|
|
18
|
+
export function linkSecretToMcpServer(_server, clientFactory) {
|
|
19
|
+
return {
|
|
20
|
+
name: 'link_secret_to_mcp_server',
|
|
21
|
+
description: `Make an auth secret available to an MCP server. This upserts a Secret (by slug) that references a 1Password item, then writes the server↔secret link that Proctor reads to inject the secret value at runtime.
|
|
22
|
+
|
|
23
|
+
The raw secret value stays in 1Password — this tool only deals in the 1Password item reference ("onepassword_item_id") and slug, never a credential value.
|
|
24
|
+
|
|
25
|
+
Behavior:
|
|
26
|
+
- If no secret with "slug" exists, it is created from "onepassword_item_id" (plus optional title/description).
|
|
27
|
+
- If a secret with "slug" already exists, it is reused. Its stored "onepassword_item_id" must match the one you pass, otherwise the tool errors instead of silently rebinding a shared secret.
|
|
28
|
+
- The link is idempotent: re-running for an already-linked server updates the "onepassword_tag" (when provided) without creating a duplicate.
|
|
29
|
+
|
|
30
|
+
Example request:
|
|
31
|
+
{
|
|
32
|
+
"slug": "linear-api-key",
|
|
33
|
+
"onepassword_item_id": "op://Shared/Linear API Key/credential",
|
|
34
|
+
"mcp_server_slug": "linear",
|
|
35
|
+
"onepassword_tag": "production"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
Use cases:
|
|
39
|
+
- Onboarding an auth-gated MCP server: register its credential reference and scope it to the server so Proctor can run it.
|
|
40
|
+
- Granting an already-registered secret to an additional server.`,
|
|
41
|
+
inputSchema: {
|
|
42
|
+
type: 'object',
|
|
43
|
+
properties: {
|
|
44
|
+
slug: { type: 'string', description: PARAM_DESCRIPTIONS.slug },
|
|
45
|
+
onepassword_item_id: {
|
|
46
|
+
type: 'string',
|
|
47
|
+
description: PARAM_DESCRIPTIONS.onepassword_item_id,
|
|
48
|
+
},
|
|
49
|
+
mcp_server_slug: { type: 'string', description: PARAM_DESCRIPTIONS.mcp_server_slug },
|
|
50
|
+
title: { type: 'string', description: PARAM_DESCRIPTIONS.title },
|
|
51
|
+
description: { type: 'string', description: PARAM_DESCRIPTIONS.description },
|
|
52
|
+
onepassword_tag: { type: 'string', description: PARAM_DESCRIPTIONS.onepassword_tag },
|
|
53
|
+
},
|
|
54
|
+
required: ['slug', 'onepassword_item_id', 'mcp_server_slug'],
|
|
55
|
+
},
|
|
56
|
+
handler: async (args) => {
|
|
57
|
+
const validatedArgs = LinkSecretToMcpServerSchema.parse(args);
|
|
58
|
+
const client = clientFactory();
|
|
59
|
+
try {
|
|
60
|
+
const existing = await client.getSecret(validatedArgs.slug);
|
|
61
|
+
if (existing && existing.onepassword_item_id !== validatedArgs.onepassword_item_id) {
|
|
62
|
+
return {
|
|
63
|
+
content: [
|
|
64
|
+
{
|
|
65
|
+
type: 'text',
|
|
66
|
+
text: `Error: a secret with slug "${validatedArgs.slug}" already exists but references a different 1Password item ` +
|
|
67
|
+
`("${existing.onepassword_item_id}", not "${validatedArgs.onepassword_item_id}"). ` +
|
|
68
|
+
`Refusing to rebind it, since other servers may depend on it. ` +
|
|
69
|
+
`Use a different slug, or update the existing secret explicitly if the reference really changed.`,
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
isError: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
let created = false;
|
|
76
|
+
if (!existing) {
|
|
77
|
+
try {
|
|
78
|
+
await client.createSecret({
|
|
79
|
+
slug: validatedArgs.slug,
|
|
80
|
+
onepassword_item_id: validatedArgs.onepassword_item_id,
|
|
81
|
+
title: validatedArgs.title,
|
|
82
|
+
description: validatedArgs.description,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (createError) {
|
|
86
|
+
// The backend also enforces uniqueness on onepassword_item_id, so the
|
|
87
|
+
// item may already be registered under a *different* slug than the one
|
|
88
|
+
// requested. getSecret(slug) can't surface that (it looks up by slug),
|
|
89
|
+
// so translate the opaque uniqueness 422 into actionable guidance
|
|
90
|
+
// instead of letting the raw validation message reach the agent.
|
|
91
|
+
const message = createError instanceof Error ? createError.message : String(createError);
|
|
92
|
+
if (/onepassword item/i.test(message) && /taken/i.test(message)) {
|
|
93
|
+
return {
|
|
94
|
+
content: [
|
|
95
|
+
{
|
|
96
|
+
type: 'text',
|
|
97
|
+
text: `Error: the 1Password item "${validatedArgs.onepassword_item_id}" is already registered under a different secret slug, ` +
|
|
98
|
+
`so it cannot be created again as "${validatedArgs.slug}". ` +
|
|
99
|
+
`To link it to "${validatedArgs.mcp_server_slug}", re-run this tool with the existing secret's slug ` +
|
|
100
|
+
`(find it via the secrets list), or register a different 1Password item under "${validatedArgs.slug}".`,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
isError: true,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
throw createError;
|
|
107
|
+
}
|
|
108
|
+
created = true;
|
|
109
|
+
}
|
|
110
|
+
const result = await client.linkSecretToServer({
|
|
111
|
+
secret: validatedArgs.slug,
|
|
112
|
+
mcp_server_slug: validatedArgs.mcp_server_slug,
|
|
113
|
+
onepassword_tag: validatedArgs.onepassword_tag,
|
|
114
|
+
});
|
|
115
|
+
let content = `Successfully linked secret "${result.slug}" to MCP server "${result.link.mcp_server_slug}".\n\n`;
|
|
116
|
+
content += `**Secret:** ${result.slug} (id ${result.id}) — ${created ? 'created' : 'reused existing'}\n`;
|
|
117
|
+
content += `**1Password item:** ${result.onepassword_item_id}\n`;
|
|
118
|
+
content += `**Linked server:** ${result.link.mcp_server_slug} (id ${result.link.mcp_server_id})\n`;
|
|
119
|
+
if (result.link.onepassword_tag) {
|
|
120
|
+
content += `**1Password tag:** ${result.link.onepassword_tag}\n`;
|
|
121
|
+
}
|
|
122
|
+
if (result.mcp_server_slugs && result.mcp_server_slugs.length > 0) {
|
|
123
|
+
content += `**All servers using this secret:** ${result.mcp_server_slugs.join(', ')}\n`;
|
|
124
|
+
}
|
|
125
|
+
return { content: [{ type: 'text', text: content }] };
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return {
|
|
129
|
+
content: [
|
|
130
|
+
{
|
|
131
|
+
type: 'text',
|
|
132
|
+
text: `Error linking secret to MCP server: ${error instanceof Error ? error.message : String(error)}`,
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
isError: true,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
package/shared/tools.d.ts
CHANGED
|
@@ -23,13 +23,14 @@ import { ClientFactory } from './server.js';
|
|
|
23
23
|
* - mcp_jsons / mcp_jsons_readonly: MCP JSON configuration tools
|
|
24
24
|
* - mcp_servers / mcp_servers_readonly: Unified MCP server tools (abstracted interface)
|
|
25
25
|
* - redirects / redirects_readonly: URL redirect management tools
|
|
26
|
+
* - secrets: Auth-secret tools (link_secret_to_mcp_server). Upserts a Secret referencing a 1Password item and writes the mcp_servers_secrets join Proctor reads to inject the value. Write-only, so there is no _readonly variant.
|
|
26
27
|
* - good_jobs / good_jobs_readonly: GoodJob background job management tools
|
|
27
28
|
* - proctor / proctor_readonly: Proctor exam execution and result storage tools. The readonly variant includes get_exam_result, list_proctor_runs, and get_proctor_metadata for retrieving stored results and metadata without running exams or saving
|
|
28
29
|
* - discovered_urls / discovered_urls_readonly: Discovered URL management tools for processing URLs into MCP implementations
|
|
29
30
|
* - moz / moz_readonly: MOZ SEO metrics tools (live URL metrics, backlinks, stored historical data)
|
|
30
31
|
* - notifications: Notification email tools (send_impl_posted_notif). Separated from server_directory so notification capability can be granted independently.
|
|
31
32
|
*/
|
|
32
|
-
export type ToolGroup = 'newsletter' | 'newsletter_readonly' | 'server_directory' | 'server_directory_readonly' | 'official_queue' | 'official_queue_readonly' | 'unofficial_mirrors' | 'unofficial_mirrors_readonly' | 'official_mirrors' | 'official_mirrors_readonly' | 'tenants' | 'tenants_readonly' | 'tenants_destructive' | 'mcp_jsons' | 'mcp_jsons_readonly' | 'mcp_servers' | 'mcp_servers_readonly' | 'redirects' | 'redirects_readonly' | 'good_jobs' | 'good_jobs_readonly' | 'proctor' | 'proctor_readonly' | 'discovered_urls' | 'discovered_urls_readonly' | 'moz' | 'moz_readonly' | 'notifications';
|
|
33
|
+
export type ToolGroup = 'newsletter' | 'newsletter_readonly' | 'server_directory' | 'server_directory_readonly' | 'official_queue' | 'official_queue_readonly' | 'unofficial_mirrors' | 'unofficial_mirrors_readonly' | 'official_mirrors' | 'official_mirrors_readonly' | 'tenants' | 'tenants_readonly' | 'tenants_destructive' | 'mcp_jsons' | 'mcp_jsons_readonly' | 'mcp_servers' | 'mcp_servers_readonly' | 'redirects' | 'redirects_readonly' | 'secrets' | 'good_jobs' | 'good_jobs_readonly' | 'proctor' | 'proctor_readonly' | 'discovered_urls' | 'discovered_urls_readonly' | 'moz' | 'moz_readonly' | 'notifications';
|
|
33
34
|
/**
|
|
34
35
|
* Parse enabled tool groups from environment variable or parameter
|
|
35
36
|
* @param enabledGroupsParam - Comma-separated list of tool groups (e.g., "newsletter,server_directory_readonly")
|
package/shared/tools.js
CHANGED
|
@@ -55,6 +55,8 @@ import { getRedirect } from './tools/get-redirect.js';
|
|
|
55
55
|
import { createRedirect } from './tools/create-redirect.js';
|
|
56
56
|
import { updateRedirect } from './tools/update-redirect.js';
|
|
57
57
|
import { deleteRedirect } from './tools/delete-redirect.js';
|
|
58
|
+
// Secret tools
|
|
59
|
+
import { linkSecretToMcpServer } from './tools/link-secret-to-mcp-server.js';
|
|
58
60
|
// GoodJob tools
|
|
59
61
|
import { listGoodJobs } from './tools/list-good-jobs.js';
|
|
60
62
|
import { getGoodJob } from './tools/get-good-job.js';
|
|
@@ -254,6 +256,8 @@ const ALL_TOOLS = [
|
|
|
254
256
|
{ factory: createRedirect, groups: ['redirects'], isWriteOperation: true },
|
|
255
257
|
{ factory: updateRedirect, groups: ['redirects'], isWriteOperation: true },
|
|
256
258
|
{ factory: deleteRedirect, groups: ['redirects'], isWriteOperation: true },
|
|
259
|
+
// Secret tools (write-only: upsert a secret + write the mcp_servers_secrets join Proctor reads)
|
|
260
|
+
{ factory: linkSecretToMcpServer, groups: ['secrets'], isWriteOperation: true },
|
|
257
261
|
// GoodJob tools
|
|
258
262
|
{ factory: listGoodJobs, groups: ['good_jobs'], isWriteOperation: false },
|
|
259
263
|
{ factory: getGoodJob, groups: ['good_jobs'], isWriteOperation: false },
|
|
@@ -328,6 +332,7 @@ const VALID_TOOL_GROUPS = [
|
|
|
328
332
|
'mcp_servers_readonly',
|
|
329
333
|
'redirects',
|
|
330
334
|
'redirects_readonly',
|
|
335
|
+
'secrets',
|
|
331
336
|
'good_jobs',
|
|
332
337
|
'good_jobs_readonly',
|
|
333
338
|
'proctor',
|
|
@@ -351,6 +356,7 @@ const BASE_TOOL_GROUPS = [
|
|
|
351
356
|
'mcp_jsons',
|
|
352
357
|
'mcp_servers',
|
|
353
358
|
'redirects',
|
|
359
|
+
'secrets',
|
|
354
360
|
'good_jobs',
|
|
355
361
|
'proctor',
|
|
356
362
|
'discovered_urls',
|
package/shared/types.d.ts
CHANGED
|
@@ -676,6 +676,37 @@ export interface UpdateRedirectParams {
|
|
|
676
676
|
to?: string;
|
|
677
677
|
status?: RedirectStatus;
|
|
678
678
|
}
|
|
679
|
+
export interface Secret {
|
|
680
|
+
id: number;
|
|
681
|
+
slug: string;
|
|
682
|
+
onepassword_item_id: string;
|
|
683
|
+
title?: string | null;
|
|
684
|
+
description?: string | null;
|
|
685
|
+
mcp_servers_count?: number;
|
|
686
|
+
mcp_server_slugs?: string[];
|
|
687
|
+
created_at?: string;
|
|
688
|
+
updated_at?: string;
|
|
689
|
+
}
|
|
690
|
+
export interface CreateSecretParams {
|
|
691
|
+
slug: string;
|
|
692
|
+
onepassword_item_id: string;
|
|
693
|
+
title?: string;
|
|
694
|
+
description?: string;
|
|
695
|
+
}
|
|
696
|
+
export interface SecretServerLink {
|
|
697
|
+
mcp_server_id: number;
|
|
698
|
+
mcp_server_slug: string;
|
|
699
|
+
onepassword_tag?: string | null;
|
|
700
|
+
}
|
|
701
|
+
export interface LinkSecretToServerParams {
|
|
702
|
+
secret: string | number;
|
|
703
|
+
mcp_server_id?: number;
|
|
704
|
+
mcp_server_slug?: string;
|
|
705
|
+
onepassword_tag?: string;
|
|
706
|
+
}
|
|
707
|
+
export interface SecretWithLink extends Secret {
|
|
708
|
+
link: SecretServerLink;
|
|
709
|
+
}
|
|
679
710
|
export type GoodJobStatus = 'scheduled' | 'queued' | 'running' | 'succeeded' | 'failed' | 'discarded';
|
|
680
711
|
export interface GoodJob {
|
|
681
712
|
id: string;
|