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.
Files changed (81) hide show
  1. package/README.md +93 -74
  2. package/build/index.js +53 -1
  3. package/build/shared/src/elicitation-config.js +69 -0
  4. package/build/shared/src/pulsemcp-admin-client/lib/bulk-update-tenant-servers.js +41 -0
  5. package/build/shared/src/pulsemcp-admin-client/lib/create-mcp-implementation.js +7 -0
  6. package/build/shared/src/pulsemcp-admin-client/lib/delete-api-key.js +25 -0
  7. package/build/shared/src/pulsemcp-admin-client/lib/delete-tenant.js +31 -0
  8. package/build/shared/src/pulsemcp-admin-client/lib/get-unified-mcp-server.js +5 -1
  9. package/build/shared/src/pulsemcp-admin-client/lib/list-tenant-servers.js +57 -0
  10. package/build/shared/src/pulsemcp-admin-client/lib/save-mcp-implementation.js +8 -0
  11. package/build/shared/src/pulsemcp-admin-client/lib/set-known-missing-init-tools-list.js +42 -0
  12. package/build/shared/src/pulsemcp-admin-client/lib/unified-mcp-server-mapper.js +3 -0
  13. package/build/shared/src/pulsemcp-admin-client/lib/update-unified-mcp-server.js +14 -0
  14. package/build/shared/src/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +39 -0
  15. package/build/shared/src/server.js +20 -0
  16. package/build/shared/src/tools/add-servers-to-tenant.js +133 -0
  17. package/build/shared/src/tools/delete-api-key.js +119 -0
  18. package/build/shared/src/tools/delete-tenant.js +129 -0
  19. package/build/shared/src/tools/get-mcp-server.js +26 -8
  20. package/build/shared/src/tools/list-mcp-servers.js +3 -0
  21. package/build/shared/src/tools/list-tenant-servers.js +89 -0
  22. package/build/shared/src/tools/remove-servers-from-tenant.js +92 -0
  23. package/build/shared/src/tools/revoke-api-key.js +119 -0
  24. package/build/shared/src/tools/save-mcp-implementation.js +89 -2
  25. package/build/shared/src/tools/set-known-missing-init-tools-list.js +75 -0
  26. package/build/shared/src/tools/update-mcp-server.js +19 -0
  27. package/build/shared/src/tools.js +30 -1
  28. package/node_modules/@pulsemcp/mcp-elicitation/build/config.d.ts +15 -0
  29. package/node_modules/@pulsemcp/mcp-elicitation/build/config.js +41 -0
  30. package/node_modules/@pulsemcp/mcp-elicitation/build/elicitation.d.ts +24 -0
  31. package/node_modules/@pulsemcp/mcp-elicitation/build/elicitation.js +175 -0
  32. package/node_modules/@pulsemcp/mcp-elicitation/build/index.d.ts +3 -0
  33. package/node_modules/@pulsemcp/mcp-elicitation/build/index.js +2 -0
  34. package/node_modules/@pulsemcp/mcp-elicitation/build/types.d.ts +114 -0
  35. package/node_modules/@pulsemcp/mcp-elicitation/build/types.js +1 -0
  36. package/node_modules/@pulsemcp/mcp-elicitation/package.json +28 -0
  37. package/package.json +7 -1
  38. package/shared/elicitation-config.d.ts +61 -0
  39. package/shared/elicitation-config.js +69 -0
  40. package/shared/pulsemcp-admin-client/lib/bulk-update-tenant-servers.d.ts +3 -0
  41. package/shared/pulsemcp-admin-client/lib/bulk-update-tenant-servers.js +41 -0
  42. package/shared/pulsemcp-admin-client/lib/create-mcp-implementation.js +7 -0
  43. package/shared/pulsemcp-admin-client/lib/delete-api-key.d.ts +3 -0
  44. package/shared/pulsemcp-admin-client/lib/delete-api-key.js +25 -0
  45. package/shared/pulsemcp-admin-client/lib/delete-tenant.d.ts +3 -0
  46. package/shared/pulsemcp-admin-client/lib/delete-tenant.js +31 -0
  47. package/shared/pulsemcp-admin-client/lib/get-unified-mcp-server.js +5 -1
  48. package/shared/pulsemcp-admin-client/lib/list-tenant-servers.d.ts +7 -0
  49. package/shared/pulsemcp-admin-client/lib/list-tenant-servers.js +57 -0
  50. package/shared/pulsemcp-admin-client/lib/save-mcp-implementation.js +8 -0
  51. package/shared/pulsemcp-admin-client/lib/set-known-missing-init-tools-list.d.ts +3 -0
  52. package/shared/pulsemcp-admin-client/lib/set-known-missing-init-tools-list.js +42 -0
  53. package/shared/pulsemcp-admin-client/lib/unified-mcp-server-mapper.d.ts +2 -0
  54. package/shared/pulsemcp-admin-client/lib/unified-mcp-server-mapper.js +3 -0
  55. package/shared/pulsemcp-admin-client/lib/update-unified-mcp-server.js +14 -0
  56. package/shared/pulsemcp-admin-client/pulsemcp-admin-client.integration-mock.js +39 -0
  57. package/shared/server.d.ts +19 -1
  58. package/shared/server.js +20 -0
  59. package/shared/tools/add-servers-to-tenant.d.ts +57 -0
  60. package/shared/tools/add-servers-to-tenant.js +133 -0
  61. package/shared/tools/delete-api-key.d.ts +30 -0
  62. package/shared/tools/delete-api-key.js +119 -0
  63. package/shared/tools/delete-tenant.d.ts +36 -0
  64. package/shared/tools/delete-tenant.js +129 -0
  65. package/shared/tools/get-mcp-server.js +26 -8
  66. package/shared/tools/list-mcp-servers.js +3 -0
  67. package/shared/tools/list-tenant-servers.d.ts +45 -0
  68. package/shared/tools/list-tenant-servers.js +89 -0
  69. package/shared/tools/remove-servers-from-tenant.d.ts +42 -0
  70. package/shared/tools/remove-servers-from-tenant.js +92 -0
  71. package/shared/tools/revoke-api-key.d.ts +30 -0
  72. package/shared/tools/revoke-api-key.js +119 -0
  73. package/shared/tools/save-mcp-implementation.d.ts +9 -1
  74. package/shared/tools/save-mcp-implementation.js +89 -2
  75. package/shared/tools/set-known-missing-init-tools-list.d.ts +38 -0
  76. package/shared/tools/set-known-missing-init-tools-list.js +75 -0
  77. package/shared/tools/update-mcp-server.d.ts +6 -0
  78. package/shared/tools/update-mcp-server.js +19 -0
  79. package/shared/tools.d.ts +5 -3
  80. package/shared/tools.js +30 -1
  81. package/shared/types.d.ts +89 -0
@@ -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,36 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import type { ClientFactory } from '../server.js';
3
+ export declare function deleteTenant(server: Server, clientFactory: ClientFactory): {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: {
7
+ type: string;
8
+ properties: {
9
+ id_or_slug: {
10
+ oneOf: {
11
+ type: string;
12
+ }[];
13
+ description: "The ID (number) or slug (string) of the tenant to delete.";
14
+ };
15
+ force: {
16
+ type: string;
17
+ description: "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.";
18
+ };
19
+ };
20
+ required: string[];
21
+ };
22
+ handler: (args: unknown) => Promise<{
23
+ content: {
24
+ type: string;
25
+ text: string;
26
+ }[];
27
+ isError: boolean;
28
+ } | {
29
+ content: {
30
+ type: string;
31
+ text: string;
32
+ }[];
33
+ isError?: undefined;
34
+ }>;
35
+ };
36
+ //# sourceMappingURL=delete-tenant.d.ts.map
@@ -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
- if (server.canonical_urls && server.canonical_urls.length > 0) {
157
- content += `\n## Canonical URLs\n`;
158
- for (const canonical of server.canonical_urls) {
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
- // Remote endpoints
167
- if (server.remotes && server.remotes.length > 0) {
168
- content += `\n## Remote Endpoints (${server.remotes.length})\n`;
169
- for (const [idx, remote] of server.remotes.entries()) {
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
  }
@@ -0,0 +1,45 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import type { ClientFactory } from '../server.js';
3
+ export declare function listTenantServers(_server: Server, clientFactory: ClientFactory): {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: {
7
+ type: string;
8
+ properties: {
9
+ tenant: {
10
+ oneOf: {
11
+ type: string;
12
+ }[];
13
+ description: "Tenant ID (number) or slug (string)";
14
+ };
15
+ status: {
16
+ type: string;
17
+ enum: string[];
18
+ description: "Association status filter: \"active\" (default), \"deleted\" (soft-deleted only), or \"all\"";
19
+ };
20
+ limit: {
21
+ type: string;
22
+ description: "Results per page, range 1-100 (default 30)";
23
+ };
24
+ offset: {
25
+ type: string;
26
+ description: "Pagination offset (default 0)";
27
+ };
28
+ };
29
+ required: string[];
30
+ };
31
+ handler: (args: unknown) => Promise<{
32
+ content: {
33
+ type: string;
34
+ text: string;
35
+ }[];
36
+ isError?: undefined;
37
+ } | {
38
+ content: {
39
+ type: string;
40
+ text: string;
41
+ }[];
42
+ isError: boolean;
43
+ }>;
44
+ };
45
+ //# sourceMappingURL=list-tenant-servers.d.ts.map
@@ -0,0 +1,89 @@
1
+ import { z } from 'zod';
2
+ const PARAM_DESCRIPTIONS = {
3
+ tenant: 'Tenant ID (number) or slug (string)',
4
+ status: 'Association status filter: "active" (default), "deleted" (soft-deleted only), or "all"',
5
+ limit: 'Results per page, range 1-100 (default 30)',
6
+ offset: 'Pagination offset (default 0)',
7
+ };
8
+ const ListTenantServersSchema = z.object({
9
+ tenant: z.union([z.number(), z.string()]).describe(PARAM_DESCRIPTIONS.tenant),
10
+ status: z.enum(['active', 'deleted', 'all']).optional().describe(PARAM_DESCRIPTIONS.status),
11
+ limit: z.number().int().min(1).max(100).optional().describe(PARAM_DESCRIPTIONS.limit),
12
+ offset: z.number().int().min(0).optional().describe(PARAM_DESCRIPTIONS.offset),
13
+ });
14
+ export function listTenantServers(_server, clientFactory) {
15
+ return {
16
+ name: 'list_tenant_servers',
17
+ description: `List a tenant's recommended MCP server associations (TenantsMcpServer rows).
18
+
19
+ Use this to inspect which public MCP servers are currently part of a tenant's recommendation list, before/after calling add_servers_to_tenant or remove_servers_from_tenant.
20
+
21
+ Each association row includes:
22
+ - id: TenantsMcpServer association ID (use this for restore_association_ids in add_servers_to_tenant)
23
+ - mcp_server_id / mcp_server_slug: the underlying public MCP server
24
+ - status: "active" or "deleted" (soft-deleted)
25
+ - server_json_selection: which server.json variant the tenant pins (always "unofficial" for entries created via these tools)
26
+ - touched / first_touched_at: whether the tenant's customization touched this association
27
+
28
+ Use cases:
29
+ - Audit a tenant's recommendation list
30
+ - Find soft-deleted associations to restore
31
+ - Verify the result of a recent add/remove call`,
32
+ inputSchema: {
33
+ type: 'object',
34
+ properties: {
35
+ tenant: {
36
+ oneOf: [{ type: 'number' }, { type: 'string' }],
37
+ description: PARAM_DESCRIPTIONS.tenant,
38
+ },
39
+ status: {
40
+ type: 'string',
41
+ enum: ['active', 'deleted', 'all'],
42
+ description: PARAM_DESCRIPTIONS.status,
43
+ },
44
+ limit: { type: 'number', description: PARAM_DESCRIPTIONS.limit },
45
+ offset: { type: 'number', description: PARAM_DESCRIPTIONS.offset },
46
+ },
47
+ required: ['tenant'],
48
+ },
49
+ handler: async (args) => {
50
+ try {
51
+ const validatedArgs = ListTenantServersSchema.parse(args);
52
+ const client = clientFactory();
53
+ const response = await client.listTenantServers(validatedArgs.tenant, {
54
+ status: validatedArgs.status,
55
+ limit: validatedArgs.limit,
56
+ offset: validatedArgs.offset,
57
+ });
58
+ const { data, pagination } = response;
59
+ const statusLabel = validatedArgs.status || 'active';
60
+ let content = `**Tenant Server Associations** (status=${statusLabel})\n\n`;
61
+ content += `Showing ${data.length} of ${pagination.total_count} associations (page ${pagination.current_page} of ${pagination.total_pages}).\n\n`;
62
+ if (data.length === 0) {
63
+ content += `_No associations matching this filter._\n`;
64
+ }
65
+ else {
66
+ for (const row of data) {
67
+ content += `- **${row.mcp_server_slug}** (server_id=${row.mcp_server_id}, association_id=${row.id})\n`;
68
+ content += ` status=${row.status}, server_json_selection=${row.server_json_selection}, touched=${row.touched}\n`;
69
+ if (row.first_touched_at) {
70
+ content += ` first_touched_at=${row.first_touched_at}\n`;
71
+ }
72
+ }
73
+ }
74
+ return { content: [{ type: 'text', text: content }] };
75
+ }
76
+ catch (error) {
77
+ return {
78
+ content: [
79
+ {
80
+ type: 'text',
81
+ text: `Error listing tenant servers: ${error instanceof Error ? error.message : String(error)}`,
82
+ },
83
+ ],
84
+ isError: true,
85
+ };
86
+ }
87
+ },
88
+ };
89
+ }
@@ -0,0 +1,42 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import type { ClientFactory } from '../server.js';
3
+ export declare function removeServersFromTenant(_server: Server, clientFactory: ClientFactory): {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: {
7
+ type: string;
8
+ properties: {
9
+ tenant: {
10
+ oneOf: {
11
+ type: string;
12
+ }[];
13
+ description: "Tenant ID (number) or slug (string) to remove servers from.";
14
+ };
15
+ mcp_servers: {
16
+ type: string;
17
+ items: {
18
+ oneOf: {
19
+ type: string;
20
+ }[];
21
+ };
22
+ description: "Array of MCP server IDs (numbers) or slugs (strings) to remove from the tenant's recommendation list. Touched associations are soft-deleted (preserving the customization history); untouched associations are hard-deleted.";
23
+ minItems: number;
24
+ };
25
+ };
26
+ required: string[];
27
+ };
28
+ handler: (args: unknown) => Promise<{
29
+ content: {
30
+ type: string;
31
+ text: string;
32
+ }[];
33
+ isError?: undefined;
34
+ } | {
35
+ content: {
36
+ type: string;
37
+ text: string;
38
+ }[];
39
+ isError: boolean;
40
+ }>;
41
+ };
42
+ //# sourceMappingURL=remove-servers-from-tenant.d.ts.map
@@ -0,0 +1,92 @@
1
+ import { z } from 'zod';
2
+ const PARAM_DESCRIPTIONS = {
3
+ tenant: 'Tenant ID (number) or slug (string) to remove servers from.',
4
+ mcp_servers: "Array of MCP server IDs (numbers) or slugs (strings) to remove from the tenant's recommendation list. Touched associations are soft-deleted (preserving the customization history); untouched associations are hard-deleted.",
5
+ };
6
+ const RemoveServersFromTenantSchema = z.object({
7
+ tenant: z.union([z.number(), z.string()]).describe(PARAM_DESCRIPTIONS.tenant),
8
+ mcp_servers: z
9
+ .array(z.union([z.number(), z.string()]))
10
+ .min(1)
11
+ .describe(PARAM_DESCRIPTIONS.mcp_servers),
12
+ });
13
+ export function removeServersFromTenant(_server, clientFactory) {
14
+ return {
15
+ name: 'remove_servers_from_tenant',
16
+ description: `Remove MCP servers from a tenant's recommendation list.
17
+
18
+ This is a convenience inverse of add_servers_to_tenant — it accepts only the tenant and a list of servers to remove. For combined add/remove/restore in a single transactional call, use add_servers_to_tenant.
19
+
20
+ This tool only changes the tenant's recommendation list (TenantsMcpServer rows). The underlying MCP server's public listing is NOT affected.
21
+
22
+ Outcome behavior:
23
+ - Untouched associations: hard-deleted (row removed entirely; outcome "hard_deleted")
24
+ - Touched associations: soft-deleted (status set to "deleted", row preserved; outcome "soft_deleted")
25
+
26
+ The response includes:
27
+ - removed: servers detached, with outcome
28
+ - skipped/unresolved_identifiers: as for add_servers_to_tenant
29
+
30
+ Use cases:
31
+ - Curate a tenant's recommendation list by trimming servers
32
+ - Cleanly remove servers from a tenant without affecting public listings`,
33
+ inputSchema: {
34
+ type: 'object',
35
+ properties: {
36
+ tenant: {
37
+ oneOf: [{ type: 'number' }, { type: 'string' }],
38
+ description: PARAM_DESCRIPTIONS.tenant,
39
+ },
40
+ mcp_servers: {
41
+ type: 'array',
42
+ items: { oneOf: [{ type: 'number' }, { type: 'string' }] },
43
+ description: PARAM_DESCRIPTIONS.mcp_servers,
44
+ minItems: 1,
45
+ },
46
+ },
47
+ required: ['tenant', 'mcp_servers'],
48
+ },
49
+ handler: async (args) => {
50
+ try {
51
+ const validatedArgs = RemoveServersFromTenantSchema.parse(args);
52
+ const client = clientFactory();
53
+ const result = await client.bulkUpdateTenantServers(validatedArgs.tenant, {
54
+ remove_server_identifiers: validatedArgs.mcp_servers,
55
+ });
56
+ let content = `**Tenant servers updated** (tenant=${result.tenant.slug}, id=${result.tenant.id})\n\n`;
57
+ content += `- Removed: ${result.removed.length}\n`;
58
+ content += `- Skipped: ${result.skipped.length}\n`;
59
+ content += `- Unresolved identifiers: ${result.unresolved_identifiers.length}\n`;
60
+ if (result.removed.length > 0) {
61
+ content += `\n**Removed:**\n`;
62
+ for (const item of result.removed) {
63
+ const assocPart = item.association_id != null ? `, association_id=${item.association_id}` : '';
64
+ content += `- ${item.mcp_server_slug} (server_id=${item.mcp_server_id}${assocPart}, outcome=${item.outcome})\n`;
65
+ }
66
+ }
67
+ if (result.skipped.length > 0) {
68
+ content += `\n**Skipped:**\n`;
69
+ for (const item of result.skipped) {
70
+ const label = item.mcp_server_slug || item.mcp_server_id || item.association_id;
71
+ content += `- ${label} — reason: ${item.reason}\n`;
72
+ }
73
+ }
74
+ if (result.unresolved_identifiers.length > 0) {
75
+ content += `\n**Unresolved identifiers:** ${result.unresolved_identifiers.join(', ')}\n`;
76
+ }
77
+ return { content: [{ type: 'text', text: content }] };
78
+ }
79
+ catch (error) {
80
+ return {
81
+ content: [
82
+ {
83
+ type: 'text',
84
+ text: `Error removing tenant servers: ${error instanceof Error ? error.message : String(error)}`,
85
+ },
86
+ ],
87
+ isError: true,
88
+ };
89
+ }
90
+ },
91
+ };
92
+ }
@@ -0,0 +1,30 @@
1
+ import { Server } from '@modelcontextprotocol/sdk/server/index.js';
2
+ import type { ClientFactory } from '../server.js';
3
+ export declare function revokeApiKey(server: Server, clientFactory: ClientFactory): {
4
+ name: string;
5
+ description: string;
6
+ inputSchema: {
7
+ type: string;
8
+ properties: {
9
+ api_key_id: {
10
+ type: string;
11
+ description: "The numeric ID of the API key to revoke (the `id` field returned by `create_api_key`).";
12
+ };
13
+ };
14
+ required: string[];
15
+ };
16
+ handler: (args: unknown) => Promise<{
17
+ content: {
18
+ type: string;
19
+ text: string;
20
+ }[];
21
+ isError: boolean;
22
+ } | {
23
+ content: {
24
+ type: string;
25
+ text: string;
26
+ }[];
27
+ isError?: undefined;
28
+ }>;
29
+ };
30
+ //# sourceMappingURL=revoke-api-key.d.ts.map