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
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { adminFetch } from './admin-fetch.js';
|
|
2
|
+
export async function setKnownMissingInitToolsList(apiKey, baseUrl, id, knownMissingInitToolsList, knownMissingInitToolsListFilterTo) {
|
|
3
|
+
const url = new URL(`/api/mcp_servers/${id}/known_missing_init_tools_list`, baseUrl);
|
|
4
|
+
const body = {
|
|
5
|
+
known_missing_init_tools_list: knownMissingInitToolsList,
|
|
6
|
+
};
|
|
7
|
+
if (knownMissingInitToolsListFilterTo !== undefined) {
|
|
8
|
+
// null is sent as JSON null; the Rails controller treats nil/blank as "clear".
|
|
9
|
+
body.known_missing_init_tools_list_filter_to = knownMissingInitToolsListFilterTo;
|
|
10
|
+
}
|
|
11
|
+
const response = await adminFetch(url.toString(), {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: {
|
|
14
|
+
'X-API-Key': apiKey,
|
|
15
|
+
Accept: 'application/json',
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
},
|
|
18
|
+
body: JSON.stringify(body),
|
|
19
|
+
});
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
if (response.status === 401) {
|
|
22
|
+
throw new Error('Invalid API key');
|
|
23
|
+
}
|
|
24
|
+
if (response.status === 403) {
|
|
25
|
+
throw new Error('User lacks write privileges');
|
|
26
|
+
}
|
|
27
|
+
if (response.status === 404) {
|
|
28
|
+
throw new Error(`MCP server not found: ${id}`);
|
|
29
|
+
}
|
|
30
|
+
if (response.status === 400) {
|
|
31
|
+
const errBody = (await response.json().catch(() => ({})));
|
|
32
|
+
throw new Error(errBody.error ?? 'Bad request');
|
|
33
|
+
}
|
|
34
|
+
if (response.status === 422) {
|
|
35
|
+
const errBody = (await response.json().catch(() => ({})));
|
|
36
|
+
const detailStr = errBody.details?.length ? ` (${errBody.details.join(', ')})` : '';
|
|
37
|
+
throw new Error(`${errBody.error ?? 'Validation failed'}${detailStr}`);
|
|
38
|
+
}
|
|
39
|
+
throw new Error(`Failed to set known_missing_init_tools_list: ${response.status} ${response.statusText}`);
|
|
40
|
+
}
|
|
41
|
+
return (await response.json());
|
|
42
|
+
}
|
|
@@ -74,6 +74,9 @@ export function mapToUnifiedServer(impl) {
|
|
|
74
74
|
downloads_estimate_total: mcpServer.downloads_estimate_total,
|
|
75
75
|
// Internal notes
|
|
76
76
|
internal_notes: impl.internal_notes,
|
|
77
|
+
// Owner tenant
|
|
78
|
+
owner_tenant_id: mcpServer.owner_tenant_id,
|
|
79
|
+
owner_tenant_slug: mcpServer.owner_tenant_slug,
|
|
77
80
|
// Timestamps (use implementation timestamps as they're more relevant)
|
|
78
81
|
created_at: impl.created_at,
|
|
79
82
|
updated_at: impl.updated_at,
|
|
@@ -54,6 +54,20 @@ export async function updateUnifiedMCPServer(apiKey, baseUrl, implementationId,
|
|
|
54
54
|
// Date overrides
|
|
55
55
|
if (params.created_on_override !== undefined)
|
|
56
56
|
implParams.created_on_override = params.created_on_override;
|
|
57
|
+
// Owner tenant: route to slug or id depending on the value type.
|
|
58
|
+
// String → slug, number → id, null → clear (sent as empty owner_tenant_id).
|
|
59
|
+
if (params.owner_tenant !== undefined) {
|
|
60
|
+
if (typeof params.owner_tenant === 'string') {
|
|
61
|
+
implParams.owner_tenant_slug = params.owner_tenant;
|
|
62
|
+
}
|
|
63
|
+
else if (typeof params.owner_tenant === 'number') {
|
|
64
|
+
implParams.owner_tenant_id = params.owner_tenant;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// null — clear the link
|
|
68
|
+
implParams.owner_tenant_id = null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
57
71
|
// Tags
|
|
58
72
|
if (params.tags !== undefined)
|
|
59
73
|
implParams.tags = params.tags;
|
|
@@ -1045,10 +1045,49 @@ export function createMockPulseMCPAdminClient(mockData) {
|
|
|
1045
1045
|
created_at: '2024-01-01T00:00:00Z',
|
|
1046
1046
|
};
|
|
1047
1047
|
},
|
|
1048
|
+
async deleteTenant() {
|
|
1049
|
+
return { success: true, message: 'Tenant deleted' };
|
|
1050
|
+
},
|
|
1051
|
+
async deleteApiKey() {
|
|
1052
|
+
return { success: true, message: 'API key revoked' };
|
|
1053
|
+
},
|
|
1054
|
+
async listTenantServers() {
|
|
1055
|
+
return {
|
|
1056
|
+
data: [],
|
|
1057
|
+
pagination: {
|
|
1058
|
+
current_page: 1,
|
|
1059
|
+
total_pages: 0,
|
|
1060
|
+
total_count: 0,
|
|
1061
|
+
has_next: false,
|
|
1062
|
+
limit: 30,
|
|
1063
|
+
},
|
|
1064
|
+
};
|
|
1065
|
+
},
|
|
1066
|
+
async bulkUpdateTenantServers(idOrSlug) {
|
|
1067
|
+
return {
|
|
1068
|
+
status: 'success',
|
|
1069
|
+
tenant: { id: typeof idOrSlug === 'number' ? idOrSlug : 1, slug: String(idOrSlug) },
|
|
1070
|
+
added: [],
|
|
1071
|
+
removed: [],
|
|
1072
|
+
restored: [],
|
|
1073
|
+
skipped: [],
|
|
1074
|
+
unresolved_identifiers: [],
|
|
1075
|
+
};
|
|
1076
|
+
},
|
|
1048
1077
|
async recacheMCPServer(slug) {
|
|
1049
1078
|
return {
|
|
1050
1079
|
message: `Cache successfully refreshed for ${slug}.`,
|
|
1051
1080
|
};
|
|
1052
1081
|
},
|
|
1082
|
+
async setKnownMissingInitToolsList(id, knownMissingInitToolsList, knownMissingInitToolsListFilterTo) {
|
|
1083
|
+
return {
|
|
1084
|
+
id,
|
|
1085
|
+
slug: `mock-server-${id}`,
|
|
1086
|
+
known_missing_init_tools_list: knownMissingInitToolsList,
|
|
1087
|
+
known_missing_init_tools_list_filter_to: knownMissingInitToolsListFilterTo === undefined
|
|
1088
|
+
? null
|
|
1089
|
+
: (knownMissingInitToolsListFilterTo ?? null),
|
|
1090
|
+
};
|
|
1091
|
+
},
|
|
1053
1092
|
};
|
|
1054
1093
|
}
|
|
@@ -72,8 +72,13 @@ import { getMozMetrics } from './pulsemcp-admin-client/lib/get-moz-metrics.js';
|
|
|
72
72
|
import { getMozBacklinks } from './pulsemcp-admin-client/lib/get-moz-backlinks.js';
|
|
73
73
|
import { getMozStoredMetrics } from './pulsemcp-admin-client/lib/get-moz-stored-metrics.js';
|
|
74
74
|
import { recacheMCPServer } from './pulsemcp-admin-client/lib/recache-mcp-server.js';
|
|
75
|
+
import { setKnownMissingInitToolsList } from './pulsemcp-admin-client/lib/set-known-missing-init-tools-list.js';
|
|
75
76
|
import { createTenant } from './pulsemcp-admin-client/lib/create-tenant.js';
|
|
76
77
|
import { createApiKey } from './pulsemcp-admin-client/lib/create-api-key.js';
|
|
78
|
+
import { deleteTenant } from './pulsemcp-admin-client/lib/delete-tenant.js';
|
|
79
|
+
import { deleteApiKey } from './pulsemcp-admin-client/lib/delete-api-key.js';
|
|
80
|
+
import { listTenantServers } from './pulsemcp-admin-client/lib/list-tenant-servers.js';
|
|
81
|
+
import { bulkUpdateTenantServers } from './pulsemcp-admin-client/lib/bulk-update-tenant-servers.js';
|
|
77
82
|
// PulseMCP Admin API client implementation
|
|
78
83
|
export class PulseMCPAdminClient {
|
|
79
84
|
apiKey;
|
|
@@ -200,6 +205,18 @@ export class PulseMCPAdminClient {
|
|
|
200
205
|
async createApiKey(params) {
|
|
201
206
|
return createApiKey(this.apiKey, this.baseUrl, params);
|
|
202
207
|
}
|
|
208
|
+
async deleteTenant(params) {
|
|
209
|
+
return deleteTenant(this.apiKey, this.baseUrl, params);
|
|
210
|
+
}
|
|
211
|
+
async deleteApiKey(id) {
|
|
212
|
+
return deleteApiKey(this.apiKey, this.baseUrl, id);
|
|
213
|
+
}
|
|
214
|
+
async listTenantServers(idOrSlug, params) {
|
|
215
|
+
return listTenantServers(this.apiKey, this.baseUrl, idOrSlug, params);
|
|
216
|
+
}
|
|
217
|
+
async bulkUpdateTenantServers(idOrSlug, params) {
|
|
218
|
+
return bulkUpdateTenantServers(this.apiKey, this.baseUrl, idOrSlug, params);
|
|
219
|
+
}
|
|
203
220
|
// MCP JSON REST API methods
|
|
204
221
|
async getMcpJsons(params) {
|
|
205
222
|
return getMcpJsons(this.apiKey, this.baseUrl, params);
|
|
@@ -229,6 +246,9 @@ export class PulseMCPAdminClient {
|
|
|
229
246
|
async recacheMCPServer(slug) {
|
|
230
247
|
return recacheMCPServer(this.apiKey, this.baseUrl, slug);
|
|
231
248
|
}
|
|
249
|
+
async setKnownMissingInitToolsList(id, knownMissingInitToolsList, knownMissingInitToolsListFilterTo) {
|
|
250
|
+
return setKnownMissingInitToolsList(this.apiKey, this.baseUrl, id, knownMissingInitToolsList, knownMissingInitToolsListFilterTo);
|
|
251
|
+
}
|
|
232
252
|
// Redirect REST API methods
|
|
233
253
|
async getRedirects(params) {
|
|
234
254
|
return getRedirects(this.apiKey, this.baseUrl, params);
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
const PARAM_DESCRIPTIONS = {
|
|
3
|
+
tenant: 'Tenant ID (number) or slug (string) to add servers to.',
|
|
4
|
+
add_server_identifiers: 'Array of MCP server IDs (numbers) or slugs (strings) to add to the tenant\'s recommendation list. Servers without an unofficial mirror are silently skipped (returned in `skipped` with reason "no_unofficial_mirror"). Already-active associations are skipped (reason "already_active").',
|
|
5
|
+
remove_server_identifiers: 'Optional array of MCP server IDs or slugs to remove from the tenant in the same call. Touched associations are soft-deleted; untouched associations are hard-deleted.',
|
|
6
|
+
restore_association_ids: 'Optional array of TenantsMcpServer association IDs to restore from a soft-deleted state. Use list_tenant_servers with status="deleted" to find these IDs.',
|
|
7
|
+
};
|
|
8
|
+
const AddServersToTenantSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
tenant: z.union([z.number(), z.string()]).describe(PARAM_DESCRIPTIONS.tenant),
|
|
11
|
+
add_server_identifiers: z
|
|
12
|
+
.array(z.union([z.number(), z.string()]))
|
|
13
|
+
.optional()
|
|
14
|
+
.describe(PARAM_DESCRIPTIONS.add_server_identifiers),
|
|
15
|
+
remove_server_identifiers: z
|
|
16
|
+
.array(z.union([z.number(), z.string()]))
|
|
17
|
+
.optional()
|
|
18
|
+
.describe(PARAM_DESCRIPTIONS.remove_server_identifiers),
|
|
19
|
+
restore_association_ids: z
|
|
20
|
+
.array(z.number())
|
|
21
|
+
.optional()
|
|
22
|
+
.describe(PARAM_DESCRIPTIONS.restore_association_ids),
|
|
23
|
+
})
|
|
24
|
+
.refine((val) => (val.add_server_identifiers && val.add_server_identifiers.length > 0) ||
|
|
25
|
+
(val.remove_server_identifiers && val.remove_server_identifiers.length > 0) ||
|
|
26
|
+
(val.restore_association_ids && val.restore_association_ids.length > 0), {
|
|
27
|
+
message: 'Must provide at least one of add_server_identifiers, remove_server_identifiers, or restore_association_ids',
|
|
28
|
+
});
|
|
29
|
+
export function addServersToTenant(_server, clientFactory) {
|
|
30
|
+
return {
|
|
31
|
+
name: 'add_servers_to_tenant',
|
|
32
|
+
description: `Add MCP servers to a tenant's recommendation list. Optionally remove and restore associations in the same transactional call.
|
|
33
|
+
|
|
34
|
+
Servers are added with server_json_selection: "unofficial" — they always pin to the unofficial mirror's server.json. A server without an unofficial mirror cannot be added and will be reported in "skipped" with reason "no_unofficial_mirror".
|
|
35
|
+
|
|
36
|
+
This tool only changes the tenant's recommendation list (TenantsMcpServer rows). The underlying MCP server's public listing is NOT affected — public servers stay public and continue to appear in the public directory.
|
|
37
|
+
|
|
38
|
+
The response includes:
|
|
39
|
+
- added: servers newly attached to the tenant (outcome "created" or "restored_from_deleted")
|
|
40
|
+
- removed: servers detached (outcome "hard_deleted" if untouched, "soft_deleted" if the row had been customized)
|
|
41
|
+
- restored: associations restored from soft-deleted state via restore_association_ids
|
|
42
|
+
- skipped: identifiers that resolved to a server but were skipped with a reason
|
|
43
|
+
- unresolved_identifiers: identifiers that didn't match any existing MCP server
|
|
44
|
+
|
|
45
|
+
Use cases:
|
|
46
|
+
- Curate a tenant's recommended-servers list
|
|
47
|
+
- Bulk-add servers by slug or ID in one call
|
|
48
|
+
- Combine adds and removes atomically (e.g., swap one server for another)`,
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
tenant: {
|
|
53
|
+
oneOf: [{ type: 'number' }, { type: 'string' }],
|
|
54
|
+
description: PARAM_DESCRIPTIONS.tenant,
|
|
55
|
+
},
|
|
56
|
+
add_server_identifiers: {
|
|
57
|
+
type: 'array',
|
|
58
|
+
items: { oneOf: [{ type: 'number' }, { type: 'string' }] },
|
|
59
|
+
description: PARAM_DESCRIPTIONS.add_server_identifiers,
|
|
60
|
+
},
|
|
61
|
+
remove_server_identifiers: {
|
|
62
|
+
type: 'array',
|
|
63
|
+
items: { oneOf: [{ type: 'number' }, { type: 'string' }] },
|
|
64
|
+
description: PARAM_DESCRIPTIONS.remove_server_identifiers,
|
|
65
|
+
},
|
|
66
|
+
restore_association_ids: {
|
|
67
|
+
type: 'array',
|
|
68
|
+
items: { type: 'number' },
|
|
69
|
+
description: PARAM_DESCRIPTIONS.restore_association_ids,
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
required: ['tenant'],
|
|
73
|
+
},
|
|
74
|
+
handler: async (args) => {
|
|
75
|
+
try {
|
|
76
|
+
const validatedArgs = AddServersToTenantSchema.parse(args);
|
|
77
|
+
const client = clientFactory();
|
|
78
|
+
const result = await client.bulkUpdateTenantServers(validatedArgs.tenant, {
|
|
79
|
+
add_server_identifiers: validatedArgs.add_server_identifiers,
|
|
80
|
+
remove_server_identifiers: validatedArgs.remove_server_identifiers,
|
|
81
|
+
restore_association_ids: validatedArgs.restore_association_ids,
|
|
82
|
+
});
|
|
83
|
+
let content = `**Tenant servers updated** (tenant=${result.tenant.slug}, id=${result.tenant.id})\n\n`;
|
|
84
|
+
content += `- Added: ${result.added.length}\n`;
|
|
85
|
+
content += `- Removed: ${result.removed.length}\n`;
|
|
86
|
+
content += `- Restored: ${result.restored.length}\n`;
|
|
87
|
+
content += `- Skipped: ${result.skipped.length}\n`;
|
|
88
|
+
content += `- Unresolved identifiers: ${result.unresolved_identifiers.length}\n`;
|
|
89
|
+
if (result.added.length > 0) {
|
|
90
|
+
content += `\n**Added:**\n`;
|
|
91
|
+
for (const item of result.added) {
|
|
92
|
+
content += `- ${item.mcp_server_slug} (server_id=${item.mcp_server_id}, association_id=${item.association_id}, outcome=${item.outcome})\n`;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (result.removed.length > 0) {
|
|
96
|
+
content += `\n**Removed:**\n`;
|
|
97
|
+
for (const item of result.removed) {
|
|
98
|
+
const assocPart = item.association_id != null ? `, association_id=${item.association_id}` : '';
|
|
99
|
+
content += `- ${item.mcp_server_slug} (server_id=${item.mcp_server_id}${assocPart}, outcome=${item.outcome})\n`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (result.restored.length > 0) {
|
|
103
|
+
content += `\n**Restored:**\n`;
|
|
104
|
+
for (const item of result.restored) {
|
|
105
|
+
content += `- ${item.mcp_server_slug} (server_id=${item.mcp_server_id}, association_id=${item.association_id})\n`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (result.skipped.length > 0) {
|
|
109
|
+
content += `\n**Skipped:**\n`;
|
|
110
|
+
for (const item of result.skipped) {
|
|
111
|
+
const label = item.mcp_server_slug || item.mcp_server_id || item.association_id;
|
|
112
|
+
content += `- ${label} — reason: ${item.reason}\n`;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (result.unresolved_identifiers.length > 0) {
|
|
116
|
+
content += `\n**Unresolved identifiers:** ${result.unresolved_identifiers.join(', ')}\n`;
|
|
117
|
+
}
|
|
118
|
+
return { content: [{ type: 'text', text: content }] };
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
return {
|
|
122
|
+
content: [
|
|
123
|
+
{
|
|
124
|
+
type: 'text',
|
|
125
|
+
text: `Error updating tenant servers: ${error instanceof Error ? error.message : String(error)}`,
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
isError: true,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { requestConfirmation, createConfirmationSchema } from '@pulsemcp/mcp-elicitation';
|
|
3
|
+
import { readCmsAdminElicitationConfig } from '../elicitation-config.js';
|
|
4
|
+
const PARAM_DESCRIPTIONS = {
|
|
5
|
+
id: 'The numeric ID of the API key to revoke.',
|
|
6
|
+
};
|
|
7
|
+
const DeleteApiKeySchema = z.object({
|
|
8
|
+
id: z.number().int().positive().describe(PARAM_DESCRIPTIONS.id),
|
|
9
|
+
});
|
|
10
|
+
const TOOL_DESCRIPTION = `Permanently revoke (delete) an API key.
|
|
11
|
+
|
|
12
|
+
**This is a destructive operation.** Revoking an API key immediately invalidates it; any clients still using the key will start receiving 401 Unauthorized. The deletion is idempotent — calling delete on a non-existent or already-deleted key returns success.
|
|
13
|
+
|
|
14
|
+
By default, the request requires explicit user approval via MCP elicitation before being sent to the admin API.
|
|
15
|
+
|
|
16
|
+
**Parameters:**
|
|
17
|
+
- \`id\` (required): Numeric ID of the API key.
|
|
18
|
+
|
|
19
|
+
**Returns:**
|
|
20
|
+
- \`success\`: Boolean indicating whether the request succeeded.
|
|
21
|
+
- \`message\`: Human-readable confirmation message.
|
|
22
|
+
|
|
23
|
+
**Use cases:**
|
|
24
|
+
- Revoke a leaked or compromised API key.
|
|
25
|
+
- Roll a tenant's keys after a permission change.
|
|
26
|
+
- Remove a deprecated key during cleanup.`;
|
|
27
|
+
export function deleteApiKey(server, clientFactory) {
|
|
28
|
+
return {
|
|
29
|
+
name: 'delete_api_key',
|
|
30
|
+
description: TOOL_DESCRIPTION,
|
|
31
|
+
inputSchema: {
|
|
32
|
+
type: 'object',
|
|
33
|
+
properties: {
|
|
34
|
+
id: {
|
|
35
|
+
type: 'number',
|
|
36
|
+
description: PARAM_DESCRIPTIONS.id,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
required: ['id'],
|
|
40
|
+
},
|
|
41
|
+
handler: async (args) => {
|
|
42
|
+
try {
|
|
43
|
+
const validatedArgs = DeleteApiKeySchema.parse(args);
|
|
44
|
+
const elicitConfig = readCmsAdminElicitationConfig();
|
|
45
|
+
if (elicitConfig.destructiveElicitationEnabled) {
|
|
46
|
+
const lines = [
|
|
47
|
+
'About to PERMANENTLY REVOKE an API key via the PulseMCP admin API:',
|
|
48
|
+
` API key ID: ${validatedArgs.id}`,
|
|
49
|
+
'',
|
|
50
|
+
'Any clients still using this key will start receiving 401 Unauthorized.',
|
|
51
|
+
'This action cannot be undone.',
|
|
52
|
+
];
|
|
53
|
+
const confirmation = await requestConfirmation({
|
|
54
|
+
server,
|
|
55
|
+
message: lines.join('\n') + '\n',
|
|
56
|
+
requestedSchema: createConfirmationSchema('Revoke this API key?', 'Confirm that you want to permanently revoke this API key.'),
|
|
57
|
+
meta: {
|
|
58
|
+
'com.pulsemcp/tool-name': 'delete_api_key',
|
|
59
|
+
},
|
|
60
|
+
}, elicitConfig.base);
|
|
61
|
+
if (confirmation.action !== 'accept') {
|
|
62
|
+
if (confirmation.action === 'expired') {
|
|
63
|
+
return {
|
|
64
|
+
content: [
|
|
65
|
+
{
|
|
66
|
+
type: 'text',
|
|
67
|
+
text: 'API key revocation confirmation expired. Please try again.',
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
isError: true,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
content: [
|
|
75
|
+
{
|
|
76
|
+
type: 'text',
|
|
77
|
+
text: 'API key revocation was cancelled by the user.',
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (confirmation.content &&
|
|
83
|
+
'confirm' in confirmation.content &&
|
|
84
|
+
confirmation.content.confirm === false) {
|
|
85
|
+
return {
|
|
86
|
+
content: [
|
|
87
|
+
{
|
|
88
|
+
type: 'text',
|
|
89
|
+
text: 'API key revocation was not confirmed.',
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const client = clientFactory();
|
|
96
|
+
const result = await client.deleteApiKey(validatedArgs.id);
|
|
97
|
+
return {
|
|
98
|
+
content: [
|
|
99
|
+
{
|
|
100
|
+
type: 'text',
|
|
101
|
+
text: `${result.success ? 'API key revoked.' : 'API key revocation failed.'} ${result.message}`,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: 'text',
|
|
111
|
+
text: `Error revoking API key: ${error instanceof Error ? error.message : String(error)}`,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
isError: true,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { requestConfirmation, createConfirmationSchema } from '@pulsemcp/mcp-elicitation';
|
|
3
|
+
import { readCmsAdminElicitationConfig } from '../elicitation-config.js';
|
|
4
|
+
const PARAM_DESCRIPTIONS = {
|
|
5
|
+
id_or_slug: 'The ID (number) or slug (string) of the tenant to delete.',
|
|
6
|
+
force: 'When true, deletes the tenant and cascades to dependent records (API keys, enrichments, etc.). When false (default), the deletion will fail with a 422 if dependents exist.',
|
|
7
|
+
};
|
|
8
|
+
const DeleteTenantSchema = z.object({
|
|
9
|
+
id_or_slug: z.union([z.number(), z.string()]).describe(PARAM_DESCRIPTIONS.id_or_slug),
|
|
10
|
+
force: z.boolean().optional().describe(PARAM_DESCRIPTIONS.force),
|
|
11
|
+
});
|
|
12
|
+
const TOOL_DESCRIPTION = `Permanently delete a tenant from the PulseMCP admin API.
|
|
13
|
+
|
|
14
|
+
**This is a destructive operation.** Deleting a tenant removes the tenant record and (with \`force: true\`) cascades to dependent records such as API keys and enrichments. By default, the request requires explicit user approval via MCP elicitation before being sent to the admin API.
|
|
15
|
+
|
|
16
|
+
**Parameters:**
|
|
17
|
+
- \`id_or_slug\` (required): Numeric ID or slug of the tenant.
|
|
18
|
+
- \`force\` (optional, default false): Cascade-delete dependents. Without this, deleting a tenant that owns API keys or other resources returns 422.
|
|
19
|
+
|
|
20
|
+
**Returns:**
|
|
21
|
+
- \`success\`: Boolean indicating whether the tenant was deleted.
|
|
22
|
+
- \`message\`: Human-readable confirmation message.
|
|
23
|
+
|
|
24
|
+
**Use cases:**
|
|
25
|
+
- Permanently remove a deprovisioned tenant.
|
|
26
|
+
- Delete a test tenant created during onboarding/QA.
|
|
27
|
+
- Cascading cleanup of a tenant and all of its API keys (\`force: true\`).`;
|
|
28
|
+
export function deleteTenant(server, clientFactory) {
|
|
29
|
+
return {
|
|
30
|
+
name: 'delete_tenant',
|
|
31
|
+
description: TOOL_DESCRIPTION,
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
id_or_slug: {
|
|
36
|
+
oneOf: [{ type: 'number' }, { type: 'string' }],
|
|
37
|
+
description: PARAM_DESCRIPTIONS.id_or_slug,
|
|
38
|
+
},
|
|
39
|
+
force: {
|
|
40
|
+
type: 'boolean',
|
|
41
|
+
description: PARAM_DESCRIPTIONS.force,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
required: ['id_or_slug'],
|
|
45
|
+
},
|
|
46
|
+
handler: async (args) => {
|
|
47
|
+
try {
|
|
48
|
+
const validatedArgs = DeleteTenantSchema.parse(args);
|
|
49
|
+
const elicitConfig = readCmsAdminElicitationConfig();
|
|
50
|
+
if (elicitConfig.destructiveElicitationEnabled) {
|
|
51
|
+
const lines = [
|
|
52
|
+
'About to PERMANENTLY DELETE a tenant via the PulseMCP admin API:',
|
|
53
|
+
` Tenant: ${validatedArgs.id_or_slug}`,
|
|
54
|
+
` Cascade dependents (force): ${validatedArgs.force ? 'yes' : 'no'}`,
|
|
55
|
+
'',
|
|
56
|
+
'This action cannot be undone.',
|
|
57
|
+
];
|
|
58
|
+
const confirmation = await requestConfirmation({
|
|
59
|
+
server,
|
|
60
|
+
message: lines.join('\n') + '\n',
|
|
61
|
+
requestedSchema: createConfirmationSchema('Delete this tenant?', 'Confirm that you want to permanently delete this tenant.'),
|
|
62
|
+
meta: {
|
|
63
|
+
'com.pulsemcp/tool-name': 'delete_tenant',
|
|
64
|
+
},
|
|
65
|
+
}, elicitConfig.base);
|
|
66
|
+
if (confirmation.action !== 'accept') {
|
|
67
|
+
if (confirmation.action === 'expired') {
|
|
68
|
+
return {
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: 'text',
|
|
72
|
+
text: 'Tenant deletion confirmation expired. Please try again.',
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
isError: true,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
return {
|
|
79
|
+
content: [
|
|
80
|
+
{
|
|
81
|
+
type: 'text',
|
|
82
|
+
text: 'Tenant deletion was cancelled by the user.',
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
// Defense-in-depth: some MCP clients may return action='accept' without the
|
|
88
|
+
// user actually checking the confirmation checkbox.
|
|
89
|
+
if (confirmation.content &&
|
|
90
|
+
'confirm' in confirmation.content &&
|
|
91
|
+
confirmation.content.confirm === false) {
|
|
92
|
+
return {
|
|
93
|
+
content: [
|
|
94
|
+
{
|
|
95
|
+
type: 'text',
|
|
96
|
+
text: 'Tenant deletion was not confirmed.',
|
|
97
|
+
},
|
|
98
|
+
],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const client = clientFactory();
|
|
103
|
+
const result = await client.deleteTenant({
|
|
104
|
+
id_or_slug: validatedArgs.id_or_slug,
|
|
105
|
+
force: validatedArgs.force,
|
|
106
|
+
});
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{
|
|
110
|
+
type: 'text',
|
|
111
|
+
text: `${result.success ? 'Tenant deleted.' : 'Tenant deletion failed.'} ${result.message}`,
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
return {
|
|
118
|
+
content: [
|
|
119
|
+
{
|
|
120
|
+
type: 'text',
|
|
121
|
+
text: `Error deleting tenant: ${error instanceof Error ? error.message : String(error)}`,
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
isError: true,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
@@ -105,6 +105,16 @@ Example response:
|
|
|
105
105
|
if (server.verified_no_remote_canonicals !== undefined) {
|
|
106
106
|
content += `**Verified No Remote Canonicals:** ${server.verified_no_remote_canonicals ? 'Yes' : 'No'}\n`;
|
|
107
107
|
}
|
|
108
|
+
if (server.owner_tenant_slug || server.owner_tenant_id) {
|
|
109
|
+
content += `**Owner Tenant:** ${server.owner_tenant_slug ?? '(no slug)'}`;
|
|
110
|
+
if (server.owner_tenant_id) {
|
|
111
|
+
content += ` (id: ${server.owner_tenant_id})`;
|
|
112
|
+
}
|
|
113
|
+
content += '\n';
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
content += `**Owner Tenant:** (none)\n`;
|
|
117
|
+
}
|
|
108
118
|
if (server.short_description) {
|
|
109
119
|
content += `\n**Short Description:**\n${server.short_description}\n`;
|
|
110
120
|
}
|
|
@@ -152,10 +162,11 @@ Example response:
|
|
|
152
162
|
content += `- **Status:** ${server.source_code.github_status}\n`;
|
|
153
163
|
}
|
|
154
164
|
}
|
|
155
|
-
// Canonical URLs
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
165
|
+
// Canonical URLs — always rendered so callers can see the field is empty vs. missing
|
|
166
|
+
const canonicalUrls = server.canonical_urls ?? [];
|
|
167
|
+
if (canonicalUrls.length > 0) {
|
|
168
|
+
content += `\n## Canonical URLs (${canonicalUrls.length})\n`;
|
|
169
|
+
for (const canonical of canonicalUrls) {
|
|
159
170
|
content += `- **${canonical.scope}:** ${canonical.url}`;
|
|
160
171
|
if (canonical.note) {
|
|
161
172
|
content += ` (${canonical.note})`;
|
|
@@ -163,10 +174,14 @@ Example response:
|
|
|
163
174
|
content += '\n';
|
|
164
175
|
}
|
|
165
176
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
177
|
+
else {
|
|
178
|
+
content += `\n## Canonical URLs\n(none)\n`;
|
|
179
|
+
}
|
|
180
|
+
// Remote endpoints — always rendered so callers can see the field is empty vs. missing
|
|
181
|
+
const remotes = server.remotes ?? [];
|
|
182
|
+
if (remotes.length > 0) {
|
|
183
|
+
content += `\n## Remote Endpoints (${remotes.length})\n`;
|
|
184
|
+
for (const [idx, remote] of remotes.entries()) {
|
|
170
185
|
content += `\n### ${idx + 1}. ${remote.display_name || 'Endpoint'}`;
|
|
171
186
|
if (remote.id) {
|
|
172
187
|
content += ` (ID: ${remote.id})`;
|
|
@@ -198,6 +213,9 @@ Example response:
|
|
|
198
213
|
}
|
|
199
214
|
}
|
|
200
215
|
}
|
|
216
|
+
else {
|
|
217
|
+
content += `\n## Remote Endpoints\n(none)\n`;
|
|
218
|
+
}
|
|
201
219
|
// Tags
|
|
202
220
|
if (server.tags && server.tags.length > 0) {
|
|
203
221
|
content += `\n## Tags\n`;
|
|
@@ -123,6 +123,9 @@ Use cases:
|
|
|
123
123
|
if (server.remotes && server.remotes.length > 0) {
|
|
124
124
|
content += ` Remote endpoints: ${server.remotes.length}\n`;
|
|
125
125
|
}
|
|
126
|
+
if (server.owner_tenant_slug || server.owner_tenant_id) {
|
|
127
|
+
content += ` Owner Tenant: ${server.owner_tenant_slug ?? `id ${server.owner_tenant_id}`}\n`;
|
|
128
|
+
}
|
|
126
129
|
if (server.downloads_estimate_total) {
|
|
127
130
|
content += ` Downloads (total): ${server.downloads_estimate_total.toLocaleString()}\n`;
|
|
128
131
|
}
|